]> git.lizzy.rs Git - rust.git/blob - tests/compile-test.rs
Don't lint `implicit_clone` when the type doesn't implement clone
[rust.git] / 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: {not_found:?}\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     );
119     crates
120         .into_iter()
121         .map(|(name, path)| format!(" --extern {name}={path}"))
122         .collect()
123 });
124
125 fn base_config(test_dir: &str) -> compiletest::Config {
126     let mut config = compiletest::Config {
127         edition: Some("2021".into()),
128         mode: TestMode::Ui,
129         ..Default::default()
130     };
131
132     if let Ok(filters) = env::var("TESTNAME") {
133         config.filters = filters.split(',').map(ToString::to_string).collect();
134     }
135
136     if let Some(path) = option_env!("RUSTC_LIB_PATH") {
137         let path = PathBuf::from(path);
138         config.run_lib_path = path.clone();
139         config.compile_lib_path = path;
140     }
141     let current_exe_path = env::current_exe().unwrap();
142     let deps_path = current_exe_path.parent().unwrap();
143     let profile_path = deps_path.parent().unwrap();
144
145     // Using `-L dependency={}` enforces that external dependencies are added with `--extern`.
146     // This is valuable because a) it allows us to monitor what external dependencies are used
147     // and b) it ensures that conflicting rlibs are resolved properly.
148     let host_libs = option_env!("HOST_LIBS")
149         .map(|p| format!(" -L dependency={}", Path::new(p).join("deps").display()))
150         .unwrap_or_default();
151     config.target_rustcflags = Some(format!(
152         "--emit=metadata -Dwarnings -Zui-testing -L dependency={}{host_libs}{}",
153         deps_path.display(),
154         &*EXTERN_FLAGS,
155     ));
156
157     config.src_base = Path::new("tests").join(test_dir);
158     config.build_base = profile_path.join("test").join(test_dir);
159     config.rustc_path = profile_path.join(if cfg!(windows) {
160         "clippy-driver.exe"
161     } else {
162         "clippy-driver"
163     });
164     config
165 }
166
167 fn run_ui() {
168     let mut config = base_config("ui");
169     config.rustfix_coverage = true;
170     // use tests/clippy.toml
171     let _g = VarGuard::set("CARGO_MANIFEST_DIR", fs::canonicalize("tests").unwrap());
172     let _threads = VarGuard::set(
173         "RUST_TEST_THREADS",
174         // if RUST_TEST_THREADS is set, adhere to it, otherwise override it
175         env::var("RUST_TEST_THREADS").unwrap_or_else(|_| {
176             std::thread::available_parallelism()
177                 .map_or(1, std::num::NonZeroUsize::get)
178                 .to_string()
179         }),
180     );
181     compiletest::run_tests(&config);
182     check_rustfix_coverage();
183 }
184
185 fn run_internal_tests() {
186     // only run internal tests with the internal-tests feature
187     if !RUN_INTERNAL_TESTS {
188         return;
189     }
190     let config = base_config("ui-internal");
191     compiletest::run_tests(&config);
192 }
193
194 fn run_ui_toml() {
195     fn run_tests(config: &compiletest::Config, mut tests: Vec<tester::TestDescAndFn>) -> Result<bool, io::Error> {
196         let mut result = true;
197         let opts = compiletest::test_opts(config);
198         for dir in fs::read_dir(&config.src_base)? {
199             let dir = dir?;
200             if !dir.file_type()?.is_dir() {
201                 continue;
202             }
203             let dir_path = dir.path();
204             let _g = VarGuard::set("CARGO_MANIFEST_DIR", &dir_path);
205             for file in fs::read_dir(&dir_path)? {
206                 let file = file?;
207                 let file_path = file.path();
208                 if file.file_type()?.is_dir() {
209                     continue;
210                 }
211                 if file_path.extension() != Some(OsStr::new("rs")) {
212                     continue;
213                 }
214                 let paths = compiletest::common::TestPaths {
215                     file: file_path,
216                     base: config.src_base.clone(),
217                     relative_dir: dir_path.file_name().unwrap().into(),
218                 };
219                 let test_name = compiletest::make_test_name(config, &paths);
220                 let index = tests
221                     .iter()
222                     .position(|test| test.desc.name == test_name)
223                     .expect("The test should be in there");
224                 result &= tester::run_tests_console(&opts, vec![tests.swap_remove(index)])?;
225             }
226         }
227         Ok(result)
228     }
229
230     let mut config = base_config("ui-toml");
231     config.src_base = config.src_base.canonicalize().unwrap();
232
233     let tests = compiletest::make_tests(&config);
234
235     let res = run_tests(&config, tests);
236     match res {
237         Ok(true) => {},
238         Ok(false) => panic!("Some tests failed"),
239         Err(e) => {
240             panic!("I/O failure during tests: {e:?}");
241         },
242     }
243 }
244
245 fn run_ui_cargo() {
246     fn run_tests(
247         config: &compiletest::Config,
248         filters: &[String],
249         mut tests: Vec<tester::TestDescAndFn>,
250     ) -> Result<bool, io::Error> {
251         let mut result = true;
252         let opts = compiletest::test_opts(config);
253
254         for dir in fs::read_dir(&config.src_base)? {
255             let dir = dir?;
256             if !dir.file_type()?.is_dir() {
257                 continue;
258             }
259
260             // Use the filter if provided
261             let dir_path = dir.path();
262             for filter in filters {
263                 if !dir_path.ends_with(filter) {
264                     continue;
265                 }
266             }
267
268             for case in fs::read_dir(&dir_path)? {
269                 let case = case?;
270                 if !case.file_type()?.is_dir() {
271                     continue;
272                 }
273
274                 let src_path = case.path().join("src");
275
276                 // When switching between branches, if the previous branch had a test
277                 // that the current branch does not have, the directory is not removed
278                 // because an ignored Cargo.lock file exists.
279                 if !src_path.exists() {
280                     continue;
281                 }
282
283                 env::set_current_dir(&src_path)?;
284
285                 let cargo_toml_path = case.path().join("Cargo.toml");
286                 let cargo_content = fs::read(cargo_toml_path)?;
287                 let cargo_parsed: toml::Value = toml::from_str(
288                     std::str::from_utf8(&cargo_content).expect("`Cargo.toml` is not a valid utf-8 file!"),
289                 )
290                 .expect("Can't parse `Cargo.toml`");
291
292                 let _g = VarGuard::set("CARGO_MANIFEST_DIR", case.path());
293                 let _h = VarGuard::set(
294                     "CARGO_PKG_RUST_VERSION",
295                     cargo_parsed
296                         .get("package")
297                         .and_then(|p| p.get("rust-version"))
298                         .and_then(toml::Value::as_str)
299                         .unwrap_or(""),
300                 );
301
302                 for file in fs::read_dir(&src_path)? {
303                     let file = file?;
304                     if file.file_type()?.is_dir() {
305                         continue;
306                     }
307
308                     // Search for the main file to avoid running a test for each file in the project
309                     let file_path = file.path();
310                     match file_path.file_name().and_then(OsStr::to_str) {
311                         Some("main.rs") => {},
312                         _ => continue,
313                     }
314                     let _g = VarGuard::set("CLIPPY_CONF_DIR", case.path());
315                     let paths = compiletest::common::TestPaths {
316                         file: file_path,
317                         base: config.src_base.clone(),
318                         relative_dir: src_path.strip_prefix(&config.src_base).unwrap().into(),
319                     };
320                     let test_name = compiletest::make_test_name(config, &paths);
321                     let index = tests
322                         .iter()
323                         .position(|test| test.desc.name == test_name)
324                         .expect("The test should be in there");
325                     result &= tester::run_tests_console(&opts, vec![tests.swap_remove(index)])?;
326                 }
327             }
328         }
329         Ok(result)
330     }
331
332     if IS_RUSTC_TEST_SUITE {
333         return;
334     }
335
336     let mut config = base_config("ui-cargo");
337     config.src_base = config.src_base.canonicalize().unwrap();
338
339     let tests = compiletest::make_tests(&config);
340
341     let current_dir = env::current_dir().unwrap();
342     let res = run_tests(&config, &config.filters, tests);
343     env::set_current_dir(current_dir).unwrap();
344
345     match res {
346         Ok(true) => {},
347         Ok(false) => panic!("Some tests failed"),
348         Err(e) => {
349             panic!("I/O failure during tests: {e:?}");
350         },
351     }
352 }
353
354 #[test]
355 fn compile_test() {
356     set_var("CLIPPY_DISABLE_DOCS_LINKS", "true");
357     run_ui();
358     run_ui_toml();
359     run_ui_cargo();
360     run_internal_tests();
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     "print_with_newline.rs",
387     "redundant_static_lifetimes_multiple.rs",
388     "ref_binding_to_reference.rs",
389     "repl_uninit.rs",
390     "result_map_unit_fn_unfixable.rs",
391     "search_is_some.rs",
392     "single_component_path_imports_nested_first.rs",
393     "string_add.rs",
394     "suspicious_to_owned.rs",
395     "toplevel_ref_arg_non_rustfix.rs",
396     "unit_arg.rs",
397     "unnecessary_clone.rs",
398     "unnecessary_lazy_eval_unfixable.rs",
399     "write_literal.rs",
400     "write_literal_2.rs",
401     "write_with_newline.rs",
402 ];
403
404 fn check_rustfix_coverage() {
405     let missing_coverage_path = Path::new("debug/test/ui/rustfix_missing_coverage.txt");
406     let missing_coverage_path = if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
407         PathBuf::from(target_dir).join(missing_coverage_path)
408     } else {
409         missing_coverage_path.to_path_buf()
410     };
411
412     if let Ok(missing_coverage_contents) = std::fs::read_to_string(missing_coverage_path) {
413         assert!(RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS.iter().is_sorted_by_key(Path::new));
414
415         for rs_file in missing_coverage_contents.lines() {
416             let rs_path = Path::new(rs_file);
417             if rs_path.starts_with("tests/ui/crashes") {
418                 continue;
419             }
420             assert!(rs_path.starts_with("tests/ui/"), "{rs_file:?}");
421             let filename = rs_path.strip_prefix("tests/ui/").unwrap();
422             assert!(
423                 RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS
424                     .binary_search_by_key(&filename, Path::new)
425                     .is_ok(),
426                 "`{rs_file}` runs `MachineApplicable` diagnostics but is missing a `run-rustfix` annotation. \
427                 Please either add `// run-rustfix` at the top of the file or add the file to \
428                 `RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS` in `tests/compile-test.rs`.",
429             );
430         }
431     }
432 }
433
434 #[test]
435 fn rustfix_coverage_known_exceptions_accuracy() {
436     for filename in RUSTFIX_COVERAGE_KNOWN_EXCEPTIONS {
437         let rs_path = Path::new("tests/ui").join(filename);
438         assert!(
439             rs_path.exists(),
440             "`{}` does not exist",
441             rs_path.strip_prefix(env!("CARGO_MANIFEST_DIR")).unwrap().display()
442         );
443         let fixed_path = rs_path.with_extension("fixed");
444         assert!(
445             !fixed_path.exists(),
446             "`{}` exists",
447             fixed_path.strip_prefix(env!("CARGO_MANIFEST_DIR")).unwrap().display()
448         );
449     }
450 }
451
452 #[test]
453 fn ui_cargo_toml_metadata() {
454     let ui_cargo_path = Path::new("tests/ui-cargo");
455     let cargo_common_metadata_path = ui_cargo_path.join("cargo_common_metadata");
456     let publish_exceptions =
457         ["fail_publish", "fail_publish_true", "pass_publish_empty"].map(|path| cargo_common_metadata_path.join(path));
458
459     for entry in walkdir::WalkDir::new(ui_cargo_path) {
460         let entry = entry.unwrap();
461         let path = entry.path();
462         if path.file_name() != Some(OsStr::new("Cargo.toml")) {
463             continue;
464         }
465
466         let toml = fs::read_to_string(path).unwrap().parse::<toml::Value>().unwrap();
467
468         let package = toml.as_table().unwrap().get("package").unwrap().as_table().unwrap();
469
470         let name = package.get("name").unwrap().as_str().unwrap().replace('-', "_");
471         assert!(
472             path.parent()
473                 .unwrap()
474                 .components()
475                 .map(|component| component.as_os_str().to_string_lossy().replace('-', "_"))
476                 .any(|s| *s == name)
477                 || path.starts_with(&cargo_common_metadata_path),
478             "{path:?} has incorrect package name"
479         );
480
481         let publish = package.get("publish").and_then(toml::Value::as_bool).unwrap_or(true);
482         assert!(
483             !publish || publish_exceptions.contains(&path.parent().unwrap().to_path_buf()),
484             "{path:?} lacks `publish = false`"
485         );
486     }
487 }
488
489 /// Restores an env var on drop
490 #[must_use]
491 struct VarGuard {
492     key: &'static str,
493     value: Option<OsString>,
494 }
495
496 impl VarGuard {
497     fn set(key: &'static str, val: impl AsRef<OsStr>) -> Self {
498         let value = var_os(key);
499         set_var(key, val);
500         Self { key, value }
501     }
502 }
503
504 impl Drop for VarGuard {
505     fn drop(&mut self) {
506         match self.value.as_deref() {
507             None => remove_var(self.key),
508             Some(value) => set_var(self.key, value),
509         }
510     }
511 }