From d55d3db3d11ec0ff26bf3a58f584fb7b9f025d91 Mon Sep 17 00:00:00 2001 From: kingchenc Date: Sun, 24 May 2026 00:48:02 +0200 Subject: [PATCH] test: 100% coverage for vertical_horizontal_filter + z_score + vpt + csv + adl (#31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(vertical_horizontal_filter): cover period accessor + name metadata Codecov flagged 6 lines (file at 94.44%): period (61-63) + name (119-121). * test(z_score): cover period accessor + name metadata Codecov flagged 6 lines (file at 93.75%): period (59-61) + name (106-108). * test(vpt): cover value() Some branch, name, zero-prev fallback Codecov flagged 5 lines (file at 94.38%): value() Some branch (57), prev==0.0 ROC fallback (77), and Indicator-impl name (100-102). Add accessors_and_metadata covering value()/name and zero_previous_ close_contributes_zero — feeding a 0.0 baseline + non-zero candle proves the divide-by-zero guard yields a 0 contribution rather than NaN. * test(csv): cover from_csv_reader + kill rejects_header dead panic arm Codecov flagged 5 lines in csv.rs (file at 96.98%): from_csv_reader (201-204) — never called by existing tests which use from_reader / open — and the cold arm in rejects_header_missing_a_column (279). Add from_csv_reader_accepts_a_ prebuilt_reader (demonstrates the API by building a custom-delimited csv::Reader and passing it in), and refactor the header-missing test to use a single matches!() assertion so the panic arm is gone. * test(adl): cover name metadata Codecov flagged 3 lines (file at 96.84%): Indicator-impl name body (94-96). --- crates/wickra-core/src/indicators/adl.rs | 8 ++++++ .../indicators/vertical_horizontal_filter.rs | 9 ++++++ crates/wickra-core/src/indicators/vpt.rs | 24 ++++++++++++++++ crates/wickra-core/src/indicators/z_score.rs | 9 ++++++ crates/wickra-data/src/csv.rs | 28 ++++++++++++++++--- 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/crates/wickra-core/src/indicators/adl.rs b/crates/wickra-core/src/indicators/adl.rs index f325f456..a13928ae 100644 --- a/crates/wickra-core/src/indicators/adl.rs +++ b/crates/wickra-core/src/indicators/adl.rs @@ -127,6 +127,14 @@ mod tests { assert!(adl.update(candle(8.0, 10.0, 8.0, 9.0, 50.0, 0)).is_some()); } + /// Cover the Indicator-impl `name` body (94-96). The other accessors + /// are exercised by existing tests; `name` was never queried. + #[test] + fn accessors_and_metadata() { + let adl = Adl::new(); + assert_eq!(adl.name(), "ADL"); + } + #[test] fn close_at_high_accumulates_full_volume() { // Every bar closes at its high: MFM = +1, so ADL grows by `volume`. diff --git a/crates/wickra-core/src/indicators/vertical_horizontal_filter.rs b/crates/wickra-core/src/indicators/vertical_horizontal_filter.rs index b1c72a53..0c55e098 100644 --- a/crates/wickra-core/src/indicators/vertical_horizontal_filter.rs +++ b/crates/wickra-core/src/indicators/vertical_horizontal_filter.rs @@ -177,6 +177,15 @@ mod tests { assert!(VerticalHorizontalFilter::new(0).is_err()); } + /// Cover the const accessor `period` (61-63) and the Indicator-impl + /// `name` body (119-121). `warmup_period` is exercised elsewhere. + #[test] + fn accessors_and_metadata() { + let vhf = VerticalHorizontalFilter::new(28).unwrap(); + assert_eq!(vhf.period(), 28); + assert_eq!(vhf.name(), "VerticalHorizontalFilter"); + } + #[test] fn reset_clears_state() { let mut vhf = VerticalHorizontalFilter::new(8).unwrap(); diff --git a/crates/wickra-core/src/indicators/vpt.rs b/crates/wickra-core/src/indicators/vpt.rs index 6005811d..0bf58c9d 100644 --- a/crates/wickra-core/src/indicators/vpt.rs +++ b/crates/wickra-core/src/indicators/vpt.rs @@ -129,6 +129,30 @@ mod tests { assert_relative_eq!(out[2].unwrap(), 20.0 - 600.0 / 11.0, epsilon = 1e-12); } + /// Cover the `value()` Some branch (line 57) and the Indicator-impl + /// `name` body (100-102). `reset_clears_state` hits only the None + /// branch; the name was never queried. + #[test] + fn accessors_and_metadata() { + let mut vpt = VolumePriceTrend::new(); + assert_eq!(vpt.name(), "VPT"); + assert_eq!(vpt.value(), None); + vpt.update(candle(100.0, 50.0, 0)); + assert_eq!(vpt.value(), Some(0.0)); + } + + /// Cover the `prev == 0.0` defensive branch (line 77) — the previous + /// close is exactly 0, making the percentage ROC undefined. The + /// indicator must contribute 0 to the running total rather than NaN. + #[test] + fn zero_previous_close_contributes_zero() { + let mut vpt = VolumePriceTrend::new(); + vpt.update(candle(0.0, 100.0, 0)); // baseline; prev_close = 0 + let v = vpt.update(candle(50.0, 200.0, 1)).expect("emits"); + // ROC fallback is 0, so total stays at 0. + assert_eq!(v, 0.0); + } + #[test] fn emits_from_first_candle_at_zero() { let mut vpt = VolumePriceTrend::new(); diff --git a/crates/wickra-core/src/indicators/z_score.rs b/crates/wickra-core/src/indicators/z_score.rs index 0603894f..eba8902d 100644 --- a/crates/wickra-core/src/indicators/z_score.rs +++ b/crates/wickra-core/src/indicators/z_score.rs @@ -161,6 +161,15 @@ mod tests { assert!(ZScore::new(0).is_err()); } + /// Cover the const accessor `period` (59-61) and the Indicator-impl + /// `name` body (106-108). `warmup_period` is exercised elsewhere. + #[test] + fn accessors_and_metadata() { + let z = ZScore::new(20).unwrap(); + assert_eq!(z.period(), 20); + assert_eq!(z.name(), "ZScore"); + } + #[test] fn reset_clears_state() { let mut z = ZScore::new(5).unwrap(); diff --git a/crates/wickra-data/src/csv.rs b/crates/wickra-data/src/csv.rs index 904405b8..3b1e46ea 100644 --- a/crates/wickra-data/src/csv.rs +++ b/crates/wickra-data/src/csv.rs @@ -274,10 +274,30 @@ mod tests { // "volume" is absent. let data = "timestamp,open,high,low,close\n1,10.0,11.0,9.0,10.5\n"; let err = CandleReader::from_reader(data.as_bytes()).unwrap_err(); - match err { - Error::Malformed(msg) => assert!(msg.contains("volume"), "msg: {msg}"), - other => panic!("expected Malformed, got {other:?}"), - } + // The error variant must be Malformed and the message must mention + // the missing column. Asserting directly (rather than match-and- + // panic-on-other) keeps the assertion's cold path branch-free for + // coverage and still pins the diagnostic. + assert!( + matches!(&err, Error::Malformed(msg) if msg.contains("volume")), + "expected Malformed mentioning 'volume', got {err:?}" + ); + } + + /// Cover `from_csv_reader` (lines 201-204): existing tests use + /// `from_reader` / `open`, which both construct the inner `csv::Reader` + /// internally. Callers that want non-default csv configuration must + /// build the reader themselves and pass it through `from_csv_reader`. + #[test] + fn from_csv_reader_accepts_a_prebuilt_reader() { + let data = "timestamp;open;high;low;close;volume\n1;10.0;11.0;9.0;10.5;100\n"; + let inner = csv::ReaderBuilder::new() + .delimiter(b';') + .from_reader(data.as_bytes()); + let mut r = CandleReader::from_csv_reader(inner).unwrap(); + let candles = r.read_all().unwrap(); + assert_eq!(candles.len(), 1); + assert_eq!(candles[0].close, 10.5); } #[test]