Skip to main content

cargo/ops/
cargo_test.rs

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/// 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_by_key(|u| u.unit.clone());
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_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
169/// Runs doc tests.
170///
171/// Returns a `Vec` of tests that failed when `--no-fail-fast` is used.
172/// If `--no-fail-fast` is *not* used, then this returns an `Err`.
173fn 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
274/// Displays human-readable descriptions of the test executables.
275///
276/// This is used when `cargo test --no-run` is used.
277fn 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
312/// Creates a [`ProcessBuilder`] for executing a single test.
313///
314/// Returns a tuple `(exe_display, process)` where `exe_display` is a string
315/// to display that describes the executable path in a human-readable form.
316/// `process` is the `ProcessBuilder` to use for executing the test.
317fn 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
360/// Returns the error code to use when *not* using `--no-fail-fast`.
361///
362/// Cargo will return the error code from the test process itself. If some
363/// other error happened (like a failure to launch the process), then it will
364/// return a standard 101 error code.
365///
366/// When using `--no-fail-fast`, Cargo always uses the 101 exit code (since
367/// there may not be just one process to report).
368fn 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
377/// Returns the `CliError` when using `--no-fail-fast` and there is at least
378/// one error.
379fn no_fail_fast_err(
380    ws: &Workspace<'_>,
381    opts: &ops::CompileOptions,
382    errors: &[UnitTestError],
383) -> CliResult {
384    // TODO: This could be improved by combining the flags on a single line when feasible.
385    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
397/// Displays an error on the console about a test failure.
398fn 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    // Don't show "process didn't exit successfully" for simple errors.
413    // libtest exits with 101 for normal errors.
414    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}