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::{CliError, CliResult, GlobalContext, add_path_args};
9use anyhow::format_err;
10use cargo_util::{ProcessBuilder, ProcessError};
11use std::collections::HashMap;
12use std::ffi::OsString;
13use std::fmt::Write;
14use std::path::{Path, PathBuf};
15
16pub struct TestOptions {
17    pub compile_opts: ops::CompileOptions,
18    pub no_run: bool,
19    pub no_fail_fast: bool,
20}
21
22/// The kind of test.
23///
24/// This is needed because `Unit` does not track whether or not something is a
25/// benchmark.
26#[derive(Copy, Clone)]
27enum TestKind {
28    Test,
29    Bench,
30    Doctest,
31}
32
33/// A unit that failed to run.
34struct UnitTestError {
35    unit: Unit,
36    kind: TestKind,
37}
38
39impl UnitTestError {
40    /// Returns the CLI args needed to target this unit.
41    fn cli_args(&self, ws: &Workspace<'_>, opts: &ops::CompileOptions) -> String {
42        let mut args = if opts.spec.needs_spec_flag(ws) {
43            format!("-p {} ", self.unit.pkg.name())
44        } else {
45            String::new()
46        };
47        let mut add = |which| write!(args, "--{which} {}", self.unit.target.name()).unwrap();
48
49        match self.kind {
50            TestKind::Test | TestKind::Bench => match self.unit.target.kind() {
51                TargetKind::Lib(_) => args.push_str("--lib"),
52                TargetKind::Bin => add("bin"),
53                TargetKind::Test => add("test"),
54                TargetKind::Bench => add("bench"),
55                TargetKind::ExampleLib(_) | TargetKind::ExampleBin => add("example"),
56                TargetKind::CustomBuild => panic!("unexpected CustomBuild kind"),
57            },
58            TestKind::Doctest => args.push_str("--doc"),
59        }
60        args
61    }
62}
63
64/// Compiles and runs tests.
65///
66/// On error, the returned [`CliError`] will have the appropriate process exit
67/// code that Cargo should use.
68pub fn run_tests(ws: &Workspace<'_>, options: &TestOptions, test_args: &[&str]) -> CliResult {
69    let compilation = compile_tests(ws, options)?;
70
71    if options.no_run {
72        if !options.compile_opts.build_config.emit_json() {
73            display_no_run_information(ws, test_args, &compilation, "unittests")?;
74        }
75        return Ok(());
76    }
77    let mut errors = run_unit_tests(ws, options, test_args, &compilation, TestKind::Test)?;
78
79    let doctest_errors = run_doc_tests(ws, options, test_args, &compilation)?;
80    errors.extend(doctest_errors);
81    no_fail_fast_err(ws, &options.compile_opts, &errors)
82}
83
84/// Compiles and runs benchmarks.
85///
86/// On error, the returned [`CliError`] will have the appropriate process exit
87/// code that Cargo should use.
88pub fn run_benches(ws: &Workspace<'_>, options: &TestOptions, args: &[&str]) -> CliResult {
89    let compilation = compile_tests(ws, options)?;
90
91    if options.no_run {
92        if !options.compile_opts.build_config.emit_json() {
93            display_no_run_information(ws, args, &compilation, "benches")?;
94        }
95        return Ok(());
96    }
97
98    let mut args = args.to_vec();
99    args.push("--bench");
100
101    let errors = run_unit_tests(ws, options, &args, &compilation, TestKind::Bench)?;
102    no_fail_fast_err(ws, &options.compile_opts, &errors)
103}
104
105fn compile_tests<'a>(ws: &Workspace<'a>, options: &TestOptions) -> CargoResult<Compilation<'a>> {
106    let mut compilation = ops::compile(ws, &options.compile_opts)?;
107    compilation.tests.sort_by_key(|u| u.unit.clone());
108    Ok(compilation)
109}
110
111/// Runs the unit and integration tests of a package.
112///
113/// Returns a `Vec` of tests that failed when `--no-fail-fast` is used.
114/// If `--no-fail-fast` is *not* used, then this returns an `Err`.
115fn run_unit_tests(
116    ws: &Workspace<'_>,
117    options: &TestOptions,
118    test_args: &[&str],
119    compilation: &Compilation<'_>,
120    test_kind: TestKind,
121) -> Result<Vec<UnitTestError>, CliError> {
122    let gctx = ws.gctx();
123    let cwd = gctx.cwd();
124    let mut errors = Vec::new();
125
126    for UnitOutput {
127        unit,
128        path,
129        script_metas,
130        env,
131    } in compilation.tests.iter()
132    {
133        let (exe_display, mut cmd) = cmd_builds(
134            gctx,
135            cwd,
136            unit,
137            path,
138            script_metas.as_ref(),
139            env,
140            test_args,
141            compilation,
142            "unittests",
143        )?;
144
145        if gctx.extra_verbose() {
146            cmd.display_env_vars();
147        }
148
149        gctx.shell()
150            .concise(|shell| shell.status("Running", &exe_display))?;
151        gctx.shell()
152            .verbose(|shell| shell.status("Running", &cmd))?;
153
154        if let Err(e) = cmd.exec() {
155            let code = fail_fast_code(&e);
156            let unit_err = UnitTestError {
157                unit: unit.clone(),
158                kind: test_kind,
159            };
160            report_test_error(ws, test_args, &options.compile_opts, &unit_err, e);
161            errors.push(unit_err);
162            if !options.no_fail_fast {
163                return Err(CliError::code(code));
164            }
165        }
166    }
167    Ok(errors)
168}
169
170/// Runs doc tests.
171///
172/// Returns a `Vec` of tests that failed when `--no-fail-fast` is used.
173/// If `--no-fail-fast` is *not* used, then this returns an `Err`.
174fn run_doc_tests(
175    ws: &Workspace<'_>,
176    options: &TestOptions,
177    test_args: &[&str],
178    compilation: &Compilation<'_>,
179) -> Result<Vec<UnitTestError>, CliError> {
180    let gctx = ws.gctx();
181    let mut errors = Vec::new();
182    let color = gctx.shell().color_choice();
183
184    for doctest_info in &compilation.to_doc_test {
185        let Doctest {
186            args,
187            unstable_opts,
188            unit,
189            linker,
190            script_metas,
191            env,
192        } = doctest_info;
193
194        gctx.shell().status("Doc-tests", unit.target.name())?;
195        let mut p = compilation.rustdoc_process(unit, script_metas.as_ref())?;
196
197        for (var, value) in env {
198            p.env(var, value);
199        }
200
201        let color_arg = match color {
202            ColorChoice::Always => "always",
203            ColorChoice::Never => "never",
204            ColorChoice::CargoAuto => "auto",
205        };
206        p.arg("--color").arg(color_arg);
207
208        p.arg("--crate-name").arg(&unit.target.crate_name());
209        p.arg("--test");
210
211        add_path_args(ws, unit, &mut p);
212        p.arg("--test-run-directory").arg(unit.pkg.root());
213
214        if let CompileKind::Target(target) = unit.kind {
215            // use `rustc_target()` to properly handle JSON target paths
216            p.arg("--target").arg(target.rustc_target());
217        }
218
219        if let Some((runtool, runtool_args)) = compilation.target_runner(unit.kind) {
220            p.arg("--test-runtool").arg(runtool);
221            for arg in runtool_args {
222                p.arg("--test-runtool-arg").arg(arg);
223            }
224        }
225        if let Some(linker) = linker {
226            let mut joined = OsString::from("linker=");
227            joined.push(linker);
228            p.arg("-C").arg(joined);
229        }
230
231        if unit.profile.panic != PanicStrategy::Unwind {
232            p.arg("-C").arg(format!("panic={}", unit.profile.panic));
233        }
234
235        for native_dep in compilation.native_dirs.iter() {
236            p.arg("-L").arg(native_dep);
237        }
238
239        for arg in test_args {
240            p.arg("--test-args").arg(arg);
241        }
242
243        if gctx.shell().verbosity() == Verbosity::Quiet {
244            p.arg("--test-args").arg("--quiet");
245        }
246
247        p.args(unit.pkg.manifest().lint_rustflags());
248
249        p.args(args);
250
251        if *unstable_opts {
252            p.arg("-Zunstable-options");
253        }
254
255        if gctx.extra_verbose() {
256            p.display_env_vars();
257        }
258
259        gctx.shell()
260            .verbose(|shell| shell.status("Running", p.to_string()))?;
261
262        if let Err(e) = p.exec() {
263            let code = fail_fast_code(&e);
264            let unit_err = UnitTestError {
265                unit: unit.clone(),
266                kind: TestKind::Doctest,
267            };
268            report_test_error(ws, test_args, &options.compile_opts, &unit_err, e);
269            errors.push(unit_err);
270            if !options.no_fail_fast {
271                return Err(CliError::code(code));
272            }
273        }
274    }
275    Ok(errors)
276}
277
278/// Displays human-readable descriptions of the test executables.
279///
280/// This is used when `cargo test --no-run` is used.
281fn display_no_run_information(
282    ws: &Workspace<'_>,
283    test_args: &[&str],
284    compilation: &Compilation<'_>,
285    exec_type: &str,
286) -> CargoResult<()> {
287    let gctx = ws.gctx();
288    let cwd = gctx.cwd();
289    for UnitOutput {
290        unit,
291        path,
292        script_metas,
293        env,
294    } in compilation.tests.iter()
295    {
296        let (exe_display, cmd) = cmd_builds(
297            gctx,
298            cwd,
299            unit,
300            path,
301            script_metas.as_ref(),
302            env,
303            test_args,
304            compilation,
305            exec_type,
306        )?;
307        gctx.shell()
308            .concise(|shell| shell.status("Executable", &exe_display))?;
309        gctx.shell()
310            .verbose(|shell| shell.status("Executable", &cmd))?;
311    }
312
313    return Ok(());
314}
315
316/// Creates a [`ProcessBuilder`] for executing a single test.
317///
318/// Returns a tuple `(exe_display, process)` where `exe_display` is a string
319/// to display that describes the executable path in a human-readable form.
320/// `process` is the `ProcessBuilder` to use for executing the test.
321fn cmd_builds(
322    gctx: &GlobalContext,
323    cwd: &Path,
324    unit: &Unit,
325    path: &PathBuf,
326    script_metas: Option<&Vec<UnitHash>>,
327    env: &HashMap<String, OsString>,
328    test_args: &[&str],
329    compilation: &Compilation<'_>,
330    exec_type: &str,
331) -> CargoResult<(String, ProcessBuilder)> {
332    let test_path = unit.target.src_path().path().unwrap();
333    let short_test_path = test_path
334        .strip_prefix(unit.pkg.root())
335        .unwrap_or(test_path)
336        .display();
337
338    let exe_display = match unit.target.kind() {
339        TargetKind::Test | TargetKind::Bench => format!(
340            "{} ({})",
341            short_test_path,
342            path.strip_prefix(cwd).unwrap_or(path).display()
343        ),
344        _ => format!(
345            "{} {} ({})",
346            exec_type,
347            short_test_path,
348            path.strip_prefix(cwd).unwrap_or(path).display()
349        ),
350    };
351
352    let mut cmd = compilation.target_process(path, unit.kind, &unit.pkg, script_metas)?;
353    cmd.args(test_args);
354    if unit.target.harness() && gctx.shell().verbosity() == Verbosity::Quiet {
355        cmd.arg("--quiet");
356    }
357    for (key, val) in env.iter() {
358        cmd.env(key, val);
359    }
360
361    Ok((exe_display, cmd))
362}
363
364/// Returns the error code to use when *not* using `--no-fail-fast`.
365///
366/// Cargo will return the error code from the test process itself. If some
367/// other error happened (like a failure to launch the process), then it will
368/// return a standard 101 error code.
369///
370/// When using `--no-fail-fast`, Cargo always uses the 101 exit code (since
371/// there may not be just one process to report).
372fn fail_fast_code(error: &anyhow::Error) -> i32 {
373    if let Some(proc_err) = error.downcast_ref::<ProcessError>() {
374        if let Some(code) = proc_err.code {
375            return code;
376        }
377    }
378    101
379}
380
381/// Returns the `CliError` when using `--no-fail-fast` and there is at least
382/// one error.
383fn no_fail_fast_err(
384    ws: &Workspace<'_>,
385    opts: &ops::CompileOptions,
386    errors: &[UnitTestError],
387) -> CliResult {
388    // TODO: This could be improved by combining the flags on a single line when feasible.
389    let args: Vec<_> = errors
390        .iter()
391        .map(|unit_err| format!("    `{}`", unit_err.cli_args(ws, opts)))
392        .collect();
393    let message = match errors.len() {
394        0 => return Ok(()),
395        1 => format!("1 target failed:\n{}", args.join("\n")),
396        n => format!("{n} targets failed:\n{}", args.join("\n")),
397    };
398    Err(anyhow::Error::msg(message).into())
399}
400
401/// Displays an error on the console about a test failure.
402fn report_test_error(
403    ws: &Workspace<'_>,
404    test_args: &[&str],
405    opts: &ops::CompileOptions,
406    unit_err: &UnitTestError,
407    test_error: anyhow::Error,
408) {
409    let which = match unit_err.kind {
410        TestKind::Test => "test failed",
411        TestKind::Bench => "bench failed",
412        TestKind::Doctest => "doctest failed",
413    };
414
415    let mut err = format_err!("{}, to rerun pass `{}`", which, unit_err.cli_args(ws, opts));
416    // Don't show "process didn't exit successfully" for simple errors.
417    // libtest exits with 101 for normal errors.
418    let (is_simple, executed) = test_error
419        .downcast_ref::<ProcessError>()
420        .and_then(|proc_err| proc_err.code)
421        .map_or((false, false), |code| (code == 101, true));
422
423    if !is_simple {
424        err = test_error.context(err);
425    }
426
427    crate::display_error(&err, &mut ws.gctx().shell());
428
429    let harness: bool = unit_err.unit.target.harness();
430    let nocapture: bool = test_args.contains(&"--nocapture") || test_args.contains(&"--no-capture");
431
432    if !is_simple && executed && harness && !nocapture {
433        drop(ws.gctx().shell().note(
434            "test exited abnormally; to see the full output pass --no-capture to the harness.",
435        ));
436    }
437}