-
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add commands for register manipulation and command execution; add asy…
…nc subscription; fix defects (#55) - Add shorter aliases for pub sub - Introduce FormatterHints - Add HACK to remove the end-of-document marker from YAML produced by ruamel.yaml - Update the formatter usage conventions to not emit extra newline at the end - Use EXIT_CODE_UNSUCCESSFUL - Suppress errors from PyCyphal during event loop finalization - Fix `yakut monitor` screen refresh regression. - Add `yakut register`. - Add `yakut register-list`. - Add `yakut register-batch`. - Add `yakut execute-command`. - Implement asynchronous subscription in `yakut subscribe` and make it the default option. - New simpler option `--sync` - Simplify transfer metadata reporting (related to the future #54 ) * Introduce format selection options --yaml/--json/--tsvh - Add a new formatter option `AUTO`, which is now the default. It selects between JSON and YAML automatically depending on `isatty(stdout)`. This is needed to enable compatibility with `jq`. - Close #30 - Close #52 - Close #3
- Loading branch information
1 parent
3566080
commit e1b0c64
Showing
50 changed files
with
3,472 additions
and
495 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
# Copyright (c) 2022 OpenCyphal | ||
# This software is distributed under the terms of the MIT License. | ||
# Author: Pavel Kirienko <[email protected]> | ||
|
||
from __future__ import annotations | ||
import asyncio | ||
from typing import Any, AsyncIterable, Callable, Awaitable | ||
import json | ||
import concurrent.futures | ||
import pytest | ||
import pycyphal | ||
from tests.dsdl import OUTPUT_DIR | ||
from tests.transport import TransportFactory | ||
from tests.subprocess import execute_cli | ||
from yakut.util import EXIT_CODE_UNSUCCESSFUL | ||
|
||
|
||
class Remote: | ||
def __init__(self, name: str, env: dict[str, str]) -> None: | ||
from pycyphal.application import make_registry, make_node, NodeInfo | ||
from uavcan.node import ExecuteCommand_1 | ||
|
||
self._node = make_node( | ||
NodeInfo(name=name), | ||
make_registry(environment_variables=env), | ||
) | ||
self.last_request: ExecuteCommand_1.Request | None = None | ||
self.next_response: ExecuteCommand_1.Response | None = None | ||
|
||
async def serve_execute_command( | ||
req: ExecuteCommand_1.Request, | ||
_meta: pycyphal.presentation.ServiceRequestMetadata, | ||
) -> ExecuteCommand_1.Response | None: | ||
# print(self._node, req, _meta, self.next_response, sep="\n\t") | ||
self.last_request = req | ||
return self.next_response | ||
|
||
self._srv = self._node.get_server(ExecuteCommand_1) | ||
self._srv.serve_in_background(serve_execute_command) | ||
self._node.start() | ||
|
||
def close(self) -> None: | ||
self._srv.close() | ||
self._node.close() | ||
|
||
|
||
Runner = Callable[..., Awaitable[Any]] | ||
|
||
|
||
@pytest.fixture | ||
async def _context( | ||
compiled_dsdl: Any, | ||
transport_factory: TransportFactory, | ||
) -> AsyncIterable[tuple[Runner, tuple[Remote, Remote]]]: | ||
asyncio.get_running_loop().slow_callback_duration = 10.0 | ||
_ = compiled_dsdl | ||
remote_nodes = ( | ||
Remote(f"remote_10", env=transport_factory(10).environment), | ||
Remote(f"remote_11", env=transport_factory(11).environment), | ||
) | ||
background_executor = concurrent.futures.ThreadPoolExecutor() | ||
|
||
async def run(*args: str) -> tuple[int, Any]: | ||
def call() -> tuple[int, Any]: | ||
status, stdout, _stderr = execute_cli( | ||
"cmd", | ||
*args, | ||
environment_variables={ | ||
**transport_factory(100).environment, | ||
"YAKUT_PATH": str(OUTPUT_DIR), | ||
}, | ||
timeout=10, | ||
ensure_success=False, | ||
) | ||
return status, json.loads(stdout) if stdout else None | ||
|
||
return await asyncio.get_running_loop().run_in_executor(background_executor, call) | ||
|
||
yield run, remote_nodes | ||
for rn in remote_nodes: | ||
rn.close() | ||
await asyncio.sleep(1.0) | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def _unittest_basic(_context: tuple[Runner, tuple[Remote, Remote]]) -> None: | ||
from uavcan.node import ExecuteCommand_1 | ||
|
||
run, (remote_10, remote_11) = _context | ||
|
||
# SUCCESS | ||
remote_10.next_response = ExecuteCommand_1.Response(status=0) | ||
remote_11.next_response = ExecuteCommand_1.Response(status=0) | ||
assert await run("10-12", "restart", "--timeout=3") == ( | ||
0, | ||
{ | ||
"10": {"status": 0}, | ||
"11": {"status": 0}, | ||
}, | ||
) | ||
assert await run("10-12", "111", "COMMAND ARGUMENT", "--timeout=3") == ( | ||
0, | ||
{ | ||
"10": {"status": 0}, | ||
"11": {"status": 0}, | ||
}, | ||
) | ||
assert ( | ||
remote_10.last_request | ||
and remote_10.last_request.command == 111 | ||
and remote_10.last_request.parameter.tobytes().decode() == "COMMAND ARGUMENT" | ||
) | ||
assert ( | ||
remote_11.last_request | ||
and remote_11.last_request.command == 111 | ||
and remote_11.last_request.parameter.tobytes().decode() == "COMMAND ARGUMENT" | ||
) | ||
|
||
# REMOTE ERROR; PROPAGATED AND IGNORED | ||
remote_10.next_response = ExecuteCommand_1.Response(status=100) | ||
remote_11.next_response = ExecuteCommand_1.Response(status=200) | ||
assert await run("10-12", "restart", "--timeout=3") == ( | ||
EXIT_CODE_UNSUCCESSFUL, | ||
{ | ||
"10": {"status": 100}, | ||
"11": {"status": 200}, | ||
}, | ||
) | ||
assert await run("10-12", "123", "--expect=100,200", "--timeout=3") == ( | ||
0, | ||
{ | ||
"10": {"status": 100}, | ||
"11": {"status": 200}, | ||
}, | ||
) | ||
assert remote_10.last_request and remote_10.last_request.command == 123 | ||
assert remote_11.last_request and remote_11.last_request.command == 123 | ||
|
||
# ONE TIMED OUT; ERROR PROPAGATED AND IGNORED | ||
remote_10.next_response = None | ||
remote_11.next_response = ExecuteCommand_1.Response(status=0) | ||
assert await run("10-12", "123", "--timeout=3") == ( | ||
EXIT_CODE_UNSUCCESSFUL, | ||
{ | ||
"10": None, | ||
"11": {"status": 0}, | ||
}, | ||
) | ||
assert await run("10-12", "123", "--expect") == ( | ||
0, | ||
{ | ||
"10": None, | ||
"11": {"status": 0}, | ||
}, | ||
) | ||
|
||
# FLAT OUTPUT (NOT GROUPED BY NODE-ID) | ||
remote_11.next_response = ExecuteCommand_1.Response(status=210) | ||
assert await run("11", "123", "FOO BAR", "--timeout=3") == ( | ||
EXIT_CODE_UNSUCCESSFUL, | ||
{"status": 210}, | ||
) | ||
assert ( | ||
remote_11.last_request | ||
and remote_11.last_request.command == 123 | ||
and remote_11.last_request.parameter.tobytes().decode() == "FOO BAR" | ||
) | ||
assert await run("11", "222", "--timeout=3", "--expect=0..256") == ( | ||
0, | ||
{"status": 210}, | ||
) | ||
assert ( | ||
remote_11.last_request | ||
and remote_11.last_request.command == 222 | ||
and remote_11.last_request.parameter.tobytes().decode() == "" | ||
) | ||
|
||
# ERRORS | ||
assert (await run("bad"))[0] != 0 | ||
assert (await run("10", "invalid_command"))[0] != 0 | ||
assert (await run("10", "99999999999"))[0] != 0 # Bad command code, serialization will fail | ||
assert (await run("10", "0", "z" * 1024))[0] != 0 # Bad parameter, serialization will fail |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.