test: 100% coverage for vertical_horizontal_filter + z_score + vpt + csv + adl (#31)
* 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).
This commit is contained in:
@@ -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`.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user