use std::path::Path;
use super::super::{build_git_report, build_predictive_churn_report};
use tokmd_analysis_types::TrendClass;
use tokmd_git::GitCommit;
use tokmd_types::{ChildIncludeMode, ExportData, FileKind, FileRow};
const DAY: i64 = 86_400;
const WEEK: i64 = 7 * DAY;
fn file_row(path: &str, module: &str, lines: usize) -> FileRow {
FileRow {
path: path.to_string(),
module: module.to_string(),
lang: "Rust".to_string(),
kind: FileKind::Parent,
code: lines,
comments: 0,
blanks: 0,
lines,
bytes: lines * 40,
tokens: lines * 3,
}
}
fn export(rows: Vec<FileRow>) -> ExportData {
ExportData {
rows,
module_roots: vec![],
module_depth: 1,
children: ChildIncludeMode::Separate,
}
}
fn commit(ts: i64, author: &str, subject: &str, files: &[&str]) -> GitCommit {
GitCommit {
timestamp: ts,
author: author.to_string(),
hash: None,
subject: subject.to_string(),
files: files.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn scenario_churn_decreasing_activity_falling() {
let exp = export(vec![file_row("src/lib.rs", "src", 100)]);
let mut commits = Vec::new();
for w in 1..=5i64 {
let count = 6 - w; for _ in 0..count {
commits.push(commit(w * WEEK, "alice", "feat: less", &["src/lib.rs"]));
}
}
let report = build_predictive_churn_report(&exp, &commits, Path::new("."));
let trend = report.per_module.get("src").expect("module present");
assert!(
trend.slope < 0.0,
"decreasing activity should have negative slope, got {}",
trend.slope
);
assert_eq!(trend.classification, TrendClass::Falling);
}
#[test]
fn scenario_churn_multiple_modules_independent() {
let exp = export(vec![
file_row("api/handler.rs", "api", 100),
file_row("db/query.rs", "db", 80),
]);
let commits = vec![
commit(WEEK, "alice", "feat: api1", &["api/handler.rs"]),
commit(2 * WEEK, "alice", "feat: api2", &["api/handler.rs"]),
commit(3 * WEEK, "alice", "feat: api3", &["api/handler.rs"]),
commit(WEEK, "bob", "feat: db", &["db/query.rs"]),
];
let report = build_predictive_churn_report(&exp, &commits, Path::new("."));
assert!(report.per_module.contains_key("api"));
assert!(report.per_module.contains_key("db"));
let db_trend = report.per_module.get("db").unwrap();
assert_eq!(db_trend.slope, 0.0);
assert_eq!(db_trend.classification, TrendClass::Flat);
}
#[test]
fn scenario_coupling_sorted_by_count_desc() {
let exp = export(vec![
file_row("a/f.rs", "a", 50),
file_row("b/f.rs", "b", 50),
file_row("c/f.rs", "c", 50),
]);
let commits = vec![
commit(1000, "alice", "feat: ab1", &["a/f.rs", "b/f.rs"]),
commit(2000, "alice", "feat: ab2", &["a/f.rs", "b/f.rs"]),
commit(3000, "alice", "feat: ab3", &["a/f.rs", "b/f.rs"]),
commit(4000, "bob", "feat: ac1", &["a/f.rs", "c/f.rs"]),
commit(5000, "bob", "feat: bc1", &["b/f.rs", "c/f.rs"]),
commit(6000, "bob", "feat: bc2", &["b/f.rs", "c/f.rs"]),
];
let report = build_git_report(Path::new("."), &exp, &commits).unwrap();
assert_eq!(report.coupling.len(), 3);
assert_eq!(report.coupling[0].count, 3);
assert_eq!(report.coupling[1].count, 2);
assert_eq!(report.coupling[2].count, 1);
}
#[test]
fn scenario_intent_unconventional_messages_high_unknown_pct() {
let exp = export(vec![file_row("src/lib.rs", "src", 100)]);
let commits = vec![
commit(1000, "alice", "updated the thing", &["src/lib.rs"]),
commit(2000, "bob", "misc changes", &["src/lib.rs"]),
commit(3000, "charlie", "wip", &["src/lib.rs"]),
commit(4000, "alice", "feat: one conventional", &["src/lib.rs"]),
];
let report = build_git_report(Path::new("."), &exp, &commits).unwrap();
let intent = report.intent.as_ref().expect("intent present");
assert_eq!(intent.overall.other, 3);
assert_eq!(intent.overall.feat, 1);
assert_eq!(intent.overall.total, 4);
assert_eq!(intent.unknown_pct, 0.75);
}
#[test]
fn scenario_bus_factor_sort_order() {
let exp = export(vec![
file_row("z/f.rs", "z", 50),
file_row("a/f.rs", "a", 50),
file_row("m/f.rs", "m", 50),
]);
let commits = vec![
commit(1000, "alice", "feat: z", &["z/f.rs"]),
commit(2000, "bob", "fix: z", &["z/f.rs"]),
commit(3000, "charlie", "feat: a", &["a/f.rs"]),
commit(4000, "dave", "fix: a", &["a/f.rs"]),
commit(5000, "eve", "feat: m", &["m/f.rs"]),
];
let report = build_git_report(Path::new("."), &exp, &commits).unwrap();
assert_eq!(report.bus_factor.len(), 3);
assert_eq!(report.bus_factor[0].module, "m"); assert_eq!(report.bus_factor[0].authors, 1);
assert_eq!(report.bus_factor[1].module, "a"); assert_eq!(report.bus_factor[1].authors, 2);
assert_eq!(report.bus_factor[2].module, "z"); assert_eq!(report.bus_factor[2].authors, 2);
}
#[test]
fn scenario_freshness_all_stale() {
let now = 1000 * DAY;
let exp = export(vec![
file_row("src/a.rs", "src", 50),
file_row("src/b.rs", "src", 50),
]);
let commits = vec![
commit(now - 500 * DAY, "alice", "feat: a", &["src/a.rs"]),
commit(now - 400 * DAY, "bob", "feat: b", &["src/b.rs"]),
commit(now, "charlie", "feat: other", &["untracked.rs"]),
];
let report = build_git_report(Path::new("."), &exp, &commits).unwrap();
assert_eq!(report.freshness.stale_files, 2);
assert_eq!(report.freshness.total_files, 2);
assert_eq!(report.freshness.stale_pct, 1.0);
}
#[test]
fn scenario_freshness_module_p90_positive() {
let now = 500 * DAY;
let exp = export(vec![
file_row("src/a.rs", "src", 50),
file_row("src/b.rs", "src", 50),
file_row("src/c.rs", "src", 50),
]);
let commits = vec![
commit(now, "alice", "feat: fresh", &["src/a.rs"]),
commit(now - 100 * DAY, "bob", "feat: medium", &["src/b.rs"]),
commit(now - 200 * DAY, "charlie", "feat: old", &["src/c.rs"]),
];
let report = build_git_report(Path::new("."), &exp, &commits).unwrap();
assert_eq!(report.freshness.by_module.len(), 1);
let module = &report.freshness.by_module[0];
assert_eq!(module.module, "src");
assert!(module.avg_days > 0.0, "avg_days should be positive");
assert!(module.p90_days > 0.0, "p90_days should be positive");
assert!(module.p90_days >= module.avg_days, "p90 should be >= avg");
}
#[test]
fn scenario_churn_r2_bounded() {
let exp = export(vec![file_row("src/lib.rs", "src", 100)]);
let commits: Vec<GitCommit> = (1..=10)
.map(|i| commit(i * WEEK, "alice", "feat: weekly", &["src/lib.rs"]))
.collect();
let report = build_predictive_churn_report(&exp, &commits, Path::new("."));
let trend = report.per_module.get("src").expect("module present");
assert!(
trend.r2 >= 0.0 && trend.r2 <= 1.0,
"r2 should be bounded [0,1], got {}",
trend.r2
);
}