xbp-analytics 11.1.7

Deterministic delivery analytics primitives for XBP.
Documentation
use chrono::{Duration, Utc};
use xbp_analytics::{
    empirical_cdf, histogram, iqr, mad, mean, median, percentile, standard_deviation,
    AnalyticsError, DataQuality, DataQualityWarning, EvidenceCoverage, FlowMetrics, FlowSamples,
    TimeWindow,
};

#[test]
fn computes_descriptive_statistics() {
    let values = [1.0, 2.0, 3.0, 4.0, 100.0];

    assert_eq!(mean(&values).unwrap(), 22.0);
    assert_eq!(median(&values).unwrap(), 3.0);
    assert_eq!(percentile(&values, 0.8).unwrap(), 4.0);
    assert!(standard_deviation(&values).unwrap() > 38.0);
    assert_eq!(iqr(&values).unwrap(), 2.0);
    assert_eq!(mad(&values).unwrap(), 1.0);
}

#[test]
fn builds_histogram_and_empirical_cdf() {
    let values = [0.0, 1.0, 2.0, 3.0];

    assert_eq!(histogram(&values, 2).unwrap(), vec![2, 2]);
    assert_eq!(empirical_cdf(&values, 2.0).unwrap(), 0.75);
}

#[test]
fn empty_samples_return_an_explicit_error() {
    assert_eq!(mean(&[]).unwrap_err(), AnalyticsError::EmptySample);
}

#[test]
fn flow_metrics_keep_cycle_time_distinct_from_active_time() {
    let now = Utc::now();
    let metrics = FlowMetrics::from_samples(
        TimeWindow::new(now - Duration::days(14), now),
        14,
        10,
        3,
        FlowSamples {
            lead_times_hours: &[48.0],
            cycle_times_hours: &[24.0],
            active_times_hours: &[8.0],
            blocked_times_hours: &[4.0],
            review_wait_times_hours: &[6.0],
            deployment_wait_times_hours: &[2.0],
        },
    )
    .unwrap();

    assert_eq!(metrics.throughput_per_day, 1.0);
    assert_eq!(metrics.arrival_rate_per_day, 10.0 / 14.0);
    assert_eq!(metrics.lead_time_hours.mean.unwrap(), 48.0);
    assert_eq!(metrics.cycle_time_hours.mean.unwrap(), 24.0);
    assert_eq!(metrics.active_time_hours.mean.unwrap(), 8.0);
    assert_eq!(metrics.flow_efficiency.unwrap(), 8.0 / 24.0);
}

#[test]
fn data_quality_keeps_missing_evidence_explicit() {
    let quality = DataQuality::new(
        EvidenceCoverage::new(10, 8, 7),
        Some(18),
        7.0,
        vec!["started_at".into()],
        vec![DataQualityWarning::PartialStateHistory],
    );

    assert_eq!(quality.coverage.ratio, 0.7);
    assert_eq!(quality.missing_fields, vec!["started_at"]);
}

#[test]
fn evidence_coverage_rejects_inconsistent_denominators() {
    assert!(EvidenceCoverage::try_new(1, 2, 1).is_err());
    assert!(EvidenceCoverage::try_new(2, 1, 2).is_err());
}