cargo/ops/
cargo_test.rs

1use crate::core::compiler::{Compilation, CompileKind, Doctest, Unit, UnitHash, UnitOutput};
2use crate::core::profiles::PanicStrategy;
3use crate::core::shell::ColorChoice;
4use crate::core::shell::Verbosity;
5use crate::core::{TargetKind, Workspace};
6use crate::ops;
7use crate::util::errors::CargoResult;
8use crate::util::{add_path_args, CliError, CliResult, GlobalContext};
9use anyhow::format_err;
10use cargo_util::{ProcessBuilder, ProcessError};
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/// The kind of test.
22///
23/// This is needed because `Unit` does not track whether or not something is a
24/// benchmark.
25#[derive(Copy, Clone)]
26enum TestKind {
27    Test,
28    Bench,
29    Doctest,
30}
31
32/// A unit that failed to run.
33struct UnitTestError {
34    unit: Unit,
35    kind: TestKind,
36}
37
38impl UnitTestError {
39    /// Returns the CLI args needed to target this unit.
40    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
63/// Compiles and runs tests.
64///
65/// On error, the returned [`CliError`] will have the appropriate process exit
66/// code that Cargo should use.
67pub 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
83/// Compiles and runs benchmarks.
84///
85/// On error, the returned [`CliError`] will have the appropriate process exit
86/// code that Cargo should use.
87pub 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();
107    Ok(compilation)
108}
109
110/// Runs the unit and integration tests of a package.
111///
112/// Returns a `Vec` of tests that failed when `--no-fail-fast` is used.
113/// If `--no-fail-fast` is *not* used, then this returns an `Err`.
114fn 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_meta,
129    } in compilation.tests.iter()
130    {
131        let (exe_display, mut cmd) = cmd_builds(
132            gctx,
133            cwd,
134            unit,
135            path,
136            script_meta,
137            test_args,
138            compilation,
139            "unittests",
140        )?;
141
142        if gctx.extra_verbose() {
143            cmd.display_env_vars();
144        }
145
146        gctx.shell()
147            .concise(|shell| shell.status("Running", &exe_display))?;
148        gctx.shell()
149            .verbose(|shell| shell.status("Running", &cmd))?;
150
151        if let Err(e) = cmd.exec() {
152            let code = fail_fast_code(&e);
153            let unit_err = UnitTestError {
154                unit: unit.clone(),
155                kind: test_kind,
156            };
157            report_test_error(ws, test_args, &options.compile_opts, &unit_err, e);
158            errors.push(unit_err);
159            if !options.no_fail_fast {
160                return Err(CliError::code(code));
161            }
162        }
163    }
164    Ok(errors)
165}
166
167/// Runs doc tests.
168///
169/// Returns a `Vec` of tests that failed when `--no-fail-fast` is used.
170/// If `--no-fail-fast` is *not* used, then this returns an `Err`.
171fn run_doc_tests(
172    ws: &Workspace<'_>,
173    options: &TestOptions,
174    test_args: &[&str],
175    compilation: &Compilation<'_>,
176) -> Result<Vec<UnitTestError>, CliError> {
177    let gctx = ws.gctx();
178    let mut errors = Vec::new();
179    let doctest_xcompile = gctx.cli_unstable().doctest_xcompile;
180    let color = gctx.shell().color_choice();
181
182    for doctest_info in &compilation.to_doc_test {
183        let Doctest {
184            args,
185            unstable_opts,
186            unit,
187            linker,
188            script_meta,
189            env,
190        } = doctest_info;
191
192        if !doctest_xcompile {
193            match unit.kind {
194                CompileKind::Host => {}
195                CompileKind::Target(target) => {
196                    if target.short_name() != compilation.host {
197                        // Skip doctests, -Zdoctest-xcompile not enabled.
198                        gctx.shell().verbose(|shell| {
199                            shell.note(format!(
200                                "skipping doctests for {} ({}), \
201                                 cross-compilation doctests are not yet supported\n\
202                                 See https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#doctest-xcompile \
203                                 for more information.",
204                                unit.pkg,
205                                unit.target.description_named()
206                            ))
207                        })?;
208                        continue;
209                    }
210                }
211            }
212        }
213
214        gctx.shell().status("Doc-tests", unit.target.name())?;
215        let mut p = compilation.rustdoc_process(unit, *script_meta)?;
216
217        for (var, value) in env {
218            p.env(var, value);
219        }
220
221        let color_arg = match color {
222            ColorChoice::Always => "always",
223            ColorChoice::Never => "never",
224            ColorChoice::CargoAuto => "auto",
225        };
226        p.arg("--color").arg(color_arg);
227
228        p.arg("--crate-name").arg(&unit.target.crate_name());
229        p.arg("--test");
230
231        add_path_args(ws, unit, &mut p);
232        p.arg("--test-run-directory")
233            .arg(unit.pkg.root().to_path_buf());
234
235        if let CompileKind::Target(target) = unit.kind {
236            // use `rustc_target()` to properly handle JSON target paths
237            p.arg("--target").arg(target.rustc_target());
238        }
239
240        if doctest_xcompile {
241            p.arg("-Zunstable-options");
242            p.arg("--enable-per-target-ignores");
243            if let Some((runtool, runtool_args)) = compilation.target_runner(unit.kind) {
244                p.arg("--runtool").arg(runtool);
245                for arg in runtool_args {
246                    p.arg("--runtool-arg").arg(arg);
247                }
248            }
249            if let Some(linker) = linker {
250                let mut joined = OsString::from("linker=");
251                joined.push(linker);
252                p.arg("-C").arg(joined);
253            }
254        }
255
256        if unit.profile.panic != PanicStrategy::Unwind {
257            p.arg("-C").arg(format!("panic={}", unit.profile.panic));
258        }
259
260        for &rust_dep in &[
261            &compilation.deps_output[&unit.kind],
262            &compilation.deps_output[&CompileKind::Host],
263        ] {
264            let mut arg = OsString::from("dependency=");
265            arg.push(rust_dep);
266            p.arg("-L").arg(arg);
267        }
268
269        for native_dep in compilation.native_dirs.iter() {
270            p.arg("-L").arg(native_dep);
271        }
272
273        for arg in test_args {
274            p.arg("--test-args").arg(arg);
275        }
276
277        if gctx.shell().verbosity() == Verbosity::Quiet {
278            p.arg("--test-args").arg("--quiet");
279        }
280
281        p.args(unit.pkg.manifest().lint_rustflags());
282
283        p.args(args);
284
285        if *unstable_opts {
286            p.arg("-Zunstable-options");
287        }
288
289        if gctx.extra_verbose() {
290            p.display_env_vars();
291        }
292
293        gctx.shell()
294            .verbose(|shell| shell.status("Running", p.to_string()))?;
295
296        if let Err(e) = p.exec() {
297            let code = fail_fast_code(&e);
298            let unit_err = UnitTestError {
299                unit: unit.clone(),
300                kind: TestKind::Doctest,
301            };
302            report_test_error(ws, test_args, &options.compile_opts, &unit_err, e);
303            errors.push(unit_err);
304            if !options.no_fail_fast {
305                return Err(CliError::code(code));
306            }
307        }
308    }
309    Ok(errors)
310}
311
312/// Displays human-readable descriptions of the test executables.
313///
314/// This is used when `cargo test --no-run` is used.
315fn display_no_run_information(
316    ws: &Workspace<'_>,
317    test_args: &[&str],
318    compilation: &Compilation<'_>,
319    exec_type: &str,
320) -> CargoResult<()> {
321    let gctx = ws.gctx();
322    let cwd = gctx.cwd();
323    for UnitOutput {
324        unit,
325        path,
326        script_meta,
327    } in compilation.tests.iter()
328    {
329        let (exe_display, cmd) = cmd_builds(
330            gctx,
331            cwd,
332            unit,
333            path,
334            script_meta,
335            test_args,
336            compilation,
337            exec_type,
338        )?;
339        gctx.shell()
340            .concise(|shell| shell.status("Executable", &exe_display))?;
341        gctx.shell()
342            .verbose(|shell| shell.status("Executable", &cmd))?;
343    }
344
345    return Ok(());
346}
347
348/// Creates a [`ProcessBuilder`] for executing a single test.
349///
350/// Returns a tuple `(exe_display, process)` where `exe_display` is a string
351/// to display that describes the executable path in a human-readable form.
352/// `process` is the `ProcessBuilder` to use for executing the test.
353fn cmd_builds(
354    gctx: &GlobalContext,
355    cwd: &Path,
356    unit: &Unit,
357    path: &PathBuf,
358    script_meta: &Option<UnitHash>,
359    test_args: &[&str],
360    compilation: &Compilation<'_>,
361    exec_type: &str,
362) -> CargoResult<(String, ProcessBuilder)> {
363    let test_path = unit.target.src_path().path().unwrap();
364    let short_test_path = test_path
365        .strip_prefix(unit.pkg.root())
366        .unwrap_or(test_path)
367        .display();
368
369    let exe_display = match unit.target.kind() {
370        TargetKind::Test | TargetKind::Bench => format!(
371            "{} ({})",
372            short_test_path,
373            path.strip_prefix(cwd).unwrap_or(path).display()
374        ),
375        _ => format!(
376            "{} {} ({})",
377            exec_type,
378            short_test_path,
379            path.strip_prefix(cwd).unwrap_or(path).display()
380        ),
381    };
382
383    let mut cmd = compilation.target_process(path, unit.kind, &unit.pkg, *script_meta)?;
384    cmd.args(test_args);
385    if unit.target.harness() && gctx.shell().verbosity() == Verbosity::Quiet {
386        cmd.arg("--quiet");
387    }
388
389    Ok((exe_display, cmd))
390}
391
392/// Returns the error code to use when *not* using `--no-fail-fast`.
393///
394/// Cargo will return the error code from the test process itself. If some
395/// other error happened (like a failure to launch the process), then it will
396/// return a standard 101 error code.
397///
398/// When using `--no-fail-fast`, Cargo always uses the 101 exit code (since
399/// there may not be just one process to report).
400fn fail_fast_code(error: &anyhow::Error) -> i32 {
401    if let Some(proc_err) = error.downcast_ref::<ProcessError>() {
402        if let Some(code) = proc_err.code {
403            return code;
404        }
405    }
406    101
407}
408
409/// Returns the `CliError` when using `--no-fail-fast` and there is at least
410/// one error.
411fn no_fail_fast_err(
412    ws: &Workspace<'_>,
413    opts: &ops::CompileOptions,
414    errors: &[UnitTestError],
415) -> CliResult {
416    // TODO: This could be improved by combining the flags on a single line when feasible.
417    let args: Vec<_> = errors
418        .iter()
419        .map(|unit_err| format!("    `{}`", unit_err.cli_args(ws, opts)))
420        .collect();
421    let message = match errors.len() {
422        0 => return Ok(()),
423        1 => format!("1 target failed:\n{}", args.join("\n")),
424        n => format!("{n} targets failed:\n{}", args.join("\n")),
425    };
426    Err(anyhow::Error::msg(message).into())
427}
428
429/// Displays an error on the console about a test failure.
430fn report_test_error(
431    ws: &Workspace<'_>,
432    test_args: &[&str],
433    opts: &ops::CompileOptions,
434    unit_err: &UnitTestError,
435    test_error: anyhow::Error,
436) {
437    let which = match unit_err.kind {
438        TestKind::Test => "test failed",
439        TestKind::Bench => "bench failed",
440        TestKind::Doctest => "doctest failed",
441    };
442
443    let mut err = format_err!("{}, to rerun pass `{}`", which, unit_err.cli_args(ws, opts));
444    // Don't show "process didn't exit successfully" for simple errors.
445    // libtest exits with 101 for normal errors.
446    let (is_simple, executed) = test_error
447        .downcast_ref::<ProcessError>()
448        .and_then(|proc_err| proc_err.code)
449        .map_or((false, false), |code| (code == 101, true));
450
451    if !is_simple {
452        err = test_error.context(err);
453    }
454
455    crate::display_error(&err, &mut ws.gctx().shell());
456
457    let harness: bool = unit_err.unit.target.harness();
458    let nocapture: bool = test_args.contains(&"--nocapture");
459
460    if !is_simple && executed && harness && !nocapture {
461        drop(ws.gctx().shell().note(
462            "test exited abnormally; to see the full output pass --nocapture to the harness.",
463        ));
464    }
465}