Skip to content

Bench-test harness for field failure modes (netns + tc netem)#27

Open
rolker wants to merge 18 commits into
jazzyfrom
feature/issue-18
Open

Bench-test harness for field failure modes (netns + tc netem)#27
rolker wants to merge 18 commits into
jazzyfrom
feature/issue-18

Conversation

@rolker

@rolker rolker commented May 20, 2026

Copy link
Copy Markdown
Owner

Closes #18

Summary

Linux-netns + tc netem bench harness reproducing udp_bridge field failure
modes off the boat — no root, no mininet. Runs both bridges co-resident in one
unprivileged user+net namespace over three simulated paths (WiFi/Cell/Starlink)
with a three-tier traffic mix, and asserts invariants from a recorded bag +
Recv-Q trace. Skips cleanly where unprivileged userns is unavailable.

What's here (Phases 0–6 + an added scenario)

  • 0–3 (landed earlier): threshold table, orchestrator + smoke, three-tier
    mix, range-degradation trajectory + 5 single-path invariants.
  • Phase 4 — multi-link invariants (same run): topic-list confinement,
    cross-path non-poisoning (Y), critical-gap over-horizon (G), drop-by-tier
    (measure-and-report; per-topic priority is Add per-topic priority/class scheduling to the rate-limiter #19). All 9 range_degradation
    invariants pass.
  • Phase 5 — retire mininet: dead ROS-1 scripts + test_mininet_*.launch
    moved to test/mininet/.archived/ with a pointer README; mainpage.dox
    repointed to test/bench/.
  • Phase 6 — docs/CI: no CI workflow in this repo; the skip-guards + opt-in
    gating are the protection (documented). README updated.
  • subscriber_death scenario (issue Bridge wedges (reader thread blocked, Recv-Q backup) when remote subscriber dies #10 wedge) — freezes (SIGSTOP) the
    Bulk operator subscriber mid-stream, traces operator Recv-Q + survivor
    delivery; forces rmw_zenoh_cpp + a Zenoh router.

Finding — the #10 wedge does not reproduce at bench scale

Across FastDDS, CycloneDDS, and Zenoh, with clean-kill and freeze triggers,
even at ~58 MB/s Bulk into a frozen consumer, the operator Recv-Q/Send-Q stayed
flat at 0 and survivors were unaffected. At bench scale publish() doesn't
block the drain thread (under Zenoh it's an async hand-off). So
test_subscriber_death is a no-wedge regression guard (asserts the healthy
behavior; would catch a reintroduced drain-block under a blocking RMW), not
a red→green reproduction. The component-level proof of the #10 fix is the
PublishQueue unit tests in PR #28. Full rationale in the test docstring and
test/bench/README.md. This PR is independent of #28.

Test plan

  • colcon build clean; test_bench_smoke registered (skips without userns).
  • range_degradation — all 9 invariants pass (~112 s, FastDDS).
  • subscriber_death — both guard invariants pass (~39 s, Zenoh); head-of-line
    measurement: survivors unaffected (no cross-topic degradation at bench scale).
  • Local pre-push review (fresh-context adversarial + flake8); 3 suggestions applied.
  • Opt-in scenarios gated behind UDP_BRIDGE_BENCH_SCENARIOS=1; not run in a default colcon test.

Authored-By: Claude Code Agent
Model: Claude Opus 4.7 (1M context)

@rolker rolker changed the title [PLAN] Bench-test harness for field failure modes (resend amp, wedge, rate-limit accuracy, saturation) Bench-test harness for field failure modes (netns + tc netem) May 26, 2026
@rolker rolker marked this pull request as ready for review May 26, 2026 04:21
Copilot AI review requested due to automatic review settings May 26, 2026 04:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a Linux user+network-namespace (unshare -Urn) + tc netem bench-test harness to reproduce/guard against udp_bridge field failure modes (range degradation, wedge/non-wedge behavior, cross-path invariants) without requiring root/mininet, and retires the legacy ROS1 mininet scaffolding.

Changes:

  • Introduces a bench orchestrator (run_scenario.py) plus rclpy pub/sub helpers and a three-path config to simulate WiFi/Cell/Starlink impairments.
  • Adds pytest coverage: always-on smoke test plus opt-in longer scenarios (range_degradation, subscriber_death) gated by UDP_BRIDGE_BENCH_SCENARIOS=1 and capability/dep checks.
  • Archives the old mininet scripts under test/mininet/.archived/ and repoints docs to the new bench harness.

Reviewed changes

Copilot reviewed 17 out of 28 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
udp_bridge/test/mininet/README.md Replaces mininet instructions with pointer to bench harness; marks mininet as retired.
udp_bridge/test/mininet/.archived/test_mininet_multilink_robot.launch Archives legacy ROS1 launch file for reference.
udp_bridge/test/mininet/.archived/test_mininet_multilink_operator.launch Archives legacy ROS1 launch file for reference.
udp_bridge/test/mininet/.archived/robot.bash Archives legacy mininet helper script for reference.
udp_bridge/test/mininet/.archived/robot_setup.bash Archives legacy ROS1 env setup script.
udp_bridge/test/mininet/.archived/robot_launch.bash Archives legacy ROS1 launch wrapper.
udp_bridge/test/mininet/.archived/README.md Preserves the previous mininet README content under .archived/.
udp_bridge/test/mininet/.archived/operator.bash Archives legacy mininet helper script for reference.
udp_bridge/test/mininet/.archived/operator_setup.bash Archives legacy ROS1 env setup script.
udp_bridge/test/mininet/.archived/operator_launch.bash Archives legacy ROS1 launch wrapper.
udp_bridge/test/mininet/.archived/multilink.py Archives legacy mininet topology script.
udp_bridge/test/bench/test_subscriber_death.py Adds opt-in system-level “no-wedge” regression guard around stalled subscriber behavior (Zenoh-forced).
udp_bridge/test/bench/test_smoke.py Adds a skip-guarded smoke test that runs under default colcon test.
udp_bridge/test/bench/test_range_degradation.py Adds opt-in range-degradation scenario invariants (single-path + multi-link).
udp_bridge/test/bench/sub.py Adds rclpy subscriber helper with optional count-tracing.
udp_bridge/test/bench/run_scenario.py Adds orchestrator to set up netns/veth/netem, launch bridges, drive scenarios, and emit artifact markers.
udp_bridge/test/bench/recv_q_trace.py Adds unix_time column to Recv-Q tracing to support cross-process correlation.
udp_bridge/test/bench/README.md Documents prerequisites, scenarios, invariants, and threshold sourcing/refinement.
udp_bridge/test/bench/pub.py Adds rclpy publisher helper for the three-tier traffic mix.
udp_bridge/test/bench/configs/three_path.yaml Adds a bench-specific 3-path udp_bridge configuration (WiFi/Cell/Starlink + tier replication).
udp_bridge/test/bench/bag_reader.py Adds a small in-tree rosbag2 reader used by invariants.
udp_bridge/test/bench/init.py Marks bench directory as a package (empty).
udp_bridge/package.xml Adds test dependencies needed for pytest + rclpy + system tools (unshare, ip, tc).
udp_bridge/mainpage.dox Updates docs to point to test/bench/ instead of mininet instructions.
udp_bridge/CMakeLists.txt Registers pytest-based bench tests with ament (smoke + opt-in scenarios).
.gitignore Ignores Python bytecode caches produced by bench tests.
.agent/work-plans/issue-18/progress.md Adds work-plan progress notes for issue #18 implementation.
.agent/work-plans/issue-18/plan.md Adds the recorded plan/approach for the bench harness implementation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +9 to +13
Usage:
run_scenario.py --scenario smoke [--duration-s 10] [--outdir DIR]
run_scenario.py --scenario full-mix [--duration-s 10] [--outdir DIR]
run_scenario.py --scenario range_degradation [--hold-s 10] [--outdir DIR]

Comment on lines +313 to +324
def launch_bridge_logged(node_name: str, params_file: Path, domain: int,
stderr_log: Path) -> _Child:
"""Like `launch_bridge` but tees the bridge's stderr to `stderr_log`.

Phase 3 needs per-bridge stderr captured so we can check invariant
2 (no ERROR-severity log during the over-horizon window). We can't
redirect to a file inside the Popen because we still need the
lifecycle transitions to be issued; the cleanest approach is to
open the log file in append mode and pass it as `stderr=`. Debug
mode (UDP_BRIDGE_BENCH_DEBUG=1) additionally tees to the terminal
via `tee` so the user sees the live output.
"""
Comment on lines +327 to +337
child = _spawn(
[
'ros2', 'run', 'udp_bridge', 'udp_bridge_node',
'--ros-args',
'-r', f'__node:={node_name}',
'--params-file', str(params_file),
],
env=env,
name=node_name,
stderr=log_fp,
)
Comment on lines +195 to +204
with artifacts.recvq_csv.open() as f:
for r in csv.DictReader(f):
try:
t = float(r['t_seconds_since_start'])
q = int(r['recv_q_bytes']) if r['recv_q_bytes'] else 0
rows.append((t, q))
except (ValueError, KeyError):
continue
if not rows:
pytest.skip('No Recv-Q samples recorded; nothing to assert.')
Comment on lines +1 to +12
"""In-tree ros2 bag reader for the bench harness.

Two surfaces:
- SQLite shims (`list_topics`, `count_messages`, `find_bag_db`) used
by the Phase 1 smoke for cheap structural checks. These only work
on sqlite3-storage bags.
- rosbag2_py-backed iterators (`iter_messages`, `read_bridge_infos`,
`read_topic_statistics_arrays`) used by `test_range_degradation.py`
for the Phase 3 single-path invariants. These work with whichever
storage backend the bag uses (mcap by default in Jazzy, sqlite3 if
the recorder was told to use it).

Comment on lines +58 to +67
def _make_image(self):
m = Image()
m.header.stamp = self.get_clock().now().to_msg()
m.header.frame_id = 'bench'
m.height = 1
m.width = self._payload_bytes
m.encoding = 'mono8'
m.step = self._payload_bytes
m.data = bytes(self._payload_bytes)
return m
Claude Code Agent and others added 17 commits May 26, 2026 00:39
Six-phase plan for the netns + tc bench-harness MVP: threshold
extraction from named bags (F=2× anchored; N/X/T/G/R/Y queries
documented), orchestrator + smoke under colcon test, topic mix
with three-tier replication, range-degradation trajectory +
assertions, multi-link invariants, mininet retirement, CI runner
check. Phase 0 (threshold extraction) gates the rest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Pub/sub: rclpy (matches Python orchestrator + bag-analysis tooling).
- Bulk tier: synthetic sensor_msgs/Image (no ffmpeg dep).
- Bag output: tempfile.mkdtemp respecting TMPDIR, UDP_BRIDGE_BENCH_OUTDIR
  override, preserve on fail.
- package.xml test_depend now lists python3-pytest + sensor_msgs +
  nav_msgs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Re-root all paths under udp_bridge/ (inner package dir, not repo
  root). Affects every test/bench/, test/mininet/, package.xml,
  CMakeLists.txt reference.
- Drop cross-repo dep on marine_tools' bag_analysis extractor;
  inline a small bag_reader.py in udp_bridge/test/bench/ that
  pulls the BridgeInfo / TopicStatistics fields the assertions
  need (no rosdep path to the sibling-repo extractor).
- Add rosdep keys util-linux (unshare) and iproute2 (tc) to the
  test_depend list — review-issue flagged these and I had dropped
  them.
- Phase 5: align archive-vs-replace on archive-with-pointer; also
  sweep launch/test_mininet_multilink_{operator,robot}.launch
  (same ROS-1-vintage scaffolding, no callers).
- Estimated Scope notes Phase 0 lands as its own reviewable commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Concrete values for the invariants defined in #18:

- F = 2× (anchored: 13× pathological 2026-05-01 vs ~30% healthy
  2026-05-18; well below pathological)
- G = 6 s (2× observed non-degraded heartbeat inter-arrival in
  2026-05-01: 18,900 samples, 99.97% under 2s, worst 3.0s)
- R = 50% over 10s (1Hz baseline; #20 stall 0% for 77 min trips
  well below; brief transients tolerated)
- N = 10 s, X = 90%, T = 30 s, Y = 10% — starting values from
  physical reasoning; refinement plan documented in README

Side-finding from the 2026-05-19 query (relevant to #20): the
boat received the operator's stats topic steadily through the
12:53 WiFi-cut, so both bridges kept exchanging stats; what
stalled is the operator-side local subscriber view. The
mechanism is operator-side downstream of the bridge, not in the
bridge itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scaffolding for the netns + tc bench harness. No orchestrator
yet (lands in 1b). Smoke test wiring lands in 1c.

- udp_bridge/test/bench/configs/three_path.yaml — bridge config
  for both sides. Three Connections per side (wifi/cell/starlink)
  with per-path bandwidth caps from 2026-05-18 calibration. Tier
  replication via topics_list duplication: Critical on all three,
  Telemetry on WiFi+Starlink, Bulk on WiFi only.
- udp_bridge/test/bench/pub.py — rclpy publisher driven by --tier
  (critical/telemetry/bulk). Critical uses std_msgs/String at 1Hz;
  Telemetry uses nav_msgs/Odometry at 10Hz; Bulk uses synthetic
  sensor_msgs/Image (512 KB) at 5Hz. No ffmpeg dep.
- udp_bridge/test/bench/sub.py — rclpy subscriber that counts
  received messages and prints the count on exit.
- udp_bridge/test/bench/bag_reader.py — minimal sqlite reader.
  Smoke counts messages; Phase 3 will extend for full BridgeInfo
  / TopicStatistics deserialization.
- udp_bridge/test/bench/README.md — adds Prerequisites section
  documenting the apparmor_restrict_unprivileged_userns=0
  one-time host setup (Ubuntu 24.04+ default blocks unshare -Urn).
- udp_bridge/package.xml — test_depends for ament_cmake_pytest,
  python3-pytest, rclpy, nav_msgs, sensor_msgs, iproute2 (tc),
  util-linux (unshare).
- .gitignore — exclude __pycache__/ and *.pyc.

Plan update: switch from "two netns connected by veth" to
"single user+net namespace with N veth pairs inside" — nested
`ip netns add` inside an unprivileged user namespace doesn't
work on Ubuntu 24.04+ (mount --make-shared /run/netns fails
without CAP_SYS_ADMIN). Single-namespace + per-path veth has
the same fidelity for the failure modes we care about.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single-file orchestrator that:

- Re-execs self under `unshare -Urn` to enter an ephemeral
  user+net namespace (no sudo; preserved by the AppArmor
  one-time prerequisite documented in test/bench/README.md).
- Inside, brings up lo, creates three veth pairs (wifi/cell/
  starlink) with /24 IPs on each end, and applies `tc qdisc
  netem` per path with the clean-baseline profile from the
  2026-05-18 pier calibration.
- Launches operator_bridge and boat_bridge (lifecycle nodes)
  in distinct ROS_DOMAIN_IDs so they communicate only via the
  bridge protocol, not direct DDS.
- Drives the smoke scenario: one Critical-tier publisher on
  boat, one subscriber on operator, ten seconds, expects
  sub_count > 0.
- Cleans up children on exit; preserves the output directory
  on failure (per the plan's preserve-on-fail rule).

`--scenario range_degradation` is a stub returning exit 2;
implementation lands in Phase 3.

Also flips exec bits on pub.py / sub.py (run_scenario.py
shebangs them too) so they can be invoked directly when
debugging interactively, in addition to via `python3 ...`.

Smoke test wiring + verification land in 1c.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- test/bench/test_smoke.py: pytest wrapper that invokes
  run_scenario.py --scenario smoke and asserts SUB_COUNT > 0.
  Skips with a clear reason if `unshare -Urn` is unavailable
  (host hasn't applied the AppArmor prerequisite) or if no
  ROS_DISTRO is sourced.
- CMakeLists.txt: register the smoke via ament_add_pytest_test
  so `colcon test` exercises the bench scaffolding on every
  build. Timeout 180s.

Two fixes uncovered while bringing it up end-to-end:

- configs/three_path.yaml: drop empty `topics_list: []` lines.
  ROS 2 can't infer element type from an empty YAML array, so
  declare_parameter sees the override as PARAMETER_NOT_SET and
  throws on get_parameter().as_string_array(). Omitting lets
  the bridge's default `std::vector<std::string>()` take over.

- run_scenario.py: harden cleanup. Children now spawn with
  `start_new_session=True`; cleanup uses os.killpg so we take
  down the whole subprocess subtree (ros2 run → udp_bridge_node
  grandchild). Added a SIGTERM/SIGINT handler so cleanup runs
  on signal-kill — Python atexit doesn't fire on SIGTERM, so
  the prior `timeout` outside the orchestrator was leaving
  orphan bridges chewing CPU. Also re-added `text=True` on
  spawn (dropped by the _spawn() refactor).

Verified end-to-end: smoke runs in ~16s in the netns, pub at
1Hz for 10s → bridge → sub receives 10/10 messages on the
WiFi-path Critical-tier topic. No orphan processes after exit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `--scenario full-mix` to run_scenario.py: spawns one publisher
per tier (Critical / Telemetry / Bulk) at the boat side and one
subscriber per destination topic at the operator side; reports
per-tier delivery counts via BENCH_RESULT_<TIER>_{PUB,SUB}_COUNT.

Bumps the Bulk tier defaults in pub.py to the pinned spec
(10 Hz × 480 KB) — was a Phase 1 placeholder (5 Hz × 512 KiB).
At 4.8 MB/s offered vs. the WiFi connection's 4 MB/s budget, the
rate limiter trims Bulk even on a clean link; that's intentional
(Phase 4 drop-by-tier invariant). Phase 2 only confirms each tier
delivers something.

Smoke stays Critical-only as planned; full-mix is a manual CLI
invocation (Phase 3's pytest is what `UDP_BRIDGE_BENCH_SCENARIOS=1`
will gate).

Verified end-to-end on this host: full-mix delivers 10/10 Critical,
99/100 Telemetry, 98-99/100 Bulk over a 10s run; exits 0; no orphan
processes after cleanup.

Refs #18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `--scenario range_degradation`: the orchestrator walks the WiFi
path through the sail-out-and-return trajectory specified in issue #18
(in_range_clean → fringe → lossy → critical → over_horizon → critical
→ lossy → fringe → in_range_clean), holding each phase for --hold-s
seconds (default 10) while the full three-tier mix runs across all
three connections. During the walk it captures:
  - a ros2 bag of `bridge_info` / `topic_statistics` (own +
    operator-side mirrors of the boat's),
  - a CSV trace of the operator UDP socket's Recv-Q via the
    recv_q_trace.py script (moved up from test/mininet/ since Phase 3
    is its first non-archive caller),
  - per-bridge stderr logs (so invariant 2 can scan for ERROR/FATAL
    during the over_horizon window),
  - a phase_log.json marking wall-clock phase boundaries.

Adds `test_range_degradation.py` (pytest opt-in via
UDP_BRIDGE_BENCH_SCENARIOS=1) with five single-path invariants drawn
from the issue body:
  1. Recv-Q never climbs monotonically for more than N seconds.
  2. No ERROR/FATAL log emitted during the over_horizon window.
  3. message.success_bytes_per_second recovers to ≥ X% of the
     pre-event in_range baseline by the final in_range phase.
  4. resend/success ratio ≤ F × applied netem loss rate in each
     lossy phase.
  5. bridge_info + topic_statistics publication rates stay ≥ R% of
     the in-range baseline in every phase (catches #20-class stalls
     in the stats path).

Thresholds (N=10s, X=90%, T=30s, F=2×, R=50%/10s) are pulled from the
existing `test/bench/README.md` values table; the test's THRESHOLDS
dict mirrors them and the per-phase loss rate is derived
programmatically from run_scenario.PHASE_TRAJECTORY so the two can't
drift.

The issue's sixth single-path invariant ("Forwarding resumes without
bridge restart") is subsumed by invariant 3 — the orchestrator has no
respawn path, so a dead bridge means post-recovery success_bps is
zero, which trips X% on its own. Documented in the test module
docstring.

Extends `bag_reader.py` with rosbag2_py-backed iterators (`iter_messages`,
`read_bridge_infos`, `read_topic_statistics_arrays`) that handle both
mcap (Jazzy default) and sqlite3 storage and deserialize via the
bag's own type catalog, so type-hash-evolved schemas still read.

Verified end-to-end on this host:
  - Manual `--scenario range_degradation --hold-s 5` produces all
    expected artifacts (bag + recv_q CSV + phase log + stderr logs).
  - `UDP_BRIDGE_BENCH_SCENARIOS=1 pytest test_range_degradation.py`
    passes all 5 invariants in 112 s on a clean run.
  - `--scenario smoke` regression: still exits 0, 10/10 critical.
  - No orphan processes after pytest exit.

Refs #18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New opt-in scenario reproducing the issue #10 trigger: freeze (SIGSTOP) the
Bulk operator subscriber mid-stream while the boat keeps forwarding Bulk, trace
the operator bridge's UDP Recv-Q (port 4200), and count-trace the surviving
Critical/Telemetry subscribers across the stall (head-of-line measurement for
#29). Forces rmw_zenoh_cpp + a zenoh router, since the back-pressure path does
not exist under DDS. sub.py gains an optional --count-trace.

Finding (measured): the wedge does NOT reproduce at bench scale under FastDDS,
CycloneDDS, OR Zenoh, even at ~58 MB/s Bulk into a frozen consumer — Recv-Q and
Send-Q stay flat at 0 and survivors are unaffected. publish() does not block the
drain thread at bench scale (under Zenoh it's an async hand-off). So
test_subscriber_death is a no-wedge REGRESSION GUARD (asserts the healthy
behavior, would catch a reintroduced drain-block under a blocking RMW), not a
red→green reproduction. The component-level proof of the #10 fix remains the
PublishQueue unit tests. Rationale captured in the test's module docstring.
Move the dead ROS-1-vintage mininet scripts (multilink.py, *.bash) and the two
launch/test_mininet_multilink_*.launch files (no callers) under
test/mininet/.archived/. Replace test/mininet/README.md with a pointer to the
netns-based test/bench/ harness, and repoint mainpage.dox's 'Simulating network
issues' section from mininet to the bench. recv_q_trace.py already lives in
test/bench/.
Four multi-link invariants on the same range_degradation run:
- topic_list_confinement: each forwarded topic advertised only on its
  configured connections (Bulk→WiFi, Telemetry→WiFi+Starlink, Critical→all3);
  checked against every bridge_info so a transient leak is caught.
- cross_path_non_poisoning (Y=10%): when WiFi goes over-horizon, cell+starlink
  received rate stays >= (1-Y) of baseline (WiFi failure can't starve them).
- critical_gap_over_horizon (G=6s): no Critical inter-arrival gap overlapping
  an over-horizon window exceeds G — the always-on paths keep Critical flowing.
- drop_by_tier_report: measure-and-report only (per-topic priority is #19, not
  implemented) — records per-tier succ/drop in the critical phase.

Adds the republished Critical heartbeat to the recorded bag (for the gap
invariant) and generalizes bag_reader's connection accessor (get_connection +
topic_connection_ids). All 9 range_degradation invariants pass (112s run).
… Phase 6)

README: add the subscriber_death scenario row + section (incl. the no-wedge
finding), the four Phase-4 multi-link invariants (6-9), the N-threshold
observation (Recv-Q stayed 0 → N=10s is conservative), and a Continuous
Integration section (no CI wires these; skip-guards + opt-in gating are the
protection; smoke skips cleanly without unprivileged userns).
- recv_q_trace.py: emit a wall-clock 'unix_time' column so consumers correlate
  against cross-process wall-clock events without subtracting a per-process
  monotonic axis. test_subscriber_death now slices the post-stall window on
  unix_time directly.
- test_subscriber_death: fail (not skip) when the Recv-Q trace has rows but the
  tracer never found the operator socket — an unbound socket is a harness
  failure, not a quiet queue, and must not pass vacuously.
- run_scenario: reap the SIGKILLed Bulk victim with wait() so it doesn't linger
  as a zombie until atexit cleanup.

Both subscriber_death invariants still pass under Zenoh (39s).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 28 changed files in this pull request and generated 5 comments.

Comment on lines +195 to +205
with artifacts.recvq_csv.open() as f:
for r in csv.DictReader(f):
try:
t = float(r['t_seconds_since_start'])
q = int(r['recv_q_bytes']) if r['recv_q_bytes'] else 0
rows.append((t, q))
except (ValueError, KeyError):
continue
if not rows:
pytest.skip('No Recv-Q samples recorded; nothing to assert.')

Comment on lines +315 to +323
"""Like `launch_bridge` but tees the bridge's stderr to `stderr_log`.

Phase 3 needs per-bridge stderr captured so we can check invariant
2 (no ERROR-severity log during the over-horizon window). We can't
redirect to a file inside the Popen because we still need the
lifecycle transitions to be issued; the cleanest approach is to
open the log file in append mode and pass it as `stderr=`. Debug
mode (UDP_BRIDGE_BENCH_DEBUG=1) additionally tees to the terminal
via `tee` so the user sees the live output.
Comment on lines +58 to +67
def _make_image(self):
m = Image()
m.header.stamp = self.get_clock().now().to_msg()
m.header.frame_id = 'bench'
m.height = 1
m.width = self._payload_bytes
m.encoding = 'mono8'
m.step = self._payload_bytes
m.data = bytes(self._payload_bytes)
return m
Comment thread udp_bridge/package.xml
<test_depend>nav_msgs</test_depend>
<test_depend>sensor_msgs</test_depend>
<test_depend>iproute2</test_depend>
<test_depend>util-linux</test_depend>
Comment on lines +9 to +13
Usage:
run_scenario.py --scenario smoke [--duration-s 10] [--outdir DIR]
run_scenario.py --scenario full-mix [--duration-s 10] [--outdir DIR]
run_scenario.py --scenario range_degradation [--hold-s 10] [--outdir DIR]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bench-test harness for field failure modes (resend amp, wedge, rate-limit accuracy, saturation)

2 participants