#![allow(unused)]
#![cfg_attr(coverage_nightly, coverage(off))]
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use trueno_graph::{pagerank, CsrGraph, NodeId};
include!("metric_trends_types.rs");
include!("metric_trends_core.rs");
include!("metric_trends_prediction.rs");
include!("metric_trends_io.rs");
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metric_trend_store_creation() {
let temp_dir = std::env::temp_dir().join("pmat-test-trends-creation");
let store = MetricTrendStore::from_path(&temp_dir);
assert!(store.is_ok());
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_record_and_trend() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-trends").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..10 {
let value = 30000.0 - (i as f64 * 500.0); let ts = now - ((9 - i) * 86400); store.record("lint", value, ts).unwrap();
}
let trend = store.trend("lint", 30).unwrap();
assert_eq!(trend.count, 10);
assert!(trend.slope < 0.0, "Slope should be negative (improving)");
assert_eq!(trend.direction, TrendDirection::Improving);
}
#[test]
fn test_trend_regressing() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-trends-2").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..10 {
let value = 20000.0 + (i as f64 * 500.0); let ts = now - ((9 - i) * 86400);
store.record("test-fast", value, ts).unwrap();
}
let trend = store.trend("test-fast", 30).unwrap();
assert!(trend.slope > 0.0, "Slope should be positive (regressing)");
assert_eq!(trend.direction, TrendDirection::Regressing);
}
#[test]
fn test_csr_graph_storage() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-csr").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..5 {
let value = 25000.0;
let ts = now - ((4 - i) * 86400);
store.record("lint", value, ts).unwrap();
}
assert_eq!(
store.graph.num_nodes(),
5,
"Should have 5 nodes in CSR graph"
);
assert_eq!(store.node_map.len(), 5, "Should have 5 node mappings");
assert_eq!(
store.reverse_node_map.len(),
5,
"Should have 5 reverse mappings"
);
assert_eq!(store.next_node_id, 5, "Next node ID should be 5");
}
#[test]
fn test_pagerank_hotness() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-pagerank").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..10 {
let value = 25000.0;
let ts = now - ((9 - i) * 86400);
store.record("lint", value, ts).unwrap();
}
for i in 0..3 {
let value = 300000.0;
let ts = now - ((2 - i) * 86400);
store.record("coverage", value, ts).unwrap();
}
store.update_hotness().unwrap();
assert!(
!store.hotness_cache.is_empty(),
"Hotness cache should have at least one metric"
);
let hot = store.hot_metrics();
assert!(
!hot.is_empty(),
"Should have at least 1 metric with hotness score, got {}",
hot.len()
);
assert!(
hot.iter()
.any(|(name, _)| name == "lint" || name == "coverage"),
"Should include at least one of lint or coverage"
);
for (name, score) in &hot {
assert!(*score > 0.0, "{} should have non-zero PageRank score", name);
}
}
#[test]
fn test_dual_write_consistency() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-dual-write").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..5 {
let value = 24000.0 + (i as f64 * 100.0);
let ts = now - ((4 - i) * 86400);
store.record("lint", value, ts).unwrap();
}
assert_eq!(
store.cache.get("lint").unwrap().len(),
5,
"Cache should have 5 observations"
);
assert_eq!(store.graph.num_nodes(), 5, "CSR graph should have 5 nodes");
let json_path = store.storage_path.join("lint.json");
assert!(json_path.exists(), "JSON file should exist");
let json_content = std::fs::read_to_string(&json_path).unwrap();
let json_obs: Vec<MetricObservation> = serde_json::from_str(&json_content).unwrap();
assert_eq!(json_obs.len(), 5, "JSON should have 5 observations");
}
#[test]
fn test_linear_model_training() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-linear-model").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..30 {
let value = 20000.0 + (i as f64 * 100.0);
let ts = now - ((29 - i) * 86400);
store.record("lint", value, ts).unwrap();
}
let observations = store.cache.get("lint").unwrap();
let model = store.train_linear_model(observations).unwrap();
assert!(
(model.slope - 100.0).abs() < 10.0,
"Slope should be ~100, got {}",
model.slope
);
assert!(
model.r_squared > 0.95,
"R² should be >0.95 for linear data, got {}",
model.r_squared
);
}
#[test]
fn test_threshold_breach_prediction() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-breach-pred").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..20 {
let value = 21000.0 + (i as f64 * 200.0);
let ts = now - ((19 - i) * 86400);
store.record("lint", value, ts).unwrap();
}
let prediction = store
.predict_threshold_breach("lint", 30_000.0, 30)
.unwrap();
assert!(prediction.breach_in_days.is_some(), "Should predict breach");
let days = prediction.breach_in_days.unwrap();
assert!(
(20..=30).contains(&days),
"Breach should be in 20-30 days, got {}",
days
);
assert!(
prediction.confidence > 0.85,
"Confidence should be >0.85, got {}",
prediction.confidence
);
assert!(
!prediction.recommendations.is_empty(),
"Should have recommendations"
);
}
#[test]
fn test_no_breach_prediction() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-no-breach-pred").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..20 {
let value = 30000.0 - (i as f64 * 200.0);
let ts = now - ((19 - i) * 86400);
store.record("lint", value, ts).unwrap();
}
let prediction = store
.predict_threshold_breach("lint", 35_000.0, 30)
.unwrap();
assert!(
prediction.breach_in_days.is_none(),
"Should not predict breach for improving trend"
);
assert!(
prediction
.recommendations
.iter()
.any(|r| r.contains("Continue")),
"Should recommend continuing current practices"
);
}
#[test]
fn test_forecast_generation() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-forecast").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..15 {
let value = 20000.0 + (i as f64 * 150.0);
let ts = now - ((14 - i) * 86400);
store.record("lint", value, ts).unwrap();
}
let prediction = store
.predict_threshold_breach("lint", 50_000.0, 30)
.unwrap();
assert_eq!(
prediction.forecast.len(),
30,
"Should have 30 forecast points"
);
for (i, point) in prediction.forecast.iter().enumerate() {
assert_eq!(point.days_ahead, i + 1, "Days ahead should match index + 1");
assert!(
point.lower_bound <= point.predicted_value,
"Lower bound should be <= prediction (got {}, predicted {})",
point.lower_bound,
point.predicted_value
);
assert!(
point.upper_bound >= point.predicted_value,
"Upper bound should be >= prediction (got {}, predicted {})",
point.upper_bound,
point.predicted_value
);
}
}
#[test]
fn test_recommendations_urgency() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-urgency").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..10 {
let value = 20000.0 + (i as f64 * 1000.0);
let ts = now - ((9 - i) * 86400);
store.record("lint", value, ts).unwrap();
}
let prediction = store
.predict_threshold_breach("lint", 35_000.0, 30)
.unwrap();
assert!(
prediction
.recommendations
.iter()
.any(|r| r.contains("URGENT")),
"Should have URGENT recommendation for imminent breach"
);
assert!(
prediction
.recommendations
.iter()
.any(|r| r.contains("dependencies") || r.contains("clippy")),
"Should have lint-specific recommendations"
);
}
#[test]
fn test_predict_breach_insufficient_observations() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-insufficient").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..5 {
let ts = now - ((4 - i) * 86400); store
.record("lint", 1000.0 + (i as f64 * 100.0), ts)
.unwrap();
}
let result = store.predict_threshold_breach("lint", 5000.0, 30);
assert!(result.is_err(), "Should fail with < 7 observations");
assert!(
result.unwrap_err().to_string().contains("at least 7"),
"Error should mention minimum observations"
);
}
#[test]
fn test_predict_breach_insufficient_recent_observations() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-insufficient-recent").unwrap();
let now = chrono::Utc::now().timestamp();
let old_base = now - (100 * 86400); for i in 0..10 {
let ts = old_base + (i * 86400); store
.record("lint", 1000.0 + (i as f64 * 100.0), ts)
.unwrap();
}
let result = store.predict_threshold_breach("lint", 5000.0, 30);
assert!(result.is_err(), "Should fail with no recent observations");
assert!(
result.unwrap_err().to_string().contains("in last 90 days"),
"Error should mention 90-day window"
);
}
#[test]
fn test_predict_breach_exactly_7_observations() {
let mut store = MetricTrendStore::from_path("/tmp/pmat-test-exactly-7").unwrap();
let now = chrono::Utc::now().timestamp();
for i in 0..7 {
let ts = now - ((6 - i) * 86400); store
.record("lint", 1000.0 + (i as f64 * 200.0), ts)
.unwrap();
}
let result = store.predict_threshold_breach("lint", 5000.0, 30);
assert!(result.is_ok(), "Should succeed with exactly 7 observations");
let prediction = result.unwrap();
assert_eq!(prediction.metric, "lint");
assert!(
prediction.current_value > 0.0,
"Should have valid current value"
);
}
}