use super::*;
use crate::entropy::pattern_extractor::{AstPattern, Location, PatternCollection, PatternType};
use std::path::PathBuf;
fn repetitive_source(tag: &str) -> String {
let mut s = String::from("pub fn dispatch_TAG(v: i32) -> i32 {\n if v == 0 {\n 0\n");
for i in 1..8 {
s.push_str(&format!(" }} else if v == {i} {{\n {i}\n"));
}
s.push_str(" } else {\n -1\n }\n}\n");
s.replace("TAG", tag)
}
fn write_project(files: &[(&str, String)]) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let src = dir.path().join("src");
std::fs::create_dir_all(&src).expect("create src");
for (name, content) in files {
std::fs::write(src.join(name), content).expect("write file");
}
dir
}
fn many_repetitive_files(count: usize) -> Vec<(String, String)> {
(0..count)
.map(|i| (format!("m{i}.rs"), repetitive_source(&format!("f{i}"))))
.collect()
}
fn location(file: &str, line: usize) -> Location {
Location {
file: PathBuf::from(file),
line,
column: 1,
}
}
fn pattern_at(hash: &str, file: &str, line: usize, frequency: usize) -> AstPattern {
AstPattern {
pattern_type: PatternType::ControlFlow,
pattern_hash: hash.to_string(),
frequency,
locations: vec![location(file, line)],
variation_score: 0.0,
example_code: format!("// {file}"),
estimated_loc: frequency * 6,
}
}
#[tokio::test]
async fn test_analyze_is_byte_identical_across_repeated_runs() {
let files = many_repetitive_files(12);
let borrowed: Vec<(&str, String)> =
files.iter().map(|(n, c)| (n.as_str(), c.clone())).collect();
let dir = write_project(&borrowed);
let analyzer = EntropyAnalyzer::new();
let mut renders = Vec::new();
for _ in 0..5 {
let report = analyzer.analyze(dir.path()).await.expect("analyze");
renders.push(serde_json::to_string(&report).expect("serialize"));
}
for (i, render) in renders.iter().enumerate().skip(1) {
assert_eq!(
&renders[0], render,
"run {i} produced different output for identical input"
);
}
}
#[test]
fn test_add_pattern_merges_across_files_instead_of_overwriting() {
let mut collection = PatternCollection::new();
collection.add_pattern(pattern_at("h1", "a.rs", 10, 3));
collection.add_pattern(pattern_at("h1", "b.rs", 20, 4));
collection.add_pattern(pattern_at("h1", "c.rs", 30, 5));
assert_eq!(
collection.patterns.len(),
1,
"one structural hash is still one pattern"
);
let merged = collection.patterns.values().next().expect("pattern");
assert_eq!(
merged.frequency, 12,
"frequency must count every file's occurrences (3+4+5), not just the last file's"
);
assert_eq!(
PatternCollection::distinct_files(merged),
3,
"all three files must survive; overwriting left exactly one"
);
}
#[test]
fn test_add_pattern_merge_is_order_independent() {
let build = |order: [(&str, usize, usize); 3]| {
let mut c = PatternCollection::new();
for (file, line, freq) in order {
c.add_pattern(pattern_at("h1", file, line, freq));
}
c
};
let forward = build([("a.rs", 10, 3), ("b.rs", 20, 4), ("c.rs", 30, 5)]);
let reverse = build([("c.rs", 30, 5), ("b.rs", 20, 4), ("a.rs", 10, 3)]);
let f = forward.patterns.values().next().expect("pattern");
let r = reverse.patterns.values().next().expect("pattern");
assert_eq!(f.frequency, r.frequency);
assert_eq!(f.estimated_loc, r.estimated_loc);
assert_eq!(f.example_code, r.example_code);
assert_eq!(
f.locations
.iter()
.map(|l| (l.file.clone(), l.line))
.collect::<Vec<_>>(),
r.locations
.iter()
.map(|l| (l.file.clone(), l.line))
.collect::<Vec<_>>(),
"location order must not depend on insertion order"
);
}
#[tokio::test]
async fn test_violation_order_is_stable_across_runs() {
let files = many_repetitive_files(8);
let borrowed: Vec<(&str, String)> =
files.iter().map(|(n, c)| (n.as_str(), c.clone())).collect();
let dir = write_project(&borrowed);
let analyzer = EntropyAnalyzer::new();
let mut orders = Vec::new();
for _ in 0..5 {
let report = analyzer.analyze(dir.path()).await.expect("analyze");
orders.push(
report
.actionable_violations
.iter()
.map(|v| (v.message.clone(), v.affected_files.clone()))
.collect::<Vec<_>>(),
);
}
for (i, order) in orders.iter().enumerate().skip(1) {
assert_eq!(&orders[0], order, "violation order changed on run {i}");
}
}
#[tokio::test]
async fn test_small_project_metrics_measure_the_input() {
let dir = write_project(&[(
"lib.rs",
"pub fn a() -> i32 {\n 1\n}\npub fn b() -> i32 {\n 2\n}\npub fn c() -> i32 {\n 3\n}\n"
.to_string(),
)]);
let empty = tempfile::tempdir().expect("tempdir");
let analyzer = EntropyAnalyzer::new();
let populated = analyzer.analyze(dir.path()).await.expect("analyze");
let nothing = analyzer.analyze(empty.path()).await.expect("analyze");
assert_eq!(populated.total_files_analyzed, 1);
assert!(
populated.entropy_metrics.total_loc >= 9,
"9 non-blank source lines were read, but total_loc reported {}",
populated.entropy_metrics.total_loc
);
assert_eq!(
nothing.entropy_metrics.total_loc, 0,
"an empty directory really has zero source lines"
);
assert_ne!(
populated.entropy_metrics.total_loc, nothing.entropy_metrics.total_loc,
"populated and empty inputs must not produce the same metrics"
);
}
#[tokio::test]
async fn test_unmeasurable_entropy_is_absent_and_explained() {
let dir = write_project(&[("lib.rs", "pub fn a() -> i32 {\n 1\n}\n".to_string())]);
let report = EntropyAnalyzer::new()
.analyze(dir.path())
.await
.expect("analyze");
assert_eq!(report.entropy_metrics.total_patterns, 0);
assert!(
report.entropy_metrics.pattern_diversity.is_none(),
"diversity must be absent, not 0.0"
);
assert!(report.entropy_metrics.file_level_entropy.is_none());
assert!(report.entropy_metrics.module_level_entropy.is_none());
assert!(report.entropy_metrics.project_level_entropy.is_none());
let note = report
.measurement_note
.as_ref()
.expect("an absent measurement must be explained");
assert!(
note.contains("not measured"),
"note should say the entropy was not measured, got: {note}"
);
let json = serde_json::to_value(&report).expect("serialize");
assert!(
json["entropy_metrics"]["pattern_diversity"].is_null(),
"JSON must carry null, never a plausible-looking 0.0"
);
assert!(
report.pattern_summary.is_none(),
"no pattern was found, so there is no most-common pattern to report"
);
assert!(json["pattern_summary"].is_null());
}
#[tokio::test]
async fn test_measured_project_reports_values_without_note() {
let files = many_repetitive_files(6);
let borrowed: Vec<(&str, String)> =
files.iter().map(|(n, c)| (n.as_str(), c.clone())).collect();
let dir = write_project(&borrowed);
let report = EntropyAnalyzer::new()
.analyze(dir.path())
.await
.expect("analyze");
assert!(report.entropy_metrics.total_patterns > 0);
assert!(report.entropy_metrics.pattern_diversity.is_some());
assert!(report.measurement_note.is_none());
}
#[tokio::test]
async fn test_repetition_count_never_saturates() {
for n in [6usize, 10, 11, 12, 20, 40, 100] {
let mut body = String::from("pub fn check(v: &str) {\n");
for _ in 0..n {
body.push_str(" if v.is_empty() && v.len() > 0 { return; }\n");
}
body.push_str("}\n");
let dir = write_project(&[("lib.rs", body)]);
let report = EntropyAnalyzer::new()
.analyze(dir.path())
.await
.expect("analyze");
assert_eq!(
report.entropy_metrics.total_instances, n,
"{n} identical lines must report {n} instances"
);
}
}
#[tokio::test]
async fn test_repetition_message_and_saving_agree_about_the_input() {
let build = |copies: usize| {
let mut body = String::from("pub fn check(v: &str) {\n");
for _ in 0..copies {
body.push_str(" if v.is_empty() && v.len() > 0 { return; }\n");
}
body.push_str("}\n");
body
};
let small = build(6);
let large = build(100);
let analyzer = EntropyAnalyzer::new();
let a = analyzer
.analyze(write_project(&[("lib.rs", small)]).path())
.await
.expect("analyze");
let b = analyzer
.analyze(write_project(&[("lib.rs", large)]).path())
.await
.expect("analyze");
let reps = |r: &EntropyReport| -> usize {
r.actionable_violations
.iter()
.filter_map(|v| v.pattern.as_ref())
.map(|p| p.repetitions)
.max()
.unwrap_or(0)
};
let saving = |r: &EntropyReport| r.total_loc_reduction();
assert_eq!(reps(&a), 6, "6 identical lines must report 6");
assert_eq!(reps(&b), 100, "100 identical lines must report 100");
assert!(saving(&b) > saving(&a));
let expected_ratio = 99.0 / 5.0;
let actual_ratio = saving(&b) as f64 / saving(&a) as f64;
assert!(
(actual_ratio - expected_ratio).abs() < 1.0,
"saving must follow the stated repetition count: {} vs {} (ratio {actual_ratio:.2}, \
expected ~{expected_ratio:.2})",
saving(&a),
saving(&b)
);
}
#[tokio::test]
async fn test_reduction_never_exceeds_the_code_it_was_measured_from() {
let analyzer = EntropyAnalyzer::new();
for copies in [6usize, 20, 100, 400] {
let mut body = String::from("pub fn check(v: &str) {\n");
for _ in 0..copies {
body.push_str(" if v.is_empty() && v.len() > 0 { return; }\n");
}
body.push_str("}\n");
let dir = write_project(&[("lib.rs", body)]);
let report = analyzer.analyze(dir.path()).await.expect("analyze");
let loc = report.entropy_metrics.total_loc;
let reduction = report.total_loc_reduction();
assert!(
reduction <= loc,
"{copies} copies: reduction {reduction} exceeds the {loc} lines analyzed"
);
assert!(
report.reduction_percentage() <= 100.0,
"{copies} copies: reduction_percentage {} is above 100",
report.reduction_percentage()
);
}
let files = many_repetitive_files(20);
let borrowed: Vec<(&str, String)> =
files.iter().map(|(n, c)| (n.as_str(), c.clone())).collect();
let dir = write_project(&borrowed);
let report = analyzer.analyze(dir.path()).await.expect("analyze");
assert!(
report.reduction_percentage() <= 100.0,
"multi-file: reduction_percentage {} is above 100",
report.reduction_percentage()
);
}
#[tokio::test]
async fn test_measurement_note_states_the_enforced_thresholds() {
let dir = write_project(&[("lib.rs", "pub fn a() -> i32 {\n 1\n}\n".to_string())]);
let report = EntropyAnalyzer::new()
.analyze(dir.path())
.await
.expect("analyze");
let note = report.measurement_note.as_ref().expect("note");
for threshold in crate::entropy::pattern_extractor::RUST_PATTERN_THRESHOLDS {
let claim = format!("{} {}", threshold.name, threshold.effective_minimum());
assert!(
note.contains(&claim),
"note must quote the enforced threshold {claim:?}, got: {note}"
);
}
assert!(
!note.contains("at least 3 structurally identical"),
"the old blanket claim was false for four of six constructs: {note}"
);
}
#[tokio::test]
async fn test_quoted_thresholds_are_the_enforced_thresholds() {
let cases: [(&str, PatternType); 3] = [
(
" if v.is_empty() && v.len() > 0 { return; }\n",
PatternType::DataValidation,
),
(
" let _ = xs.iter().map(|x| x).count();\n",
PatternType::DataTransformation,
),
(" client.send(); // call\n", PatternType::ApiCall),
];
let analyzer = EntropyAnalyzer::new();
for (line, pattern_type) in cases {
let minimum =
crate::entropy::pattern_extractor::rust_threshold(pattern_type).effective_minimum();
for (copies, expect_pattern) in [(minimum - 1, false), (minimum, true)] {
let mut body = String::from("pub fn f(v: &str, xs: &[i32], client: &C) {\n");
for _ in 0..copies {
body.push_str(line);
}
body.push_str("}\n");
let dir = write_project(&[("lib.rs", body)]);
let report = analyzer.analyze(dir.path()).await.expect("analyze");
let found = report
.entropy_metrics
.patterns_by_type
.contains_key(&pattern_type);
assert_eq!(
found, expect_pattern,
"{pattern_type:?}: {copies} copies (threshold {minimum}) \
expected detected={expect_pattern}, got {found}"
);
}
}
}
#[tokio::test]
async fn test_low_diversity_finding_carries_no_fabricated_fields() {
let files = many_repetitive_files(6);
let borrowed: Vec<(&str, String)> =
files.iter().map(|(n, c)| (n.as_str(), c.clone())).collect();
let dir = write_project(&borrowed);
let config = EntropyConfig {
min_pattern_diversity: 0.99, ..Default::default()
};
let report = EntropyAnalyzer::with_config(config)
.analyze(dir.path())
.await
.expect("analyze");
let diversity = report
.actionable_violations
.iter()
.find(|v| v.message.starts_with("Low pattern diversity"))
.expect("a 99% requirement must not be silently satisfied");
assert!(
diversity.pattern.is_none(),
"a project-level finding must not carry a placeholder pattern summary"
);
assert!(
diversity.estimated_loc_reduction.is_none(),
"a saving that was never derived from a measured pattern size must be absent"
);
let json = serde_json::to_value(&report).expect("serialize");
let rendered = serde_json::to_string(&json).expect("string");
assert!(
!rendered.contains("Various repetitive patterns"),
"the placeholder example_code must be gone: {rendered}"
);
}
#[tokio::test]
async fn test_diversity_finding_reports_no_fixed_percentage_saving() {
let mut percentages = Vec::new();
for count in [4usize, 10, 24] {
let files = many_repetitive_files(count);
let borrowed: Vec<(&str, String)> =
files.iter().map(|(n, c)| (n.as_str(), c.clone())).collect();
let dir = write_project(&borrowed);
let config = EntropyConfig {
min_pattern_diversity: 0.99,
..Default::default()
};
let report = EntropyAnalyzer::with_config(config)
.analyze(dir.path())
.await
.expect("analyze");
let diversity_saving = report
.actionable_violations
.iter()
.find(|v| v.message.starts_with("Low pattern diversity"))
.expect("diversity finding")
.estimated_loc_reduction;
assert_eq!(diversity_saving, None);
percentages.push(report.reduction_percentage());
}
assert!(
percentages
.iter()
.any(|p| (p - percentages[0]).abs() > 0.01),
"reduction percentage was identical at every project size: {percentages:?}"
);
}
#[tokio::test]
async fn test_analysis_path_must_exist() {
let dir = tempfile::tempdir().expect("tempdir");
let missing = dir.path().join("no-such-directory");
let err = EntropyAnalyzer::new()
.analyze(&missing)
.await
.expect_err("a missing path must be an error, not an empty clean report");
assert!(
err.to_string().contains("does not exist"),
"error should name the missing path, got: {err}"
);
}