1#![allow(
2 clippy::enum_variant_names,
3 clippy::useless_format,
4 clippy::too_many_arguments,
5 rustc::internal,
6 // `unnameable_types` was stabilized in 1.79 which is higher than the current MSRV.
7 // Ignore the "unknown lint" warning which is emitted on older versions.
8 unknown_lints
9)]
10#![warn(unreachable_pub, unnameable_types)]
11#![deny(missing_docs)]
12#![doc = include_str!("../README.md")]
13
14use build_manager::BuildManager;
15use build_manager::NewJob;
16pub use color_eyre;
17use color_eyre::eyre::eyre;
18#[cfg(feature = "rustc")]
19use color_eyre::eyre::Context as _;
20pub use color_eyre::eyre::Result;
21pub use core::run_and_collect;
22pub use core::CrateType;
23use crossbeam_channel::Sender;
24use crossbeam_channel::TryRecvError;
25pub use filter::Match;
26use per_test_config::TestConfig;
27use spanned::Spanned;
28use status_emitter::RevisionStyle;
29use status_emitter::SilentStatus;
30use status_emitter::{StatusEmitter, TestStatus};
31use std::collections::VecDeque;
32use std::panic::AssertUnwindSafe;
33use std::path::Path;
34#[cfg(feature = "rustc")]
35use std::process::Command;
36use std::sync::Arc;
37use test_result::TestRun;
38pub use test_result::{Errored, TestOk};
39
40pub mod aux_builds;
41pub mod build_manager;
42mod cmd;
43mod config;
44pub mod core;
45pub mod custom_flags;
46#[cfg(feature = "rustc")]
47pub mod dependencies;
48pub mod diagnostics;
49mod diff;
50mod error;
51pub mod filter;
52#[cfg(feature = "gha")]
53pub mod github_actions;
54mod mode;
55pub mod nextest;
56mod parser;
57pub mod per_test_config;
58pub mod status_emitter;
59pub mod test_result;
60
61#[cfg(test)]
62mod tests;
63
64pub use cmd::*;
65pub use config::*;
66pub use error::*;
67pub use parser::*;
68pub use spanned;
69
70/// Run all tests as described in the config argument.
71/// Will additionally process command line arguments.
72pub fn run_tests(mut config: Config) -> Result<()> {
73 let args = Args::test()?;
74 if let Format::Pretty = args.format {
75 println!(
76 "Compiler: {}",
77 config.program.display().to_string().replace('\\', "/")
78 );
79 }
80
81 #[cfg(feature = "gha")]
82 let name = display(&config.root_dir);
83
84 let text = match args.format {
85 Format::Terse => status_emitter::Text::quiet(),
86 Format::Pretty => status_emitter::Text::verbose(),
87 };
88 config.with_args(&args);
89
90 run_tests_generic(
91 vec![config],
92 default_file_filter,
93 default_per_file_config,
94 (
95 text,
96 #[cfg(feature = "gha")]
97 status_emitter::Gha::<true> { name },
98 ),
99 )
100}
101
102/// The filter used by `run_tests` to only run on `.rs` files that are
103/// specified by [`Config::filter_files`] and [`Config::skip_files`].
104///
105/// Returns `None` if there is no extension or the extension is not `.rs`.
106pub fn default_file_filter(path: &Path, config: &Config) -> Option<bool> {
107 path.extension().filter(|&ext: &OsStr| ext == "rs")?;
108 Some(default_any_file_filter(path, config))
109}
110
111/// Run on all files that are specified by [`Config::filter_files`] and
112/// [`Config::skip_files`].
113///
114/// To only include rust files see [`default_file_filter`].
115pub fn default_any_file_filter(path: &Path, config: &Config) -> bool {
116 let path: String = display(path);
117 let contains_path: impl Fn(&[String]) -> bool = |files: &[String]| {
118 files.iter().any(|f: &String| {
119 if config.filter_exact {
120 *f == path
121 } else {
122 path.contains(f)
123 }
124 })
125 };
126
127 if contains_path(&config.skip_files) {
128 return false;
129 }
130
131 config.filter_files.is_empty() || contains_path(&config.filter_files)
132}
133
134/// The default per-file config used by `run_tests`.
135pub fn default_per_file_config(config: &mut Config, file_contents: &Spanned<Vec<u8>>) {
136 config.program.args.push(
137 match&'static str CrateType::from_file_contents(file_contents) {
138 CrateType::ProcMacro => "--crate-type=proc-macro",
139 CrateType::Test => "--test",
140 CrateType::Bin => return,
141 CrateType::Lib => "--crate-type=lib",
142 }
143 .into(),
144 )
145}
146
147/// Create a command for running a single file, with the settings from the `config` argument.
148/// Ignores various settings from `Config` that relate to finding test files.
149#[cfg(feature = "rustc")]
150pub fn test_command(mut config: Config, path: &Path) -> Result<Command> {
151 config.fill_host_and_target()?;
152
153 let content: Spanned> = SpannedResult>, …>::read_from_file(path)
154 .wrap_err_with(|| format!("failed to read {}", display(path)))?;
155 let comments: Comments = Comments::parse(content.as_ref(), &config)
156 .map_err(|errors: Vec| color_eyre::eyre::eyre!("{errors:#?}"))?;
157 let config: TestConfig = TestConfig {
158 config,
159 comments: Arc::new(data:comments),
160 aux_dir: path.parent().unwrap().join(path:"auxiliary"),
161 status: Box::new(SilentStatus {
162 revision: String::new(),
163 path: path.to_path_buf(),
164 }),
165 };
166 let build_manager: BuildManager = BuildManager::new(config.config.clone(), new_job_submitter:crossbeam_channel::bounded(cap:0).0);
167
168 Ok(config.build_command(&build_manager).unwrap())
169}
170
171/// A version of `run_tests` that allows more fine-grained control over running tests.
172///
173/// All `configs` are being run in parallel.
174/// If multiple configs are provided, the [`Config::threads`] value of the first one is used;
175/// the thread count of all other configs is ignored.
176/// The file filter is supposed to return `None` if it was filtered because of file extensions
177/// and `Some(false)` if the file was rejected out of other reasons like the file path not matching
178/// a user defined filter.
179pub fn run_tests_generic(
180 mut configs: Vec<Config>,
181 file_filter: impl Fn(&Path, &Config) -> Option<bool> + Sync,
182 per_file_config: impl Copy + Fn(&mut Config, &Spanned<Vec<u8>>) + Send + Sync + 'static,
183 status_emitter: impl StatusEmitter + Send,
184) -> Result<()> {
185 if nextest::emulate(&mut configs) {
186 return Ok(());
187 }
188
189 for (i, config) in configs.iter_mut().enumerate() {
190 config.fill_host_and_target()?;
191 config.out_dir.push(i.to_string())
192 }
193
194 let mut results = vec![];
195
196 let num_threads = match configs.first().and_then(|config| config.threads) {
197 Some(threads) => threads,
198 None => match std::env::var_os("RUST_TEST_THREADS") {
199 Some(n) => n
200 .to_str()
201 .ok_or_else(|| eyre!("could not parse RUST_TEST_THREADS env var"))?
202 .parse()?,
203 None => std::thread::available_parallelism()?,
204 },
205 };
206
207 let mut filtered = 0;
208 core::run_and_collect(
209 num_threads,
210 |[submit, priority_submit]| {
211 let mut todo = VecDeque::new();
212
213 let configs: Vec<_> = configs
214 .into_iter()
215 .map(|config| Arc::new(BuildManager::new(config, priority_submit.clone())))
216 .collect();
217 for build_manager in &configs {
218 todo.push_back((
219 build_manager.config().root_dir.clone(),
220 build_manager.clone(),
221 ));
222 }
223 while let Some((path, build_manager)) = todo.pop_front() {
224 if path.is_dir() {
225 if path.file_name().unwrap() == "auxiliary" {
226 continue;
227 }
228 // Enqueue everything inside this directory.
229 // We want it sorted, to have some control over scheduling of slow tests.
230 let mut entries = std::fs::read_dir(path)
231 .unwrap()
232 .map(|e| e.unwrap().path())
233 .collect::<Vec<_>>();
234 entries.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
235 for entry in entries {
236 todo.push_back((entry, build_manager.clone()));
237 }
238 } else if let Some(matched) = file_filter(&path, build_manager.config()) {
239 if matched {
240 let status = status_emitter.register_test(path.clone());
241 // Forward .rs files to the test workers.
242 submit
243 .send(Box::new(move |finished_files_sender: &Sender<TestRun>| {
244 let file_contents = Spanned::read_from_file(&path).unwrap();
245 let mut config = build_manager.config().clone();
246 let abort_check = config.abort_check.clone();
247 per_file_config(&mut config, &file_contents);
248 let status = AssertUnwindSafe(status);
249 let result = std::panic::catch_unwind(|| {
250 let status = status;
251 parse_and_test_file(
252 build_manager,
253 status.0,
254 config,
255 file_contents,
256 )
257 });
258 let result = match result {
259 Ok(Ok(res)) => res,
260 Ok(Err((status, err))) => {
261 finished_files_sender.send(TestRun {
262 result: Err(err),
263 status,
264 abort_check,
265 })?;
266 return Ok(());
267 }
268 Err(err) => {
269 finished_files_sender.send(TestRun {
270 result: Err(Errored {
271 command: "<unknown>".into(),
272 errors: vec![Error::Bug(
273 *Box::<
274 dyn std::any::Any + Send + 'static,
275 >::downcast::<String>(
276 err
277 )
278 .unwrap(),
279 )],
280 stderr: vec![],
281 stdout: vec![],
282 }),
283 status: Box::new(SilentStatus {
284 revision: String::new(),
285 path,
286 }),
287 abort_check,
288 })?;
289 return Ok(());
290 }
291 };
292 for result in result {
293 finished_files_sender.send(result)?;
294 }
295 Ok(())
296 }) as NewJob)
297 .unwrap();
298 } else {
299 filtered += 1;
300 }
301 }
302 }
303 },
304 |[receive, priority_receive], finished_files_sender| -> Result<()> {
305 loop {
306 for closure in priority_receive.try_iter() {
307 closure(&finished_files_sender)?;
308 }
309 match receive.try_recv() {
310 Ok(closure) => {
311 closure(&finished_files_sender)?;
312 }
313 Err(TryRecvError::Empty) => {}
314 Err(TryRecvError::Disconnected) => {
315 for closure in priority_receive {
316 closure(&finished_files_sender)?;
317 }
318 return Ok(());
319 }
320 }
321 }
322 },
323 |finished_files_recv| {
324 for run in finished_files_recv {
325 let aborted = run.abort_check.aborted();
326 run.status.done(&run.result, aborted);
327
328 // Do not write summaries for cancelled tests
329 if !aborted {
330 results.push(run);
331 }
332 }
333 },
334 )?;
335
336 let mut failures = vec![];
337 let mut succeeded = 0;
338 let mut ignored = 0;
339 let mut aborted = false;
340
341 for run in results {
342 aborted |= run.abort_check.aborted();
343 match run.result {
344 Ok(TestOk::Ok) => succeeded += 1,
345 Ok(TestOk::Ignored) => ignored += 1,
346 Err(errored) => failures.push((run.status, errored)),
347 }
348 }
349
350 let mut failure_emitter =
351 status_emitter.finalize(failures.len(), succeeded, ignored, filtered, aborted);
352 for (
353 status,
354 Errored {
355 command,
356 errors,
357 stderr,
358 stdout,
359 },
360 ) in &failures
361 {
362 let _guard = status.failed_test(command, stderr, stdout);
363 failure_emitter.test_failure(status, errors);
364 }
365
366 if failures.is_empty() {
367 Ok(())
368 } else {
369 Err(eyre!("tests failed"))
370 }
371}
372
373fn parse_and_test_file(
374 build_manager: Arc<BuildManager>,
375 status: Box<dyn TestStatus>,
376 config: Config,
377 file_contents: Spanned<Vec<u8>>,
378) -> Result<Vec<TestRun>, (Box<dyn TestStatus>, Errored)> {
379 let comments = match Comments::parse(file_contents.as_ref(), &config) {
380 Ok(t) => t,
381 Err(errors) => return Err((status, Errored::new(errors, "parse comments"))),
382 };
383 let comments = Arc::new(comments);
384 // Run the test for all revisions
385 let mut runs = vec![];
386 for i in 0.. {
387 match comments.revisions.as_deref() {
388 Some(revisions) => {
389 let Some(revision) = revisions.get(i) else {
390 status.done(&Ok(TestOk::Ok), build_manager.aborted());
391 break;
392 };
393 let status = status.for_revision(revision, RevisionStyle::Show);
394 test_file(&config, &comments, status, &mut runs, &build_manager)
395 }
396 None => {
397 test_file(&config, &comments, status, &mut runs, &build_manager);
398 break;
399 }
400 }
401 }
402 Ok(runs)
403}
404
405fn test_file(
406 config: &Config,
407 comments: &Arc<Comments>,
408 status: Box<dyn TestStatus>,
409 runs: &mut Vec<TestRun>,
410 build_manager: &Arc<BuildManager>,
411) {
412 if !config.test_file_conditions(comments, status.revision()) {
413 runs.push(TestRun {
414 result: Ok(TestOk::Ignored),
415 status,
416 abort_check: config.abort_check.clone(),
417 });
418 return;
419 }
420 let mut test_config: TestConfig = TestConfig {
421 config: config.clone(),
422 comments: comments.clone(),
423 aux_dir: status.path().parent().unwrap().join(path:"auxiliary"),
424 status,
425 };
426 let result: Result = test_config.run_test(build_manager);
427 // Ignore file if only/ignore rules do (not) apply
428
429 runs.push(TestRun {
430 result,
431 status: test_config.status,
432 abort_check: test_config.config.abort_check,
433 });
434}
435
436fn display(path: &Path) -> String {
437 path.display().to_string().replace(from:'\\', to:"/")
438}
439