1use crate::core::compiler::{Compilation, Doctest, Unit, UnitHash, UnitOutput};
2use crate::core::profiles::PanicStrategy;
3use crate::core::{TargetKind, Workspace};
4use crate::ops;
5use crate::util::errors::CargoResult;
6use crate::util::{CliError, CliResult, GlobalContext, add_path_args};
7use anyhow::format_err;
8use cargo_util::{ProcessBuilder, ProcessError};
9use cargo_util_terminal::{ColorChoice, Verbosity};
10use std::collections::HashMap;
11use std::ffi::OsString;
12use std::fmt::Write;
13use std::path::{Path, PathBuf};
14
15pub struct TestOptions {
16 pub compile_opts: ops::CompileOptions,
17 pub no_run: bool,
18 pub no_fail_fast: bool,
19}
20
21#[derive(Copy, Clone)]
26enum TestKind {
27 Test,
28 Bench,
29 Doctest,
30}
31
32struct UnitTestError {
34 unit: Unit,
35 kind: TestKind,
36}
37
38impl UnitTestError {
39 fn cli_args(&self, ws: &Workspace<'_>, opts: &ops::CompileOptions) -> String {
41 let mut args = if opts.spec.needs_spec_flag(ws) {
42 format!("-p {} ", self.unit.pkg.name())
43 } else {
44 String::new()
45 };
46 let mut add = |which| write!(args, "--{which} {}", self.unit.target.name()).unwrap();
47
48 match self.kind {
49 TestKind::Test | TestKind::Bench => match self.unit.target.kind() {
50 TargetKind::Lib(_) => args.push_str("--lib"),
51 TargetKind::Bin => add("bin"),
52 TargetKind::Test => add("test"),
53 TargetKind::Bench => add("bench"),
54 TargetKind::ExampleLib(_) | TargetKind::ExampleBin => add("example"),
55 TargetKind::CustomBuild => panic!("unexpected CustomBuild kind"),
56 },
57 TestKind::Doctest => args.push_str("--doc"),
58 }
59 args
60 }
61}
62
63pub fn run_tests(ws: &Workspace<'_>, options: &TestOptions, test_args: &[&str]) -> CliResult {
68 let compilation = compile_tests(ws, options)?;
69
70 if options.no_run {
71 if !options.compile_opts.build_config.emit_json() {
72 display_no_run_information(ws, test_args, &compilation, "unittests")?;
73 }
74 return Ok(());
75 }
76 let mut errors = run_unit_tests(ws, options, test_args, &compilation, TestKind::Test)?;
77
78 let doctest_errors = run_doc_tests(ws, options, test_args, &compilation)?;
79 errors.extend(doctest_errors);
80 no_fail_fast_err(ws, &options.compile_opts, &errors)
81}
82
83pub fn run_benches(ws: &Workspace<'_>, options: &TestOptions, args: &[&str]) -> CliResult {
88 let compilation = compile_tests(ws, options)?;
89
90 if options.no_run {
91 if !options.compile_opts.build_config.emit_json() {
92 display_no_run_information(ws, args, &compilation, "benches")?;
93 }
94 return Ok(());
95 }
96
97 let mut args = args.to_vec();
98 args.push("--bench");
99
100 let errors = run_unit_tests(ws, options, &args, &compilation, TestKind::Bench)?;
101 no_fail_fast_err(ws, &options.compile_opts, &errors)
102}
103
104fn compile_tests<'a>(ws: &Workspace<'a>, options: &TestOptions) -> CargoResult<Compilation<'a>> {
105 let mut compilation = ops::compile(ws, &options.compile_opts)?;
106 compilation.tests.sort_by_key(|u| u.unit.clone());
107 Ok(compilation)
108}
109
110fn run_unit_tests(
115 ws: &Workspace<'_>,
116 options: &TestOptions,
117 test_args: &[&str],
118 compilation: &Compilation<'_>,
119 test_kind: TestKind,
120) -> Result<Vec<UnitTestError>, CliError> {
121 let gctx = ws.gctx();
122 let cwd = gctx.cwd();
123 let mut errors = Vec::new();
124
125 for UnitOutput {
126 unit,
127 path,
128 script_metas,
129 env,
130 } in compilation.tests.iter()
131 {
132 let (exe_display, mut cmd) = cmd_builds(
133 gctx,
134 cwd,
135 unit,
136 path,
137 script_metas.as_ref(),
138 env,
139 test_args,
140 compilation,
141 "unittests",
142 )?;
143
144 if gctx.extra_verbose() {
145 cmd.display_env_vars();
146 }
147
148 gctx.shell()
149 .concise(|shell| shell.status("Running", &exe_display))?;
150 gctx.shell()
151 .verbose(|shell| shell.status("Running", &cmd))?;
152
153 if let Err(e) = cmd.exec() {
154 let code = fail_fast_code(&e);
155 let unit_err = UnitTestError {
156 unit: unit.clone(),
157 kind: test_kind,
158 };
159 report_test_error(ws, test_args, &options.compile_opts, &unit_err, e);
160 errors.push(unit_err);
161 if !options.no_fail_fast {
162 return Err(CliError::code(code));
163 }
164 }
165 }
166 Ok(errors)
167}
168
169fn run_doc_tests(
174 ws: &Workspace<'_>,
175 options: &TestOptions,
176 test_args: &[&str],
177 compilation: &Compilation<'_>,
178) -> Result<Vec<UnitTestError>, CliError> {
179 let gctx = ws.gctx();
180 let mut errors = Vec::new();
181 let color = gctx.shell().color_choice();
182
183 for doctest_info in &compilation.to_doc_test {
184 let Doctest {
185 args,
186 unstable_opts,
187 unit,
188 linker,
189 script_metas,
190 env,
191 } = doctest_info;
192
193 gctx.shell().status("Doc-tests", unit.target.name())?;
194 let mut p = compilation.rustdoc_process(unit, script_metas.as_ref())?;
195
196 for (var, value) in env {
197 p.env(var, value);
198 }
199
200 let color_arg = match color {
201 ColorChoice::Always => "always",
202 ColorChoice::Never => "never",
203 ColorChoice::CargoAuto => "auto",
204 };
205 p.arg("--color").arg(color_arg);
206
207 p.arg("--crate-name").arg(&unit.target.crate_name());
208 p.arg("--test");
209
210 add_path_args(ws, unit, &mut p);
211 p.arg("--test-run-directory").arg(unit.pkg.root());
212
213 unit.kind.add_target_arg(&mut p);
214
215 if let Some((runtool, runtool_args)) = compilation.target_runner(unit.kind) {
216 p.arg("--test-runtool").arg(runtool);
217 for arg in runtool_args {
218 p.arg("--test-runtool-arg").arg(arg);
219 }
220 }
221 if let Some(linker) = linker {
222 let mut joined = OsString::from("linker=");
223 joined.push(linker);
224 p.arg("-C").arg(joined);
225 }
226
227 if unit.profile.panic != PanicStrategy::Unwind {
228 p.arg("-C").arg(format!("panic={}", unit.profile.panic));
229 }
230
231 for native_dep in compilation.native_dirs.iter() {
232 p.arg("-L").arg(native_dep);
233 }
234
235 for arg in test_args {
236 p.arg("--test-args").arg(arg);
237 }
238
239 if gctx.shell().verbosity() == Verbosity::Quiet {
240 p.arg("--test-args").arg("--quiet");
241 }
242
243 p.args(unit.pkg.manifest().lint_rustflags());
244
245 p.args(args);
246
247 if *unstable_opts {
248 p.arg("-Zunstable-options");
249 }
250
251 if gctx.extra_verbose() {
252 p.display_env_vars();
253 }
254
255 gctx.shell()
256 .verbose(|shell| shell.status("Running", p.to_string()))?;
257
258 if let Err(e) = p.exec() {
259 let code = fail_fast_code(&e);
260 let unit_err = UnitTestError {
261 unit: unit.clone(),
262 kind: TestKind::Doctest,
263 };
264 report_test_error(ws, test_args, &options.compile_opts, &unit_err, e);
265 errors.push(unit_err);
266 if !options.no_fail_fast {
267 return Err(CliError::code(code));
268 }
269 }
270 }
271 Ok(errors)
272}
273
274fn display_no_run_information(
278 ws: &Workspace<'_>,
279 test_args: &[&str],
280 compilation: &Compilation<'_>,
281 exec_type: &str,
282) -> CargoResult<()> {
283 let gctx = ws.gctx();
284 let cwd = gctx.cwd();
285 for UnitOutput {
286 unit,
287 path,
288 script_metas,
289 env,
290 } in compilation.tests.iter()
291 {
292 let (exe_display, cmd) = cmd_builds(
293 gctx,
294 cwd,
295 unit,
296 path,
297 script_metas.as_ref(),
298 env,
299 test_args,
300 compilation,
301 exec_type,
302 )?;
303 gctx.shell()
304 .concise(|shell| shell.status("Executable", &exe_display))?;
305 gctx.shell()
306 .verbose(|shell| shell.status("Executable", &cmd))?;
307 }
308
309 return Ok(());
310}
311
312fn cmd_builds(
318 gctx: &GlobalContext,
319 cwd: &Path,
320 unit: &Unit,
321 path: &PathBuf,
322 script_metas: Option<&Vec<UnitHash>>,
323 env: &HashMap<String, OsString>,
324 test_args: &[&str],
325 compilation: &Compilation<'_>,
326 exec_type: &str,
327) -> CargoResult<(String, ProcessBuilder)> {
328 let test_path = unit.target.src_path().path().unwrap();
329 let short_test_path = test_path
330 .strip_prefix(unit.pkg.root())
331 .unwrap_or(test_path)
332 .display();
333
334 let exe_display = match unit.target.kind() {
335 TargetKind::Test | TargetKind::Bench => format!(
336 "{} ({})",
337 short_test_path,
338 path.strip_prefix(cwd).unwrap_or(path).display()
339 ),
340 _ => format!(
341 "{} {} ({})",
342 exec_type,
343 short_test_path,
344 path.strip_prefix(cwd).unwrap_or(path).display()
345 ),
346 };
347
348 let mut cmd = compilation.target_process(path, unit.kind, &unit.pkg, script_metas)?;
349 cmd.args(test_args);
350 if unit.target.harness() && gctx.shell().verbosity() == Verbosity::Quiet {
351 cmd.arg("--quiet");
352 }
353 for (key, val) in env.iter() {
354 cmd.env(key, val);
355 }
356
357 Ok((exe_display, cmd))
358}
359
360fn fail_fast_code(error: &anyhow::Error) -> i32 {
369 if let Some(proc_err) = error.downcast_ref::<ProcessError>() {
370 if let Some(code) = proc_err.code {
371 return code;
372 }
373 }
374 101
375}
376
377fn no_fail_fast_err(
380 ws: &Workspace<'_>,
381 opts: &ops::CompileOptions,
382 errors: &[UnitTestError],
383) -> CliResult {
384 let args: Vec<_> = errors
386 .iter()
387 .map(|unit_err| format!(" `{}`", unit_err.cli_args(ws, opts)))
388 .collect();
389 let message = match errors.len() {
390 0 => return Ok(()),
391 1 => format!("1 target failed:\n{}", args.join("\n")),
392 n => format!("{n} targets failed:\n{}", args.join("\n")),
393 };
394 Err(anyhow::Error::msg(message).into())
395}
396
397fn report_test_error(
399 ws: &Workspace<'_>,
400 test_args: &[&str],
401 opts: &ops::CompileOptions,
402 unit_err: &UnitTestError,
403 test_error: anyhow::Error,
404) {
405 let which = match unit_err.kind {
406 TestKind::Test => "test failed",
407 TestKind::Bench => "bench failed",
408 TestKind::Doctest => "doctest failed",
409 };
410
411 let mut err = format_err!("{}, to rerun pass `{}`", which, unit_err.cli_args(ws, opts));
412 let (is_simple, executed) = test_error
415 .downcast_ref::<ProcessError>()
416 .and_then(|proc_err| proc_err.code)
417 .map_or((false, false), |code| (code == 101, true));
418
419 if !is_simple {
420 err = test_error.context(err);
421 }
422
423 crate::display_error(&err, &mut ws.gctx().shell());
424
425 let harness: bool = unit_err.unit.target.harness();
426 let nocapture: bool = test_args.contains(&"--nocapture") || test_args.contains(&"--no-capture");
427
428 if !is_simple && executed && harness && !nocapture {
429 drop(ws.gctx().shell().note(
430 "test exited abnormally; to see the full output pass --no-capture to the harness.",
431 ));
432 }
433}