emdashes begone (#10847)

This commit is contained in:
Tom
2026-07-01 19:01:27 -05:00
committed by GitHub
co-authored by GitHub
parent dee94e0758
commit 3becaf2d95
276 changed files with 1795 additions and 1793 deletions
+42 -42
View File
@@ -36,7 +36,7 @@ import pytest
# Ensure the MCP server is on `sys.path` without requiring installation in
# development mode for every checkout (we DO install in .venv but this makes
# `pytest tests/` work from a fresh clone too). The path mutation must
# happen before `meshtastic_mcp.*` imports below hence the `noqa: E402`
# happen before `meshtastic_mcp.*` imports below - hence the `noqa: E402`
# markers on those imports (ruff's "module-level import not at top of file"
# rule doesn't understand path-bootstrapping patterns).
_HERE = pathlib.Path(__file__).resolve().parent
@@ -106,8 +106,8 @@ def pytest_collection_modifyitems(
def sort_key(item: pytest.Item) -> tuple[int, str]:
path = str(getattr(item, "fspath", "") or item.nodeid)
# Session-start bake runs FIRST. `baked_mesh` only verifies state
# nothing else actually reflashes so if test_00_bake doesn't run
# Session-start bake runs FIRST. `baked_mesh` only verifies state -
# nothing else actually reflashes - so if test_00_bake doesn't run
# before the tier tests, `--force-bake` silently becomes a no-op for
# the tier tests and only flashes at the very end of the session.
# Top-level nodeid ("tests/test_00_bake.py") otherwise falls into the
@@ -128,7 +128,7 @@ def pytest_collection_modifyitems(
# after it starts from a known re-enumerated + re-verified state.
if "/recovery/" in path or "tests/recovery" in path:
return (4, item.nodeid)
# UI tier slots here read-only w.r.t. mesh state, only mutates
# UI tier slots here - read-only w.r.t. mesh state, only mutates
# the on-screen UI (BACK×5 guard restores home before each test).
if "/ui/" in path or "tests/ui" in path:
return (5, item.nodeid)
@@ -152,12 +152,12 @@ def pytest_collection_modifyitems(
def session_seed(request: pytest.FixtureRequest) -> str:
"""Deterministic PSK seed for this pytest session.
Logged in the HTML report header so two runs can be correlated and so a
Logged in the HTML report header so two runs can be correlated - and so a
flaky-looking test can be reproduced exactly by passing the seed back via
an env var (future extension).
"""
# Pytest session `starttime` isn't directly exposed on the pytest API we
# care about, so derive from process start time unique enough for human
# care about, so derive from process start time - unique enough for human
# purposes and stable across the session.
seed = os.environ.get("MESHTASTIC_MCP_SEED") or f"pytest-{int(time.time())}"
return seed
@@ -169,7 +169,7 @@ def test_profile(session_seed: str) -> dict[str, Any]:
`enable_ui_log=True` stamps `USERPREFS_UI_TEST_LOG` so the firmware
emits `Screen: frame N/M name=... reason=...` log lines per UI
transition consumed by the `tests/ui/` tier. Harmless on boards
transition - consumed by the `tests/ui/` tier. Harmless on boards
without a screen (the `#ifdef` sits behind `HAS_SCREEN`).
"""
return userprefs.build_testing_profile(
@@ -186,25 +186,25 @@ def test_profile(session_seed: str) -> dict[str, Any]:
def _session_userprefs(test_profile: dict[str, Any]) -> Any:
"""Snapshot `userPrefs.jsonc`, apply the session test profile, restore at
session end. Guards against the suite leaving test-profile USERPREFS
values baked into the file if that happened, any firmware build a
values baked into the file - if that happened, any firmware build a
contributor ran next would silently inherit the test PSK / test channel
name / test admin key etc.
Layered safety:
1. In-memory snapshot taken before any mutation; teardown writes it back.
2. Sidecar `userPrefs.jsonc.mcp-session-bak` on disk belt to the
2. Sidecar `userPrefs.jsonc.mcp-session-bak` on disk - belt to the
in-memory suspenders. If Python segfaults or SIGKILLs, the next
session self-heals from this file at startup.
3. `atexit.register()` fallback: if pytest exits abnormally (Ctrl-C
mid-test, fatal exception before teardown), the atexit hook still
restores from the in-memory snapshot.
4. Startup self-heal: if the sidecar exists at session start, a prior
session crashed without cleanup the sidecar IS the truth; restore
session crashed without cleanup - the sidecar IS the truth; restore
from it before taking this session's snapshot. That way a crash
during test A doesn't propagate dirty state into test B's baseline.
Autouse + depends on `test_profile` so it applies on every run (even
unit-only) cheap, unified code path, no ordering surprises.
unit-only) - cheap, unified code path, no ordering surprises.
"""
path = userprefs.jsonc_path()
backup_path = path.with_name(path.name + ".mcp-session-bak")
@@ -214,7 +214,7 @@ def _session_userprefs(test_profile: dict[str, Any]) -> Any:
yield
return
# (4) Startup self-heal prior session crashed without teardown.
# (4) Startup self-heal - prior session crashed without teardown.
if backup_path.is_file():
try:
sidecar_bytes = backup_path.read_bytes()
@@ -241,7 +241,7 @@ def _session_userprefs(test_profile: dict[str, Any]) -> Any:
except Exception as exc:
print(f"[userprefs] could not write sidecar: {exc!r}", file=sys.stderr)
# (3) atexit fallback fires even if pytest aborts before fixture teardown.
# (3) atexit fallback - fires even if pytest aborts before fixture teardown.
restored = {"done": False}
def _atexit_restore() -> None:
@@ -263,8 +263,8 @@ def _session_userprefs(test_profile: dict[str, Any]) -> Any:
# Apply the session test profile on top of the snapshot. The firmware
# reads userPrefs.jsonc at build time via `bin/platformio-custom.py`,
# so every `pio run` during the session picks up the test values.
# Delegate to `userprefs.merge_active` the public API that already
# parses, merges, validates, and writes rather than reaching into
# Delegate to `userprefs.merge_active` - the public API that already
# parses, merges, validates, and writes - rather than reaching into
# the private parser/renderer machinery from here.
try:
userprefs.merge_active(test_profile)
@@ -276,7 +276,7 @@ def _session_userprefs(test_profile: dict[str, Any]) -> Any:
# tests that don't (unit) still run. But the restore below is
# unconditional, so we can't leave a half-written file behind.
print(
f"[userprefs] failed to apply test profile: {exc!r} "
f"[userprefs] failed to apply test profile: {exc!r} - "
f"file left at original state",
file=sys.stderr,
)
@@ -298,7 +298,7 @@ def _session_userprefs(test_profile: dict[str, Any]) -> Any:
# exception from the yielded body); use a flag so the cleanup
# control-flow stays linear and exceptions propagate normally.
print(
f"[userprefs] teardown restore failed: {exc!r} "
f"[userprefs] teardown restore failed: {exc!r} - "
f"sidecar {backup_path} retained for manual recovery",
file=sys.stderr,
)
@@ -388,7 +388,7 @@ def hub_devices(hub_profile: dict[str, dict[str, Any]]) -> dict[str, str]:
"""
# include_unknown=True so non-whitelisted VIDs (e.g. CP2102 at 0x10c4) that
# are configured as hub roles still match. The hub_profile itself gates
# which VIDs we consider no risk of unrelated serial ports sneaking in.
# which VIDs we consider - no risk of unrelated serial ports sneaking in.
found = devices_module.list_devices(include_unknown=True)
# Coalesce alt roles into their base name (esp32s3_alt → esp32s3)
resolved: dict[str, str] = {}
@@ -413,7 +413,7 @@ def hub_devices(hub_profile: dict[str, dict[str, Any]]) -> dict[str, str]:
def _reset_transmit_history_state(role: str, port: str) -> str:
"""Wipe `/prefs/transmit_history.dat` + in-memory throttle cache via
delete_file_request + reboot. Returns the post-reboot port (nRF52
re-enumerates). Best-effort errors log to stderr + return original
re-enumerates). Best-effort - errors log to stderr + return original
port so a flaky start doesn't block the session.
"""
from ._port_discovery import resolve_port_by_role
@@ -467,7 +467,7 @@ def _session_clear_transmit_history(hub_devices: dict[str, str]) -> None:
if not hub_devices:
yield
return
# Iterate over a snapshot _reset_transmit_history_state can mutate
# Iterate over a snapshot - _reset_transmit_history_state can mutate
# hub_devices mid-loop via the update below, and dict-iteration isn't
# safe during mutation.
for role, port in list(hub_devices.items()):
@@ -491,12 +491,12 @@ def baked_mesh(
comparing the live config to the expected profile.
Raises with an actionable error if state is missing or mismatched:
"device nrf52 at /dev/cu.X not baked with session profile
"device nrf52 at /dev/cu.X not baked with session profile -
run test_00_bake.py first or pass --force-bake"
Returns a per-role dict with `{port, iface_fresh: callable, my_node_num}`.
"""
# Verify every role that's present don't require a fixed set.
# Verify every role that's present - don't require a fixed set.
# Tests that NEED a specific role (mesh_pair, bidirectional) check
# presence in their own fixtures and skip there with an actionable
# message. That keeps single-device tests runnable on a one-device
@@ -518,7 +518,7 @@ def baked_mesh(
try:
live = info.device_info(port=port, timeout_s=12.0)
except Exception as exc:
# Per-role failure drop this role from the baked set and let
# Per-role failure - drop this role from the baked set and let
# any test parametrized against it skip with the actionable
# message. Other roles still proceed.
per_role_errors[role] = f"device_info failed: {exc!r}"
@@ -576,11 +576,11 @@ def baked_mesh(
# the SerialInterface. Operators who want log capture can opt in via the
# `set_debug_log_api` MCP tool (or `admin.set_debug_log_api` directly) on
# a case-by-case basis. The autouse `_debug_log_buffer` fixture is still
# armed below if a test explicitly enables the flag, its output will
# armed below - if a test explicitly enables the flag, its output will
# be captured and attached to failures. Firmware-side fix would need
# a separate tx buffer or a mutex out of scope for the MCP harness.
# a separate tx buffer or a mutex - out of scope for the MCP harness.
# If EVERY detected role errored, skip the session nothing testable.
# If EVERY detected role errored, skip the session - nothing testable.
# Otherwise yield the partial set. Tests parametrized against a role
# not in `out` will skip via the `baked_single`/`mesh_pair` presence
# check with "role not present on the hub".
@@ -608,7 +608,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
the test still COLLECTS cleanly (it'll just skip via the
`hub_devices` missing-role check inside the fixture).
Honors `--hub-profile=<yaml>` for non-default hardware when set, only
Honors `--hub-profile=<yaml>` for non-default hardware - when set, only
roles defined in the YAML are parametrized. (So e.g. a yaml with only
`esp32s3` skips every `[nrf52]` variant at collection time.)
"""
@@ -675,7 +675,7 @@ def baked_single(
"""Function-scoped: a single verified baked device.
Auto-parametrized by `pytest_generate_tests` over every detected hub
role so any test taking this fixture runs once per connected device
role - so any test taking this fixture runs once per connected device
(e.g. `test_owner_survives_reboot[nrf52]` +
`test_owner_survives_reboot[esp32s3]`). Tests never hardcode a role
and never skip a device that happens to be connected.
@@ -727,7 +727,7 @@ def power_cycle(
) -> Callable[..., str]:
"""Return a callable `(role, delay_s=2) -> new_port` that hard-resets the
hub port hosting `role`. Skips the test cleanly when uhubctl isn't
installed never want "no uhubctl" to look like a test failure.
installed - never want "no uhubctl" to look like a test failure.
The callable mutates `hub_devices[role]` in place so subsequent fixture
lookups pick up the post-cycle port (mirrors the pattern in
@@ -905,19 +905,19 @@ def _firmware_log_stream() -> Any:
firmware logs *in memory* for pytest-html failure attachments, but a
live viewer (``meshtastic-mcp-test-tui``) can't read in-process
pubsub events from a different process. This fixture adds a
session-long, durable mirror one JSON object per line, with
``port``, ``ts``, and ``line`` fields that the TUI tails from a
session-long, durable mirror - one JSON object per line, with
``port``, ``ts``, and ``line`` fields - that the TUI tails from a
worker thread.
Schema (kept trivially small so the file grows slowly):
{"ts": 1729100000.123, "port": "/dev/cu.usbmodem1101", "line": "INFO | ... [SerialConsole] Boot..."}
The file is truncated at session start (no append across runs the
The file is truncated at session start (no append across runs - the
TUI also unlinks it on launch, so double-truncate is deliberate).
Gitignored via ``mcp-server/.gitignore``.
Runs alongside ``_debug_log_buffer`` both subscribe to the same
Runs alongside ``_debug_log_buffer`` - both subscribe to the same
pubsub topic; pubsub fans out to every subscriber so there's no
interference.
"""
@@ -943,7 +943,7 @@ def _firmware_log_stream() -> Any:
def handler(line: str, interface: Any) -> None:
# `interface` is the meshtastic SerialInterface; `.devPath`
# carries the /dev/cu.* we care about. Defensive about missing
# attribute the pubsub handler must never raise.
# attribute - the pubsub handler must never raise.
try:
port = getattr(interface, "devPath", None) or getattr(
interface, "stream", None
@@ -959,7 +959,7 @@ def _firmware_log_stream() -> Any:
fh.write(json.dumps(record) + "\n")
fh.flush()
except Exception:
# Swallow firmware log mirroring is best-effort.
# Swallow - firmware log mirroring is best-effort.
pass
pub.subscribe(handler, "meshtastic.log.line")
@@ -980,7 +980,7 @@ def _firmware_log_stream() -> Any:
def _debug_log_buffer(request: pytest.FixtureRequest) -> Any:
"""Per-test capture of `meshtastic.log.line` pubsub events.
Automatic every test gets this for free. The pubsub topic fires when
Automatic - every test gets this for free. The pubsub topic fires when
a connected device has `security.debug_log_api_enabled=True` AND the
client (us) is talking protobufs over its SerialInterface. `baked_mesh`
flips the flag on at session start, so every subsequent test that opens
@@ -989,7 +989,7 @@ def _debug_log_buffer(request: pytest.FixtureRequest) -> Any:
The captured lines are attached to the test's pytest-html failure report
by `pytest_runtest_makereport`, so mesh/telemetry failures ship with the
firmware-side log context inline no separate pio monitor, no
firmware-side log context inline - no separate pio monitor, no
port-lock conflict.
"""
import threading as _threading
@@ -1026,7 +1026,7 @@ def _run_with_timeout(fn: Callable[[], Any], timeout: float) -> Any:
`meshtastic.SerialInterface` construction can hang indefinitely on a
misconfigured or unresponsive port. pytest-timeout fires from the main
thread via SIGALRM, which doesn't protect code running inside
`pytest_runtest_makereport` that hook runs outside the test's timer. So
`pytest_runtest_makereport` - that hook runs outside the test's timer. So
we wrap each device query in a bounded worker.
"""
import concurrent.futures
@@ -1064,7 +1064,7 @@ def _attach_ui_captures(item: pytest.Item, report: Any) -> None:
label = f"{cap.get('step', '?')}: {cap.get('label', '')}"
frame = cap.get("frame") or {}
frame_str = (
f" frame {frame.get('idx')} {frame.get('name')!r}" if frame else ""
f" - frame {frame.get('idx')} {frame.get('name')!r}" if frame else ""
)
if png_path:
try:
@@ -1085,7 +1085,7 @@ def _attach_ui_captures(item: pytest.Item, report: Any) -> None:
def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo[Any]) -> Any:
"""On test failure, attach serial capture + device state as report artifacts.
Hard-bounded by `_run_with_timeout` if the device is unreachable (stuck
Hard-bounded by `_run_with_timeout` - if the device is unreachable (stuck
port, unbaked firmware, dead board), the dump is skipped rather than
hanging the session.
@@ -1096,7 +1096,7 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo[Any]) ->
outcome = yield
report = outcome.get_result()
# Attach UI captures on any outcome (pass + fail) these are the whole
# Attach UI captures on any outcome (pass + fail) - these are the whole
# point of the UI tier. Do this before the failure-only branch below so
# passing tests still get their image strip.
if report.when == "call":
@@ -1108,7 +1108,7 @@ def pytest_runtest_makereport(item: pytest.Item, call: pytest.CallInfo[Any]) ->
extras: list[str] = []
# Attach firmware log stream captured via the StreamAPI (populated only
# when the device has security.debug_log_api_enabled=True baked_mesh
# when the device has security.debug_log_api_enabled=True - baked_mesh
# flips this on at session start). Cheap and high-signal: last 200 lines
# of firmware log interleaved with whatever the test was doing.
log_buffer = getattr(item, "_debug_log_buffer", None)