Add USB camera and uhubctl support for new test suite. Also included some bug fixes (#10204)
* Add USB camera and uhubctl support for new test suite. Also added some bug fixes * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Refactor test messages for clarity and consistency in regex tests --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
GitHub
Copilot Autofix powered by AI
parent
6b15571e14
commit
de23e5199d
@@ -0,0 +1,155 @@
|
||||
"""Isolation test for peer-offline-then-back mid-conversation.
|
||||
|
||||
Verifies the mesh stack's behavior when a peer is physically powered
|
||||
off mid-send via uhubctl, then powered back on.
|
||||
|
||||
Flow (parametrized over every directed mesh_pair):
|
||||
1. Bilateral PKI warmup (same pattern as test_direct_with_ack).
|
||||
2. TX sends a broadcast text "msg-1" — RX confirms receipt via pubsub.
|
||||
3. Power OFF RX via uhubctl. The RX device disappears from the OS.
|
||||
4. TX sends a directed text "msg-2" with wantAck=True. Firmware retries
|
||||
internally for ~30s before giving up. Assertion: the packet object
|
||||
was accepted by the TX stack (non-None) — we don't assert an ACK
|
||||
since there's no peer to send one.
|
||||
5. Power ON RX. Wait for re-enumeration + boot.
|
||||
6. Bilateral PKI re-nudge — RX's in-RAM PKI cache was wiped on reboot,
|
||||
so the first directed send may err=35 without a fresh NodeInfo ping.
|
||||
7. TX sends a directed "msg-3" — RX receives it via pubsub, confirming
|
||||
the mesh recovered.
|
||||
|
||||
Skips cleanly if uhubctl isn't installed (via the `power_cycle` fixture's
|
||||
auto-skip). Skips for pair directions where RX isn't power-controllable
|
||||
(e.g. a USB-IF hub that doesn't support PPPS for its port).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from meshtastic_mcp.connection import connect
|
||||
from tests import _power
|
||||
from tests._port_discovery import resolve_port_by_role
|
||||
|
||||
from ._receive import ReceiveCollector, nudge_nodeinfo
|
||||
|
||||
|
||||
@pytest.mark.timeout(360)
|
||||
def test_peer_offline_then_recovers(
|
||||
mesh_pair: dict[str, Any],
|
||||
power_cycle, # noqa: ARG001 — forces uhubctl-availability skip
|
||||
hub_devices: dict[str, str],
|
||||
) -> None:
|
||||
tx_port = mesh_pair["tx"]["port"]
|
||||
rx_node_num = mesh_pair["rx"]["my_node_num"]
|
||||
tx_role = mesh_pair["tx_role"]
|
||||
rx_role = mesh_pair["rx_role"]
|
||||
|
||||
unique_pre = f"peer-offline-pre-{tx_role}-to-{rx_role}-{int(time.time())}"
|
||||
unique_post = f"peer-offline-post-{tx_role}-to-{rx_role}-{int(time.time())}"
|
||||
|
||||
# Step 1 + 2: warm up + confirm baseline delivery works before the test.
|
||||
with ReceiveCollector(
|
||||
mesh_pair["rx"]["port"], topic="meshtastic.receive.text"
|
||||
) as rx:
|
||||
rx.broadcast_nodeinfo_ping()
|
||||
with connect(port=tx_port) as tx_iface:
|
||||
nudge_nodeinfo(tx_iface)
|
||||
# Wait for bilateral PKI (RX pubkey in TX's nodesByNum).
|
||||
deadline = time.monotonic() + 45.0
|
||||
last_nudge = time.monotonic()
|
||||
while time.monotonic() < deadline:
|
||||
rec = (tx_iface.nodesByNum or {}).get(rx_node_num, {})
|
||||
if rec.get("user", {}).get("publicKey"):
|
||||
break
|
||||
if time.monotonic() - last_nudge > 15.0:
|
||||
rx.broadcast_nodeinfo_ping()
|
||||
nudge_nodeinfo(tx_iface)
|
||||
last_nudge = time.monotonic()
|
||||
time.sleep(1.0)
|
||||
else:
|
||||
pytest.skip(
|
||||
f"bilateral PKI never completed ({tx_role}→{rx_role}); "
|
||||
"can't run the offline test without a warm baseline"
|
||||
)
|
||||
|
||||
tx_iface.sendText(unique_pre, destinationId=rx_node_num, wantAck=True)
|
||||
got = rx.wait_for(
|
||||
lambda pkt: pkt.get("decoded", {}).get("text") == unique_pre,
|
||||
timeout=30,
|
||||
)
|
||||
assert got is not None, (
|
||||
f"baseline directed send ({tx_role}→{rx_role}) didn't land — "
|
||||
"skipping offline test to avoid false positive"
|
||||
)
|
||||
|
||||
# Step 3: power off RX. uhubctl skips the test with a clear message if
|
||||
# the RX role isn't on a controllable hub.
|
||||
try:
|
||||
_power.power_off(rx_role)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
pytest.skip(f"can't power-control {rx_role!r}: {exc}")
|
||||
|
||||
try:
|
||||
_power.wait_for_absence(rx_role, timeout_s=10.0)
|
||||
except TimeoutError:
|
||||
_power.power_on(rx_role) # restore hub state before failing
|
||||
resolve_port_by_role(rx_role, timeout_s=30.0)
|
||||
pytest.fail(f"{rx_role!r} didn't disappear after power_off")
|
||||
|
||||
# Step 4: send to a peer that isn't there. Firmware will retry
|
||||
# internally. We don't wait for an ACK (there won't be one); we just
|
||||
# confirm TX's stack accepts the packet without crashing.
|
||||
try:
|
||||
with connect(port=tx_port) as tx_iface:
|
||||
packet = tx_iface.sendText(
|
||||
f"while-offline-{rx_role}",
|
||||
destinationId=rx_node_num,
|
||||
wantAck=True,
|
||||
)
|
||||
assert packet is not None
|
||||
# Give firmware a moment to do a retry or two while RX is down.
|
||||
time.sleep(5.0)
|
||||
except Exception as exc: # noqa: BLE001 — TX should survive the peer being gone
|
||||
# Restore RX before reraising so the bench state is sane.
|
||||
_power.power_on(rx_role)
|
||||
resolve_port_by_role(rx_role, timeout_s=30.0)
|
||||
raise AssertionError(f"TX crashed when sending to offline peer: {exc}") from exc
|
||||
|
||||
# Step 5: power RX back on + rediscover.
|
||||
_power.power_on(rx_role)
|
||||
time.sleep(0.5)
|
||||
new_rx_port = resolve_port_by_role(rx_role, timeout_s=30.0)
|
||||
hub_devices[rx_role] = new_rx_port
|
||||
|
||||
# Step 6 + 7: bilateral re-warmup + directed send that should now work.
|
||||
with ReceiveCollector(new_rx_port, topic="meshtastic.receive.text") as rx:
|
||||
# RX rebooted → its PKI cache is gone. Re-warm.
|
||||
rx.broadcast_nodeinfo_ping()
|
||||
with connect(port=tx_port) as tx_iface:
|
||||
nudge_nodeinfo(tx_iface)
|
||||
time.sleep(3.0)
|
||||
|
||||
got = None
|
||||
for _attempt in range(3):
|
||||
packet = tx_iface.sendText(
|
||||
unique_post,
|
||||
destinationId=rx_node_num,
|
||||
wantAck=True,
|
||||
)
|
||||
assert packet is not None
|
||||
got = rx.wait_for(
|
||||
lambda pkt: pkt.get("decoded", {}).get("text") == unique_post,
|
||||
timeout=30,
|
||||
)
|
||||
if got is not None:
|
||||
break
|
||||
rx.broadcast_nodeinfo_ping()
|
||||
nudge_nodeinfo(tx_iface)
|
||||
time.sleep(5.0)
|
||||
|
||||
assert got is not None, (
|
||||
f"post-recovery directed send {unique_post!r} ({tx_role}→{rx_role}) "
|
||||
"never landed — recovery path may be broken"
|
||||
)
|
||||
Reference in New Issue
Block a user