mirror of
https://github.com/QIDITECH/moonraker.git
synced 2026-01-31 00:28:45 +03:00
QIDI moonraker
This commit is contained in:
5
tests/fixtures/__init__.py
vendored
Normal file
5
tests/fixtures/__init__.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from .klippy_process import KlippyProcess
|
||||
from .http_client import HttpClient
|
||||
from .websocket_client import WebsocketClient
|
||||
|
||||
__all__ = ("KlippyProcess", "HttpClient", "WebsocketClient")
|
||||
78
tests/fixtures/http_client.py
vendored
Normal file
78
tests/fixtures/http_client.py
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
import json
|
||||
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPError
|
||||
from tornado.httputil import HTTPHeaders
|
||||
from tornado.escape import url_escape
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class HttpClient:
|
||||
error = HTTPError
|
||||
def __init__(self,
|
||||
type: str = "http",
|
||||
port: int = 7010
|
||||
) -> None:
|
||||
self.client = AsyncHTTPClient()
|
||||
assert type in ["http", "https"]
|
||||
self.prefix = f"{type}://127.0.0.1:{port}/"
|
||||
self.last_response_headers: HTTPHeaders = HTTPHeaders()
|
||||
|
||||
def get_response_headers(self) -> HTTPHeaders:
|
||||
return self.last_response_headers
|
||||
|
||||
async def _do_request(self,
|
||||
method: str,
|
||||
endpoint: str,
|
||||
args: Dict[str, Any] = {},
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
ep = "/".join([url_escape(part, plus=False) for part in
|
||||
endpoint.lstrip("/").split("/")])
|
||||
url = self.prefix + ep
|
||||
method = method.upper()
|
||||
body: Optional[str] = "" if method == "POST" else None
|
||||
if args:
|
||||
if method in ["GET", "DELETE"]:
|
||||
parts = []
|
||||
for key, val in args.items():
|
||||
if isinstance(val, list):
|
||||
val = ",".join(val)
|
||||
if val:
|
||||
parts.append(f"{url_escape(key)}={url_escape(val)}")
|
||||
else:
|
||||
parts.append(url_escape(key))
|
||||
qs = "&".join(parts)
|
||||
url += "?" + qs
|
||||
else:
|
||||
body = json.dumps(args)
|
||||
if headers is None:
|
||||
headers = {}
|
||||
headers["Content-Type"] = "application/json"
|
||||
request = HTTPRequest(url, method, headers, body=body,
|
||||
request_timeout=2., connect_timeout=2.)
|
||||
ret = await self.client.fetch(request)
|
||||
self.last_response_headers = HTTPHeaders(ret.headers)
|
||||
return json.loads(ret.body)
|
||||
|
||||
async def get(self,
|
||||
endpoint: str,
|
||||
args: Dict[str, Any] = {},
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
return await self._do_request("GET", endpoint, args, headers)
|
||||
|
||||
async def post(self,
|
||||
endpoint: str,
|
||||
args: Dict[str, Any] = {},
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
return await self._do_request("POST", endpoint, args, headers)
|
||||
|
||||
async def delete(self,
|
||||
endpoint: str,
|
||||
args: Dict[str, Any] = {},
|
||||
headers: Optional[Dict[str, str]] = None
|
||||
) -> Dict[str, Any]:
|
||||
return await self._do_request("DELETE", endpoint, args, headers)
|
||||
|
||||
def close(self):
|
||||
self.client.close()
|
||||
81
tests/fixtures/klippy_process.py
vendored
Normal file
81
tests/fixtures/klippy_process.py
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
import pytest
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import pathlib
|
||||
import shlex
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
class KlippyProcess:
|
||||
def __init__(self,
|
||||
base_cmd: str,
|
||||
path_args: Dict[str, pathlib.Path],
|
||||
) -> None:
|
||||
self.base_cmd = base_cmd
|
||||
self.config_path = path_args['printer.cfg']
|
||||
self.orig_config = self.config_path
|
||||
self.dict_path = path_args["klipper.dict"]
|
||||
self.pty_path = path_args["klippy_pty_path"]
|
||||
self.uds_path = path_args["klippy_uds_path"]
|
||||
self.proc: Optional[subprocess.Popen] = None
|
||||
self.fd: int = -1
|
||||
|
||||
def start(self):
|
||||
if self.proc is not None:
|
||||
return
|
||||
args = (
|
||||
f"{self.config_path} -o /dev/null -d {self.dict_path} "
|
||||
f"-a {self.uds_path} -I {self.pty_path}"
|
||||
)
|
||||
cmd = f"{self.base_cmd} {args}"
|
||||
cmd_parts = shlex.split(cmd)
|
||||
self.proc = subprocess.Popen(cmd_parts)
|
||||
for _ in range(250):
|
||||
if self.pty_path.exists():
|
||||
try:
|
||||
self.fd = os.open(
|
||||
str(self.pty_path), os.O_RDWR | os.O_NONBLOCK)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
break
|
||||
time.sleep(.01)
|
||||
else:
|
||||
self.stop()
|
||||
pytest.fail("Unable to start Klippy process")
|
||||
return False
|
||||
return True
|
||||
|
||||
def send_gcode(self, gcode: str) -> None:
|
||||
if self.fd == -1:
|
||||
return
|
||||
try:
|
||||
os.write(self.fd, f"{gcode}\n".encode())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def restart(self):
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
def stop(self):
|
||||
if self.fd != -1:
|
||||
os.close(self.fd)
|
||||
self.fd = -1
|
||||
if self.proc is not None:
|
||||
self.proc.terminate()
|
||||
try:
|
||||
self.proc.wait(2.)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.proc.kill()
|
||||
self.proc = None
|
||||
|
||||
def get_paths(self) -> Dict[str, pathlib.Path]:
|
||||
return {
|
||||
"printer.cfg": self.config_path,
|
||||
"klipper.dict": self.dict_path,
|
||||
"klippy_uds_path": self.uds_path,
|
||||
"klippy_pty_path": self.pty_path,
|
||||
}
|
||||
136
tests/fixtures/websocket_client.py
vendored
Normal file
136
tests/fixtures/websocket_client.py
vendored
Normal file
@@ -0,0 +1,136 @@
|
||||
from __future__ import annotations
|
||||
import pytest
|
||||
import json
|
||||
import asyncio
|
||||
import tornado.websocket
|
||||
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
Tuple,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Any,
|
||||
Optional,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tornado.websocket import WebSocketClientConnection
|
||||
|
||||
class WebsocketError(Exception):
|
||||
def __init__(self, code, *args: object) -> None:
|
||||
super().__init__(*args)
|
||||
self.code = code
|
||||
|
||||
class WebsocketClient:
|
||||
error = WebsocketError
|
||||
def __init__(self,
|
||||
type: str = "ws",
|
||||
port: int = 7010
|
||||
) -> None:
|
||||
self.ws: Optional[WebSocketClientConnection] = None
|
||||
self.pending_requests: Dict[int, asyncio.Future] = {}
|
||||
self.notify_cbs: Dict[str, List[Callable[..., None]]] = {}
|
||||
assert type in ["ws", "wss"]
|
||||
self.url = f"{type}://127.0.0.1:{port}/websocket"
|
||||
|
||||
async def connect(self, token: Optional[str] = None) -> None:
|
||||
url = self.url
|
||||
if token is not None:
|
||||
url += f"?token={token}"
|
||||
self.ws = await tornado.websocket.websocket_connect(
|
||||
url, connect_timeout=2.,
|
||||
on_message_callback=self._on_message_received)
|
||||
|
||||
async def request(self,
|
||||
remote_method: str,
|
||||
args: Dict[str, Any] = {}
|
||||
) -> Dict[str, Any]:
|
||||
if self.ws is None:
|
||||
pytest.fail("Websocket Not Connected")
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = loop.create_future()
|
||||
req, req_id = self._encode_request(remote_method, args)
|
||||
self.pending_requests[req_id] = fut
|
||||
await self.ws.write_message(req)
|
||||
return await asyncio.wait_for(fut, 2.)
|
||||
|
||||
def _encode_request(self,
|
||||
method: str,
|
||||
args: Dict[str, Any]
|
||||
) -> Tuple[str, int]:
|
||||
request: Dict[str, Any] = {
|
||||
'jsonrpc': "2.0",
|
||||
'method': method,
|
||||
}
|
||||
if args:
|
||||
request['params'] = args
|
||||
req_id = id(request)
|
||||
request["id"] = req_id
|
||||
return json.dumps(request), req_id
|
||||
|
||||
def _on_message_received(self, message: Union[str, bytes, None]) -> None:
|
||||
if isinstance(message, str):
|
||||
self._decode_jsonrpc(message)
|
||||
|
||||
def _decode_jsonrpc(self, data: str) -> None:
|
||||
try:
|
||||
resp: Dict[str, Any] = json.loads(data)
|
||||
except json.JSONDecodeError:
|
||||
pytest.fail(f"Websocket JSON Decode Error: {data}")
|
||||
header = resp.get('jsonrpc', "")
|
||||
if header != "2.0":
|
||||
# Invalid Json, set error if we can get the id
|
||||
pytest.fail(f"Invalid jsonrpc header: {data}")
|
||||
req_id: Optional[int] = resp.get("id")
|
||||
method: Optional[str] = resp.get("method")
|
||||
if method is not None:
|
||||
if req_id is None:
|
||||
params = resp.get("params", [])
|
||||
if not isinstance(params, list):
|
||||
pytest.fail("jsonrpc notification params"
|
||||
f"should always be a list: {data}")
|
||||
if method in self.notify_cbs:
|
||||
for func in self.notify_cbs[method]:
|
||||
func(*params)
|
||||
else:
|
||||
# This is a request from the server (should not happen)
|
||||
pytest.fail(f"Server should not request from client: {data}")
|
||||
elif req_id is not None:
|
||||
pending_fut = self.pending_requests.pop(req_id, None)
|
||||
if pending_fut is None:
|
||||
# No future pending for this response
|
||||
return
|
||||
# This is a response
|
||||
if "result" in resp:
|
||||
pending_fut.set_result(resp["result"])
|
||||
elif "error" in resp:
|
||||
err = resp["error"]
|
||||
try:
|
||||
code = err["code"]
|
||||
msg = err["message"]
|
||||
except Exception:
|
||||
pytest.fail(f"Invalid jsonrpc error: {data}")
|
||||
exc = WebsocketError(code, msg)
|
||||
pending_fut.set_exception(exc)
|
||||
else:
|
||||
pytest.fail(
|
||||
f"Invalid jsonrpc packet, no result or error: {data}")
|
||||
else:
|
||||
# Invalid json
|
||||
pytest.fail(f"Invalid jsonrpc packet, no id: {data}")
|
||||
|
||||
def register_notify_callback(self, name: str, callback) -> None:
|
||||
if name in self.notify_cbs:
|
||||
self.notify_cbs[name].append(callback)
|
||||
else:
|
||||
self.notify_cbs[name][callback]
|
||||
|
||||
def close(self):
|
||||
for fut in self.pending_requests.values():
|
||||
if not fut.done():
|
||||
fut.set_exception(WebsocketError(
|
||||
0, "Closing Websocket Client"))
|
||||
if self.ws is not None:
|
||||
self.ws.close(1000, "Test Complete")
|
||||
Reference in New Issue
Block a user