Skip to content

Commit e9343a7

Browse files
committed
test(ros): fail the interop job when it runs no tests
The ROS interop step captured nextest's output with `complete` and never printed it, so a green job showed the command echo followed by "All ROS 2 <distro> tests passed!" and nothing in between. That banner could not be falsified: nextest exits 0 having run zero tests, and each interop test returns early -- still passing -- when check_ros2_available says no. Print the captured output and require a nextest summary reporting a non-zero count. Also correct two doc claims in pubsub.rs: the dispatcher and queue-mode capacities are not the same expression (they differ at a zero depth, harmlessly -- now pinned by two queue tests), and catch_unwind around a user callback is inert under the abort-on-panic opt profile.
1 parent e4973c0 commit e9343a7

3 files changed

Lines changed: 98 additions & 9 deletions

File tree

crates/hiroz/src/pubsub.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -110,16 +110,23 @@ pub(crate) const DISPATCH_UNBOUNDED: usize = usize::MAX;
110110

111111
/// The dispatcher capacity implied by a subscriber's history QoS.
112112
///
113-
/// Deliberately the *same* expression [`ZSubBuilder::build`] uses to size the
114-
/// queue-mode [`BoundedQueue`]: `KeepLast(depth)` keeps `depth`, `KeepAll` keeps
115-
/// everything. A callback subscriber and a queue subscriber declared with the
116-
/// same QoS therefore retain the same number of undelivered samples, which is
117-
/// the only reading of ROS `KEEP_LAST(depth)` that does not depend on which
118-
/// hiroz API the user happened to pick.
113+
/// Matches what [`ZSubBuilder::build`] gives the queue-mode [`BoundedQueue`]:
114+
/// `KeepLast(depth)` keeps `depth`, `KeepAll` keeps everything. A callback
115+
/// subscriber and a queue subscriber declared with the same QoS therefore retain
116+
/// the same number of undelivered samples, which is the only reading of ROS
117+
/// `KEEP_LAST(depth)` that does not depend on which hiroz API the user happened
118+
/// to pick.
119119
///
120-
/// A zero depth (the rmw spelling of "system default", which cannot be produced
121-
/// through [`QosProfile`] but can arrive over the wire) is floored at 1 rather
122-
/// than being allowed to degenerate into "keep nothing".
120+
/// The two are *not* the same expression, and the difference is confined to a
121+
/// zero depth (the rmw spelling of "system default", which cannot be produced
122+
/// through [`QosProfile`] but can arrive over the wire). Here it is floored at 1
123+
/// rather than degenerating into "keep nothing"; the queue path passes the 0
124+
/// through. Retention still agrees, because [`BoundedQueue::push`] evicts before
125+
/// it inserts (`len >= capacity` → `pop_front`, then `push_back`), so a capacity
126+
/// of 0 also retains exactly one sample — see `queue::tests::
127+
/// zero_capacity_retains_one_sample`. What differs is bookkeeping, not data: at
128+
/// capacity 0 every push reports a drop, including the first one into an empty
129+
/// queue.
123130
pub(crate) fn dispatch_capacity(qos: &hiroz_protocol::qos::QosProfile) -> usize {
124131
match qos.history {
125132
QosHistory::KeepLast(depth) => depth.max(1),
@@ -394,6 +401,16 @@ impl CallbackDispatcher {
394401
while let Some(sample) = drain_queue.dequeue() {
395402
// A panicking user callback must not kill the drain thread —
396403
// that would silently stop all further delivery.
404+
//
405+
// This holds only where panics unwind. Under `panic = "abort"`
406+
// — which this workspace's `[profile.opt]` sets — the panic
407+
// aborts the process before `catch_unwind` can return `Err`,
408+
// so neither the recovery below nor the log line happens. The
409+
// guard is therefore effective for dev, test and `release`
410+
// builds (including everything CI runs) and inert for `opt`.
411+
// That is a deliberate consequence of choosing `abort` for
412+
// that profile, not an oversight here: a build that opts into
413+
// aborting on panic has opted out of surviving one.
397414
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (*handler)(sample)))
398415
.is_err()
399416
{

crates/hiroz/src/queue.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,43 @@ impl<T> BoundedQueue<T> {
114114
}
115115
}
116116
}
117+
118+
#[cfg(test)]
119+
mod tests {
120+
use super::*;
121+
122+
/// A zero capacity retains one sample, not zero.
123+
///
124+
/// `pubsub::dispatch_capacity` floors a zero history depth at 1 while the
125+
/// queue-mode path passes the 0 straight through, so the two sizing
126+
/// expressions differ. This pins the reason that divergence is harmless:
127+
/// `push` evicts *before* it inserts, so capacity 0 behaves as capacity 1
128+
/// for retention. If `push` is ever reordered to insert-then-evict, a
129+
/// zero-depth queue starts discarding every sample and this fails.
130+
#[test]
131+
fn zero_capacity_retains_one_sample() {
132+
let q = BoundedQueue::new(0);
133+
134+
assert!(q.push(1), "capacity 0 reports a drop even on the first push");
135+
assert_eq!(q.len(), 1, "capacity 0 must retain one sample, not zero");
136+
137+
q.push(2);
138+
assert_eq!(q.len(), 1);
139+
assert_eq!(q.try_recv(), Some(2), "the newest sample is the one kept");
140+
assert!(q.is_empty());
141+
}
142+
143+
/// The capacity-1 comparison the doc claims equivalence against: same
144+
/// retention, but no spurious drop report on the first push.
145+
#[test]
146+
fn capacity_one_retains_one_sample_without_reporting_a_drop() {
147+
let q = BoundedQueue::new(1);
148+
149+
assert!(!q.push(1), "an empty capacity-1 queue must not report a drop");
150+
assert_eq!(q.len(), 1);
151+
152+
assert!(q.push(2), "the second push evicts the first");
153+
assert_eq!(q.len(), 1);
154+
assert_eq!(q.try_recv(), Some(2));
155+
}
156+
}

scripts/test-ros.nu

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,45 @@ def run-ros-interop [] {
7575
# Try without verbose logging first (faster)
7676
let result = (do -i { run-cmd $cmd --distro $distro | complete })
7777

78+
# Always surface the runner's own output.
79+
#
80+
# This used to capture with `complete` and then never print, so a passing
81+
# ROS job logged the nextest command, produced not one line of test output,
82+
# and printed "All ROS 2 <distro> tests passed!". That banner was
83+
# unfalsifiable: nextest exits 0 when it runs *zero* tests, and each interop
84+
# test additionally returns early (still passing) when `check_ros2_available`
85+
# says no. Nothing in the log distinguished "57 interop tests passed against
86+
# rmw_zenoh_cpp" from "the binary matched no tests".
87+
print $result.stdout
88+
print $result.stderr
89+
7890
# If tests failed, retry with trace logging for detailed diagnostics
7991
# This is CRITICAL for debugging interop issues - shows type hashes, key expressions, service calls
8092
if $result.exit_code != 0 {
8193
print "\n⚠️ ROS interop tests failed. Retrying with trace logging..."
8294
$env.RUST_LOG = "hiroz=trace,rmw_zenoh_cpp=debug,warn"
8395
run-cmd $cmd --distro $distro
8496
}
97+
98+
# An exit code of 0 is necessary but not sufficient — require evidence that
99+
# tests actually ran. nextest's last line is
100+
# `Summary [ 12.345s] 57 tests run: 57 passed, 0 skipped`.
101+
let summary = ([$result.stdout, $result.stderr] | str join "\n" | lines
102+
| where {|l| $l =~ 'tests run:' })
103+
104+
if ($summary | is-empty) {
105+
error make {
106+
msg: $"ROS interop run produced no nextest summary line, so it is unknown whether any test ran. Command: ($cmd)"
107+
}
108+
}
109+
110+
let ran = ($summary | last | parse --regex '(?<n>\d+) tests run' | get n.0 | into int)
111+
if $ran == 0 {
112+
error make {
113+
msg: $"ROS interop run executed 0 tests -- a vacuous pass, not a pass. Command: ($cmd)"
114+
}
115+
}
116+
print $"\n($ran) ROS interop tests ran against rmw_zenoh_cpp."
85117
}
86118

87119
# ============================================================================

0 commit comments

Comments
 (0)