test: constrain SectorSource speed forwarding and PassProgress percentages

Mutation testing left both unconstrained.

sector/mod.rs — set_speed on the Box<dyn> and &mut dyn forwarding impls
could be replaced with an empty body and nothing failed. This one hides
better than the read methods because the trait's own default body is
already a no-op, so a forwarder that swallowed the call is
indistinguishable from a source with no speed control. Consequence is a
silently absent value, not a wrong one: the recovery path lowers read
speed through a damaged region, and a swallowed call leaves the drive
at full speed while the caller believes it slowed down. Routed through
a generic S: SectorSource bound, since a direct call on a &mut dyn
receiver auto-derefs to the vtable and never enters the forwarding body.

progress.rs — 42 survivors. All four percentage accessors could return
a constant, read the wrong byte counter, or have their divide-by-zero
guard inverted. Added exact-value tests (25%, not 'some percentage'),
both sides of each guard, the overshoot clamp, and one test setting all
three disc counters to distinct values at once — without it, a swapped
field still passes every single-counter test.

The Progress blanket impl for closures could return a constant true.
That return value is the cancellation signal, so a constant-true body
makes every closure-based consumer uncancellable.

Each mutation applied, observed red, reverted.
This commit is contained in:
Matthew Jackson
2026-07-30 13:26:31 -07:00
parent e4b1e5b19e
commit c610285910
2 changed files with 276 additions and 0 deletions
+47
View File
@@ -415,6 +415,13 @@ mod tests {
s.read_sectors(lba, count, buf, recovery)
}
/// Same, for the speed lever. The trait's own `set_speed` default is a
/// no-op, so a forwarding body that also did nothing is indistinguishable
/// from the default unless the call is routed through the generic bound.
fn set_speed_generic<S: SectorSource>(mut s: S, kbs: u16) {
s.set_speed(kbs);
}
/// Same, for the FUA entry point.
fn read_fua_generic<S: SectorSource>(
mut s: S,
@@ -496,4 +503,44 @@ mod tests {
"the FUA entry point must be the one reached, with fua=true intact"
);
}
/// The `&mut dyn SectorSource` forwarding impl must delegate `set_speed`.
///
/// This one hides better than the read methods, because the trait's own
/// default body is `fn set_speed(&mut self, _kbs: u16) {}` — so a forwarding
/// impl that dropped the call on the floor compiles, type-checks, and looks
/// exactly like a source that legitimately has no speed control. The
/// consequence is not a wrong value but a silently absent one: the recovery
/// path throttles a struggling drive by lowering its read speed, and a
/// forwarder that swallowed the call would leave the drive at full speed
/// through the damaged region while the caller believed it had slowed down.
#[test]
fn mut_ref_dyn_forwards_set_speed_to_the_inner_source() {
let (mut spy, _reads, speeds, _bases) = Spy::new(0);
let r: &mut dyn SectorSource = &mut spy;
set_speed_generic(r, 5540);
assert_eq!(
*speeds.lock().unwrap(),
vec![5540],
"the forwarding impl must pass set_speed through to the inner \
source; swallowing it is indistinguishable from the trait default \
and silently disables recovery-path throttling"
);
}
/// The same for `Box<dyn SectorSource>`, which is the receiver the mux read
/// paths actually hold.
#[test]
fn boxed_dyn_forwards_set_speed_to_the_inner_source() {
let (spy, _reads, speeds, _bases) = Spy::new(0);
let b: Box<dyn SectorSource> = Box::new(spy);
set_speed_generic(b, 11080);
assert_eq!(
*speeds.lock().unwrap(),
vec![11080],
"the boxed forwarding impl must pass set_speed through unchanged"
);
}
}