test(bollinger_bandwidth): cover accessors, zero-middle branch, kill dead arm
Codecov flagged 17 uncovered lines in
crates/wickra-core/src/indicators/bollinger_bandwidth.rs (file at 79.51%):
- the const accessors period (54-56), multiplier (59-61), value (64-66)
- the zero-middle defensive fallback 0.0 (line 77) inside update
- the Indicator-impl bodies warmup_period (90-92) and name (98-100)
- the unreachable `_ => panic!("warmup mismatch")` arm (line 140) in
the existing matches_bands_definition test
None of the existing tests inspected the metadata surface — every test
fed numeric updates and asserted on bandwidth values, leaving the five
getter bodies dead. The zero-middle path was unreachable because all
existing tests used positive price levels ≈100, so the rolling SMA was
always strictly positive and the divide-by-zero guard never fired.
The panic arm in matches_bands_definition was an invariant guard that
by design cannot fire when the two streams share a warmup period; that
invariant is now asserted directly with assert_eq!(w.is_some(),
b.is_some()), and the catch-all arm is gone.
Add two new tests and refactor one existing:
- accessors_and_metadata asserts period == 20, multiplier == 2.0,
value() == None before warmup, warmup_period == 20, name ==
"BollingerBandwidth", then drives 20 updates so value() also
exercises the Some branch.
- zero_middle_band_yields_zero_bandwidth feeds [-2, -1, 0, 1, 2] so
the 5-bar SMA lands on exactly 0.0 at the fifth input, and asserts
the emitted bandwidth is exactly 0.0 (rather than inf/nan from the
would-be divide-by-zero).
- matches_bands_definition now uses an explicit assert_eq! on
is_some() agreement plus an if let for the numeric compare,
removing the unreachable panic arm without weakening the
invariant check.
bollinger_bandwidth.rs is now at 83/83 lines, no behavioural change.
This commit is contained in:
@@ -113,6 +113,27 @@ mod tests {
|
||||
assert!(BollingerBandwidth::new(20, -1.0).is_err());
|
||||
}
|
||||
|
||||
/// Cover the public const accessors `period`, `multiplier`, `value` and
|
||||
/// the Indicator-impl `warmup_period` + `name` methods. None of the
|
||||
/// pre-existing tests inspected the metadata surface — they only fed
|
||||
/// numeric updates and asserted on the bandwidth values, leaving the
|
||||
/// five getter bodies (lines 54-66, 90-92, 98-100) untouched.
|
||||
#[test]
|
||||
fn accessors_and_metadata() {
|
||||
let mut bbw = BollingerBandwidth::new(20, 2.0).unwrap();
|
||||
assert_eq!(bbw.period(), 20);
|
||||
assert_relative_eq!(bbw.multiplier(), 2.0, epsilon = 1e-12);
|
||||
// value() before warmup must be the literal None branch of self.last.
|
||||
assert_eq!(bbw.value(), None);
|
||||
assert_eq!(bbw.warmup_period(), 20);
|
||||
assert_eq!(bbw.name(), "BollingerBandwidth");
|
||||
// Drive past warmup so value() exercises the Some branch as well.
|
||||
for i in 1..=20 {
|
||||
bbw.update(f64::from(i));
|
||||
}
|
||||
assert!(bbw.value().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn constant_series_yields_zero() {
|
||||
// Flat prices: the bands collapse onto the middle, so width is 0.
|
||||
@@ -123,6 +144,23 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cover the defensive `o.middle == 0.0` branch in `update` (line 77).
|
||||
/// All other tests use price levels ≈100, so the rolling SMA is always
|
||||
/// strictly positive and the zero-middle fallback is unreachable. Feed
|
||||
/// a symmetric series whose 5-bar mean is exactly 0 to force the branch
|
||||
/// and assert the indicator yields exactly 0.0 (rather than inf/nan).
|
||||
#[test]
|
||||
fn zero_middle_band_yields_zero_bandwidth() {
|
||||
let mut bbw = BollingerBandwidth::new(5, 2.0).unwrap();
|
||||
// sum(-2, -1, 0, 1, 2) = 0 exactly in IEEE-754, so the SMA middle
|
||||
// lands on exactly 0.0 at the fifth input. Stddev > 0, so absent
|
||||
// the guard the next line would divide by zero.
|
||||
let out = bbw.batch(&[-2.0, -1.0, 0.0, 1.0, 2.0]);
|
||||
assert_eq!(out[..4], [None, None, None, None]);
|
||||
let v = out[4].expect("warmed up");
|
||||
assert_eq!(v, 0.0, "zero-middle fallback must emit exactly 0.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_bands_definition() {
|
||||
// Bandwidth must equal (upper - lower) / middle from BollingerBands.
|
||||
@@ -131,13 +169,11 @@ mod tests {
|
||||
.collect();
|
||||
let bbw_out = BollingerBandwidth::new(20, 2.0).unwrap().batch(&prices);
|
||||
let bands_out = BollingerBands::new(20, 2.0).unwrap().batch(&prices);
|
||||
for (w, b) in bbw_out.iter().zip(bands_out.iter()) {
|
||||
match (w, b) {
|
||||
(Some(wv), Some(bv)) => {
|
||||
assert_relative_eq!(*wv, (bv.upper - bv.lower) / bv.middle, epsilon = 1e-12);
|
||||
}
|
||||
(None, None) => {}
|
||||
_ => panic!("warmup mismatch"),
|
||||
for (i, (w, b)) in bbw_out.iter().zip(bands_out.iter()).enumerate() {
|
||||
// Same warmup period on both — emission shape must agree at every index.
|
||||
assert_eq!(w.is_some(), b.is_some(), "warmup mismatch at index {i}");
|
||||
if let (Some(wv), Some(bv)) = (w, b) {
|
||||
assert_relative_eq!(*wv, (bv.upper - bv.lower) / bv.middle, epsilon = 1e-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user