use crate::models::dead_code::{
ConfidenceLevel, DeadCodeItem, DeadCodeResult, DeadCodeSummary, DeadCodeType,
FileDeadCodeMetrics,
};
fn summary() -> DeadCodeSummary {
DeadCodeSummary {
total_files_analyzed: 40,
files_with_dead_code: 1,
total_dead_lines: 12,
dead_percentage: 3.0,
dead_functions: 1,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
}
}
fn listed_file() -> FileDeadCodeMetrics {
FileDeadCodeMetrics {
path: "examples/demo.rs".to_string(),
dead_lines: 12,
total_lines: 100,
dead_percentage: 12.0,
dead_functions: 1,
dead_classes: 0,
dead_modules: 0,
unreachable_blocks: 0,
dead_score: 12.0,
confidence: ConfidenceLevel::High,
items: vec![DeadCodeItem {
item_type: DeadCodeType::Function,
name: "helper".to_string(),
line: 3,
reason: "no callers".to_string(),
}],
}
}
fn narrowed_result() -> DeadCodeResult {
DeadCodeResult {
summary: summary(),
files: vec![listed_file()],
total_files: 50,
analyzed_files: 40,
files_with_dead_code_found: 3,
files_truncated: false,
}
}
fn render(result: &DeadCodeResult, scope: super::DeadCodeReportScope) -> String {
super::format_dead_code_as_summary_scoped(result, scope).expect("summary renders")
}
#[test]
fn the_skipped_line_repeats_the_analyzers_own_words() {
let rendered = render(
&narrowed_result(),
super::DeadCodeReportScope {
skipped_kind: Some("test code; --include-tests scans it too"),
..super::DeadCodeReportScope::default()
},
);
assert!(
rendered
.contains("Files skipped (out of scope): 10 (test code; --include-tests scans it too)"),
"{rendered}"
);
}
#[test]
fn the_skipped_line_does_not_claim_examples_or_benches_were_skipped() {
let rendered = render(
&narrowed_result(),
super::DeadCodeReportScope {
skipped_kind: Some("test code; --include-tests scans it too"),
..super::DeadCodeReportScope::default()
},
);
let skipped_line = rendered
.lines()
.find(|l| l.contains("Files skipped"))
.expect("the skipped line must be present");
assert!(
!skipped_line.contains("examples") && !skipped_line.contains("benches"),
"skipped line still names in-scope trees: {skipped_line:?}"
);
assert!(rendered.contains("examples/demo.rs"), "{rendered}");
}
#[test]
fn an_unattributed_skip_says_so() {
let rendered = render(&narrowed_result(), super::DeadCodeReportScope::default());
assert!(
rendered.contains("(the analyzer did not say which files)"),
"{rendered}"
);
}
#[test]
fn the_cargo_path_no_longer_hardcodes_examples_and_benches() {
let src = include_str!("dead_code_handlers_output.rs");
assert!(
!src.contains("tests, examples and benches"),
"the renderer must not hardcode a skipped-file category"
);
}
#[test]
fn an_unmeasured_project_percentage_is_disclosed_beside_the_figure() {
let rendered = render(&narrowed_result(), super::DeadCodeReportScope::default());
assert!(
rendered.contains("no project-wide figure was measured for this project"),
"the multi-language report printed a bare percentage the gate then \
refused to have measured: {rendered}"
);
}
#[test]
fn a_measured_project_percentage_is_printed_next_to_the_listed_one() {
let rendered = render(
&narrowed_result(),
super::DeadCodeReportScope {
project_dead_percentage: Some(7.5),
..super::DeadCodeReportScope::default()
},
);
assert!(
rendered.contains("project-wide: 7.5%"),
"the gate's figure must be visible beside the list-scoped one: {rendered}"
);
assert!(!rendered.contains("no project-wide figure"), "{rendered}");
}
#[test]
fn an_unnarrowed_report_carries_no_scope_note() {
let result = DeadCodeResult {
files_with_dead_code_found: 1,
..narrowed_result()
};
let rendered = render(
&result,
super::DeadCodeReportScope {
project_dead_percentage: Some(3.0),
..super::DeadCodeReportScope::default()
},
);
assert!(!rendered.contains("listed files only"), "{rendered}");
}
#[test]
fn a_filtered_report_says_the_filters_did_not_narrow_the_scan() {
let rendered = render(
&narrowed_result(),
super::DeadCodeReportScope {
project_dead_percentage: Some(3.0),
list_filtered: true,
..super::DeadCodeReportScope::default()
},
);
assert!(
rendered.contains("--include/--exclude filter this report, not the scan"),
"'Files analyzed: 40' must not read as a filtered count: {rendered}"
);
}
#[test]
fn the_omission_reason_names_the_filters_when_filters_ran() {
let rendered = render(
&narrowed_result(),
super::DeadCodeReportScope {
list_filtered: true,
..super::DeadCodeReportScope::default()
},
);
let line = rendered
.lines()
.find(|l| l.contains("Files found with dead code"))
.expect("the omission line must be present");
assert!(
line.contains("removed by --include/--exclude"),
"omission reason blames the wrong knob: {line:?}"
);
}
#[test]
fn the_omission_reason_omits_filters_that_did_not_run() {
let rendered = render(&narrowed_result(), super::DeadCodeReportScope::default());
let line = rendered
.lines()
.find(|l| l.contains("Files found with dead code"))
.expect("the omission line must be present");
assert!(!line.contains("--include/--exclude"), "{line:?}");
assert!(!line.contains("--top-files"), "{line:?}");
assert!(line.contains("below --min-dead-lines"), "{line:?}");
}
#[test]
fn the_multi_language_path_reports_one_analyzed_file_count() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let root = tmp.path();
std::fs::write(
root.join("used.py"),
"def used():\n return 1\n\nused()\n",
)
.expect("write used.py");
std::fs::write(root.join("dead.py"), "def never_called():\n return 2\n")
.expect("write dead.py");
std::fs::write(root.join("a.go"), "package main\nfunc main() {}\n").expect("write a.go");
std::fs::write(root.join("b.go"), "package main\nfunc helper() {}\n").expect("write b.go");
std::fs::write(root.join("c.ts"), "export function t() {}\n").expect("write c.ts");
let filters = super::DeadCodeAnalysisFilters {
include_unreachable: false,
include_tests: false,
min_dead_lines: 0,
top_files: None,
include: Vec::new(),
exclude: Vec::new(),
max_depth: 10,
};
let outcome =
super::run_multi_language_dead_code(root, &filters, "python").expect("analysis runs");
assert_eq!(
outcome.report.summary.total_files_analyzed, outcome.report.analyzed_files,
"stdout reads analyzed_files while stderr and JSON read \
summary.total_files_analyzed; they must be the same number"
);
assert!(
outcome.report.analyzed_files > 0,
"two readable .py files were analyzed, got {}",
outcome.report.analyzed_files
);
}
#[test]
fn the_multi_language_path_discloses_the_files_it_did_not_read() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let root = tmp.path();
std::fs::write(root.join("dead.py"), "def never_called():\n return 2\n")
.expect("write dead.py");
std::fs::write(root.join("a.go"), "package main\nfunc main() {}\n").expect("write a.go");
std::fs::write(root.join("b.go"), "package main\nfunc helper() {}\n").expect("write b.go");
std::fs::write(root.join("c.ts"), "export function t() {}\n").expect("write c.ts");
let filters = super::DeadCodeAnalysisFilters {
include_unreachable: false,
include_tests: false,
min_dead_lines: 0,
top_files: None,
include: Vec::new(),
exclude: Vec::new(),
max_depth: 10,
};
let outcome =
super::run_multi_language_dead_code(root, &filters, "python").expect("analysis runs");
assert!(
outcome.report.total_files > outcome.report.analyzed_files,
"3 of 4 source files went unread but total_files ({}) does not exceed \
analyzed_files ({})",
outcome.report.total_files,
outcome.report.analyzed_files
);
let rendered = render(&outcome.report, outcome.scope);
assert!(
rendered.contains("Files skipped (out of scope):"),
"{rendered}"
);
assert!(
rendered.contains("languages this run did not read"),
"{rendered}"
);
}
#[test]
fn count_source_files_counts_source_and_ignores_the_rest() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let root = tmp.path();
for name in ["a.py", "b.go", "c.ts", "d.rs"] {
std::fs::write(root.join(name), "x\n").expect("write source");
}
for name in ["README.md", "Cargo.lock", "notes.txt"] {
std::fs::write(root.join(name), "x\n").expect("write non-source");
}
assert_eq!(super::count_source_files(root), 4);
}