1 | #[cfg (feature = "rustc" )] |
2 | use crate::{ |
3 | aux_builds::AuxBuilder, custom_flags::edition::Edition, |
4 | custom_flags::revision_args::RustcRevisionArgs, custom_flags::run::Run, |
5 | custom_flags::rustfix::RustfixMode, custom_flags::Flag, filter::Match, |
6 | }; |
7 | use crate::{ |
8 | diagnostics::{self, Diagnostics}, |
9 | parser::CommandParserFunc, |
10 | per_test_config::{Comments, Condition, TestConfig}, |
11 | CommandBuilder, Error, Errored, Errors, |
12 | }; |
13 | use color_eyre::eyre::Result; |
14 | use regex::bytes::Regex; |
15 | use spanned::Spanned; |
16 | use std::{ |
17 | collections::BTreeMap, |
18 | num::NonZeroUsize, |
19 | path::{Path, PathBuf}, |
20 | process::{Command, Output}, |
21 | sync::{atomic::AtomicBool, Arc}, |
22 | }; |
23 | |
24 | mod args; |
25 | pub use args::{Args, Format}; |
26 | |
27 | #[derive (Debug, Clone)] |
28 | /// Central datastructure containing all information to run the tests. |
29 | pub struct Config { |
30 | /// Host triple; usually will be auto-detected. |
31 | pub host: Option<String>, |
32 | /// `None` to run on the host, otherwise a target triple |
33 | pub target: Option<String>, |
34 | /// The folder in which to start searching for .rs files |
35 | pub root_dir: PathBuf, |
36 | /// The binary to actually execute. |
37 | pub program: CommandBuilder, |
38 | /// What to do in case the stdout/stderr output differs from the expected one. |
39 | pub output_conflict_handling: OutputConflictHandling, |
40 | /// The recommended command to bless failing tests. |
41 | pub bless_command: Option<String>, |
42 | /// Where to dump files like the binaries compiled from tests. |
43 | /// Defaults to `target/ui/index_of_config` in the current directory. |
44 | pub out_dir: PathBuf, |
45 | /// Skip test files whose names contain any of these entries. |
46 | pub skip_files: Vec<String>, |
47 | /// Only test files whose names contain any of these entries. |
48 | pub filter_files: Vec<String>, |
49 | /// Override the number of threads to use. |
50 | pub threads: Option<NonZeroUsize>, |
51 | /// Nextest emulation: only list the test itself, not its components. |
52 | pub list: bool, |
53 | /// Only run the tests that are ignored. |
54 | pub run_only_ignored: bool, |
55 | /// Filters must match exactly instead of just checking for substrings. |
56 | pub filter_exact: bool, |
57 | /// The default settings settable via `@` comments |
58 | pub comment_defaults: Comments, |
59 | /// The symbol(s) that signify the start of a comment. |
60 | pub comment_start: &'static str, |
61 | /// Custom comment parsers |
62 | pub custom_comments: BTreeMap<&'static str, CommandParserFunc>, |
63 | /// Custom diagnostic extractor (invoked on the output of tests) |
64 | pub diagnostic_extractor: fn(&Path, &[u8]) -> Diagnostics, |
65 | /// Handle to the global abort check. |
66 | pub abort_check: AbortCheck, |
67 | } |
68 | |
69 | /// An atomic bool that can be set to `true` to abort all tests. |
70 | /// Will not cancel child processes, but if set from a Ctrl+C handler, |
71 | /// the pressing of Ctrl+C will already have cancelled child processes. |
72 | #[derive (Clone, Debug, Default)] |
73 | pub struct AbortCheck(Arc<AtomicBool>); |
74 | |
75 | impl AbortCheck { |
76 | /// Whether any test has been aborted. |
77 | pub fn aborted(&self) -> bool { |
78 | self.0.load(order:std::sync::atomic::Ordering::Relaxed) |
79 | } |
80 | |
81 | /// Inform everyone that an abort has been requested |
82 | pub fn abort(&self) { |
83 | self.0.store(val:true, order:std::sync::atomic::Ordering::Relaxed) |
84 | } |
85 | } |
86 | |
87 | /// Function that performs the actual output conflict handling. |
88 | pub type OutputConflictHandling = fn(&Path, &[u8], &mut Errors, &TestConfig); |
89 | |
90 | impl Config { |
91 | /// Create a blank configuration that doesn't do anything interesting |
92 | pub fn dummy() -> Self { |
93 | let mut comment_defaults = Comments::default(); |
94 | comment_defaults.base().require_annotations = Spanned::dummy(true).into(); |
95 | Self { |
96 | host: Default::default(), |
97 | target: Default::default(), |
98 | root_dir: Default::default(), |
99 | program: CommandBuilder::cmd("" ), |
100 | output_conflict_handling: error_on_output_conflict, |
101 | bless_command: Default::default(), |
102 | out_dir: Default::default(), |
103 | skip_files: Default::default(), |
104 | filter_files: Default::default(), |
105 | threads: Default::default(), |
106 | list: Default::default(), |
107 | run_only_ignored: Default::default(), |
108 | filter_exact: Default::default(), |
109 | comment_defaults, |
110 | comment_start: "//" , |
111 | custom_comments: Default::default(), |
112 | diagnostic_extractor: diagnostics::default_diagnostics_extractor, |
113 | abort_check: Default::default(), |
114 | } |
115 | } |
116 | |
117 | /// Create a configuration for testing the output of running |
118 | /// `rustc` on the test files. |
119 | #[cfg (feature = "rustc" )] |
120 | pub fn rustc(root_dir: impl Into<PathBuf>) -> Self { |
121 | let mut comment_defaults = Comments::default(); |
122 | |
123 | #[derive (Debug)] |
124 | struct NeedsAsmSupport; |
125 | |
126 | impl Flag for NeedsAsmSupport { |
127 | fn must_be_unique(&self) -> bool { |
128 | true |
129 | } |
130 | fn clone_inner(&self) -> Box<dyn Flag> { |
131 | Box::new(NeedsAsmSupport) |
132 | } |
133 | fn test_condition( |
134 | &self, |
135 | config: &Config, |
136 | _comments: &Comments, |
137 | _revision: &str, |
138 | ) -> bool { |
139 | let target = config.target.as_ref().unwrap(); |
140 | static ASM_SUPPORTED_ARCHS: &[&str] = &[ |
141 | "x86" , "x86_64" , "arm" , "aarch64" , "riscv32" , |
142 | "riscv64" , |
143 | // These targets require an additional asm_experimental_arch feature. |
144 | // "nvptx64", "hexagon", "mips", "mips64", "spirv", "wasm32", |
145 | ]; |
146 | !ASM_SUPPORTED_ARCHS.iter().any(|arch| target.contains(arch)) |
147 | } |
148 | } |
149 | |
150 | comment_defaults |
151 | .base() |
152 | .add_custom("rustc-revision-args" , RustcRevisionArgs); |
153 | comment_defaults |
154 | .base() |
155 | .add_custom("edition" , Edition("2021" .into())); |
156 | comment_defaults |
157 | .base() |
158 | .add_custom("rustfix" , RustfixMode::MachineApplicable); |
159 | let filters = vec![ |
160 | (Match::PathBackslash, b"/" .to_vec()), |
161 | #[cfg (windows)] |
162 | (Match::Exact(vec![b' \r' ]), b"" .to_vec()), |
163 | #[cfg (windows)] |
164 | (Match::Exact(br"\\?\" .to_vec()), b"" .to_vec()), |
165 | ]; |
166 | comment_defaults |
167 | .base() |
168 | .normalize_stderr |
169 | .clone_from(&filters); |
170 | comment_defaults.base().normalize_stdout = filters; |
171 | comment_defaults.base().exit_status = Spanned::dummy(1).into(); |
172 | comment_defaults.base().require_annotations = Spanned::dummy(true).into(); |
173 | let mut config = Self { |
174 | host: None, |
175 | target: None, |
176 | root_dir: root_dir.into(), |
177 | program: CommandBuilder::rustc(), |
178 | output_conflict_handling: error_on_output_conflict, |
179 | bless_command: None, |
180 | out_dir: std::env::var_os("CARGO_TARGET_DIR" ) |
181 | .map(PathBuf::from) |
182 | .unwrap_or_else(|| std::env::current_dir().unwrap().join("target" )) |
183 | .join("ui" ), |
184 | skip_files: Vec::new(), |
185 | filter_files: Vec::new(), |
186 | threads: None, |
187 | list: false, |
188 | run_only_ignored: false, |
189 | filter_exact: false, |
190 | comment_defaults, |
191 | comment_start: "//" , |
192 | custom_comments: Default::default(), |
193 | diagnostic_extractor: diagnostics::rustc::rustc_diagnostics_extractor, |
194 | abort_check: Default::default(), |
195 | }; |
196 | config |
197 | .custom_comments |
198 | .insert("no-rustfix" , |parser, _args, span| { |
199 | // args are ignored (can be used as comment) |
200 | parser.set_custom_once("no-rustfix" , (), span); |
201 | }); |
202 | |
203 | config |
204 | .custom_comments |
205 | .insert("edition" , |parser, args, _span| { |
206 | parser.set_custom_once("edition" , Edition((*args).into()), args.span()); |
207 | }); |
208 | |
209 | config |
210 | .custom_comments |
211 | .insert("needs-asm-support" , |parser, _args, span| { |
212 | // args are ignored (can be used as comment) |
213 | parser.set_custom_once("needs-asm-support" , NeedsAsmSupport, span); |
214 | }); |
215 | |
216 | config.custom_comments.insert("run" , |parser, args, span| { |
217 | let set = |exit_code| { |
218 | parser.set_custom_once( |
219 | "run" , |
220 | Run { |
221 | exit_code, |
222 | output_conflict_handling: None, |
223 | }, |
224 | args.span(), |
225 | ); |
226 | parser.exit_status = Spanned::new(0, span.clone()).into(); |
227 | parser.require_annotations = Spanned::new(false, span.clone()).into(); |
228 | |
229 | let prev = parser |
230 | .custom |
231 | .insert("no-rustfix" , Spanned::new(vec![Box::new(())], span.clone())); |
232 | parser.check(span, prev.is_none(), "`run` implies `no-rustfix`" ); |
233 | }; |
234 | if args.is_empty() { |
235 | set(0); |
236 | } else { |
237 | match args.content.parse() { |
238 | Ok(exit_code) => { |
239 | set(exit_code); |
240 | } |
241 | Err(err) => parser.error(args.span(), err.to_string()), |
242 | } |
243 | } |
244 | }); |
245 | config.custom_comments.insert("aux-build" , |parser, args, span| { |
246 | let name = match args.split_once(":" ) { |
247 | Some((name, rest)) => { |
248 | parser.error(rest.span(), "proc macros are now auto-detected, you can remove the `:proc-macro` after the file name" ); |
249 | name |
250 | }, |
251 | None => args, |
252 | }; |
253 | |
254 | parser |
255 | .add_custom_spanned("aux-build" , AuxBuilder { aux_file: name.map(|n| n.into())}, span); |
256 | }); |
257 | config |
258 | } |
259 | |
260 | /// Create a configuration for testing the output of running |
261 | /// `cargo` on the test `Cargo.toml` files. |
262 | #[cfg (feature = "rustc" )] |
263 | pub fn cargo(root_dir: impl Into<PathBuf>) -> Self { |
264 | let mut this = Self { |
265 | program: CommandBuilder::cargo(), |
266 | custom_comments: Default::default(), |
267 | diagnostic_extractor: diagnostics::rustc::cargo_diagnostics_extractor, |
268 | comment_start: "#" , |
269 | ..Self::rustc(root_dir) |
270 | }; |
271 | this.comment_defaults.base().custom.clear(); |
272 | this |
273 | } |
274 | |
275 | /// Populate the config with the values from parsed command line arguments. |
276 | pub fn with_args(&mut self, args: &Args) { |
277 | let Args { |
278 | ref filters, |
279 | check, |
280 | bless, |
281 | list, |
282 | exact, |
283 | ignored, |
284 | format: _, |
285 | threads, |
286 | ref skip, |
287 | } = *args; |
288 | |
289 | self.threads = threads.or(self.threads); |
290 | |
291 | self.filter_files.extend_from_slice(filters); |
292 | self.skip_files.extend_from_slice(skip); |
293 | self.run_only_ignored = ignored; |
294 | self.filter_exact = exact; |
295 | |
296 | self.list = list; |
297 | |
298 | if check { |
299 | self.output_conflict_handling = error_on_output_conflict; |
300 | } else if bless { |
301 | self.output_conflict_handling = bless_output_files; |
302 | } |
303 | } |
304 | |
305 | /// Replace all occurrences of a path in stderr/stdout with a byte string. |
306 | #[track_caller ] |
307 | pub fn path_filter(&mut self, path: &Path, replacement: &'static (impl AsRef<[u8]> + ?Sized)) { |
308 | self.path_stderr_filter(path, replacement); |
309 | self.path_stdout_filter(path, replacement); |
310 | } |
311 | |
312 | /// Replace all occurrences of a path in stderr with a byte string. |
313 | #[track_caller ] |
314 | pub fn path_stderr_filter( |
315 | &mut self, |
316 | path: &Path, |
317 | replacement: &'static (impl AsRef<[u8]> + ?Sized), |
318 | ) { |
319 | let pattern = path.canonicalize().unwrap(); |
320 | self.comment_defaults.base().normalize_stderr.push(( |
321 | pattern.parent().unwrap().into(), |
322 | replacement.as_ref().to_owned(), |
323 | )); |
324 | } |
325 | |
326 | /// Replace all occurrences of a path in stdout with a byte string. |
327 | #[track_caller ] |
328 | pub fn path_stdout_filter( |
329 | &mut self, |
330 | path: &Path, |
331 | replacement: &'static (impl AsRef<[u8]> + ?Sized), |
332 | ) { |
333 | let pattern = path.canonicalize().unwrap(); |
334 | self.comment_defaults.base().normalize_stdout.push(( |
335 | pattern.parent().unwrap().into(), |
336 | replacement.as_ref().to_owned(), |
337 | )); |
338 | } |
339 | |
340 | /// Replace all occurrences of a regex pattern in stderr/stdout with a byte string. |
341 | #[track_caller ] |
342 | pub fn filter(&mut self, pattern: &str, replacement: &'static (impl AsRef<[u8]> + ?Sized)) { |
343 | self.stderr_filter(pattern, replacement); |
344 | self.stdout_filter(pattern, replacement); |
345 | } |
346 | |
347 | /// Replace all occurrences of a regex pattern in stderr with a byte string. |
348 | #[track_caller ] |
349 | pub fn stderr_filter( |
350 | &mut self, |
351 | pattern: &str, |
352 | replacement: &'static (impl AsRef<[u8]> + ?Sized), |
353 | ) { |
354 | self.comment_defaults.base().normalize_stderr.push(( |
355 | Regex::new(pattern).unwrap().into(), |
356 | replacement.as_ref().to_owned(), |
357 | )); |
358 | } |
359 | |
360 | /// Replace all occurrences of a regex pattern in stdout with a byte string. |
361 | #[track_caller ] |
362 | pub fn stdout_filter( |
363 | &mut self, |
364 | pattern: &str, |
365 | replacement: &'static (impl AsRef<[u8]> + ?Sized), |
366 | ) { |
367 | self.comment_defaults.base().normalize_stdout.push(( |
368 | Regex::new(pattern).unwrap().into(), |
369 | replacement.as_ref().to_owned(), |
370 | )); |
371 | } |
372 | |
373 | /// Make sure we have the host and target triples. |
374 | pub fn fill_host_and_target(&mut self) -> Result<()> { |
375 | if self.host.is_none() { |
376 | self.host = Some( |
377 | rustc_version::VersionMeta::for_command(std::process::Command::new( |
378 | &self.program.program, |
379 | )) |
380 | .map_err(|err| { |
381 | color_eyre::eyre::Report::new(err).wrap_err(format!( |
382 | "failed to parse rustc version info: {}" , |
383 | self.program.display().to_string().replace(' \\' , "/" ) |
384 | )) |
385 | })? |
386 | .host, |
387 | ); |
388 | } |
389 | if self.target.is_none() { |
390 | self.target = Some(self.host.clone().unwrap()); |
391 | } |
392 | Ok(()) |
393 | } |
394 | |
395 | /// Check whether the host is the specified string |
396 | pub fn host_matches_target(&self) -> bool { |
397 | self.host.as_ref().expect("host should have been filled in" ) |
398 | == self |
399 | .target |
400 | .as_ref() |
401 | .expect("target should have been filled in" ) |
402 | } |
403 | |
404 | pub(crate) fn get_pointer_width(&self) -> u8 { |
405 | // Taken 1:1 from compiletest-rs |
406 | fn get_pointer_width(triple: &str) -> u8 { |
407 | if (triple.contains("64" ) |
408 | && !triple.ends_with("gnux32" ) |
409 | && !triple.ends_with("gnu_ilp32" )) |
410 | || triple.starts_with("s390x" ) |
411 | { |
412 | 64 |
413 | } else if triple.starts_with("avr" ) { |
414 | 16 |
415 | } else { |
416 | 32 |
417 | } |
418 | } |
419 | get_pointer_width(self.target.as_ref().unwrap()) |
420 | } |
421 | |
422 | pub(crate) fn test_condition(&self, condition: &Condition) -> bool { |
423 | let target = self.target.as_ref().unwrap(); |
424 | match condition { |
425 | Condition::Bitwidth(bits) => bits.iter().any(|bits| self.get_pointer_width() == *bits), |
426 | Condition::Target(t) => t.iter().any(|t| target.contains(&**t)), |
427 | Condition::Host(t) => t.iter().any(|t| self.host.as_ref().unwrap().contains(&**t)), |
428 | Condition::OnHost => self.host_matches_target(), |
429 | } |
430 | } |
431 | |
432 | /// Returns whether according to the in-file conditions, this file should be run. |
433 | pub fn test_file_conditions(&self, comments: &Comments, revision: &str) -> bool { |
434 | if comments |
435 | .for_revision(revision) |
436 | .flat_map(|r| r.ignore.iter()) |
437 | .any(|c| self.test_condition(c)) |
438 | { |
439 | return self.run_only_ignored; |
440 | } |
441 | if comments.for_revision(revision).any(|r| { |
442 | r.custom.values().any(|flags| { |
443 | flags |
444 | .content |
445 | .iter() |
446 | .any(|flag| flag.test_condition(self, comments, revision)) |
447 | }) |
448 | }) { |
449 | return self.run_only_ignored; |
450 | } |
451 | comments |
452 | .for_revision(revision) |
453 | .flat_map(|r| r.only.iter()) |
454 | .all(|c| self.test_condition(c)) |
455 | ^ self.run_only_ignored |
456 | } |
457 | |
458 | pub(crate) fn aborted(&self) -> Result<(), Errored> { |
459 | if self.abort_check.aborted() { |
460 | Err(Errored::aborted()) |
461 | } else { |
462 | Ok(()) |
463 | } |
464 | } |
465 | |
466 | pub(crate) fn run_command(&self, cmd: &mut Command) -> Result<Output, Errored> { |
467 | self.aborted()?; |
468 | |
469 | let output = cmd.output().map_err(|err| Errored { |
470 | errors: vec![], |
471 | stderr: err.to_string().into_bytes(), |
472 | stdout: format!("could not spawn ` {:?}` as a process" , cmd.get_program()).into_bytes(), |
473 | command: format!(" {cmd:?}" ), |
474 | })?; |
475 | |
476 | self.aborted()?; |
477 | |
478 | Ok(output) |
479 | } |
480 | } |
481 | |
482 | /// Fail the test when mismatches are found, if provided the command string |
483 | /// in [`Config::bless_command`] will be suggested as a way to bless the |
484 | /// test. |
485 | pub fn error_on_output_conflict( |
486 | path: &Path, |
487 | output: &[u8], |
488 | errors: &mut Errors, |
489 | config: &TestConfig, |
490 | ) { |
491 | let normalized: Vec = config.normalize(text:output, &path.extension().unwrap().to_string_lossy()); |
492 | let expected: Vec = std::fs::read(path).unwrap_or_default(); |
493 | if normalized != expected { |
494 | errors.push(Error::OutputDiffers { |
495 | path: path.to_path_buf(), |
496 | actual: normalized, |
497 | output: output.to_vec(), |
498 | expected, |
499 | bless_command: config.config.bless_command.clone(), |
500 | }); |
501 | } |
502 | } |
503 | |
504 | /// Ignore mismatches in the stderr/stdout files. |
505 | pub fn ignore_output_conflict( |
506 | _path: &Path, |
507 | _output: &[u8], |
508 | _errors: &mut Errors, |
509 | _config: &TestConfig, |
510 | ) { |
511 | } |
512 | |
513 | /// Instead of erroring if the stderr/stdout differs from the expected |
514 | /// automatically replace it with the found output (after applying filters). |
515 | pub fn bless_output_files(path: &Path, output: &[u8], _errors: &mut Errors, config: &TestConfig) { |
516 | if output.is_empty() { |
517 | let _ = std::fs::remove_file(path); |
518 | } else { |
519 | let actual: Vec = config.normalize(text:output, &path.extension().unwrap().to_string_lossy()); |
520 | std::fs::write(path, contents:actual).unwrap(); |
521 | } |
522 | } |
523 | |