]> git.proxmox.com Git - rustc.git/blob - src/tools/clippy/tests/compile-test.rs
New upstream version 1.73.0+dfsg1
[rustc.git] / src / tools / clippy / tests / compile-test.rs
1 #![feature(test)] // compiletest_rs requires this attribute
2 #![feature(lazy_cell)]
3 #![feature(is_sorted)]
4 #![cfg_attr(feature = "deny-warnings", deny(warnings))]
5 #![warn(rust_2018_idioms, unused_lifetimes)]
6 #![allow(unused_extern_crates)]
7
8 use compiletest::{status_emitter, CommandBuilder, OutputConflictHandling};
9 use ui_test as compiletest;
10 use ui_test::Mode as TestMode;
11
12 use std::collections::BTreeMap;
13 use std::env::{self, remove_var, set_var, var_os};
14 use std::ffi::{OsStr, OsString};
15 use std::fs;
16 use std::path::{Path, PathBuf};
17 use std::sync::LazyLock;
18 use test_utils::IS_RUSTC_TEST_SUITE;
19
20 // Test dependencies may need an `extern crate` here to ensure that they show up
21 // in the depinfo file (otherwise cargo thinks they are unused)
22 extern crate clippy_lints;
23 extern crate clippy_utils;
24 extern crate derive_new;
25 extern crate futures;
26 extern crate if_chain;
27 extern crate itertools;
28 extern crate parking_lot;
29 extern crate quote;
30 extern crate syn;
31 extern crate tokio;
32
33 /// All crates used in UI tests are listed here
34 static TEST_DEPENDENCIES: &[&str] = &[
35 "clippy_lints",
36 "clippy_utils",
37 "derive_new",
38 "futures",
39 "if_chain",
40 "itertools",
41 "parking_lot",
42 "quote",
43 "regex",
44 "serde_derive",
45 "serde",
46 "syn",
47 "tokio",
48 ];
49
50 /// Produces a string with an `--extern` flag for all UI test crate
51 /// dependencies.
52 ///
53 /// The dependency files are located by parsing the depinfo file for this test
54 /// module. This assumes the `-Z binary-dep-depinfo` flag is enabled. All test
55 /// dependencies must be added to Cargo.toml at the project root. Test
56 /// dependencies that are not *directly* used by this test module require an
57 /// `extern crate` declaration.
58 static EXTERN_FLAGS: LazyLock<Vec<String>> = LazyLock::new(|| {
59 let current_exe_depinfo = {
60 let mut path = env::current_exe().unwrap();
61 path.set_extension("d");
62 fs::read_to_string(path).unwrap()
63 };
64 let mut crates = BTreeMap::<&str, &str>::new();
65 for line in current_exe_depinfo.lines() {
66 // each dependency is expected to have a Makefile rule like `/path/to/crate-hash.rlib:`
67 let parse_name_path = || {
68 if line.starts_with(char::is_whitespace) {
69 return None;
70 }
71 let path_str = line.strip_suffix(':')?;
72 let path = Path::new(path_str);
73 if !matches!(path.extension()?.to_str()?, "rlib" | "so" | "dylib" | "dll") {
74 return None;
75 }
76 let (name, _hash) = path.file_stem()?.to_str()?.rsplit_once('-')?;
77 // the "lib" prefix is not present for dll files
78 let name = name.strip_prefix("lib").unwrap_or(name);
79 Some((name, path_str))
80 };
81 if let Some((name, path)) = parse_name_path() {
82 if TEST_DEPENDENCIES.contains(&name) {
83 // A dependency may be listed twice if it is available in sysroot,
84 // and the sysroot dependencies are listed first. As of the writing,
85 // this only seems to apply to if_chain.
86 crates.insert(name, path);
87 }
88 }
89 }
90 let not_found: Vec<&str> = TEST_DEPENDENCIES
91 .iter()
92 .copied()
93 .filter(|n| !crates.contains_key(n))
94 .collect();
95 assert!(
96 not_found.is_empty(),
97 "dependencies not found in depinfo: {not_found:?}\n\
98 help: Make sure the `-Z binary-dep-depinfo` rust flag is enabled\n\
99 help: Try adding to dev-dependencies in Cargo.toml\n\
100 help: Be sure to also add `extern crate ...;` to tests/compile-test.rs",
101 );
102 crates
103 .into_iter()
104 .map(|(name, path)| format!("--extern={name}={path}"))
105 .collect()
106 });
107
108 mod test_utils;
109
110 // whether to run internal tests or not
111 const RUN_INTERNAL_TESTS: bool = cfg!(feature = "internal");
112
113 fn base_config(test_dir: &str) -> compiletest::Config {
114 let mut config = compiletest::Config {
115 mode: TestMode::Yolo,
116 stderr_filters: vec![],
117 stdout_filters: vec![],
118 output_conflict_handling: if var_os("RUSTC_BLESS").is_some_and(|v| v != "0")
119 || env::args().any(|arg| arg == "--bless")
120 {
121 OutputConflictHandling::Bless
122 } else {
123 OutputConflictHandling::Error("cargo uibless".into())
124 },
125 target: None,
126 out_dir: PathBuf::from(std::env::var_os("CARGO_TARGET_DIR").unwrap_or("target".into())).join("ui_test"),
127 ..compiletest::Config::rustc(Path::new("tests").join(test_dir))
128 };
129
130 if let Some(_path) = option_env!("RUSTC_LIB_PATH") {
131 //let path = PathBuf::from(path);
132 //config.run_lib_path = path.clone();
133 //config.compile_lib_path = path;
134 }
135 let current_exe_path = env::current_exe().unwrap();
136 let deps_path = current_exe_path.parent().unwrap();
137 let profile_path = deps_path.parent().unwrap();
138
139 config.program.args.extend(
140 [
141 "--emit=metadata",
142 "-Aunused",
143 "-Ainternal_features",
144 "-Zui-testing",
145 "-Dwarnings",
146 &format!("-Ldependency={}", deps_path.display()),
147 ]
148 .map(OsString::from),
149 );
150
151 config.program.args.extend(EXTERN_FLAGS.iter().map(OsString::from));
152
153 if let Some(host_libs) = option_env!("HOST_LIBS") {
154 let dep = format!("-Ldependency={}", Path::new(host_libs).join("deps").display());
155 config.program.args.push(dep.into());
156 }
157
158 // Normalize away slashes in windows paths.
159 config.stderr_filter(r"\\", "/");
160
161 //config.build_base = profile_path.join("test").join(test_dir);
162 config.program.program = profile_path.join(if cfg!(windows) {
163 "clippy-driver.exe"
164 } else {
165 "clippy-driver"
166 });
167 config
168 }
169
170 fn test_filter() -> Box<dyn Sync + Fn(&Path) -> bool> {
171 if let Ok(filters) = env::var("TESTNAME") {
172 let filters: Vec<_> = filters.split(',').map(ToString::to_string).collect();
173 Box::new(move |path| filters.iter().any(|f| path.to_string_lossy().contains(f)))
174 } else {
175 Box::new(|_| true)
176 }
177 }
178
179 fn run_ui() {
180 let config = base_config("ui");
181 //config.rustfix_coverage = true;
182 // use tests/clippy.toml
183 let _g = VarGuard::set("CARGO_MANIFEST_DIR", fs::canonicalize("tests").unwrap());
184 let _threads = VarGuard::set(
185 "RUST_TEST_THREADS",
186 // if RUST_TEST_THREADS is set, adhere to it, otherwise override it
187 env::var("RUST_TEST_THREADS").unwrap_or_else(|_| {
188 std::thread::available_parallelism()
189 .map_or(1, std::num::NonZeroUsize::get)
190 .to_string()
191 }),
192 );
193
194 let test_filter = test_filter();
195
196 compiletest::run_tests_generic(
197 config,
198 move |path| compiletest::default_file_filter(path) && test_filter(path),
199 compiletest::default_per_file_config,
200 status_emitter::Text,
201 )
202 .unwrap();
203 check_rustfix_coverage();
204 }
205
206 fn run_internal_tests() {
207 // only run internal tests with the internal-tests feature
208 if !RUN_INTERNAL_TESTS {
209 return;
210 }
211 let mut config = base_config("ui-internal");
212 if let OutputConflictHandling::Error(err) = &mut config.output_conflict_handling {
213 *err = "cargo uitest --features internal -- -- --bless".into();
214 }
215 let test_filter = test_filter();
216
217 compiletest::run_tests_generic(
218 config,
219 move |path| compiletest::default_file_filter(path) && test_filter(path),
220 compiletest::default_per_file_config,
221 status_emitter::Text,
222 )
223 .unwrap();
224 }
225
226 fn run_ui_toml() {
227 let mut config = base_config("ui-toml");
228
229 config.stderr_filter(
230 &regex::escape(
231 &fs::canonicalize("tests")
232 .unwrap()
233 .parent()
234 .unwrap()
235 .display()
236 .to_string()
237 .replace('\\', "/"),
238 ),
239 "$$DIR",
240 );
241
242 let test_filter = test_filter();
243
244 ui_test::run_tests_generic(
245 config,
246 |path| compiletest::default_file_filter(path) && test_filter(path),
247 |config, path| {
248 let mut config = config.clone();
249 config
250 .program
251 .envs
252 .push(("CLIPPY_CONF_DIR".into(), Some(path.parent().unwrap().into())));
253 Some(config)
254 },
255 status_emitter::Text,
256 )
257 .unwrap();
258 }
259
260 fn run_ui_cargo() {
261 if IS_RUSTC_TEST_SUITE {
262 return;
263 }
264
265 let mut config = base_config("ui-cargo");
266 config.program.input_file_flag = CommandBuilder::cargo().input_file_flag;
267 config.program.out_dir_flag = CommandBuilder::cargo().out_dir_flag;
268 config.program.args = vec!["clippy".into(), "--color".into(), "never".into(), "--quiet".into()];
269 config
270 .program
271 .envs
272 .push(("RUSTFLAGS".into(), Some("-Dwarnings".into())));
273 // We need to do this while we still have a rustc in the `program` field.
274 config.fill_host_and_target().unwrap();
275 config.dependencies_crate_manifest_path = None;
276 config.program.program.set_file_name(if cfg!(windows) {
277 "cargo-clippy.exe"
278 } else {
279 "cargo-clippy"
280 });
281 config.edition = None;
282
283 config.stderr_filter(
284 &regex::escape(
285 &fs::canonicalize("tests")
286 .unwrap()
287 .parent()
288 .unwrap()
289 .display()
290 .to_string()
291 .replace('\\', "/"),
292 ),
293 "$$DIR",
294 );
295
296 let test_filter = test_filter();
297
298 ui_test::run_tests_generic(
299 config,
300 |path| test_filter(path) && path.ends_with("Cargo.toml"),
301 |config, path| {
302 let mut config = config.clone();
303 config.out_dir = PathBuf::from("target/ui_test_cargo/").join(path.parent().unwrap());
304 Some(config)
305 },
306 status_emitter::Text,
307 )
308 .unwrap();
309 }
310
311 fn main() {
312 // Support being run by cargo nextest - https://nexte.st/book/custom-test-harnesses.html
313 if env::args().any(|arg| arg == "--list") {
314 if !env::args().any(|arg| arg == "--ignored") {
315 println!("compile_test: test");
316 }
317
318 return;
319 }
320
321 set_var("CLIPPY_DISABLE_DOCS_LINKS", "true");
322 // The SPEEDTEST_* env variables can be used to check Clippy's performance on your PR. It runs the
323 // affected test 1000 times and gets the average.
324 if let Ok(speedtest) = std::env::var("SPEEDTEST") {
325 println!("----------- STARTING SPEEDTEST -----------");
326 let f = match speedtest.as_str() {
327 "ui" => run_ui as fn(),
328 "cargo" => run_ui_cargo as fn(),
329 "toml" => run_ui_toml as fn(),
330 "internal" => run_internal_tests as fn(),
331 "rustfix-coverage-known-exceptions-accuracy" => rustfix_coverage_known_exceptions_accuracy as fn(),
332 "ui-cargo-toml-metadata" => ui_cargo_toml_metadata as fn(),
333
334 _ => panic!("unknown speedtest: {speedtest} || accepted speedtests are: [ui, cargo, toml, internal]"),
335 };
336
337 let iterations;
338 if let Ok(iterations_str) = std::env::var("SPEEDTEST_ITERATIONS") {
339 iterations = iterations_str
340 .parse::<u64>()
341 .unwrap_or_else(|_| panic!("Couldn't parse `{iterations_str}`, please use a valid u64"));
342 } else {
343 iterations = 1000;
344 }
345
346 let mut sum = 0;
347 for _ in 0..iterations {
348 let start = std::time::Instant::now();
349 f();
350 sum += start.elapsed().as_millis();
351 }
352 println!("average {} time: {} millis.", speedtest.to_uppercase(), sum / 1000);
353 } else {
354 run_ui();
355 run_ui_toml();
356 run_ui_cargo();
357 run_internal_tests();
358 rustfix_coverage_known_exceptions_accuracy();
359 ui_cargo_toml_metadata();
360 }
361 }
362
363 const RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS: &[&str] = &[
364 "assign_ops2.rs",
365 "borrow_deref_ref_unfixable.rs",
366 "cast_size_32bit.rs",
367 "char_lit_as_u8.rs",
368 "cmp_owned/without_suggestion.rs",
369 "dbg_macro.rs",
370 "deref_addrof_double_trigger.rs",
371 "doc/unbalanced_ticks.rs",
372 "eprint_with_newline.rs",
373 "explicit_counter_loop.rs",
374 "iter_skip_next_unfixable.rs",
375 "let_and_return.rs",
376 "literals.rs",
377 "map_flatten.rs",
378 "map_unwrap_or.rs",
379 "match_bool.rs",
380 "mem_replace_macro.rs",
381 "needless_arbitrary_self_type_unfixable.rs",
382 "needless_borrow_pat.rs",
383 "needless_for_each_unfixable.rs",
384 "nonminimal_bool.rs",
385 "print_literal.rs",
386 "redundant_static_lifetimes_multiple.rs",
387 "ref_binding_to_reference.rs",
388 "repl_uninit.rs",
389 "result_map_unit_fn_unfixable.rs",
390 "search_is_some.rs",
391 "single_component_path_imports_nested_first.rs",
392 "string_add.rs",
393 "suspicious_to_owned.rs",
394 "toplevel_ref_arg_non_rustfix.rs",
395 "unit_arg.rs",
396 "unnecessary_clone.rs",
397 "unnecessary_lazy_eval_unfixable.rs",
398 "write_literal.rs",
399 "write_literal_2.rs",
400 ];
401
402 fn check_rustfix_coverage() {
403 let missing_coverage_path = Path::new("debug/test/ui/rustfix_missing_coverage.txt");
404 let missing_coverage_path = if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
405 PathBuf::from(target_dir).join(missing_coverage_path)
406 } else {
407 missing_coverage_path.to_path_buf()
408 };
409
410 if let Ok(missing_coverage_contents) = std::fs::read_to_string(missing_coverage_path) {
411 assert!(RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS.iter().is_sorted_by_key(Path::new));
412
413 for rs_file in missing_coverage_contents.lines() {
414 let rs_path = Path::new(rs_file);
415 if rs_path.starts_with("tests/ui/crashes") {
416 continue;
417 }
418 assert!(rs_path.starts_with("tests/ui/"), "{rs_file:?}");
419 let filename = rs_path.strip_prefix("tests/ui/").unwrap();
420 assert!(
421 RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS
422 .binary_search_by_key(&filename, Path::new)
423 .is_ok(),
424 "`{rs_file}` runs `MachineApplicable` diagnostics but is missing a `run-rustfix` annotation. \
425 Please either add `//@run-rustfix` at the top of the file or add the file to \
426 `RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS` in `tests/compile-test.rs`.",
427 );
428 }
429 }
430 }
431
432 fn rustfix_coverage_known_exceptions_accuracy() {
433 for filename in RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS {
434 let rs_path = Path::new("tests/ui").join(filename);
435 assert!(rs_path.exists(), "`{}` does not exist", rs_path.display());
436 let fixed_path = rs_path.with_extension("fixed");
437 assert!(!fixed_path.exists(), "`{}` exists", fixed_path.display());
438 }
439 }
440
441 fn ui_cargo_toml_metadata() {
442 let ui_cargo_path = Path::new("tests/ui-cargo");
443 let cargo_common_metadata_path = ui_cargo_path.join("cargo_common_metadata");
444 let publish_exceptions =
445 ["fail_publish", "fail_publish_true", "pass_publish_empty"].map(|path| cargo_common_metadata_path.join(path));
446
447 for entry in walkdir::WalkDir::new(ui_cargo_path) {
448 let entry = entry.unwrap();
449 let path = entry.path();
450 if path.file_name() != Some(OsStr::new("Cargo.toml")) {
451 continue;
452 }
453
454 let toml = fs::read_to_string(path).unwrap().parse::<toml::Value>().unwrap();
455
456 let package = toml.as_table().unwrap().get("package").unwrap().as_table().unwrap();
457
458 let name = package.get("name").unwrap().as_str().unwrap().replace('-', "_");
459 assert!(
460 path.parent()
461 .unwrap()
462 .components()
463 .map(|component| component.as_os_str().to_string_lossy().replace('-', "_"))
464 .any(|s| *s == name)
465 || path.starts_with(&cargo_common_metadata_path),
466 "{path:?} has incorrect package name"
467 );
468
469 let publish = package.get("publish").and_then(toml::Value::as_bool).unwrap_or(true);
470 assert!(
471 !publish || publish_exceptions.contains(&path.parent().unwrap().to_path_buf()),
472 "{path:?} lacks `publish = false`"
473 );
474 }
475 }
476
477 /// Restores an env var on drop
478 #[must_use]
479 struct VarGuard {
480 key: &'static str,
481 value: Option<OsString>,
482 }
483
484 impl VarGuard {
485 fn set(key: &'static str, val: impl AsRef<OsStr>) -> Self {
486 let value = var_os(key);
487 set_var(key, val);
488 Self { key, value }
489 }
490 }
491
492 impl Drop for VarGuard {
493 fn drop(&mut self) {
494 match self.value.as_deref() {
495 None => remove_var(self.key),
496 Some(value) => set_var(self.key, value),
497 }
498 }
499 }