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());
|
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]
|
#[test]
|
||||||
fn constant_series_yields_zero() {
|
fn constant_series_yields_zero() {
|
||||||
// Flat prices: the bands collapse onto the middle, so width is 0.
|
// 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]
|
#[test]
|
||||||
fn matches_bands_definition() {
|
fn matches_bands_definition() {
|
||||||
// Bandwidth must equal (upper - lower) / middle from BollingerBands.
|
// Bandwidth must equal (upper - lower) / middle from BollingerBands.
|
||||||
@@ -131,13 +169,11 @@ mod tests {
|
|||||||
.collect();
|
.collect();
|
||||||
let bbw_out = BollingerBandwidth::new(20, 2.0).unwrap().batch(&prices);
|
let bbw_out = BollingerBandwidth::new(20, 2.0).unwrap().batch(&prices);
|
||||||
let bands_out = BollingerBands::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()) {
|
for (i, (w, b)) in bbw_out.iter().zip(bands_out.iter()).enumerate() {
|
||||||
match (w, b) {
|
// Same warmup period on both — emission shape must agree at every index.
|
||||||
(Some(wv), Some(bv)) => {
|
assert_eq!(w.is_some(), b.is_some(), "warmup mismatch at index {i}");
|
||||||
assert_relative_eq!(*wv, (bv.upper - bv.lower) / bv.middle, epsilon = 1e-12);
|
if let (Some(wv), Some(bv)) = (w, b) {
|
||||||
}
|
assert_relative_eq!(*wv, (bv.upper - bv.lower) / bv.middle, epsilon = 1e-12);
|
||||||
(None, None) => {}
|
|
||||||
_ => panic!("warmup mismatch"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user