use crate::adapters::analyzers::iosp::{
CallOccurrence, Classification, FunctionAnalysis, LogicOccurrence,
};
use crate::adapters::report::test_support::{make_result, violation};
use crate::report::baseline::{create_baseline, print_comparison};
use crate::report::Summary;
fn make_summary(results: &[FunctionAnalysis]) -> Summary {
let mut s = Summary::from_results(results);
s.compute_quality_score(&crate::config::sections::DEFAULT_QUALITY_WEIGHTS);
s
}
fn baseline_json(results: &[FunctionAnalysis]) -> serde_json::Value {
let summary = make_summary(results);
serde_json::from_str(&create_baseline(results, &summary)).unwrap()
}
fn compare(old: &[FunctionAnalysis], new: &[FunctionAnalysis]) -> bool {
let baseline = create_baseline(old, &make_summary(old));
print_comparison(&baseline, new, &make_summary(new))
}
fn ab(b_class: Classification) -> Vec<FunctionAnalysis> {
vec![
make_result("a", Classification::Integration),
make_result("b", b_class),
]
}
#[test]
fn test_create_baseline_empty() {
let results: Vec<FunctionAnalysis> = vec![];
let summary = make_summary(&results);
let json = create_baseline(&results, &summary);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["violations"].as_u64().unwrap(), 0);
assert!(parsed["violation_details"].as_array().unwrap().is_empty());
}
#[test]
fn test_create_baseline_with_violations() {
let results = vec![
make_result("good_fn", Classification::Integration),
make_result(
"bad_fn",
Classification::Violation {
has_logic: true,
has_own_calls: true,
logic_locations: vec![LogicOccurrence {
kind: "if".into(),
line: 1,
}],
call_locations: vec![CallOccurrence {
name: "helper".into(),
line: 2,
}],
},
),
];
let summary = make_summary(&results);
let json = create_baseline(&results, &summary);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["violations"].as_u64().unwrap(), 1);
assert_eq!(parsed["violation_details"].as_array().unwrap().len(), 1);
}
#[test]
fn test_create_baseline_perfect_scores_are_one() {
let parsed = baseline_json(&ab(Classification::Operation));
for field in ["iosp_score", "quality_score"] {
let score = parsed[field].as_f64().unwrap();
assert!(
(score - 1.0).abs() < 1e-10,
"{field} should be 1.0 with no violations; got {score}"
);
}
}
#[test]
fn test_create_baseline_is_valid_json() {
let results = vec![make_result("f", Classification::Operation)];
let summary = make_summary(&results);
let json = create_baseline(&results, &summary);
let parsed: Result<serde_json::Value, _> = serde_json::from_str(&json);
assert!(parsed.is_ok(), "Baseline must be valid JSON");
}
#[test]
fn test_create_baseline_suppressed_excluded() {
let mut func = make_result(
"suppressed_fn",
Classification::Violation {
has_logic: true,
has_own_calls: true,
logic_locations: vec![LogicOccurrence {
kind: "if".into(),
line: 1,
}],
call_locations: vec![CallOccurrence {
name: "f".into(),
line: 2,
}],
},
);
func.suppressed = true;
let results = vec![func];
let summary = make_summary(&results);
let json = create_baseline(&results, &summary);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(
parsed["violation_details"].as_array().unwrap().is_empty(),
"Suppressed violations should not appear in baseline"
);
}
#[test]
fn test_print_comparison_detects_only_regressions() {
let cases: Vec<(&str, Vec<FunctionAnalysis>, Vec<FunctionAnalysis>, bool)> = vec![
(
"identical scores → no regression",
vec![make_result("a", Classification::Integration)],
vec![make_result("a", Classification::Integration)],
false,
),
(
"improvement (violation removed) → no regression",
ab(violation("if", "x")),
ab(Classification::Operation),
false,
),
(
"new violation (lower score) → regression",
ab(Classification::Operation),
ab(violation("if", "x")),
true,
),
];
for (label, old, new, regressed) in cases {
assert_eq!(compare(&old, &new), regressed, "case {label}");
}
}
#[test]
fn test_print_comparison_invalid_json() {
let results = vec![make_result("a", Classification::Integration)];
let summary = make_summary(&results);
let regressed = print_comparison("not valid json {{{", &results, &summary);
assert!(!regressed, "Invalid JSON should return false");
}
#[test]
fn test_create_baseline_v2_has_version() {
let results = vec![make_result("f", Classification::Operation)];
let summary = make_summary(&results);
let json = create_baseline(&results, &summary);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["version"].as_u64().unwrap(), 2);
}
#[test]
fn test_create_baseline_v2_has_all_dimensions() {
let results = vec![make_result("f", Classification::Operation)];
let mut summary = make_summary(&results);
summary.complexity_warnings = 1;
summary.duplicate_groups = 2;
summary.srp_struct_warnings = 1;
summary.coupling_warnings = 1;
summary.compute_quality_score(&crate::config::sections::DEFAULT_QUALITY_WEIGHTS);
let json = create_baseline(&results, &summary);
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["complexity_warnings"].as_u64().unwrap(), 1);
assert_eq!(parsed["duplicate_groups"].as_u64().unwrap(), 2);
assert_eq!(parsed["srp_struct_warnings"].as_u64().unwrap(), 1);
assert_eq!(parsed["coupling_warnings"].as_u64().unwrap(), 1);
assert_eq!(parsed["total_findings"].as_u64().unwrap(), 5);
}
#[test]
fn test_baseline_v1_compat_no_version() {
let v1_json = r#"{"iosp_score":1.0,"violations":0,"total":2,"violation_details":[]}"#;
let results = vec![make_result("a", Classification::Integration)];
let summary = make_summary(&results);
let regressed = print_comparison(v1_json, &results, &summary);
assert!(!regressed, "V1 baseline with same score should not regress");
}
#[test]
fn test_baseline_v2_regression_by_quality_score() {
let regressed = compare(&ab(Classification::Operation), &ab(violation("if", "x")));
assert!(regressed, "Quality score regression should be detected");
}
#[test]
fn test_format_score_delta_direction_and_magnitude() {
use crate::report::baseline::format_score_delta;
let up = format_score_delta("Quality", 80.0, 90.0);
assert!(up.contains('\u{2191}'), "score rise shows ↑: {up}");
assert!(up.contains("10.0%"), "delta magnitude is 10.0%: {up}");
let down = format_score_delta("Quality", 90.0, 80.0);
assert!(down.contains('\u{2193}'), "score drop shows ↓: {down}");
assert!(down.contains("10.0%"), "abs delta is 10.0%: {down}");
let same = format_score_delta("Quality", 50.0, 50.0);
assert!(
same.contains("unchanged"),
"equal scores are unchanged: {same}"
);
assert!(
!same.contains('\u{2191}') && !same.contains('\u{2193}'),
"unchanged shows no arrow: {same}"
);
}
#[test]
fn test_comparison_lines_pins_deltas_and_regression() {
use crate::report::baseline::comparison_lines;
let raw = serde_json::json!({
"version": 2,
"iosp_score": 0.5,
"violations": 2,
"total_findings": 3,
"quality_score": 0.8,
"tq_no_assertion_warnings": 1
});
let summary = Summary {
violations: 5,
iosp_score: 0.6,
quality_score: 0.7,
tq_no_assertion_warnings: 1,
tq_no_sut_warnings: 1,
tq_untested_warnings: 1,
tq_uncovered_warnings: 1,
tq_untested_logic_warnings: 1,
..Summary::default()
};
let (lines, regressed) = comparison_lines(&raw, &summary);
let joined = lines.join("\n");
assert!(joined.contains("(+3)"), "violation delta +3: {joined}");
assert!(joined.contains("(+7)"), "finding delta +7: {joined}");
assert!(
joined.contains("(+4)"),
"TQ delta +4 (Σ pins the 5-field sum): {joined}"
);
assert!(
joined.contains("50.0%"),
"old IOSP 0.5 → 50.0% (pins *MULTIPLIER): {joined}"
);
assert!(joined.contains("60.0%"), "new IOSP 0.6 → 60.0%: {joined}");
assert!(
joined.contains("80.0%"),
"old quality 0.8 → 80.0%: {joined}"
);
assert!(
joined.contains("70.0%"),
"new quality 0.7 → 70.0%: {joined}"
);
assert!(regressed, "quality 0.7 < baseline 0.8 → regressed");
}
#[test]
fn test_comparison_lines_v1_skips_findings_line() {
use crate::report::baseline::comparison_lines;
let raw = serde_json::json!({"iosp_score": 0.9, "violations": 0});
let summary = Summary {
iosp_score: 0.8,
..Summary::default()
};
let (lines, regressed) = comparison_lines(&raw, &summary);
let joined = lines.join("\n");
assert!(
!joined.contains("Findings:"),
"v1 has no findings line: {joined}"
);
assert!(regressed, "v1 IOSP 0.8 < 0.9 → regressed");
}