]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/tests/compile-test.rs
Auto merge of #99422 - Dylan-DPC:rollup-htjofm6, r=Dylan-DPC
[rust.git] / src / tools / clippy / tests / compile-test.rs
1 #![feature(test)] // compiletest_rs requires this attribute
2 #![feature(once_cell)]
3 #![feature(is_sorted)]
4 #![cfg_attr(feature = "deny-warnings", deny(warnings))]
5 #![warn(rust_2018_idioms, unused_lifetimes)]
6
7 use compiletest_rs as compiletest;
8 use compiletest_rs::common::Mode as TestMode;
9
10 use std::collections::HashMap;
11 use std::env::{self, remove_var, set_var, var_os};
12 use std::ffi::{OsStr, OsString};
13 use std::fs;
14 use std::io;
15 use std::path::{Path, PathBuf};
16 use std::sync::LazyLock;
17 use test_utils::IS_RUSTC_TEST_SUITE;
18
19 mod test_utils;
20
21 // whether to run internal tests or not
22 const RUN_INTERNAL_TESTS: bool = cfg!(feature = "internal");
23
24 /// All crates used in UI tests are listed here
25 static TEST_DEPENDENCIES: &[&str] = &[
26     "clippy_lints",
27     "clippy_utils",
28     "derive_new",
29     "futures",
30     "if_chain",
31     "itertools",
32     "quote",
33     "regex",
34     "serde",
35     "serde_derive",
36     "syn",
37     "tokio",
38     "parking_lot",
39     "rustc_semver",
40 ];
41
42 // Test dependencies may need an `extern crate` here to ensure that they show up
43 // in the depinfo file (otherwise cargo thinks they are unused)
44 #[allow(unused_extern_crates)]
45 extern crate clippy_lints;
46 #[allow(unused_extern_crates)]
47 extern crate clippy_utils;
48 #[allow(unused_extern_crates)]
49 extern crate derive_new;
50 #[allow(unused_extern_crates)]
51 extern crate futures;
52 #[allow(unused_extern_crates)]
53 extern crate if_chain;
54 #[allow(unused_extern_crates)]
55 extern crate itertools;
56 #[allow(unused_extern_crates)]
57 extern crate parking_lot;
58 #[allow(unused_extern_crates)]
59 extern crate quote;
60 #[allow(unused_extern_crates)]
61 extern crate rustc_semver;
62 #[allow(unused_extern_crates)]
63 extern crate syn;
64 #[allow(unused_extern_crates)]
65 extern crate tokio;
66
67 /// Produces a string with an `--extern` flag for all UI test crate
68 /// dependencies.
69 ///
70 /// The dependency files are located by parsing the depinfo file for this test
71 /// module. This assumes the `-Z binary-dep-depinfo` flag is enabled. All test
72 /// dependencies must be added to Cargo.toml at the project root. Test
73 /// dependencies that are not *directly* used by this test module require an
74 /// `extern crate` declaration.
75 static EXTERN_FLAGS: LazyLock<String> = LazyLock::new(|| {
76     let current_exe_depinfo = {
77         let mut path = env::current_exe().unwrap();
78         path.set_extension("d");
79         fs::read_to_string(path).unwrap()
80     };
81     let mut crates: HashMap<&str, &str> = HashMap::with_capacity(TEST_DEPENDENCIES.len());
82     for line in current_exe_depinfo.lines() {
83         // each dependency is expected to have a Makefile rule like `/path/to/crate-hash.rlib:`
84         let parse_name_path = || {
85             if line.starts_with(char::is_whitespace) {
86                 return None;
87             }
88             let path_str = line.strip_suffix(':')?;
89             let path = Path::new(path_str);
90             if !matches!(path.extension()?.to_str()?, "rlib" | "so" | "dylib" | "dll") {
91                 return None;
92             }
93             let (name, _hash) = path.file_stem()?.to_str()?.rsplit_once('-')?;
94             // the "lib" prefix is not present for dll files
95             let name = name.strip_prefix("lib").unwrap_or(name);
96             Some((name, path_str))
97         };
98         if let Some((name, path)) = parse_name_path() {
99             if TEST_DEPENDENCIES.contains(&name) {
100                 // A dependency may be listed twice if it is available in sysroot,
101                 // and the sysroot dependencies are listed first. As of the writing,
102                 // this only seems to apply to if_chain.
103                 crates.insert(name, path);
104             }
105         }
106     }
107     let not_found: Vec<&str> = TEST_DEPENDENCIES
108         .iter()
109         .copied()
110         .filter(|n| !crates.contains_key(n))
111         .collect();
112     assert!(
113         not_found.is_empty(),
114         "dependencies not found in depinfo: {:?}\n\
115         help: Make sure the `-Z binary-dep-depinfo` rust flag is enabled\n\
116         help: Try adding to dev-dependencies in Cargo.toml\n\
117         help: Be sure to also add `extern crate ...;` to tests/compile-test.rs",
118         not_found,
119     );
120     crates
121         .into_iter()
122         .map(|(name, path)| format!(" --extern {}={}", name, path))
123         .collect()
124 });
125
126 fn base_config(test_dir: &str) -> compiletest::Config {
127     let mut config = compiletest::Config {
128         edition: Some("2021".into()),
129         mode: TestMode::Ui,
130         ..Default::default()
131     };
132
133     if let Ok(filters) = env::var("TESTNAME") {
134         config.filters = filters.split(',').map(ToString::to_string).collect();
135     }
136
137     if let Some(path) = option_env!("RUSTC_LIB_PATH") {
138         let path = PathBuf::from(path);
139         config.run_lib_path = path.clone();
140         config.compile_lib_path = path;
141     }
142     let current_exe_path = env::current_exe().unwrap();
143     let deps_path = current_exe_path.parent().unwrap();
144     let profile_path = deps_path.parent().unwrap();
145
146     // Using `-L dependency={}` enforces that external dependencies are added with `--extern`.
147     // This is valuable because a) it allows us to monitor what external dependencies are used
148     // and b) it ensures that conflicting rlibs are resolved properly.
149     let host_libs = option_env!("HOST_LIBS")
150         .map(|p| format!(" -L dependency={}", Path::new(p).join("deps").display()))
151         .unwrap_or_default();
152     config.target_rustcflags = Some(format!(
153         "--emit=metadata -Dwarnings -Zui-testing -L dependency={}{}{}",
154         deps_path.display(),
155         host_libs,
156         &*EXTERN_FLAGS,
157     ));
158
159     config.src_base = Path::new("tests").join(test_dir);
160     config.build_base = profile_path.join("test").join(test_dir);
161     config.rustc_path = profile_path.join(if cfg!(windows) {
162         "clippy-driver.exe"
163     } else {
164         "clippy-driver"
165     });
166     config
167 }
168
169 fn run_ui() {
170     let mut config = base_config("ui");
171     config.rustfix_coverage = true;
172     // use tests/clippy.toml
173     let _g = VarGuard::set("CARGO_MANIFEST_DIR", fs::canonicalize("tests").unwrap());
174     let _threads = VarGuard::set(
175         "RUST_TEST_THREADS",
176         // if RUST_TEST_THREADS is set, adhere to it, otherwise override it
177         env::var("RUST_TEST_THREADS").unwrap_or_else(|_| {
178             std::thread::available_parallelism()
179                 .map_or(1, std::num::NonZeroUsize::get)
180                 .to_string()
181         }),
182     );
183     compiletest::run_tests(&config);
184     check_rustfix_coverage();
185 }
186
187 fn run_internal_tests() {
188     // only run internal tests with the internal-tests feature
189     if !RUN_INTERNAL_TESTS {
190         return;
191     }
192     let config = base_config("ui-internal");
193     compiletest::run_tests(&config);
194 }
195
196 fn run_ui_toml() {
197     fn run_tests(config: &compiletest::Config, mut tests: Vec<tester::TestDescAndFn>) -> Result<bool, io::Error> {
198         let mut result = true;
199         let opts = compiletest::test_opts(config);
200         for dir in fs::read_dir(&config.src_base)? {
201             let dir = dir?;
202             if !dir.file_type()?.is_dir() {
203                 continue;
204             }
205             let dir_path = dir.path();
206             let _g = VarGuard::set("CARGO_MANIFEST_DIR", &dir_path);
207             for file in fs::read_dir(&dir_path)? {
208                 let file = file?;
209                 let file_path = file.path();
210                 if file.file_type()?.is_dir() {
211                     continue;
212                 }
213                 if file_path.extension() != Some(OsStr::new("rs")) {
214                     continue;
215                 }
216                 let paths = compiletest::common::TestPaths {
217                     file: file_path,
218                     base: config.src_base.clone(),
219                     relative_dir: dir_path.file_name().unwrap().into(),
220                 };
221                 let test_name = compiletest::make_test_name(config, &paths);
222                 let index = tests
223                     .iter()
224                     .position(|test| test.desc.name == test_name)
225                     .expect("The test should be in there");
226                 result &= tester::run_tests_console(&opts, vec![tests.swap_remove(index)])?;
227             }
228         }
229         Ok(result)
230     }
231
232     let mut config = base_config("ui-toml");
233     config.src_base = config.src_base.canonicalize().unwrap();
234
235     let tests = compiletest::make_tests(&config);
236
237     let res = run_tests(&config, tests);
238     match res {
239         Ok(true) => {},
240         Ok(false) => panic!("Some tests failed"),
241         Err(e) => {
242             panic!("I/O failure during tests: {:?}", e);
243         },
244     }
245 }
246
247 fn run_ui_cargo() {
248     fn run_tests(
249         config: &compiletest::Config,
250         filters: &[String],
251         mut tests: Vec<tester::TestDescAndFn>,
252     ) -> Result<bool, io::Error> {
253         let mut result = true;
254         let opts = compiletest::test_opts(config);
255
256         for dir in fs::read_dir(&config.src_base)? {
257             let dir = dir?;
258             if !dir.file_type()?.is_dir() {
259                 continue;
260             }
261
262             // Use the filter if provided
263             let dir_path = dir.path();
264             for filter in filters {
265                 if !dir_path.ends_with(filter) {
266                     continue;
267                 }
268             }
269
270             for case in fs::read_dir(&dir_path)? {
271                 let case = case?;
272                 if !case.file_type()?.is_dir() {
273                     continue;
274                 }
275
276                 let src_path = case.path().join("src");
277
278                 // When switching between branches, if the previous branch had a test
279                 // that the current branch does not have, the directory is not removed
280                 // because an ignored Cargo.lock file exists.
281                 if !src_path.exists() {
282                     continue;
283                 }
284
285                 env::set_current_dir(&src_path)?;
286
287                 let cargo_toml_path = case.path().join("Cargo.toml");
288                 let cargo_content = fs::read(&cargo_toml_path)?;
289                 let cargo_parsed: toml::Value = toml::from_str(
290                     std::str::from_utf8(&cargo_content).expect("`Cargo.toml` is not a valid utf-8 file!"),
291                 )
292                 .expect("Can't parse `Cargo.toml`");
293
294                 let _g = VarGuard::set("CARGO_MANIFEST_DIR", case.path());
295                 let _h = VarGuard::set(
296                     "CARGO_PKG_RUST_VERSION",
297                     cargo_parsed
298                         .get("package")
299                         .and_then(|p| p.get("rust-version"))
300                         .and_then(toml::Value::as_str)
301                         .unwrap_or(""),
302                 );
303
304                 for file in fs::read_dir(&src_path)? {
305                     let file = file?;
306                     if file.file_type()?.is_dir() {
307                         continue;
308                     }
309
310                     // Search for the main file to avoid running a test for each file in the project
311                     let file_path = file.path();
312                     match file_path.file_name().and_then(OsStr::to_str) {
313                         Some("main.rs") => {},
314                         _ => continue,
315                     }
316                     let _g = VarGuard::set("CLIPPY_CONF_DIR", case.path());
317                     let paths = compiletest::common::TestPaths {
318                         file: file_path,
319                         base: config.src_base.clone(),
320                         relative_dir: src_path.strip_prefix(&config.src_base).unwrap().into(),
321                     };
322                     let test_name = compiletest::make_test_name(config, &paths);
323                     let index = tests
324                         .iter()
325                         .position(|test| test.desc.name == test_name)
326                         .expect("The test should be in there");
327                     result &= tester::run_tests_console(&opts, vec![tests.swap_remove(index)])?;
328                 }
329             }
330         }
331         Ok(result)
332     }
333
334     if IS_RUSTC_TEST_SUITE {
335         return;
336     }
337
338     let mut config = base_config("ui-cargo");
339     config.src_base = config.src_base.canonicalize().unwrap();
340
341     let tests = compiletest::make_tests(&config);
342
343     let current_dir = env::current_dir().unwrap();
344     let res = run_tests(&config, &config.filters, tests);
345     env::set_current_dir(current_dir).unwrap();
346
347     match res {
348         Ok(true) => {},
349         Ok(false) => panic!("Some tests failed"),
350         Err(e) => {
351             panic!("I/O failure during tests: {:?}", e);
352         },
353     }
354 }
355
356 #[test]
357 fn compile_test() {
358     set_var("CLIPPY_DISABLE_DOCS_LINKS", "true");
359     run_ui();
360     run_ui_toml();
361     run_ui_cargo();
362     run_internal_tests();
363 }
364
365 const RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS: &[&str] = &[
366     "assign_ops2.rs",
367     "borrow_deref_ref_unfixable.rs",
368     "cast_size_32bit.rs",
369     "char_lit_as_u8.rs",
370     "cmp_owned/without_suggestion.rs",
371     "dbg_macro.rs",
372     "deref_addrof_double_trigger.rs",
373     "doc/unbalanced_ticks.rs",
374     "eprint_with_newline.rs",
375     "explicit_counter_loop.rs",
376     "iter_skip_next_unfixable.rs",
377     "let_and_return.rs",
378     "literals.rs",
379     "map_flatten.rs",
380     "map_unwrap_or.rs",
381     "match_bool.rs",
382     "mem_replace_macro.rs",
383     "needless_arbitrary_self_type_unfixable.rs",
384     "needless_borrow_pat.rs",
385     "needless_for_each_unfixable.rs",
386     "nonminimal_bool.rs",
387     "print_literal.rs",
388     "print_with_newline.rs",
389     "redundant_static_lifetimes_multiple.rs",
390     "ref_binding_to_reference.rs",
391     "repl_uninit.rs",
392     "result_map_unit_fn_unfixable.rs",
393     "search_is_some.rs",
394     "single_component_path_imports_nested_first.rs",
395     "string_add.rs",
396     "toplevel_ref_arg_non_rustfix.rs",
397     "trait_duplication_in_bounds.rs",
398     "unit_arg.rs",
399     "unnecessary_clone.rs",
400     "unnecessary_lazy_eval_unfixable.rs",
401     "write_literal.rs",
402     "write_literal_2.rs",
403     "write_with_newline.rs",
404 ];
405
406 fn check_rustfix_coverage() {
407     let missing_coverage_path = Path::new("target/debug/test/ui/rustfix_missing_coverage.txt");
408
409     if let Ok(missing_coverage_contents) = std::fs::read_to_string(missing_coverage_path) {
410         assert!(RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS.iter().is_sorted_by_key(Path::new));
411
412         for rs_path in missing_coverage_contents.lines() {
413             if Path::new(rs_path).starts_with("tests/ui/crashes") {
414                 continue;
415             }
416             let filename = Path::new(rs_path).strip_prefix("tests/ui/").unwrap();
417             assert!(
418                 RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS
419                     .binary_search_by_key(&filename, Path::new)
420                     .is_ok(),
421                 "`{}` runs `MachineApplicable` diagnostics but is missing a `run-rustfix` annotation. \
422                 Please either add `// run-rustfix` at the top of the file or add the file to \
423                 `RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS` in `tests/compile-test.rs`.",
424                 rs_path,
425             );
426         }
427     }
428 }
429
430 #[test]
431 fn rustfix_coverage_known_exceptions_accuracy() {
432     for filename in RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS {
433         let rs_path = Path::new("tests/ui").join(filename);
434         assert!(
435             rs_path.exists(),
436             "`{}` does not exists",
437             rs_path.strip_prefix(env!("CARGO_MANIFEST_DIR")).unwrap().display()
438         );
439         let fixed_path = rs_path.with_extension("fixed");
440         assert!(
441             !fixed_path.exists(),
442             "`{}` exists",
443             fixed_path.strip_prefix(env!("CARGO_MANIFEST_DIR")).unwrap().display()
444         );
445     }
446 }
447
448 /// Restores an env var on drop
449 #[must_use]
450 struct VarGuard {
451     key: &'static str,
452     value: Option<OsString>,
453 }
454
455 impl VarGuard {
456     fn set(key: &'static str, val: impl AsRef<OsStr>) -> Self {
457         let value = var_os(key);
458         set_var(key, val);
459         Self { key, value }
460     }
461 }
462
463 impl Drop for VarGuard {
464     fn drop(&mut self) {
465         match self.value.as_deref() {
466             None => remove_var(self.key),
467             Some(value) => set_var(self.key, value),
468         }
469     }
470 }