1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
//! Regressions for the two contradictions the dead-code report shipped with:
//! a suppressed function that belonged to no category (#928) and a `--timeout`
//! that was printed and never enforced (#929).
//!
//! Both run a real `cargo check` over a throwaway crate, because both defects
//! lived in the seam between this handler and the compiler — a hand-built
//! `FileDeadCode` would have passed on the broken code.
use super::{run_dead_code_analysis_with_filters, DeadCodeAnalysisFilters};
use std::time::{Duration, Instant};
fn filters(min_dead_lines: usize) -> DeadCodeAnalysisFilters {
DeadCodeAnalysisFilters {
include_unreachable: false,
include_tests: false,
min_dead_lines,
top_files: None,
include: Vec::new(),
exclude: Vec::new(),
max_depth: 10,
}
}
fn write_crate(root: &std::path::Path, name: &str, lib_rs: &str, build_rs: Option<&str>) {
std::fs::create_dir_all(root.join("src")).expect("src dir");
let build_line = if build_rs.is_some() {
"build=\"build.rs\"\n"
} else {
""
};
std::fs::write(
root.join("Cargo.toml"),
format!("[package]\nname=\"{name}\"\nversion=\"0.1.0\"\nedition=\"2021\"\n{build_line}"),
)
.expect("Cargo.toml");
std::fs::write(root.join("src/lib.rs"), lib_rs).expect("lib.rs");
if let Some(build) = build_rs {
std::fs::write(root.join("build.rs"), build).expect("build.rs");
}
}
/// The report listed six dead functions and headed them with
/// `dead_functions: 0, dead_classes: 0, dead_modules: 0, unreachable_blocks: 0`
/// — the twelve dead lines belonged to no category at all — and typed each one
/// `item_type: "variable"` in a record whose own `reason` said `fn`. The cause
/// was `DeadCodeKind::Suppressed`, a "kind" that replaced the item's real kind
/// with the way it had been discovered.
#[tokio::test]
async fn a_suppressed_function_is_counted_and_typed_as_a_function() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let root = tmp.path();
// The attribute is assembled rather than written literally: pmat's own
// suppression scanner would otherwise find this file and report it.
let allow = format!("#[allow({})]", "dead_code");
write_crate(
root,
"suppressed_fn_crate",
&format!(
"{allow}\nfn admitted_dead(x: i32) -> i32 {{ x + 1 }}\npub fn used() -> i32 {{ 1 }}\n"
),
None,
);
// Generous on purpose: this test asserts what the analysis FOUND, not how
// fast it ran, and the budget is wall clock over a `cargo check` of a fresh
// crate. At 120s it failed in CI — not because the two-function fixture is
// slow, but because a runner executing ~19,800 tests starved the blocking
// task. A timing bound in a test that is not about timing is a flake with no
// upside; the one test that IS about the budget uses 1 second, below.
let outcome = run_dead_code_analysis_with_filters(root, filters(0), Duration::from_secs(600))
.await
.expect("analysis runs");
let summary = &outcome.report.summary;
let items: Vec<_> = outcome
.report
.files
.iter()
.flat_map(|f| f.items.iter())
.collect();
assert_eq!(
items.len(),
1,
"expected the one suppressed item: {:?}",
outcome.report.files
);
assert_eq!(
items[0].item_type,
crate::models::dead_code::DeadCodeType::Function,
"an item whose reason says `fn` was typed {:?}",
items[0].item_type
);
assert_eq!(
summary.dead_functions, 1,
"the summary counts 0 dead functions over a listed dead function: {summary:?}"
);
// The contradiction in one assertion: nothing may be listed that no counter
// accounts for.
assert!(
summary.dead_functions + summary.dead_classes + summary.dead_modules > 0,
"{} dead lines in {} files, and every category counter is 0: {summary:?}",
summary.total_dead_lines,
summary.files_with_dead_code
);
}
/// `--timeout N` printed "⏰ Analysis timeout set to N seconds" and ran to
/// completion: the work was a blocking `Command::output()` inside an `async`
/// block, so neither of the two `tokio::time::timeout`s wrapped around it could
/// fire. Measured at 20.2s under `--timeout 1`, exit 0.
///
/// The 20-second sleep is in a build script, so `cargo check` is deterministically
/// slower than the budget without depending on machine speed.
#[tokio::test(flavor = "multi_thread")]
async fn a_cargo_check_that_outruns_the_budget_is_killed_and_reported() {
let tmp = tempfile::TempDir::new().expect("tempdir");
let root = tmp.path();
write_crate(
root,
"slowcheck_crate",
"fn dead_one() -> i32 { 1 }\npub fn used() -> i32 { 2 }\n",
Some("fn main() { std::thread::sleep(std::time::Duration::from_secs(20)); }\n"),
);
let started = Instant::now();
let result =
run_dead_code_analysis_with_filters(root, filters(0), Duration::from_secs(1)).await;
let elapsed = started.elapsed();
let error = result
.err()
.unwrap_or_else(|| panic!("--timeout 1 ran the 20s check to completion in {elapsed:?}"));
assert!(
error.to_string().contains("timed out after 1 seconds"),
"unexpected error: {error}"
);
// Generous, because the budget only starts once cargo is spawned and the
// build script has to be compiled first; the point is that it is nowhere
// near the 20s the check itself takes.
assert!(
elapsed < Duration::from_secs(15),
"the budget was not enforced: {elapsed:?}"
);
}
/// #928 RESIDUAL. Every `DeadCodeKind` the parser can produce must reach a
/// `DeadCodeType` that NAMES it. `Module` and the unclassified `Other` both used
/// to land on `Variable`, so a record could read
/// `"item_type": "variable"` beside `"reason": "module `x` is never used"` —
/// the report contradicting itself inside one object.
///
/// This is a pure mapping test on purpose: rustc emits the `module` wording
/// rarely enough that a fixture cannot be relied on to produce one, while the
/// parser accepts it unconditionally (`("module `", "` is never used", …)`).
#[test]
fn every_dead_code_kind_maps_to_a_type_that_names_it() {
use crate::models::dead_code::DeadCodeType;
use crate::services::cargo_dead_code_analyzer::{DeadCodeKind, DeadItem};
let item = |kind: DeadCodeKind, message: &str| DeadItem {
name: "x".to_string(),
kind,
line: 1,
column: 1,
message: message.to_string(),
};
let cases = [
(
item(DeadCodeKind::Module, "module `x` is never used"),
DeadCodeType::Module,
),
(
item(
DeadCodeKind::Other("union".to_string()),
"union `x` is never used",
),
DeadCodeType::Other,
),
(
item(DeadCodeKind::Constant, "constant `x` is never used"),
DeadCodeType::Variable,
),
(
item(DeadCodeKind::Function, "function `x` is never used"),
DeadCodeType::Function,
),
];
for (dead_item, expected) in cases {
let reason = dead_item.message.clone();
let reported = super::dead_items_to_report_items(std::slice::from_ref(&dead_item));
assert_eq!(
reported[0].item_type, expected,
"`{reason}` must not be reported as {:?}",
reported[0].item_type
);
}
}