diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 1ee5d9d..0000000 --- a/.appveyor.yml +++ /dev/null @@ -1,95 +0,0 @@ -environment: - matrix: - - - job_group: tests - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - PYTHON: 'C:\Python310-x64' # YAML treats '\\' and "\\" differently. - - - job_group: tests - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004 - PYTHON: '3.10' - - - job_group: tests - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004 - PYTHON: '3.9' - - - job_group: tests - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004 - PYTHON: '3.8' - - - job_name: deploy - job_depends_on: tests - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004 - PYTHON: '3.10' - - GIT_TOKEN: - secure: +jQhxLpePj6hdDryfET/XpLo7VL9fhDXVHlwLOPp/nRDYe97TJAfd0XCTuPz1qkT - TWINE_USERNAME: __token__ - TWINE_PASSWORD: - secure: +ZVhECKV0ESBrvUGXVd9whoSYHcVs5Gr2toLizARlAIiZitWAZekz4kYpqk0XfXtJidB8ivGTq0bNPdUL1NuTu0qIagKEBDuPKjjdHvCM/22yAZEZCm1oZEqHwnRiNmHqhac4sFBtklyGk0y9zk7J/dJmn183/OmNCwCT20v0346c9ZPf4EM75S190t9ndRR8fYIASuki33DkhQHpuTiaGECPsSGpa5C/6eTEnV7nO4XekasvMwLrtr+J6+7HnImwk3EDaeVb2Pv4R6JzDC3zw== - -stack: python %PYTHON% -build: off -skip_tags: true - -for: - - matrix: - only: - - job_group: tests - APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - install: - - 'set PATH=%PYTHON%;%PYTHON%\Scripts;%PATH%' - - git submodule update --init --recursive - - python -m pip install --upgrade pip setuptools nox - test_script: - - nox --non-interactive --session test lint - - - - matrix: - only: - - job_group: tests - APPVEYOR_BUILD_WORKER_IMAGE: Ubuntu2004 - install: - - 'sudo apt-get --ignore-missing update || true' - - 'export extras_pkg="linux-*-extra-$(uname -r)"' - - 'sudo apt-get install -y $extras_pkg ncat' - - 'sudo apt-get install -y libsdl2-2.0-0' # For PySDL2. On Windows/macOS the binaries are pulled from PyPI. - - 'sudo apt-get install -y libasound2-dev' # For RtMidi. - - git submodule update --init --recursive - - python -m pip install --upgrade pip setuptools nox - test_script: - - nox --non-interactive --session test --python $PYTHON - - nox --non-interactive --session lint - - - - matrix: - only: - - job_name: deploy - branches: - only: - - main - install: - - git submodule update --init --recursive - - python -m pip install --upgrade pip setuptools wheel twine - deploy_script: - - '[[ -z "$APPVEYOR_PULL_REQUEST_HEAD_COMMIT" ]] || exit' - - - echo "https://${GIT_TOKEN}:x-oauth-basic@github.com" > ~/.git-credentials - - git config --global credential.helper store - - git config --global user.email "devnull@opencyphal.org" - - git config --global user.name "Release Automation" - - - 'git tag "$(cat yakut/VERSION)"' - - 'python setup.py sdist bdist_wheel' - - 'python -m twine upload dist/*' - - - git push --tags - -# https://github.com/appveyor/ci/issues/401#issuecomment-301649481 -# https://www.appveyor.com/docs/packaging-artifacts/#pushing-artifacts-from-scripts -on_finish: -- ps: $ErrorActionPreference = 'SilentlyContinue' -- ps: > - $root = Resolve-Path .; - [IO.Directory]::GetFiles($root.Path, '*.log', 'AllDirectories') | - % { Push-AppveyorArtifact $_ -FileName $_.Substring($root.Path.Length + 1) -DeploymentName to-publish } diff --git a/.github/workflows/test-and-release.yml b/.github/workflows/test-and-release.yml new file mode 100644 index 0000000..ebf768c --- /dev/null +++ b/.github/workflows/test-and-release.yml @@ -0,0 +1,102 @@ +name: 'Test and Release Yakut' +on: push + +# Ensures that only one workflow is running at a time +concurrency: + group: ${{ github.workflow_sha }} + cancel-in-progress: true + +jobs: + yakut-test: + name: Test Yakut + strategy: + fail-fast: false + matrix: + # The Windows NPcap runner is an ordinary Windows machine with the NPcap driver installed manually. + # We chose to do it this way because NPcap driver installation requires a reboot, which is difficult to + # automate. The NPcap driver is required for the Cyphal/UDP transport tests to work. + os: [ubuntu-22.04, windows-2019-npcap] + python: ['3.8', '3.9', '3.10', '3.11'] + exclude: # We don't test Windows with old Python versions because it takes too much effort. + - os: windows-2019-npcap + python: 3.8 + - os: windows-2019-npcap + python: 3.9 + runs-on: ${{ matrix.os }} + steps: + - name: Check out + uses: actions/checkout@v3 + + - name: Install Python3 + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + + - name: Log Python version + run: python --version + + - name: Install dependencies + # language=bash + run: | + if [ "$RUNNER_OS" == "Linux" ]; then + sudo apt-get --ignore-missing update || true + sudo apt-get install -y linux-*-extra-$(uname -r) ncat + sudo apt-get install -y libsdl2-2.0-0 # For PySDL2. On Windows/macOS the binaries are pulled from PyPI. + sudo apt-get install -y libasound2-dev # For RtMidi. + fi + git submodule update --init --recursive + python -m pip install --upgrade pip setuptools nox + shell: bash + + - name: Run build and test + # language=bash + run: | + nox --non-interactive --session test --python ${{ matrix.python }} + nox --non-interactive --session lint + shell: bash + env: + FORCE_COLOR: 1 + + - name: Upload diagnostics + uses: actions/upload-artifact@v3 + if: (success() || failure()) + with: + # The matrix is shown for convenience but this is fragile because the values may not be string-convertible. + # Shall it break one day, feel free to remove the matrix from here. + # The job status is per matrix item, which is super convenient. + name: ${{github.job}}-#${{strategy.job-index}}-${{job.status}}-${{join(matrix.*, ',')}} + path: "**/*.log" + retention-days: 7 + + yakut-release: + name: Release Yakut + runs-on: ubuntu-latest + if: contains(github.event.head_commit.message, '#release') || contains(github.ref, '/main') + needs: yakut-test + steps: + - name: Check out + uses: actions/checkout@v3 + + - name: Create distribution wheel + # language=bash + run: | + git submodule update --init --recursive + python -m pip install --upgrade pip setuptools wheel twine + python setup.py sdist bdist_wheel + + - name: Get release version + run: echo "yakut_version=$(cat yakut/VERSION)" >> $GITHUB_ENV + + - name: Upload distribution + run: | + python -m twine upload dist/* + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN_YAKUT }} + + - name: Push version tag + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + custom_tag: ${{ env.yakut_version }} + tag_prefix: '' diff --git a/.idea/dictionaries/pavel.xml b/.idea/dictionaries/pavel.xml index a604601..f2b612e 100644 --- a/.idea/dictionaries/pavel.xml +++ b/.idea/dictionaries/pavel.xml @@ -147,7 +147,9 @@ linkcode lnid loopback + lrzuntar lsmod + mathieudutour mcfloatface mebibytes memcpy @@ -318,6 +320,7 @@ uname undisable undoc + unexplode unhashable unicast unraisableexception diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c52b0a3..51a489b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ This document is intended for developers only. ## Testing Install dependencies into your current Python environment: `pip install .` -Aside from that, you will need to install other dependencies listed in `.appveyor.yml` +Aside from that, you will need to install other dependencies listed in the CI/CD workflow files (e.g., [Ncat](https://nmap.org/ncat/); for Debian-based distros try `apt install ncat`). Write unit tests as functions without arguments prefixed with ``_unittest_``; @@ -37,6 +37,14 @@ If you want to start from scratch, use `clean`: nox -s clean ``` +Positional arguments given to Nox are passed over to PyTest as-is, +which can be used to run tests selectively or bail at the first failure: + +```bash +nox -s test -- -x yakut/param/formatter.py -k test_format_param +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PyTest options +``` + #### Running tests/linters selectively from a virtual environment created by Nox Running the full test suite using Nox takes too much time for interactive testing during development. diff --git a/noxfile.py b/noxfile.py index 7509e2c..72fedad 100644 --- a/noxfile.py +++ b/noxfile.py @@ -15,7 +15,7 @@ assert DEPS_DIR.is_dir(), "Invalid configuration" -PYTHONS = ["3.8", "3.9", "3.10"] +PYTHONS = ["3.8", "3.9", "3.10", "3.11"] @nox.session(python=False) @@ -50,9 +50,9 @@ def test(session): # Now we can install dependencies for the full integration test suite. session.install( - "pytest ~= 7.1", - "pytest-asyncio == 0.18", - "coverage ~= 6.3", + "pytest ~= 7.4", + "pytest-asyncio ~= 0.21.0", + "coverage ~= 7.4", ) # The test suite generates a lot of temporary files, so we change the working directory. @@ -64,20 +64,26 @@ def test(session): if not (tmp_dir / fn).exists(): (tmp_dir / fn).symlink_to(ROOT_DIR / fn) - # The directories to test may be overridden if needed when invoking Nox. - src_dirs = [(ROOT_DIR / t) for t in (session.posargs or ["yakut", "tests"])] + if sys.platform.startswith("linux"): + # Enable packet capture for the Python executable. This is necessary for commands that rely on low-level + # network packet capture, such as the Monitor when used with Cyphal/UDP. + # We do it here because the sudo may ask the user for the password; doing that from the suite is inconvenient. + session.run("sudo", "setcap", "cap_net_raw+eip", str(Path(session.bin, "python").resolve()), external=True) + + src_dirs = [(ROOT_DIR / t) for t in ["yakut", "tests"]] # Run PyTest and make a code coverage report. - env = { - "PYTHONPATH": str(DEPS_DIR), - "PATH": os.pathsep.join([session.env["PATH"], str(DEPS_DIR)]), - } + # Positional arguments passed to Nox after "--" are forwarded to PyTest as-is. session.run( "pytest", *map(str, src_dirs), # Log format cannot be specified in setup.cfg https://github.com/pytest-dev/pytest/issues/3062 r"--log-file-format=%(asctime)s %(levelname)-3.3s %(name)s: %(message)s", - env=env, + *session.posargs, + env={ + "PYTHONPATH": str(DEPS_DIR), + "PATH": os.pathsep.join([session.env["PATH"], str(DEPS_DIR)]), + }, ) # The coverage threshold is intentionally set low for interactive runs because when running locally @@ -98,14 +104,14 @@ def test(session): # 1. It requires access to the code generated by the test suite. # 2. It has to be run separately per Python version we support. # If the interpreter is not CPython, this may need to be conditionally disabled. - session.install("mypy == 0.991") + session.install("mypy ~= 1.8") session.run("mypy", "--strict", *map(str, src_dirs)) @nox.session(reuse_venv=True) def lint(session): - session.install("pylint == 2.13.*") + session.install("pylint ~= 3.0.3") session.run("pylint", "yakut", "tests") - session.install("black == 22.*") + session.install("black ~= 23.12") session.run("black", "--check", ".") diff --git a/setup.cfg b/setup.cfg index 4344434..cc101f5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -93,13 +93,16 @@ testpaths = yakut tests python_files = *.py python_classes = _UnitTest python_functions = _unittest_ -log_level = DEBUG +log_level = INFO log_cli_level = WARNING log_cli = true log_file = pytest.log +log_file_level = DEBUG # Unraisable exceptions are filtered because PyTest yields false-positives coming from PyCyphal. addopts = --doctest-modules -v -p no:unraisableexception asyncio_mode = auto +filterwarnings = + ignore:.*SDL2.*:UserWarning # ---------------------------------------- MYPY ---------------------------------------- [mypy] @@ -187,7 +190,9 @@ disable= eval-used, unspecified-encoding, not-callable, - unbalanced-tuple-unpacking + unbalanced-tuple-unpacking, + no-name-in-module, + isinstance-second-argument-not-valid-type, [pylint.REPORTS] output-format=colorized diff --git a/tests/cmd/accommodate.py b/tests/cmd/accommodate.py index 3b29536..768b1d5 100644 --- a/tests/cmd/accommodate.py +++ b/tests/cmd/accommodate.py @@ -15,7 +15,7 @@ def _unittest_accommodate_swarm(transport_factory: TransportFactory, compiled_ds # We spawn a lot of processes here, which might strain the test system a little, so beware. I've tested it # with 120 processes and it made my workstation (24 GB RAM ~4 GHz Core i7) struggle to the point of being # unable to maintain sufficiently real-time operation for the test to pass. Hm. - used_node_ids = list(range(10)) + used_node_ids = list(range(5)) pubs = [ Subprocess.cli( f"--transport={transport_factory(idx).expression}", diff --git a/tests/cmd/call.py b/tests/cmd/call.py index 68728c0..06b6ab8 100644 --- a/tests/cmd/call.py +++ b/tests/cmd/call.py @@ -30,7 +30,7 @@ async def _unittest_call_custom(transport_factory: TransportFactory, compiled_ds import pycyphal.application import uavcan.node - from sirius_cyber_corp import PerformLinearLeastSquaresFit_1_0 + from sirius_cyber_corp import PerformLinearLeastSquaresFit_1 # Set up the server that we will be testing the client against. server_node = pycyphal.application.make_node( @@ -41,29 +41,29 @@ async def _unittest_call_custom(transport_factory: TransportFactory, compiled_ds server_node.registry["uavcan.srv.least_squares.id"] = pycyphal.application.register.ValueProxy( pycyphal.application.register.Natural16([222]) ) - server = server_node.get_server(PerformLinearLeastSquaresFit_1_0, "least_squares") + server = server_node.get_server(PerformLinearLeastSquaresFit_1, "least_squares") last_metadata: typing.Optional[pycyphal.presentation.ServiceRequestMetadata] = None async def handle_request( - request: PerformLinearLeastSquaresFit_1_0.Request, + request: PerformLinearLeastSquaresFit_1.Request, metadata: pycyphal.presentation.ServiceRequestMetadata, - ) -> PerformLinearLeastSquaresFit_1_0.Response: + ) -> PerformLinearLeastSquaresFit_1.Response: nonlocal last_metadata last_metadata = metadata print("REQUEST OBJECT :", request) print("REQUEST METADATA:", metadata) - sum_x = sum(map(lambda p: p.x, request.points)) # type: ignore - sum_y = sum(map(lambda p: p.y, request.points)) # type: ignore - a = sum_x * sum_y - len(request.points) * sum(map(lambda p: p.x * p.y, request.points)) # type: ignore - b = sum_x * sum_x - len(request.points) * sum(map(lambda p: p.x**2, request.points)) # type: ignore + sum_x = sum(map(lambda p: p.x, request.points)) + sum_y = sum(map(lambda p: p.y, request.points)) + a = sum_x * sum_y - len(request.points) * sum(map(lambda p: p.x * p.y, request.points)) + b = sum_x * sum_x - len(request.points) * sum(map(lambda p: p.x**2, request.points)) slope = a / b y_intercept = (sum_y - slope * sum_x) / len(request.points) - response = PerformLinearLeastSquaresFit_1_0.Response(slope=slope, y_intercept=y_intercept) + response = PerformLinearLeastSquaresFit_1.Response(slope=slope, y_intercept=y_intercept) print("RESPONSE OBJECT:", response) return response # Invoke the service without discovery and then run the server for a few seconds to let it process the request. - proc = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc = Subprocess.cli( "-j", "call", "22", @@ -92,7 +92,7 @@ async def handle_request( # Invoke the service with ID discovery and static type. last_metadata = None - proc = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc = Subprocess.cli( "-j", "call", "22", @@ -122,7 +122,7 @@ async def handle_request( # Invoke the service with full discovery. last_metadata = None - proc = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc = Subprocess.cli( "-j", "call", "22", @@ -204,7 +204,7 @@ async def _unittest_call_fixed(transport_factory: TransportFactory, compiled_dsd server_node.start() # Invoke a fixed port-ID service. - proc = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc = Subprocess.cli( "-j", "call", "22", diff --git a/tests/cmd/file_server.py b/tests/cmd/file_server.py index e202bbd..fe79703 100644 --- a/tests/cmd/file_server.py +++ b/tests/cmd/file_server.py @@ -10,15 +10,13 @@ import tempfile from pathlib import Path from typing import Tuple, Optional -import pytest import pycyphal -from pycyphal.transport.serial import SerialTransport +from pycyphal.transport.udp import UDPTransport from tests.subprocess import Subprocess from tests.dsdl import OUTPUT_DIR -@pytest.mark.asyncio -async def _unittest_file_server_pnp(compiled_dsdl: typing.Any, serial_broker: str) -> None: +async def _unittest_file_server_pnp(compiled_dsdl: typing.Any) -> None: from pycyphal.application import make_node, NodeInfo, make_registry from pycyphal.application.file import FileClient from pycyphal.application.plug_and_play import Allocatee @@ -33,7 +31,7 @@ async def _unittest_file_server_pnp(compiled_dsdl: typing.Any, serial_broker: st root, f"--plug-and-play={root}/allocation_table.db", environment_variables={ - "UAVCAN__SERIAL__IFACE": serial_broker, + "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "42", "YAKUT_PATH": str(OUTPUT_DIR), }, @@ -43,14 +41,14 @@ async def _unittest_file_server_pnp(compiled_dsdl: typing.Any, serial_broker: st make_registry( None, { - "UAVCAN__SERIAL__IFACE": serial_broker, + "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "43", "YAKUT_PATH": str(OUTPUT_DIR), }, ), ) try: - fc = FileClient(cln_node, 42, response_timeout=10.0) + fc = FileClient(cln_node, 42, response_timeout=15.0) await asyncio.sleep(3.0) # Let the server initialize. assert srv_proc.alive @@ -71,7 +69,7 @@ async def ls(path: str) -> typing.List[str]: assert ["allocation_table.db"] == await ls("/") # Check the allocator. - alloc_transport = SerialTransport(serial_broker, None) + alloc_transport = UDPTransport("127.0.0.1", None) try: alloc_client = Allocatee(alloc_transport, b"badc0ffee0ddf00d") assert alloc_client.get_result() is None @@ -90,11 +88,10 @@ async def ls(path: str) -> typing.List[str]: shutil.rmtree(root, ignore_errors=True) # Do not remove on failure for diagnostics. -@pytest.mark.asyncio -async def _unittest_file_server_update(compiled_dsdl: typing.Any, serial_broker: str) -> None: +async def _unittest_file_server_update(compiled_dsdl: typing.Any) -> None: from pycyphal.application import make_node, NodeInfo, make_registry, make_transport, Node from pycyphal.application.plug_and_play import Allocatee - from uavcan.node import ExecuteCommand_1_1 as ExecuteCommand + from uavcan.node import ExecuteCommand_1 as ExecuteCommand _ = compiled_dsdl asyncio.get_running_loop().slow_callback_duration = 10.0 @@ -143,7 +140,7 @@ async def new( if sw_crc is not None: info.software_image_crc = [sw_crc] - reg = make_registry(None, {"UAVCAN__SERIAL__IFACE": serial_broker, "YAKUT_PATH": str(OUTPUT_DIR)}) + reg = make_registry(None, {"UAVCAN__UDP__IFACE": "127.0.0.1", "YAKUT_PATH": str(OUTPUT_DIR)}) trans = make_transport(reg) if trans.local_node_id is None: print("Starting a node-ID allocator for", info.unique_id.tobytes()) @@ -172,7 +169,7 @@ def __del__(self) -> None: f"--plug-and-play={root}/allocation_table.db", f"-u", environment_variables={ - "UAVCAN__SERIAL__IFACE": serial_broker, + "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": "42", "YAKUT_PATH": str(OUTPUT_DIR), }, diff --git a/tests/cmd/joystick.py b/tests/cmd/joystick.py index 2040bb2..8e376f8 100644 --- a/tests/cmd/joystick.py +++ b/tests/cmd/joystick.py @@ -4,12 +4,10 @@ # pylint: disable=consider-using-with import asyncio -import pytest from tests.subprocess import Subprocess # noinspection SpellCheckingInspection -@pytest.mark.asyncio async def _unittest_joystick() -> None: asyncio.get_running_loop().slow_callback_duration = 10.0 @@ -17,12 +15,9 @@ async def _unittest_joystick() -> None: # We can't test it end-to-end in a virtualized environment without being able to emulate the connected hardware. # For now, we just check if it runs at all, which is not very helpful but is better than nothing. # Eventually we should find a way to emulate connected joysticks and MIDI controllers. - proc = Subprocess.cli("joy", stdout=open("stdout", "wb"), stderr=open("stderr", "wb")) + proc = Subprocess.cli("joy") assert proc.alive await asyncio.sleep(5) - assert proc.alive, open("stderr", "r").read() - proc.wait(10.0, interrupt=True) - - # The null controller shall always be available. - with open("stdout", "r") as f: - assert "null" in f.read() + assert proc.alive + _, stdout, _stderr = proc.wait(10.0, interrupt=True) + assert "null" in stdout, "The null controller shall always be available." diff --git a/tests/cmd/main.py b/tests/cmd/main.py index 90b4087..d634995 100644 --- a/tests/cmd/main.py +++ b/tests/cmd/main.py @@ -19,4 +19,4 @@ def _unittest_help() -> None: def _unittest_error() -> None: with pytest.raises(CalledProcessError): - execute_cli("invalid-command", timeout=2.0, log=False) + execute_cli("invalid-command", timeout=10.0, log=False) diff --git a/tests/cmd/monitor.py b/tests/cmd/monitor.py index 7eff2b7..fa423f0 100755 --- a/tests/cmd/monitor.py +++ b/tests/cmd/monitor.py @@ -3,13 +3,19 @@ # This software is distributed under the terms of the MIT License. # Author: Pavel Kirienko +# Disable unused ignore warning for this file only because there appears to be no other way to make MyPy +# accept this file both on Windows and GNU/Linux. +# mypy: warn_unused_ignores=False + from typing import Any, Optional, Awaitable -import os +import sys +import socket import asyncio import itertools +import logging import pytest import pycyphal -from pycyphal.transport.serial import SerialTransport +from pycyphal.transport.udp import UDPTransport from tests.subprocess import Subprocess from tests.dsdl import OUTPUT_DIR import yakut @@ -17,13 +23,16 @@ # noinspection SpellCheckingInspection @pytest.mark.asyncio -async def _unittest_monitor_nodes(compiled_dsdl: Any, serial_broker: str) -> None: +async def _unittest_monitor_nodes(compiled_dsdl: Any) -> None: _ = compiled_dsdl asyncio.get_running_loop().slow_callback_duration = 10.0 + asyncio.get_running_loop().set_exception_handler(lambda *_: None) - task = asyncio.create_task(_run_nodes(serial_broker)) - cells = [x.split() for x in (await _monitor_and_get_last_screen(serial_broker, 10.0, 42)).splitlines()] - task.cancel() + task = asyncio.create_task(_run_nodes()) + try: + cells = [x.split() for x in (await _monitor_and_get_last_screen(30.0, 42)).splitlines()] + finally: + task.cancel() await asyncio.sleep(3.0) # Own node @@ -63,10 +72,10 @@ async def _unittest_monitor_nodes(compiled_dsdl: Any, serial_broker: str) -> Non # Same but the nodes go offline plus there is an anonymous node. tasks = [ - asyncio.create_task(_delay(_run_nodes(serial_broker), 1.0, duration=5.0)), - asyncio.create_task(_delay(_run_anonymous(serial_broker), 1.0, duration=5.0)), + asyncio.create_task(_delay(_run_nodes(), 1.0, duration=5.0)), + asyncio.create_task(_delay(_run_anonymous(), 1.0, duration=5.0)), ] - cells = [x.split() for x in (await _monitor_and_get_last_screen(serial_broker, 15.0, 42)).splitlines()] + cells = [x.split() for x in (await _monitor_and_get_last_screen(30.0, 42)).splitlines()] await asyncio.gather(*tasks) await asyncio.sleep(3.0) @@ -102,18 +111,18 @@ async def _unittest_monitor_nodes(compiled_dsdl: Any, serial_broker: str) -> Non # noinspection SpellCheckingInspection @pytest.mark.asyncio -async def _unittest_monitor_errors(compiled_dsdl: Any, serial_broker: str) -> None: +async def _unittest_monitor_errors(compiled_dsdl: Any) -> None: _ = compiled_dsdl asyncio.get_running_loop().slow_callback_duration = 10.0 asyncio.get_running_loop().set_exception_handler(lambda *_: None) # This time the monitor node is anonymous. task = asyncio.gather( - _run_nodes(serial_broker), - _run_zombie(serial_broker), - _delay(_inject_error(serial_broker), 7.0), + _run_nodes(), + _run_zombie(), + _delay(_inject_error(), 7.0), ) - cells = [x.split() for x in (await _monitor_and_get_last_screen(serial_broker, 12.0, None)).splitlines()] + cells = [x.split() for x in (await _monitor_and_get_last_screen(30.0, None)).splitlines()] task.cancel() await asyncio.sleep(3.0) @@ -135,9 +144,7 @@ async def _unittest_monitor_errors(compiled_dsdl: Any, serial_broker: str) -> No await asyncio.sleep(3.0) -async def _monitor_and_get_last_screen(serial_iface: str, duration: float, node_id: Optional[int]) -> str: - stdout_file = "monitor_stdout" - stdout = open(stdout_file, "wb") # pylint: disable=consider-using-with +async def _monitor_and_get_last_screen(duration: float, node_id: Optional[int]) -> str: args = ["monitor"] if node_id is not None: args.append("--plug-and-play=allocation_table.db") @@ -145,42 +152,40 @@ async def _monitor_and_get_last_screen(serial_iface: str, duration: float, node_ *args, environment_variables={ "YAKUT_PATH": str(OUTPUT_DIR), - "UAVCAN__SERIAL__IFACE": serial_iface, + "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": str(node_id if node_id is not None else 0xFFFF), }, - stdout=stdout, ) try: await asyncio.sleep(1.0) - assert proc.alive + if not proc.alive: + exit_code, _, _ = proc.wait(1.0) + assert False, exit_code await asyncio.sleep(duration) - assert proc.alive + if not proc.alive: + exit_code, _, _ = proc.wait(1.0) + assert False, exit_code - _, _, stderr = proc.wait(10.0, interrupt=True) + _, stdout, stderr = proc.wait(10.0, interrupt=True) assert " ERR" not in stderr assert " CRI" not in stderr assert "Traceback" not in stderr - stdout.flush() - os.fsync(stdout.fileno()) - stdout.close() - with open(stdout_file, "r") as f: - screens = f.read().split("\n" * 3) - assert len(screens) > 1 - assert len(screens) < (duration * 0.5 + 10) + screens = stdout.replace("\r", "").split("\n" * 3) + assert len(screens) >= 1 + assert len(screens) < (duration * 0.5 + 10) last_screen = screens[-1] - print("=== LAST SCREEN ===") - print(last_screen) + _logger.info("=== LAST SCREEN ===\n%s", last_screen) return last_screen except Exception: # pragma: no cover proc.kill() raise -async def _run_nodes(serial_iface: str) -> None: +async def _run_nodes() -> None: from pycyphal.application import make_registry, make_node, NodeInfo, Node - from uavcan.node import Mode_1_0 as Mode, Health_1_0 as Health, Version_1_0 as Version - from uavcan.primitive import String_1_0 + from uavcan.node import Mode_1 as Mode, Health_1 as Health, Version_1 as Version + from uavcan.primitive import String_1 import uavcan.register async def subscription_sink(_msg: Any, _meta: pycyphal.transport.TransferFrom) -> None: @@ -189,7 +194,7 @@ async def subscription_sink(_msg: Any, _meta: pycyphal.transport.TransferFrom) - def instantiate(info: NodeInfo, node_id: int, mode: int, health: int, vssc: int) -> Node: reg = make_registry( environment_variables={ - "UAVCAN__SERIAL__IFACE": serial_iface, + "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__NODE__ID": str(node_id), "UAVCAN__PUB__SPAM__ID": "2222", "UAVCAN__SUB__SPAM__ID": "2222", @@ -252,81 +257,91 @@ def instantiate(info: NodeInfo, node_id: int, mode: int, health: int, vssc: int) vssc=4, ), ] - pub = nodes[0].make_publisher(String_1_0, "spam") - nodes[1].make_subscriber(String_1_0, "spam").receive_in_background(subscription_sink) - nodes[2].make_subscriber(String_1_0, "spam").receive_in_background(subscription_sink) - nodes[3].make_subscriber(String_1_0, "null").receive_in_background(subscription_sink) # No publishers. - reg_client_a = nodes[1].make_client(uavcan.register.List_1_0, 1111) - reg_client_b = nodes[1].make_client(uavcan.register.List_1_0, 3210) - print("NODES STARTED") + pub = nodes[0].make_publisher(String_1, "spam") + pub.send_timeout = 5.0 + nodes[1].make_subscriber(String_1, "spam").receive_in_background(subscription_sink) + nodes[2].make_subscriber(String_1, "spam").receive_in_background(subscription_sink) + nodes[3].make_subscriber(String_1, "null").receive_in_background(subscription_sink) # No publishers. + reg_client_a = nodes[1].make_client(uavcan.register.List_1, 1111) + reg_client_b = nodes[1].make_client(uavcan.register.List_1, 3210) + _logger.info("NODES STARTED") try: for i in itertools.count(): - assert await pub.publish(String_1_0(f"Hello world! This is message number #{i+1}.")) + assert await pub.publish(String_1(f"Hello world! This is message number #{i+1}.")) if (i % 2000) > 1000: if i % 2 == 0: - await reg_client_a.call(uavcan.register.List_1_0.Request(i % 11)) + await reg_client_a.call(uavcan.register.List_1.Request(i % 11)) if i % 5 == 0: - await reg_client_b.call(uavcan.register.List_1_0.Request(i % 11)) - await asyncio.sleep(0.01) - except (asyncio.TimeoutError, asyncio.CancelledError): # pragma: no cover + await reg_client_b.call(uavcan.register.List_1.Request(i % 11)) + await asyncio.sleep(0.2) + except (asyncio.TimeoutError, asyncio.CancelledError, GeneratorExit): # pragma: no cover pass finally: - print("STOPPING THE NODES...") + _logger.info("STOPPING THE NODES...") for n in nodes: n.close() - print("NODES STOPPED") + _logger.info("NODES STOPPED") -async def _run_zombie(serial_iface: str) -> None: - from uavcan.primitive import Empty_1_0 +async def _run_zombie() -> None: + from uavcan.primitive import Empty_1 - tr = SerialTransport(serial_iface, 2571) + tr = UDPTransport("127.0.0.1", 2571) pres = pycyphal.presentation.Presentation(tr) try: - pub = pres.make_publisher(Empty_1_0, 99) + pub = pres.make_publisher(Empty_1, 99) + pub.send_timeout = 5.0 + sub = pres.make_subscriber(Empty_1, 99) # Ensure there's an RX socket on Windows. while True: - await pub.publish(Empty_1_0()) + assert await pub.publish(Empty_1()) await asyncio.sleep(0.5) - except (asyncio.TimeoutError, asyncio.CancelledError): # pragma: no cover + _ = await sub.receive_for(0.0) # Avoid queue overrun. + except (asyncio.TimeoutError, asyncio.CancelledError, GeneratorExit): # pragma: no cover pass finally: pres.close() -async def _run_anonymous(serial_iface: str) -> None: +async def _run_anonymous() -> None: from pycyphal.application import make_registry, make_node, NodeInfo - from uavcan.primitive import String_1_0 + from uavcan.primitive import String_1 reg = make_registry( environment_variables={ - "UAVCAN__SERIAL__IFACE": serial_iface, + "UAVCAN__UDP__IFACE": "127.0.0.1", "UAVCAN__PUB__SPAM__ID": "2222", } ) node = make_node(NodeInfo(), reg) try: node.start() - pub = node.make_publisher(String_1_0, "spam") + pub = node.make_publisher(String_1, "spam") while True: await asyncio.sleep(1.0) - await pub.publish(String_1_0("I am here incognito.")) + await pub.publish(String_1("I am here incognito.")) except (asyncio.TimeoutError, asyncio.CancelledError): # pragma: no cover pass finally: node.close() -async def _inject_error(serial_iface: str) -> None: - from serial import serial_for_url # type: ignore - - p = serial_for_url(serial_iface) - p.write(b"\x00 this is not a valid frame \x00") - p.close() +async def _inject_error() -> None: + # To test, open Yakut monitor as shown below and run this script; the error count will increase: + # UAVCAN__UDP__IFACE="127.0.0.1" y mon + bad_heartbeat = bytes.fromhex( + "01046400ffff551d09000000000000000000008000001d7e00000000000032" + "00000000" # Correct CRC: 2b53e66a + ) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + if sys.platform.lower().startswith("linux"): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # type: ignore + sock.bind(("127.0.0.1", 0)) + sock.sendto(bad_heartbeat, ("239.0.29.85", 9382)) async def _delay(target: Awaitable[None], delay: float, duration: Optional[float] = None) -> None: await asyncio.sleep(delay) - print("LAUNCHING", target) + _logger.info("LAUNCHING %s", target) try: if duration is None: await target @@ -334,13 +349,13 @@ async def _delay(target: Awaitable[None], delay: float, duration: Optional[float await asyncio.wait_for(target, duration) except (asyncio.TimeoutError, asyncio.CancelledError): # pragma: no cover pass - print("FINISHED", target) + _logger.info("FINISHED %s", target) async def _main() -> None: # pragma: no cover """ This is intended to aid in manual testing of the UI. - Run this file having launched the serial broker beforehand and visually validate the behavior of the tool. + Run this file and visually validate the behavior of the tool. This is how you can record the screen if you want a gif or something: @@ -349,14 +364,15 @@ async def _main() -> None: # pragma: no cover Docs on ffmpeg: https://trac.ffmpeg.org/wiki/Capture/Desktop """ - serial_iface = "socket://127.0.0.1:50905" await asyncio.gather( - _delay(_run_nodes(serial_iface), 0.0, 30.0), - _delay(_run_zombie(serial_iface), 6.0, 10.0), - _delay(_run_anonymous(serial_iface), 3.0, 10.0), - _delay(_inject_error(serial_iface), 3.0), + _delay(_run_nodes(), 0.0, 30.0), + _delay(_run_zombie(), 6.0, 10.0), + _delay(_run_anonymous(), 3.0, 10.0), + _delay(_inject_error(), 3.0), ) +_logger = logging.getLogger(__name__) + if __name__ == "__main__": # pragma: no cover asyncio.run(_main()) diff --git a/tests/cmd/publish/expression.py b/tests/cmd/publish/expression.py index 0b72097..9c71450 100644 --- a/tests/cmd/publish/expression.py +++ b/tests/cmd/publish/expression.py @@ -11,20 +11,20 @@ from tests.subprocess import execute_cli, Subprocess -def _unittest_publish_expression_a(compiled_dsdl: typing.Any, serial_broker: str) -> None: +def _unittest_publish_expression_a(compiled_dsdl: typing.Any) -> None: _ = compiled_dsdl - env = { - "YAKUT_PATH": str(OUTPUT_DIR), - "UAVCAN__SERIAL__IFACE": serial_broker, - "UAVCAN__NODE__ID": "1234", - } proc_sub = Subprocess.cli( "-j", "sub", "7654:uavcan.primitive.array.Real64.1.0", - environment_variables=env, + environment_variables={ + "YAKUT_PATH": str(OUTPUT_DIR), + "UAVCAN__UDP__IFACE": "127.0.0.1", + "UAVCAN__NODE__ID": "1234", + }, ) + time.sleep(3.0) # Let the subscriber start. wall_time_when_started = time.time() execute_cli( @@ -35,7 +35,11 @@ def _unittest_publish_expression_a(compiled_dsdl: typing.Any, serial_broker: str # {1.0, 2.1} 1.0 0 0 123456 123456 (time) "--count=2", timeout=10.0, - environment_variables=env, + environment_variables={ + "YAKUT_PATH": str(OUTPUT_DIR), + "UAVCAN__UDP__IFACE": "127.0.0.1", + "UAVCAN__NODE__ID": "1235", + }, ) _, stdout, _ = proc_sub.wait(10.0, interrupt=True) @@ -62,20 +66,19 @@ def _unittest_publish_expression_a(compiled_dsdl: typing.Any, serial_broker: str ] -def _unittest_publish_expression_b(compiled_dsdl: typing.Any, serial_broker: str) -> None: +def _unittest_publish_expression_b(compiled_dsdl: typing.Any) -> None: _ = compiled_dsdl - env = { - "YAKUT_PATH": str(OUTPUT_DIR), - "UAVCAN__SERIAL__IFACE": serial_broker, - "UAVCAN__NODE__ID": "1234", - } - proc_sub = Subprocess.cli( "-j", "sub", "7654:uavcan.primitive.String.1.0", - environment_variables=env, + environment_variables={ + "YAKUT_PATH": str(OUTPUT_DIR), + "UAVCAN__UDP__IFACE": "127.0.0.1", + "UAVCAN__NODE__ID": "1234", + }, ) + time.sleep(3.0) # Let the subscriber start. execute_cli( "pub", @@ -83,7 +86,11 @@ def _unittest_publish_expression_b(compiled_dsdl: typing.Any, serial_broker: str "value: !$ str(pycyphal.dsdl.get_model(dtype))", "--count=1", timeout=10.0, - environment_variables=env, + environment_variables={ + "YAKUT_PATH": str(OUTPUT_DIR), + "UAVCAN__UDP__IFACE": "127.0.0.1", + "UAVCAN__NODE__ID": "1235", + }, ) _, stdout, _ = proc_sub.wait(10.0, interrupt=True) diff --git a/tests/cmd/pubsub.py b/tests/cmd/pubsub.py index e2f2bce..769e0aa 100644 --- a/tests/cmd/pubsub.py +++ b/tests/cmd/pubsub.py @@ -3,6 +3,8 @@ # Author: Pavel Kirienko from __future__ import annotations + +import sys import time import json import typing @@ -53,7 +55,7 @@ def _unittest_pub_sub_regular(transport_factory: TransportFactory, compiled_dsdl ) time.sleep(1.0) # Time to let the background processes finish initialization - proc_pub = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_pub = Subprocess.cli( "--heartbeat-vssc=54", "--heartbeat-priority=high", "--node-info", @@ -149,21 +151,21 @@ def _unittest_slow_cli_pub_sub_anon(transport_factory: TransportFactory, compile "YAKUT_TRANSPORT": transport_factory(None).expression, "YAKUT_PATH": str(OUTPUT_DIR), } - proc_sub_heartbeat = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_sub_heartbeat = Subprocess.cli( "-j", "sub", "uavcan.node.heartbeat", "--with-metadata", environment_variables=env, ) - proc_sub_diagnostic_with_meta = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_sub_diagnostic_with_meta = Subprocess.cli( "-j", "sub", "uavcan.diagnostic.record", "--with-metadata", environment_variables=env, ) - proc_sub_diagnostic_no_meta = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_sub_diagnostic_no_meta = Subprocess.cli( "-j", "sub", "uavcan.diagnostic.record", @@ -173,53 +175,38 @@ def _unittest_slow_cli_pub_sub_anon(transport_factory: TransportFactory, compile time.sleep(3.0) # Time to let the background processes finish initialization - if transport_factory(None).can_transmit: - proc = Subprocess.cli( - "pub", - "uavcan.diagnostic.record", - "{}", - "--count=2", - "--period=2", - environment_variables=env, - ) - proc.wait(timeout=8) + proc = Subprocess.cli( + "pub", + "uavcan.diagnostic.record", + "{}", + "--count=2", + "--period=2", + environment_variables=env, + ) + proc.wait(timeout=30 if sys.platform.startswith("win") else 8) # On Windows Python takes a long time to start. - time.sleep(2.0) # Time to sync up + time.sleep(2.0) # Time to sync up - assert ( - proc_sub_heartbeat.wait(1.0, interrupt=True)[1].strip() == "" - ), "Anonymous nodes must not broadcast heartbeat" + assert proc_sub_heartbeat.wait(1.0, interrupt=True)[1].strip() == "", "Anonymous nodes must not broadcast heartbeat" - diagnostics = list( - json.loads(s) for s in proc_sub_diagnostic_with_meta.wait(1.0, interrupt=True)[1].splitlines() - ) - print("diagnostics:", diagnostics) - # Remember that anonymous transfers over redundant transports are NOT deduplicated. - # Hence, to support the case of redundant transports, we use 'greater or equal' here. - assert len(diagnostics) >= 2 - for m in diagnostics: - assert "nominal" in m["8184"]["_meta_"]["priority"].lower() - assert m["8184"]["_meta_"]["transfer_id"] >= 0 - assert m["8184"]["_meta_"]["source_node_id"] is None - assert m["8184"]["timestamp"]["microsecond"] == 0 - assert m["8184"]["text"] == "" + diagnostics = list(json.loads(s) for s in proc_sub_diagnostic_with_meta.wait(1.0, interrupt=True)[1].splitlines()) + print("diagnostics:", diagnostics) + # Remember that anonymous transfers over redundant transports are NOT deduplicated. + # Hence, to support the case of redundant transports, we use 'greater or equal' here. + assert len(diagnostics) >= 2 + for m in diagnostics: + assert "nominal" in m["8184"]["_meta_"]["priority"].lower() + assert m["8184"]["_meta_"]["transfer_id"] >= 0 + assert m["8184"]["_meta_"]["source_node_id"] is None + assert m["8184"]["timestamp"]["microsecond"] == 0 + assert m["8184"]["text"] == "" - diagnostics = list(json.loads(s) for s in proc_sub_diagnostic_no_meta.wait(1.0, interrupt=True)[1].splitlines()) - print("diagnostics:", diagnostics) - assert len(diagnostics) >= 2 # >= because see above - for m in diagnostics: - assert m["8184"]["timestamp"]["microsecond"] == 0 - assert m["8184"]["text"] == "" - else: - proc = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. - "pub", - "uavcan.diagnostic.Record", - "{}", - "--count=2", - "--period=2", - environment_variables=env, - ) - assert 0 < proc.wait(timeout=8, log=False)[0] + diagnostics = list(json.loads(s) for s in proc_sub_diagnostic_no_meta.wait(1.0, interrupt=True)[1].splitlines()) + print("diagnostics:", diagnostics) + assert len(diagnostics) >= 2 # >= because see above + for m in diagnostics: + assert m["8184"]["timestamp"]["microsecond"] == 0 + assert m["8184"]["text"] == "" def _unittest_e2e_discovery_pub(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: @@ -238,7 +225,7 @@ def _unittest_e2e_discovery_pub(transport_factory: TransportFactory, compiled_ds }, ) time.sleep(3.0) # Let the subscriber boot up. - proc_pub = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_pub = Subprocess.cli( "pub", "1000", # Use discovery. "hello", @@ -260,7 +247,7 @@ def _unittest_e2e_discovery_pub(transport_factory: TransportFactory, compiled_ds def _unittest_e2e_discovery_sub(transport_factory: TransportFactory, compiled_dsdl: typing.Any) -> None: _ = compiled_dsdl - proc_pub = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_pub = Subprocess.cli( "pub", "1000:uavcan.primitive.string", "hello", diff --git a/tests/cmd/pubsub_sync.py b/tests/cmd/pubsub_sync.py index 9af019c..ca11e4f 100644 --- a/tests/cmd/pubsub_sync.py +++ b/tests/cmd/pubsub_sync.py @@ -27,7 +27,7 @@ def _unittest_monoclust_ts_field_auto(transport_factory: TransportFactory, compi }, ) time.sleep(3.0) - proc_pub = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_pub = Subprocess.cli( "pub", "1000:uavcan.si.sample.mass.Scalar", "!$ n * 1e6", @@ -72,7 +72,7 @@ def _unittest_monoclust_ts_field_manual(transport_factory: TransportFactory, com }, ) time.sleep(3.0) - proc_pub = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_pub = Subprocess.cli( "pub", "1000:uavcan.si.sample.mass.Scalar", "!$ n * 1.00 * 1e6", @@ -142,7 +142,7 @@ def _unittest_monoclust_ts_arrival_auto(transport_factory: TransportFactory, com }, ) time.sleep(3.0) - proc_pub = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_pub = Subprocess.cli( "pub", "1000:uavcan.primitive.String", "!$ str(n)", @@ -179,7 +179,7 @@ def _unittest_transfer_id(transport_factory: TransportFactory, compiled_dsdl: ty }, ) time.sleep(3.0) - proc_pub = Subprocess.cli( # Windows compat: -v blocks stderr pipe on Windows. + proc_pub = Subprocess.cli( "pub", "1000:uavcan.primitive.String", "!$ str(n)", diff --git a/tests/cmd/register_access.py b/tests/cmd/register_access.py index 7efbd9c..699e770 100644 --- a/tests/cmd/register_access.py +++ b/tests/cmd/register_access.py @@ -155,6 +155,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "-j", "register-access", + "--timeout=10", "10,", expect_register, environment_variables={ @@ -172,6 +173,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "-j", "register-access", + "--timeout=10", "10", expect_register, environment_variables={ @@ -188,6 +190,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "-j", "register-access", + "--timeout=10", "10,", expect_register, "Reference value", diff --git a/tests/cmd/register_batch.py b/tests/cmd/register_batch.py index a265cb6..4ff50a9 100644 --- a/tests/cmd/register_batch.py +++ b/tests/cmd/register_batch.py @@ -91,6 +91,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "register-batch", f"--file={file}", + "--timeout=10", environment_variables={ **transport_factory(100).environment, "YAKUT_PATH": str(OUTPUT_DIR), @@ -109,6 +110,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "register-batch", f"--file={file}", + "--timeout=10", environment_variables={ **transport_factory(100).environment, "YAKUT_PATH": str(OUTPUT_DIR), @@ -126,6 +128,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "register-batch", f"--file={file}", + "--timeout=10", "10", environment_variables={ **transport_factory(100).environment, @@ -144,6 +147,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "register-batch", f"--file={file}", + "--timeout=10", "10,11", environment_variables={ **transport_factory(100).environment, @@ -162,6 +166,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "register-batch", f"--file={file}", + "--timeout=3", # Shorter timeout here because we know one is going to time out. "10-13", environment_variables={ **transport_factory(100).environment, @@ -182,6 +187,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "register-batch", f"--file={file}", + "--timeout=10", "10,11", environment_variables={ **transport_factory(100).environment, @@ -201,6 +207,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "register-batch", f"--file={file}", + "--timeout=10", "10,11", "--optional-register", environment_variables={ @@ -220,6 +227,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "register-batch", f"--file={file}", + "--timeout=10", "10", "--detailed", environment_variables={ @@ -238,6 +246,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "register-batch", f"--file={file}", + "--timeout=10", "10", "--only=iv", # The requested register is not immutable-volatile so it will be skipped. environment_variables={ diff --git a/tests/cmd/register_list.py b/tests/cmd/register_list.py index 7e00ca5..625314f 100644 --- a/tests/cmd/register_list.py +++ b/tests/cmd/register_list.py @@ -93,6 +93,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "-j", "register-list", + "--timeout=10", "10", environment_variables={ "YAKUT_TRANSPORT": transport_factory(100).expression, @@ -109,6 +110,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "-j", "register-list", + "--timeout=10", "10,", # Mind the comma! environment_variables={ "YAKUT_TRANSPORT": transport_factory(100).expression, @@ -125,6 +127,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "-j", "register-list", + "--timeout=3", # Shorter timeout because some nodes are expected to not respond. "10..13", environment_variables={ "YAKUT_TRANSPORT": transport_factory(100).expression, @@ -144,6 +147,7 @@ def _unittest_cmd(compiled_dsdl: Any, transport_factory: TransportFactory) -> No status, stdout, _ = execute_cli( "-j", "register-list", + "--timeout=3", # Shorter timeout because some nodes are expected to not respond. "10..13", "--optional-service", environment_variables={ diff --git a/tests/conftest.py b/tests/conftest.py index 0268c09..a8acf42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ from pathlib import Path import pytest from .dsdl import compiled_dsdl as compiled_dsdl -from .transport import transport_factory as transport_factory, serial_broker as serial_broker +from .transport import transport_factory as transport_factory @pytest.fixture() @@ -22,7 +22,7 @@ def stdout_file() -> Iterator[Path]: """ s = sys.stdout p = Path(tempfile.mktemp("stdout")).resolve() - sys.stdout = p.open("a+") + sys.stdout = p.open("a+") # pylint: disable=consider-using-with yield p sys.stdout = s @@ -34,6 +34,6 @@ def stderr_file() -> Iterator[Path]: """ s = sys.stderr p = Path(tempfile.mktemp("stderr")).resolve() - sys.stderr = p.open("a+") + sys.stderr = p.open("a+") # pylint: disable=consider-using-with yield p sys.stderr = s diff --git a/tests/custom_data_types/sirius_cyber_corp/PerformLinearLeastSquaresFit.1.0.uavcan b/tests/custom_data_types/sirius_cyber_corp/PerformLinearLeastSquaresFit.1.0.dsdl similarity index 100% rename from tests/custom_data_types/sirius_cyber_corp/PerformLinearLeastSquaresFit.1.0.uavcan rename to tests/custom_data_types/sirius_cyber_corp/PerformLinearLeastSquaresFit.1.0.dsdl diff --git a/tests/custom_data_types/sirius_cyber_corp/PointXY.1.0.uavcan b/tests/custom_data_types/sirius_cyber_corp/PointXY.1.0.dsdl similarity index 100% rename from tests/custom_data_types/sirius_cyber_corp/PointXY.1.0.uavcan rename to tests/custom_data_types/sirius_cyber_corp/PointXY.1.0.dsdl diff --git a/tests/deps/npcap-0.96.exe b/tests/deps/npcap-0.96.exe deleted file mode 100644 index 14541ed..0000000 Binary files a/tests/deps/npcap-0.96.exe and /dev/null differ diff --git a/tests/subject_specifier_processor.py b/tests/subject_specifier_processor.py index d064fb9..74ae35b 100644 --- a/tests/subject_specifier_processor.py +++ b/tests/subject_specifier_processor.py @@ -78,7 +78,7 @@ def advertise(kind: str, name: str, dtype_name: str, port_id: int) -> None: async def once(specifier: str) -> tuple[int, Any]: return await process_subject_specifier(specifier, lambda: subject_resolver) - assert (500, uavcan.primitive.scalar.Bit_1_0) == await once("500") + assert (500, uavcan.primitive.scalar.Bit_1) == await once("500") assert (600, uavcan.primitive.scalar.Integer8_1) == await once("600") # minor version ignored with pytest.raises(NetworkDiscoveryError): diff --git a/tests/subprocess.py b/tests/subprocess.py index e39a63a..d2a21ca 100644 --- a/tests/subprocess.py +++ b/tests/subprocess.py @@ -8,9 +8,10 @@ import shutil import typing import logging +from tempfile import NamedTemporaryFile import subprocess from pathlib import Path -from subprocess import CalledProcessError as CalledProcessError # pylint: disable=unused-import +from subprocess import CalledProcessError as CalledProcessError _logger = logging.getLogger(__name__) @@ -45,21 +46,26 @@ def execute( _logger.debug("Environment: %s", env) if environment_variables: env.update(environment_variables) - # Can't use shell=True with timeout; see https://stackoverflow.com/questions/36952245/subprocess-timeout-failure - out = subprocess.run( # pylint: disable=subprocess-run-check - cmd, - timeout=timeout, - encoding="utf8", - env=env, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - stdout, stderr = out.stdout, out.stderr + # Can't use PIPE because it is too small on Windows, causing the process to block on stdout/stderr writes. + # Instead we redirect stdout/stderr to temporary files whose size is unlimited, and read them later. + with NamedTemporaryFile(suffix=".out", buffering=0) as stdout_file: + with NamedTemporaryFile(suffix=".err", buffering=0) as stderr_file: + # Can't use shell=True with timeout; see https://stackoverflow.com/questions/36952245 + out = subprocess.run( # pylint: disable=subprocess-run-check + cmd, + timeout=timeout, + encoding="utf8", + env=env, + stdout=stdout_file, + stderr=stderr_file, + ) + stdout = _read_stream(stdout_file) + stderr = _read_stream(stderr_file) if log: _logger.debug("%s stdout:\n%s", cmd, stdout) _logger.debug("%s stderr:\n%s", cmd, stderr) if out.returncode != 0 and ensure_success: - raise subprocess.CalledProcessError(out.returncode, cmd, stdout, stderr) + raise CalledProcessError(out.returncode, cmd, stdout, stderr) assert isinstance(stdout, str) and isinstance(stderr, str) return out.returncode, stdout, stderr @@ -112,15 +118,9 @@ class Subprocess: False """ - def __init__( - self, - *args: str, - environment_variables: typing.Optional[typing.Dict[str, str]] = None, - stdout: typing.Optional[typing.BinaryIO] = None, - stderr: typing.Optional[typing.BinaryIO] = None, - ): + def __init__(self, *args: str, environment_variables: typing.Optional[typing.Dict[str, str]] = None): cmd = _make_process_args(*args) - _logger.info("Starting subprocess: %s", cmd) + _logger.debug("Starting subprocess: %s", cmd) if sys.platform.startswith("win"): # pragma: no cover # If the current process group is used, CTRL_C_EVENT will kill the parent and everyone in the group! @@ -129,52 +129,45 @@ def __init__( creationflags = 0 env = _get_env(environment_variables) - _logger.debug("Environment: %s", env) - + # Can't use PIPE because it is too small on Windows, causing the process to block on stdout/stderr writes. + # Instead we redirect stdout/stderr to temporary files whose size is unlimited, and read them later. + self._stdout = NamedTemporaryFile(suffix=".out", buffering=0) # pylint: disable=consider-using-with + self._stderr = NamedTemporaryFile(suffix=".err", buffering=0) # pylint: disable=consider-using-with # Buffering must be DISABLED, otherwise we can't read data on Windows after the process is interrupted. # For some reason stdout is not flushed at exit there. self._inferior = subprocess.Popen( # pylint: disable=consider-using-with cmd, - stdout=stdout or subprocess.PIPE, - stderr=stderr or subprocess.PIPE, + stdout=self._stdout, + stderr=self._stderr, encoding="utf8", env=env, creationflags=creationflags, bufsize=0, ) + _logger.info("PID %d started: %s\n%s", self.pid, cmd, env) @staticmethod - def cli( - *args: str, - environment_variables: typing.Optional[typing.Dict[str, str]] = None, - stdout: typing.Optional[typing.BinaryIO] = None, - stderr: typing.Optional[typing.BinaryIO] = None, - ) -> Subprocess: + def cli(*args: str, environment_variables: typing.Optional[typing.Dict[str, str]] = None) -> Subprocess: """ A convenience factory for running the CLI tool. """ - return Subprocess( - "python", - "-m", - "yakut", - *args, - environment_variables=environment_variables, - stdout=stdout, - stderr=stderr, - ) + return Subprocess("python", "-m", "yakut", *args, environment_variables=environment_variables) def wait( self, timeout: float, interrupt: typing.Optional[bool] = False, log: bool = True ) -> typing.Tuple[int, str, str]: if interrupt and self._inferior.poll() is None: self.interrupt() - - stdout, stderr = self._inferior.communicate(timeout=timeout) + # stdout/stderr values returned by communicate() are not usable here because we don't use PIPE. + # Frankly I think the subprocess module API is not very well designed. + self._inferior.communicate(timeout=timeout) + stdout = _read_stream(self._stdout) + stderr = _read_stream(self._stderr) + exit_code = int(self._inferior.returncode) if log: + _logger.debug("PID %d exit code: %d", self.pid, exit_code) _logger.debug("PID %d stdout:\n%s", self.pid, stdout) _logger.debug("PID %d stderr:\n%s", self.pid, stderr) - - exit_code = int(self._inferior.returncode) return exit_code, stdout, stderr def kill(self) -> None: @@ -198,8 +191,21 @@ def alive(self) -> bool: return self._inferior.poll() is None def __del__(self) -> None: - if self._inferior.poll() is None: - self._inferior.kill() + try: + inf = self._inferior + except AttributeError: + pass # Ignore semi-constructed objects. + else: + if inf.poll() is None: + inf.kill() + + +def _read_stream(io: typing.Any) -> str: + io.flush() + io.seek(0) + out = io.read().decode("utf8") + assert isinstance(out, str) + return out _ENV_COPY_KEYS = { diff --git a/tests/transport.py b/tests/transport.py index e722b47..f61b617 100644 --- a/tests/transport.py +++ b/tests/transport.py @@ -20,11 +20,6 @@ class TransportConfig: """ expression: str - """ - Please do not use this in new tests, - consider using the environment variables instead as they are the recommended form now. - """ - can_transmit: bool environment: dict[str, str] @@ -69,7 +64,6 @@ def sudo(cmd: str, ensure_success: bool = True) -> None: def vcan() -> typing.Iterator[TransportFactory]: yield lambda nid: TransportConfig( expression=f"CAN(can.media.socketcan.SocketCANMedia('vcan0',64),local_node_id={nid})", - can_transmit=True, environment=mk_env( nid, UAVCAN__CAN__IFACE="socketcan:vcan0", @@ -86,7 +80,6 @@ def vcan_tmr() -> typing.Iterator[TransportFactory]: for idx, mtu in enumerate([8, 32, 64]) ) ), - can_transmit=True, environment=mk_env( nid, UAVCAN__CAN__IFACE="socketcan:vcan0 socketcan:vcan1 socketcan:vcan2", @@ -114,7 +107,6 @@ def serial_tunneled_via_tcp() -> typing.Iterator[TransportFactory]: assert broker.alive yield lambda nid: TransportConfig( expression=f"Serial('{serial_endpoint}',local_node_id={nid})", - can_transmit=True, environment=mk_env( nid, UAVCAN__SERIAL__IFACE=serial_endpoint, @@ -128,17 +120,7 @@ def serial_tunneled_via_tcp() -> typing.Iterator[TransportFactory]: def udp_loopback() -> typing.Iterator[TransportFactory]: yield lambda nid: ( TransportConfig( - expression=f"UDP('127.0.0.0',{nid})", - can_transmit=True, - environment=mk_env( - nid, - UAVCAN__UDP__IFACE="127.0.0.0", - ), - ) - if nid is not None - else TransportConfig( - expression="UDP('127.0.0.1',None)", - can_transmit=False, + expression=f"UDP('127.0.0.1',{nid})", environment=mk_env( nid, UAVCAN__UDP__IFACE="127.0.0.1", @@ -158,7 +140,6 @@ def heterogeneous_udp_serial() -> typing.Iterator[TransportFactory]: ] ) ), - can_transmit=nid is not None, environment=mk_env( nid, UAVCAN__SERIAL__IFACE=serial_endpoint, @@ -178,17 +159,3 @@ def heterogeneous_udp_serial() -> typing.Iterator[TransportFactory]: @pytest.fixture(params=_generate()) def transport_factory(request: typing.Any) -> typing.Iterable[TransportFactory]: yield from request.param() - - -@pytest.fixture() -def serial_broker() -> typing.Iterable[str]: - """ - Ensures that the serial broker is available for the test. - The value is the endpoint where the broker is reachable; e.g., ``socket://127.0.0.1:50905``. - """ - proc = Subprocess("ncat", "--broker", "--listen", "--verbose", f"--source-port={SERIAL_BROKER_PORT}") - # The sleep is needed to let the broker initialize before starting the tests to avoid connection error. - # This is only relevant for Windows. See details: https://github.com/OpenCyphal/yakut/issues/26 - time.sleep(2) - yield f"socket://127.0.0.1:{SERIAL_BROKER_PORT}" - proc.wait(5.0, interrupt=True) diff --git a/yakut/__init__.py b/yakut/__init__.py index e85c83a..1e6696c 100644 --- a/yakut/__init__.py +++ b/yakut/__init__.py @@ -2,10 +2,25 @@ # This software is distributed under the terms of the MIT License. # Author: Pavel Kirienko +# Disabling unused ignores because we need to support different versions of importlib.resources. +# mypy: warn_unused_ignores=False +# pylint: disable=wrong-import-position + import typing -from importlib.resources import read_text as _read_text -__version__: str = _read_text(__name__, "VERSION", encoding="utf8").strip() + +def _read_package_file(name: str) -> str: + try: + from importlib.resources import files # type: ignore + + return (files(__name__) / name).read_text(encoding="utf8") # type: ignore + except ImportError: # This is for the old Pythons; read_text is deprecated in 3.11 + from importlib.resources import read_text # type: ignore + + return read_text(__name__, name, encoding="utf8") # type: ignore + + +__version__: str = _read_package_file("VERSION").strip() __version_info__: typing.Tuple[int, ...] = tuple(map(int, __version__.split(".")[:3])) __author__ = "OpenCyphal" __email__ = "consortium@opencyphal.org" diff --git a/yakut/__main__.py b/yakut/__main__.py index 8edf42f..51904a1 100644 --- a/yakut/__main__.py +++ b/yakut/__main__.py @@ -3,6 +3,10 @@ # Author: Pavel Kirienko if __name__ == "__main__": + from warnings import filterwarnings + + filterwarnings("ignore") # Warnings are meant for developers and should not be shown to users. + from yakut import main main() # pylint: disable=no-value-for-parameter diff --git a/yakut/cmd/accommodate.py b/yakut/cmd/accommodate.py index 6b93a91..1d8f4d8 100644 --- a/yakut/cmd/accommodate.py +++ b/yakut/cmd/accommodate.py @@ -67,8 +67,8 @@ async def accommodate(purser: yakut.Purser) -> None: pres = pycyphal.presentation.Presentation(transport) with contextlib.closing(pres): - deadline = asyncio.get_event_loop().time() + uavcan.node.Heartbeat_1_0.MAX_PUBLICATION_PERIOD * 2.0 - sub = pres.make_subscriber_with_fixed_subject_id(uavcan.node.Heartbeat_1_0) + deadline = asyncio.get_event_loop().time() + uavcan.node.Heartbeat_1.MAX_PUBLICATION_PERIOD * 2.0 + sub = pres.make_subscriber_with_fixed_subject_id(uavcan.node.Heartbeat_1) while asyncio.get_event_loop().time() <= deadline: result = await sub.receive(deadline) if result is None: @@ -92,8 +92,8 @@ async def accommodate(purser: yakut.Purser) -> None: else: # If at least one node is in the Initialization state, the network might be starting, # so we need to listen longer to minimize the chance of collision. - multiplier = 3.0 if msg.mode.value == uavcan.node.Mode_1_0.INITIALIZATION else 1.0 - advancement = uavcan.node.Heartbeat_1_0.MAX_PUBLICATION_PERIOD * multiplier + multiplier = 3.0 if msg.mode.value == uavcan.node.Mode_1.INITIALIZATION else 1.0 + advancement = uavcan.node.Heartbeat_1.MAX_PUBLICATION_PERIOD * multiplier _logger.debug( "Deadline advanced by %.1f s; %d candidates left of %d possible", advancement, diff --git a/yakut/cmd/file_server/_app_descriptor.py b/yakut/cmd/file_server/_app_descriptor.py index 0359219..53b4766 100644 --- a/yakut/cmd/file_server/_app_descriptor.py +++ b/yakut/cmd/file_server/_app_descriptor.py @@ -188,7 +188,7 @@ def _unittest_app_descriptor_from_node_info() -> None: ensure_compiled_dsdl() from pycyphal.application import NodeInfo - from uavcan.node import Version_1_0 as Version + from uavcan.node import Version_1 as Version ad = AppDescriptor.from_node_info( NodeInfo( diff --git a/yakut/cmd/file_server/_cmd.py b/yakut/cmd/file_server/_cmd.py index 855dcfa..6f2f33c 100644 --- a/yakut/cmd/file_server/_cmd.py +++ b/yakut/cmd/file_server/_cmd.py @@ -155,8 +155,8 @@ async def file_server( from pycyphal.application import NodeInfo from pycyphal.application.file import FileServer from pycyphal.application.node_tracker import NodeTracker, Entry - from uavcan.node import ExecuteCommand_1_1 as ExecuteCommand - from uavcan.node import Heartbeat_1_0 as Heartbeat + from uavcan.node import ExecuteCommand_1 as ExecuteCommand + from uavcan.node import Heartbeat_1 as Heartbeat except ImportError as ex: from yakut.cmd.compile import make_usage_suggestion diff --git a/yakut/cmd/monitor/_model.py b/yakut/cmd/monitor/_model.py index 48f6377..8add024 100644 --- a/yakut/cmd/monitor/_model.py +++ b/yakut/cmd/monitor/_model.py @@ -33,12 +33,12 @@ class NodeState: Online means that the node is emitting any transfers whatsoever. """ - heartbeat: Optional[uavcan.node.Heartbeat_1_0] + heartbeat: Optional[uavcan.node.Heartbeat_1] """ An online node without a heartbeat is a zombie, which is an error condition because heartbeats are required for all nodes unconditionally. """ - info: Optional[uavcan.node.GetInfo_1_0.Response] + info: Optional[uavcan.node.GetInfo_1.Response] ports: Optional[PortSet] """ @@ -51,14 +51,13 @@ def __init__( self, iface: Iface, node_id: Optional[int], - info: Optional[uavcan.node.GetInfo_1_0.Response] = None, + info: Optional[uavcan.node.GetInfo_1.Response] = None, ) -> None: - import uavcan.node import uavcan.node.port self._node_id = node_id - self._heartbeat: Optional[uavcan.node.Heartbeat_1_0] = None + self._heartbeat: Optional[uavcan.node.Heartbeat_1] = None self._iface = iface self._info = info self._num_info_requests = 0 @@ -70,14 +69,17 @@ def __init__( self._ports = PortSet() - self._dispatch: dict[Any | tuple[Any, ServiceDataSpecifier.Role], Callable[[float, Any], None],] = { - (uavcan.node.GetInfo_1_0, ServiceDataSpecifier.Role.RESPONSE): self._on_info_response, - uavcan.node.port.List_0_1: self._on_port_list, - uavcan.node.Heartbeat_1_0: self._on_heartbeat, + self._dispatch: dict[ + Any | tuple[Any, ServiceDataSpecifier.Role], + Callable[[float, Any], None], + ] = { + (uavcan.node.GetInfo_1, ServiceDataSpecifier.Role.RESPONSE): self._on_info_response, + uavcan.node.port.List_1: self._on_port_list, + uavcan.node.Heartbeat_1: self._on_heartbeat, } - self._iface.add_standard_subscription(uavcan.node.Heartbeat_1_0) - self._iface.add_standard_subscription(uavcan.node.port.List_0_1) + self._iface.add_standard_subscription(uavcan.node.Heartbeat_1) + self._iface.add_standard_subscription(uavcan.node.port.List_1) self._iface.add_trace_handler(self._on_trace) def _restart(self) -> None: @@ -89,14 +91,14 @@ def _on_info_response(self, ts: float, obj: Any) -> None: import uavcan.node _logger.info("%r: Received node info", self) - assert isinstance(obj, uavcan.node.GetInfo_1_0.Response) + assert isinstance(obj, uavcan.node.GetInfo_1.Response) _ = ts self._info = obj def _on_port_list(self, ts: float, obj: Any) -> None: import uavcan.node.port - assert isinstance(obj, uavcan.node.port.List_0_1) + assert isinstance(obj, uavcan.node.port.List_1) self._ports.pub = expand_subjects(obj.publishers) self._ports.sub = expand_subjects(obj.subscribers) self._ports.cln = expand_mask(obj.clients.mask) @@ -104,7 +106,7 @@ def _on_port_list(self, ts: float, obj: Any) -> None: self._ts_port_list = ts def _on_heartbeat(self, ts: float, obj: Any) -> None: - from uavcan.node import Heartbeat_1_0 as Heartbeat, GetInfo_1_0 as GetInfo + from uavcan.node import Heartbeat_1 as Heartbeat, GetInfo_1 as GetInfo assert isinstance(obj, Heartbeat) @@ -165,8 +167,8 @@ def _on_trace(self, ts: Timestamp, tr: AlienTransfer) -> None: assert False def update(self, ts: float) -> NodeState: - from uavcan.node import Heartbeat_1_0 as Heartbeat - from uavcan.node.port import List_0_1 as PortList + from uavcan.node import Heartbeat_1 as Heartbeat + from uavcan.node.port import List_1 as PortList if self._heartbeat and self._ts_activity - self._ts_heartbeat > Heartbeat.OFFLINE_TIMEOUT: _logger.info("%r: Much more recent activity than the last heartbeat, we've gone zombie", self) @@ -186,7 +188,7 @@ def __repr__(self) -> str: return str(pycyphal.util.repr_attributes(self, node_id=self._node_id)) -def expand_subjects(m: uavcan.node.port.SubjectIDList_0_1) -> AbstractSet[int]: +def expand_subjects(m: uavcan.node.port.SubjectIDList_1) -> AbstractSet[int]: if m.sparse_list is not None: return frozenset(int(x.value) for x in m.sparse_list) if m.mask: diff --git a/yakut/cmd/monitor/_view.py b/yakut/cmd/monitor/_view.py index 7455783..cfb070a 100644 --- a/yakut/cmd/monitor/_view.py +++ b/yakut/cmd/monitor/_view.py @@ -468,7 +468,7 @@ def get_matrix_cell_style(tx: Optional[bool], rx: Optional[bool], recently_activ # noinspection SpellCheckingInspection -def render_mode(val: uavcan.node.Mode_1_0) -> tuple[str, Optional[Style]]: +def render_mode(val: uavcan.node.Mode_1) -> tuple[str, Optional[Style]]: if val.value == val.OPERATIONAL: return "oper", None if val.value == val.INITIALIZATION: @@ -481,7 +481,7 @@ def render_mode(val: uavcan.node.Mode_1_0) -> tuple[str, Optional[Style]]: # noinspection SpellCheckingInspection -def render_health(val: uavcan.node.Health_1_0) -> tuple[str, Optional[Style]]: +def render_health(val: uavcan.node.Health_1) -> tuple[str, Optional[Style]]: if val.value == val.NOMINAL: return "nomina", None if val.value == val.ADVISORY: @@ -497,11 +497,11 @@ def render_uptime(val: int) -> str: return f"{val // (3600 * 24):5d}d{(val // 3600) % 24:02d}:{(val // 60) % 60:02d}:{val % 60:02d}" -def render_version(val: uavcan.node.Version_1_0) -> str: +def render_version(val: uavcan.node.Version_1) -> str: return "% 3d.%-3d" % (val.major, val.minor) # pylint: disable=consider-using-f-string -def render_full_software_version(version: uavcan.node.Version_1_0, vcs_revision_id: int, crc: Optional[int]) -> str: +def render_full_software_version(version: uavcan.node.Version_1, vcs_revision_id: int, crc: Optional[int]) -> str: out = f"{version.major:3d}.{version.minor}" if vcs_revision_id != 0 or crc is not None: out += f".{vcs_revision_id:016x}" diff --git a/yakut/cmd/orchestrate/_child.py b/yakut/cmd/orchestrate/_child.py index b91a482..a226707 100644 --- a/yakut/cmd/orchestrate/_child.py +++ b/yakut/cmd/orchestrate/_child.py @@ -1,6 +1,7 @@ # Copyright (c) 2021 OpenCyphal # This software is distributed under the terms of the MIT License. # Author: Pavel Kirienko +# pylint: disable=consider-using-with from __future__ import annotations import os diff --git a/yakut/main.py b/yakut/main.py index 30c3252..50e0b48 100644 --- a/yakut/main.py +++ b/yakut/main.py @@ -1,6 +1,7 @@ # Copyright (c) 2020 OpenCyphal # This software is distributed under the terms of the MIT License. # Author: Pavel Kirienko +# mypy: warn_unused_ignores=False from __future__ import annotations import sys diff --git a/yakut/register.py b/yakut/register.py index 6f600ca..aa46db8 100644 --- a/yakut/register.py +++ b/yakut/register.py @@ -70,7 +70,7 @@ async def fetch_registers( def unexplode_value(xpl: Any, prototype: Optional["Value"] = None) -> Optional["Value"]: """ - Reverse the effect of :func:`explode`. + Reverse the effect of :func:`explode_value`. Returns None if the exploded form is invalid or not applicable to the prototype. Some simplified exploded forms can be unexploded only if the prototype is given because simplification erases type information.