1#![crate_name = "compiletest"]
2#![warn(unreachable_pub)]
3
4#[cfg(test)]
5mod tests;
6
7pub mod cli;
9pub mod rustdoc_gui_test;
10
11mod common;
12mod debuggers;
13mod diagnostics;
14mod directives;
15mod edition;
16mod errors;
17mod executor;
18mod json;
19mod output_capture;
20mod panic_hook;
21mod raise_fd_limit;
22mod read2;
23mod runtest;
24mod util;
25
26use core::panic;
27use std::collections::HashSet;
28use std::fmt::Write;
29use std::io::{self, ErrorKind};
30use std::sync::Arc;
31use std::time::SystemTime;
32use std::{env, fs, vec};
33
34use build_helper::git::{get_git_modified_files, get_git_untracked_files};
35use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
36use rayon::iter::{ParallelBridge, ParallelIterator};
37use tracing::debug;
38use walkdir::WalkDir;
39
40use self::directives::{EarlyProps, make_test_description};
41use crate::common::{
42 CodegenBackend, Config, Debugger, TestMode, TestPaths, UI_EXTENSIONS, expected_output_path,
43 output_base_dir, output_relative_path,
44};
45use crate::directives::{AuxProps, DirectivesCache, FileDirectives};
46use crate::executor::{CollectedTest, TestVariant};
47
48fn run_tests(config: Arc<Config>) {
50 debug!(?config, "run_tests");
51
52 panic_hook::install_panic_hook();
53
54 if config.rustfix_coverage {
58 let mut coverage_file_path = config.build_test_suite_root.clone();
59 coverage_file_path.push("rustfix_missing_coverage.txt");
60 if coverage_file_path.exists() {
61 if let Err(e) = fs::remove_file(&coverage_file_path) {
62 panic!("Could not delete {} due to {}", coverage_file_path, e)
63 }
64 }
65 }
66
67 unsafe {
71 raise_fd_limit::raise_fd_limit();
72 }
73 unsafe { env::set_var("__COMPAT_LAYER", "RunAsInvoker") };
78
79 let ignore_tests = config.mode == TestMode::DebugInfo && config.target.contains("emscripten");
81
82 if let TestMode::DebugInfo = config.mode {
83 if config.target.contains("android") {
85 println!("{} debug-info test uses tcp 5039 port. please reserve it", config.target);
86
87 unsafe { env::set_var("RUST_TEST_THREADS", "1") };
96 }
97 };
98
99 let mut tests = Vec::new();
102 if !ignore_tests {
103 tests.extend(collect_and_make_tests(config.clone()));
104 }
105
106 tests.sort_by(|a, b| Ord::cmp(&a.desc.name, &b.desc.name));
107
108 let ok = executor::run_tests(&config, tests);
112
113 if !ok {
115 let mut msg = String::from("Some tests failed in compiletest");
123 write!(msg, " suite={}", config.suite).unwrap();
124
125 if let Some(compare_mode) = config.compare_mode.as_ref() {
126 write!(msg, " compare_mode={}", compare_mode).unwrap();
127 }
128
129 if let Some(pass_mode) = config.force_pass_mode.as_ref() {
130 write!(msg, " pass_mode={}", pass_mode).unwrap();
131 }
132
133 write!(msg, " mode={}", config.mode).unwrap();
134 write!(msg, " host={}", config.host).unwrap();
135 write!(msg, " target={}", config.target).unwrap();
136
137 println!("{msg}");
138
139 std::process::exit(1);
140 }
141}
142
143struct TestCollectorCx {
145 config: Arc<Config>,
146 cache: DirectivesCache,
147 common_inputs_stamp: Stamp,
148 modified_tests: Vec<Utf8PathBuf>,
149}
150
151struct TestCollector {
153 tests: Vec<CollectedTest>,
154 found_path_stems: HashSet<Utf8PathBuf>,
155 poisoned: bool,
156}
157
158impl TestCollector {
159 fn new() -> Self {
160 TestCollector { tests: vec![], found_path_stems: HashSet::new(), poisoned: false }
161 }
162
163 fn merge(&mut self, mut other: Self) {
164 self.tests.append(&mut other.tests);
165 self.found_path_stems.extend(other.found_path_stems);
166 self.poisoned |= other.poisoned;
167 }
168}
169
170fn collect_and_make_tests(config: Arc<Config>) -> Vec<CollectedTest> {
180 debug!("making tests from {}", config.src_test_suite_root);
181 let common_inputs_stamp = common_inputs_stamp(&config);
182 let modified_tests =
183 modified_tests(&config, &config.src_test_suite_root).unwrap_or_else(|err| {
184 fatal!("modified_tests: {}: {err}", config.src_test_suite_root);
185 });
186 let cache = DirectivesCache::load(&config);
187
188 let cx = TestCollectorCx { config, cache, common_inputs_stamp, modified_tests };
189 let collector = collect_tests_from_dir(&cx, &cx.config.src_test_suite_root, Utf8Path::new(""))
190 .unwrap_or_else(|reason| {
191 panic!("Could not read tests from {}: {reason}", cx.config.src_test_suite_root)
192 });
193
194 let TestCollector { tests, found_path_stems, poisoned } = collector;
195
196 if poisoned {
197 eprintln!();
198 panic!("there are errors in tests");
199 }
200
201 check_for_overlapping_test_paths(&found_path_stems);
202
203 tests
204}
205
206fn common_inputs_stamp(config: &Config) -> Stamp {
214 let src_root = &config.src_root;
215
216 let mut stamp = Stamp::from_path(&config.rustc_path);
217
218 let pretty_printer_files = [
220 "src/etc/rust_types.py",
221 "src/etc/gdb_load_rust_pretty_printers.py",
222 "src/etc/gdb_lookup.py",
223 "src/etc/gdb_providers.py",
224 "src/etc/lldb_batchmode",
225 "src/etc/lldb_lookup.py",
226 "src/etc/lldb_providers.py",
227 ];
228 for file in &pretty_printer_files {
229 let path = src_root.join(file);
230 stamp.add_path(&path);
231 }
232
233 stamp.add_dir(&src_root.join("src/etc/natvis"));
234
235 stamp.add_dir(&config.target_run_lib_path);
236
237 if let Some(ref rustdoc_path) = config.rustdoc_path {
238 stamp.add_path(&rustdoc_path);
239 stamp.add_path(&src_root.join("src/etc/htmldocck.py"));
240 }
241
242 if let Some(coverage_dump_path) = &config.coverage_dump_path {
245 stamp.add_path(coverage_dump_path)
246 }
247
248 stamp.add_dir(&src_root.join("src/tools/run-make-support"));
249
250 stamp.add_dir(&src_root.join("src/tools/compiletest"));
252
253 stamp
254}
255
256fn modified_tests(config: &Config, dir: &Utf8Path) -> Result<Vec<Utf8PathBuf>, String> {
261 if !config.only_modified {
264 return Ok(vec![]);
265 }
266
267 let files = get_git_modified_files(
268 &config.git_config(),
269 Some(dir.as_std_path()),
270 &vec!["rs", "stderr", "fixed"],
271 )?;
272 let untracked_files = get_git_untracked_files(Some(dir.as_std_path()))?.unwrap_or(vec![]);
274
275 let all_paths = [&files[..], &untracked_files[..]].concat();
276 let full_paths = {
277 let mut full_paths: Vec<Utf8PathBuf> = all_paths
278 .into_iter()
279 .map(|f| Utf8PathBuf::from(f).with_extension("").with_extension("rs"))
280 .filter_map(
281 |f| if Utf8Path::new(&f).exists() { f.canonicalize_utf8().ok() } else { None },
282 )
283 .collect();
284 full_paths.dedup();
285 full_paths.sort_unstable();
286 full_paths
287 };
288 Ok(full_paths)
289}
290
291fn collect_tests_from_dir(
294 cx: &TestCollectorCx,
295 dir: &Utf8Path,
296 relative_dir_path: &Utf8Path,
297) -> io::Result<TestCollector> {
298 if dir.join("compiletest-ignore-dir").exists() {
300 return Ok(TestCollector::new());
301 }
302
303 let mut components = dir.components().rev();
304 if let Some(Utf8Component::Normal(last)) = components.next()
305 && let Some(("assembly" | "codegen", backend)) = last.split_once('-')
306 && let Some(Utf8Component::Normal(parent)) = components.next()
307 && parent == "tests"
308 && let Ok(backend) = backend.parse::<CodegenBackend>()
309 && backend != cx.config.default_codegen_backend
310 {
311 warning!(
313 "Ignoring tests in `{dir}` because they don't match the configured codegen \
314 backend (`{}`)",
315 cx.config.default_codegen_backend.as_str(),
316 );
317 return Ok(TestCollector::new());
318 }
319
320 if cx.config.mode == TestMode::RunMake {
322 let mut collector = TestCollector::new();
323 if dir.join("rmake.rs").exists() {
324 let paths = TestPaths {
325 file: dir.to_path_buf(),
326 relative_dir: relative_dir_path.parent().unwrap().to_path_buf(),
327 };
328 make_test(cx, &mut collector, &paths);
329 return Ok(collector);
331 }
332 }
333
334 let build_dir = output_relative_path(&cx.config, relative_dir_path);
341 fs::create_dir_all(&build_dir).unwrap();
342
343 fs::read_dir(dir.as_std_path())?
348 .par_bridge()
349 .map(|file| {
350 let mut collector = TestCollector::new();
351 let file = file?;
352 let file_path = Utf8PathBuf::try_from(file.path()).unwrap();
353 let file_name = file_path.file_name().unwrap();
354
355 if is_test(file_name)
356 && (!cx.config.only_modified || cx.modified_tests.contains(&file_path))
357 {
358 debug!(%file_path, "found test file");
360
361 let rel_test_path = relative_dir_path.join(file_path.file_stem().unwrap());
363 collector.found_path_stems.insert(rel_test_path);
364
365 let paths =
366 TestPaths { file: file_path, relative_dir: relative_dir_path.to_path_buf() };
367 make_test(cx, &mut collector, &paths);
368 } else if file_path.is_dir() {
369 let relative_file_path = relative_dir_path.join(file_name);
371 if file_name != "auxiliary" {
372 debug!(%file_path, "found directory");
373 collector.merge(collect_tests_from_dir(cx, &file_path, &relative_file_path)?);
374 }
375 } else {
376 debug!(%file_path, "found other file/directory");
377 }
378 Ok(collector)
379 })
380 .reduce(
381 || Ok(TestCollector::new()),
382 |a, b| {
383 let mut a = a?;
384 a.merge(b?);
385 Ok(a)
386 },
387 )
388}
389
390fn is_test(file_name: &str) -> bool {
392 if !file_name.ends_with(".rs") {
393 return false;
394 }
395
396 let invalid_prefixes = &[".", "#", "~"];
398 !invalid_prefixes.iter().any(|p| file_name.starts_with(p))
399}
400
401fn make_test(cx: &TestCollectorCx, collector: &mut TestCollector, testpaths: &TestPaths) {
404 let test_path = if cx.config.mode == TestMode::RunMake {
408 testpaths.file.join("rmake.rs")
409 } else {
410 testpaths.file.clone()
411 };
412
413 let file_contents =
415 fs::read_to_string(&test_path).expect("reading test file for directives should succeed");
416 let file_directives = FileDirectives::from_file_contents(&test_path, &file_contents);
417
418 if let Err(message) = directives::do_early_directives_check(cx.config.mode, &file_directives) {
419 panic!("directives check failed:\n{message}");
422 }
423 let early_props = EarlyProps::from_file_directives(&cx.config, &file_directives);
424
425 let revisions = if early_props.revisions.is_empty() || cx.config.mode == TestMode::Incremental {
432 vec![None]
433 } else {
434 early_props.revisions.iter().map(|r| Some(r.as_str())).collect()
435 };
436
437 let debuggers = if cx.config.mode == TestMode::DebugInfo {
440 vec![Some(Debugger::Cdb), Some(Debugger::Gdb), Some(Debugger::Lldb)]
441 } else {
442 vec![None]
443 };
444
445 for debugger in debuggers {
448 collector.tests.extend(revisions.iter().map(|&revision| {
449 let revision = revision.map(str::to_owned);
450 let variant = TestVariant { revision, debugger };
451
452 let (test_name, filterable_path) =
454 make_test_name_and_filterable_path(&cx.config, testpaths, &variant);
455
456 let mut aux_props = AuxProps::default();
459
460 let mut desc = make_test_description(
464 &cx.config,
465 &cx.cache,
466 test_name,
467 &test_path,
468 &filterable_path,
469 &file_directives,
470 &variant,
471 &mut collector.poisoned,
472 &mut aux_props,
473 );
474
475 if !desc.is_ignored()
478 && !cx.config.force_rerun
479 && is_up_to_date(cx, testpaths, &aux_props, &variant)
480 {
481 desc.ignore_message = Some("up-to-date".into());
485 }
486
487 let config = Arc::clone(&cx.config);
488 let testpaths = testpaths.clone();
489
490 CollectedTest { desc, config, testpaths, variant }
491 }));
492 }
493}
494
495fn stamp_file_path(config: &Config, testpaths: &TestPaths, variant: &TestVariant) -> Utf8PathBuf {
498 output_base_dir(config, testpaths, variant).join("stamp")
499}
500
501fn files_related_to_test(
506 config: &Config,
507 testpaths: &TestPaths,
508 aux_props: &AuxProps,
509 revision: Option<&str>,
510) -> Vec<Utf8PathBuf> {
511 let mut related = vec![];
512
513 if testpaths.file.is_dir() {
514 for entry in WalkDir::new(&testpaths.file) {
516 let path = entry.unwrap().into_path();
517 if path.is_file() {
518 related.push(Utf8PathBuf::try_from(path).unwrap());
519 }
520 }
521 } else {
522 related.push(testpaths.file.clone());
523 }
524
525 for aux in aux_props.all_aux_path_strings() {
526 let path = testpaths.file.parent().unwrap().join("auxiliary").join(aux);
531 related.push(path);
532 }
533
534 for extension in UI_EXTENSIONS {
536 let path = expected_output_path(testpaths, revision, &config.compare_mode, extension);
537 related.push(path);
538 }
539
540 related.push(config.src_root.join("tests").join("auxiliary").join("minicore.rs"));
542
543 related
544}
545
546fn is_up_to_date(
552 cx: &TestCollectorCx,
553 testpaths: &TestPaths,
554 aux_props: &AuxProps,
555 variant: &TestVariant,
556) -> bool {
557 let stamp_file_path = stamp_file_path(&cx.config, testpaths, variant);
558 let contents = match fs::read_to_string(&stamp_file_path) {
560 Ok(f) => f,
561 Err(ref e) if e.kind() == ErrorKind::InvalidData => panic!("Can't read stamp contents"),
562 Err(_) => return false,
564 };
565 let expected_hash = runtest::compute_stamp_hash(&cx.config, variant);
566 if contents != expected_hash {
567 return false;
570 }
571
572 let mut inputs_stamp = cx.common_inputs_stamp.clone();
575 for path in files_related_to_test(&cx.config, testpaths, aux_props, variant.revision()) {
576 inputs_stamp.add_path(&path);
577 }
578
579 inputs_stamp < Stamp::from_path(&stamp_file_path)
582}
583
584#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
586struct Stamp {
587 time: SystemTime,
588}
589
590impl Stamp {
591 fn from_path(path: &Utf8Path) -> Self {
593 let mut stamp = Stamp { time: SystemTime::UNIX_EPOCH };
594 stamp.add_path(path);
595 stamp
596 }
597
598 fn add_path(&mut self, path: &Utf8Path) {
601 let modified = fs::metadata(path.as_std_path())
602 .and_then(|metadata| metadata.modified())
603 .unwrap_or(SystemTime::UNIX_EPOCH);
604 self.time = self.time.max(modified);
605 }
606
607 fn add_dir(&mut self, path: &Utf8Path) {
611 let path = path.as_std_path();
612 for entry in WalkDir::new(path) {
613 let entry = entry.unwrap();
614 if entry.file_type().is_file() {
615 let modified = entry
616 .metadata()
617 .ok()
618 .and_then(|metadata| metadata.modified().ok())
619 .unwrap_or(SystemTime::UNIX_EPOCH);
620 self.time = self.time.max(modified);
621 }
622 }
623 }
624}
625
626fn make_test_name_and_filterable_path(
628 config: &Config,
629 testpaths: &TestPaths,
630 variant: &TestVariant,
631) -> (String, Utf8PathBuf) {
632 let path = testpaths.file.strip_prefix(&config.src_root).unwrap();
634 let debugger = match variant.debugger.as_ref() {
635 Some(d) => format!("-{d}"),
636 None => String::new(),
637 };
638 let mode_suffix = match config.compare_mode {
639 Some(ref mode) => format!(" ({})", mode.to_str()),
640 None => String::new(),
641 };
642
643 let name = format!(
644 "[{}{}{}] {}{}",
645 config.mode,
646 debugger,
647 mode_suffix,
648 path,
649 variant.revision().map_or("".to_string(), |rev| format!("#{}", rev))
650 );
651
652 let mut filterable_path = path.strip_prefix("tests").unwrap().to_owned();
656 filterable_path = filterable_path.components().skip(1).collect();
658
659 (name, filterable_path)
660}
661
662fn check_for_overlapping_test_paths(found_path_stems: &HashSet<Utf8PathBuf>) {
680 let mut collisions = Vec::new();
681 for path in found_path_stems {
682 for ancestor in path.ancestors().skip(1) {
683 if found_path_stems.contains(ancestor) {
684 collisions.push((path, ancestor));
685 }
686 }
687 }
688 if !collisions.is_empty() {
689 collisions.sort();
690 let collisions: String = collisions
691 .into_iter()
692 .map(|(path, check_parent)| format!("test {path} clashes with {check_parent}\n"))
693 .collect();
694 panic!(
695 "{collisions}\n\
696 Tests cannot have overlapping names. Make sure they use unique prefixes."
697 );
698 }
699}
700
701fn early_config_check(config: &Config) {
702 if !config.profiler_runtime && config.mode == TestMode::CoverageRun {
703 let actioned = if config.bless { "blessed" } else { "checked" };
704 warning!("profiler runtime is not available, so `.coverage` files won't be {actioned}");
705 help!("try setting `profiler = true` in the `[build]` section of `bootstrap.toml`");
706 }
707
708 if env::var("RUST_TEST_NOCAPTURE").is_ok() {
710 warning!("`RUST_TEST_NOCAPTURE` is not supported; use the `--no-capture` flag instead");
711 }
712}