]> git.lizzy.rs Git - rust.git/blob - tests/compile-test.rs
Add lint `manual_split_once`
[rust.git] / tests / compile-test.rs
1 #![feature(test)] // compiletest_rs requires this attribute
2 #![feature(once_cell)]
3 #![feature(try_blocks)]
4
5 use compiletest_rs as compiletest;
6 use compiletest_rs::common::Mode as TestMode;
7
8 use std::env::{self, remove_var, set_var, var_os};
9 use std::ffi::{OsStr, OsString};
10 use std::fs;
11 use std::io;
12 use std::path::{Path, PathBuf};
13
14 mod cargo;
15
16 // whether to run internal tests or not
17 const RUN_INTERNAL_TESTS: bool = cfg!(feature = "internal-lints");
18
19 fn host_lib() -> PathBuf {
20     option_env!("HOST_LIBS").map_or(cargo::CARGO_TARGET_DIR.join(env!("PROFILE")), PathBuf::from)
21 }
22
23 fn clippy_driver_path() -> PathBuf {
24     option_env!("CLIPPY_DRIVER_PATH").map_or(cargo::TARGET_LIB.join("clippy-driver"), PathBuf::from)
25 }
26
27 // When we'll want to use `extern crate ..` for a dependency that is used
28 // both by the crate and the compiler itself, we can't simply pass -L flags
29 // as we'll get a duplicate matching versions. Instead, disambiguate with
30 // `--extern dep=path`.
31 // See https://github.com/rust-lang/rust-clippy/issues/4015.
32 //
33 // FIXME: We cannot use `cargo build --message-format=json` to resolve to dependency files.
34 //        Because it would force-rebuild if the options passed to `build` command is not the same
35 //        as what we manually pass to `cargo` invocation
36 fn third_party_crates() -> String {
37     use std::collections::HashMap;
38     static CRATES: &[&str] = &[
39         "clippy_lints",
40         "clippy_utils",
41         "if_chain",
42         "itertools",
43         "quote",
44         "regex",
45         "serde",
46         "serde_derive",
47         "syn",
48     ];
49     let dep_dir = cargo::TARGET_LIB.join("deps");
50     let mut crates: HashMap<&str, Vec<PathBuf>> = HashMap::with_capacity(CRATES.len());
51     let mut flags = String::new();
52     for entry in fs::read_dir(dep_dir).unwrap().flatten() {
53         let path = entry.path();
54         if let Some(name) = try {
55             let name = path.file_name()?.to_str()?;
56             let (name, _) = name.strip_suffix(".rlib")?.strip_prefix("lib")?.split_once('-')?;
57             CRATES.iter().copied().find(|&c| c == name)?
58         } {
59             flags += &format!(" --extern {}={}", name, path.display());
60             crates.entry(name).or_default().push(path.clone());
61         }
62     }
63     crates.retain(|_, paths| paths.len() > 1);
64     if !crates.is_empty() {
65         let crate_names = crates.keys().map(|s| format!("`{}`", s)).collect::<Vec<_>>().join(", ");
66         // add backslashes for an easy copy-paste `rm` command
67         let paths = crates
68             .into_values()
69             .flatten()
70             .map(|p| strip_current_dir(&p).display().to_string())
71             .collect::<Vec<_>>()
72             .join(" \\\n");
73         // Check which action should be done in order to remove compiled deps.
74         // If pre-installed version of compiler is used, `cargo clean` will do.
75         // Otherwise (for bootstrapped compiler), the dependencies directory
76         // must be removed manually.
77         let suggested_action = if std::env::var_os("RUSTC_BOOTSTRAP").is_some() {
78             "removing the stageN-tools directory"
79         } else {
80             "running `cargo clean`"
81         };
82
83         panic!(
84             "\n----------------------------------------------------------------------\n\
85             ERROR: Found multiple rlibs for crates: {}\n\
86             Try {} or remove the following files:\n\n{}\n\n\
87             For details on this error see https://github.com/rust-lang/rust-clippy/issues/7343\n\
88             ----------------------------------------------------------------------\n",
89             crate_names, suggested_action, paths
90         );
91     }
92     flags
93 }
94
95 fn default_config() -> compiletest::Config {
96     let mut config = compiletest::Config::default();
97
98     if let Ok(filters) = env::var("TESTNAME") {
99         config.filters = filters.split(',').map(std::string::ToString::to_string).collect();
100     }
101
102     if let Some(path) = option_env!("RUSTC_LIB_PATH") {
103         let path = PathBuf::from(path);
104         config.run_lib_path = path.clone();
105         config.compile_lib_path = path;
106     }
107
108     config.target_rustcflags = Some(format!(
109         "--emit=metadata -L {0} -L {1} -Dwarnings -Zui-testing {2}",
110         host_lib().join("deps").display(),
111         cargo::TARGET_LIB.join("deps").display(),
112         third_party_crates(),
113     ));
114
115     config.build_base = host_lib().join("test_build_base");
116     config.rustc_path = clippy_driver_path();
117     config
118 }
119
120 fn run_ui(cfg: &mut compiletest::Config) {
121     cfg.mode = TestMode::Ui;
122     cfg.src_base = Path::new("tests").join("ui");
123     // use tests/clippy.toml
124     let _g = VarGuard::set("CARGO_MANIFEST_DIR", std::fs::canonicalize("tests").unwrap());
125     compiletest::run_tests(cfg);
126 }
127
128 fn run_internal_tests(cfg: &mut compiletest::Config) {
129     // only run internal tests with the internal-tests feature
130     if !RUN_INTERNAL_TESTS {
131         return;
132     }
133     cfg.mode = TestMode::Ui;
134     cfg.src_base = Path::new("tests").join("ui-internal");
135     compiletest::run_tests(cfg);
136 }
137
138 fn run_ui_toml(config: &mut compiletest::Config) {
139     fn run_tests(config: &compiletest::Config, mut tests: Vec<tester::TestDescAndFn>) -> Result<bool, io::Error> {
140         let mut result = true;
141         let opts = compiletest::test_opts(config);
142         for dir in fs::read_dir(&config.src_base)? {
143             let dir = dir?;
144             if !dir.file_type()?.is_dir() {
145                 continue;
146             }
147             let dir_path = dir.path();
148             let _g = VarGuard::set("CARGO_MANIFEST_DIR", &dir_path);
149             for file in fs::read_dir(&dir_path)? {
150                 let file = file?;
151                 let file_path = file.path();
152                 if file.file_type()?.is_dir() {
153                     continue;
154                 }
155                 if file_path.extension() != Some(OsStr::new("rs")) {
156                     continue;
157                 }
158                 let paths = compiletest::common::TestPaths {
159                     file: file_path,
160                     base: config.src_base.clone(),
161                     relative_dir: dir_path.file_name().unwrap().into(),
162                 };
163                 let test_name = compiletest::make_test_name(config, &paths);
164                 let index = tests
165                     .iter()
166                     .position(|test| test.desc.name == test_name)
167                     .expect("The test should be in there");
168                 result &= tester::run_tests_console(&opts, vec![tests.swap_remove(index)])?;
169             }
170         }
171         Ok(result)
172     }
173
174     config.mode = TestMode::Ui;
175     config.src_base = Path::new("tests").join("ui-toml").canonicalize().unwrap();
176
177     let tests = compiletest::make_tests(config);
178
179     let res = run_tests(config, tests);
180     match res {
181         Ok(true) => {},
182         Ok(false) => panic!("Some tests failed"),
183         Err(e) => {
184             panic!("I/O failure during tests: {:?}", e);
185         },
186     }
187 }
188
189 fn run_ui_cargo(config: &mut compiletest::Config) {
190     fn run_tests(
191         config: &compiletest::Config,
192         filters: &[String],
193         mut tests: Vec<tester::TestDescAndFn>,
194     ) -> Result<bool, io::Error> {
195         let mut result = true;
196         let opts = compiletest::test_opts(config);
197
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
204             // Use the filter if provided
205             let dir_path = dir.path();
206             for filter in filters {
207                 if !dir_path.ends_with(filter) {
208                     continue;
209                 }
210             }
211
212             for case in fs::read_dir(&dir_path)? {
213                 let case = case?;
214                 if !case.file_type()?.is_dir() {
215                     continue;
216                 }
217
218                 let src_path = case.path().join("src");
219
220                 // When switching between branches, if the previous branch had a test
221                 // that the current branch does not have, the directory is not removed
222                 // because an ignored Cargo.lock file exists.
223                 if !src_path.exists() {
224                     continue;
225                 }
226
227                 env::set_current_dir(&src_path)?;
228                 for file in fs::read_dir(&src_path)? {
229                     let file = file?;
230                     if file.file_type()?.is_dir() {
231                         continue;
232                     }
233
234                     // Search for the main file to avoid running a test for each file in the project
235                     let file_path = file.path();
236                     match file_path.file_name().and_then(OsStr::to_str) {
237                         Some("main.rs") => {},
238                         _ => continue,
239                     }
240                     let _g = VarGuard::set("CLIPPY_CONF_DIR", case.path());
241                     let paths = compiletest::common::TestPaths {
242                         file: file_path,
243                         base: config.src_base.clone(),
244                         relative_dir: src_path.strip_prefix(&config.src_base).unwrap().into(),
245                     };
246                     let test_name = compiletest::make_test_name(config, &paths);
247                     let index = tests
248                         .iter()
249                         .position(|test| test.desc.name == test_name)
250                         .expect("The test should be in there");
251                     result &= tester::run_tests_console(&opts, vec![tests.swap_remove(index)])?;
252                 }
253             }
254         }
255         Ok(result)
256     }
257
258     if cargo::is_rustc_test_suite() {
259         return;
260     }
261
262     config.mode = TestMode::Ui;
263     config.src_base = Path::new("tests").join("ui-cargo").canonicalize().unwrap();
264
265     let tests = compiletest::make_tests(config);
266
267     let current_dir = env::current_dir().unwrap();
268     let res = run_tests(config, &config.filters, tests);
269     env::set_current_dir(current_dir).unwrap();
270
271     match res {
272         Ok(true) => {},
273         Ok(false) => panic!("Some tests failed"),
274         Err(e) => {
275             panic!("I/O failure during tests: {:?}", e);
276         },
277     }
278 }
279
280 fn prepare_env() {
281     set_var("CLIPPY_DISABLE_DOCS_LINKS", "true");
282     set_var("__CLIPPY_INTERNAL_TESTS", "true");
283     //set_var("RUST_BACKTRACE", "0");
284 }
285
286 #[test]
287 fn compile_test() {
288     prepare_env();
289     let mut config = default_config();
290     run_ui(&mut config);
291     run_ui_toml(&mut config);
292     run_ui_cargo(&mut config);
293     run_internal_tests(&mut config);
294 }
295
296 /// Restores an env var on drop
297 #[must_use]
298 struct VarGuard {
299     key: &'static str,
300     value: Option<OsString>,
301 }
302
303 impl VarGuard {
304     fn set(key: &'static str, val: impl AsRef<OsStr>) -> Self {
305         let value = var_os(key);
306         set_var(key, val);
307         Self { key, value }
308     }
309 }
310
311 impl Drop for VarGuard {
312     fn drop(&mut self) {
313         match self.value.as_deref() {
314             None => remove_var(self.key),
315             Some(value) => set_var(self.key, value),
316         }
317     }
318 }
319
320 fn strip_current_dir(path: &Path) -> &Path {
321     if let Ok(curr) = env::current_dir() {
322         if let Ok(stripped) = path.strip_prefix(curr) {
323             return stripped;
324         }
325     }
326     path
327 }