#
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            if let Some((runtool, runtool_args)) = compilation.target_runner(unit.kind) {
    242                p.arg("--test-runtool").arg(runtool);
    243                for arg in runtool_args {
    244                    p.arg("--test-runtool-arg").arg(arg);
    245                }
    246            }
    247            if let Some(linker) = linker {
    248                let mut joined = OsString::from("linker=");
    249                joined.push(linker);
    250                p.arg("-C").arg(joined);
    251            }
    252        }
    253
    254        if unit.profile.panic != PanicStrategy::Unwind {
    255            p.arg("-C").arg(format!("panic={}", unit.profile.panic));
    256        }
    257
    258        for &rust_dep in &[
    259            &compilation.deps_output[&unit.kind],
    260            &compilation.deps_output[&CompileKind::Host],
    261        ] {
    262            let mut arg = OsString::from("dependency=");
    263            arg.push(rust_dep);
    264            p.arg("-L").arg(arg);
    265        }
    266
    267        for native_dep in compilation.native_dirs.iter() {
    268            p.arg("-L").arg(native_dep);
    269        }
    270
    271        for arg in test_args {
    272            p.arg("--test-args").arg(arg);
    273        }
    274
    275        if gctx.shell().verbosity() == Verbosity::Quiet {
    276            p.arg("--test-args").arg("--quiet");
    277        }
    278
    279        p.args(unit.pkg.manifest().lint_rustflags());
    280
    281        p.args(args);
    282
    283        if *unstable_opts {
    284            p.arg("-Zunstable-options");
    285        }
    286
    287        if gctx.extra_verbose() {
    288            p.display_env_vars();
    289        }
    290
    291        gctx.shell()
    292            .verbose(|shell| shell.status("Running", p.to_string()))?;
    293
    294        if let Err(e) = p.exec() {
    295            let code = fail_fast_code(&e);
    296            let unit_err = UnitTestError {
    297                unit: unit.clone(),
    298                kind: TestKind::Doctest,
    299            };
    300            report_test_error(ws, test_args, &options.compile_opts, &unit_err, e);
    301            errors.push(unit_err);
    302            if !options.no_fail_fast {
    303                return Err(CliError::code(code));
    304            }
    305        }
    306    }
    307    Ok(errors)
    308}
    309
    310/// Displays human-readable descriptions of the test executables.
    311///
    312/// This is used when `cargo test --no-run` is used.
    313fn display_no_run_information(
    314    ws: &Workspace<'_>,
    315    test_args: &[&str],
    316    compilation: &Compilation<'_>,
    317    exec_type: &str,
    318) -> CargoResult<()> {
    319    let gctx = ws.gctx();
    320    let cwd = gctx.cwd();
    321    for UnitOutput {
    322        unit,
    323        path,
    324        script_meta,
    325    } in compilation.tests.iter()
    326    {
    327        let (exe_display, cmd) = cmd_builds(
    328            gctx,
    329            cwd,
    330            unit,
    331            path,
    332            script_meta,
    333            test_args,
    334            compilation,
    335            exec_type,
    336        )?;
    337        gctx.shell()
    338            .concise(|shell| shell.status("Executable", &exe_display))?;
    339        gctx.shell()
    340            .verbose(|shell| shell.status("Executable", &cmd))?;
    341    }
    342
    343    return Ok(());
    344}
    345
    346/// Creates a [`ProcessBuilder`] for executing a single test.
    347///
    348/// Returns a tuple `(exe_display, process)` where `exe_display` is a string
    349/// to display that describes the executable path in a human-readable form.
    350/// `process` is the `ProcessBuilder` to use for executing the test.
    351fn cmd_builds(
    352    gctx: &GlobalContext,
    353    cwd: &Path,
    354    unit: &Unit,
    355    path: &PathBuf,
    356    script_meta: &Option<UnitHash>,
    357    test_args: &[&str],
    358    compilation: &Compilation<'_>,
    359    exec_type: &str,
    360) -> CargoResult<(String, ProcessBuilder)> {
    361    let test_path = unit.target.src_path().path().unwrap();
    362    let short_test_path = test_path
    363        .strip_prefix(unit.pkg.root())
    364        .unwrap_or(test_path)
    365        .display();
    366
    367    let exe_display = match unit.target.kind() {
    368        TargetKind::Test | TargetKind::Bench => format!(
    369            "{} ({})",
    370            short_test_path,
    371            path.strip_prefix(cwd).unwrap_or(path).display()
    372        ),
    373        _ => format!(
    374            "{} {} ({})",
    375            exec_type,
    376            short_test_path,
    377            path.strip_prefix(cwd).unwrap_or(path).display()
    378        ),
    379    };
    380
    381    let mut cmd = compilation.target_process(path, unit.kind, &unit.pkg, *script_meta)?;
    382    cmd.args(test_args);
    383    if unit.target.harness() && gctx.shell().verbosity() == Verbosity::Quiet {
    384        cmd.arg("--quiet");
    385    }
    386
    387    Ok((exe_display, cmd))
    388}
    389
    390/// Returns the error code to use when *not* using `--no-fail-fast`.
    391///
    392/// Cargo will return the error code from the test process itself. If some
    393/// other error happened (like a failure to launch the process), then it will
    394/// return a standard 101 error code.
    395///
    396/// When using `--no-fail-fast`, Cargo always uses the 101 exit code (since
    397/// there may not be just one process to report).
    398fn fail_fast_code(error: &anyhow::Error) -> i32 {
    399    if let Some(proc_err) = error.downcast_ref::<ProcessError>() {
    400        if let Some(code) = proc_err.code {
    401            return code;
    402        }
    403    }
    404    101
    405}
    406
    407/// Returns the `CliError` when using `--no-fail-fast` and there is at least
    408/// one error.
    409fn no_fail_fast_err(
    410    ws: &Workspace<'_>,
    411    opts: &ops::CompileOptions,
    412    errors: &[UnitTestError],
    413) -> CliResult {
    414    // TODO: This could be improved by combining the flags on a single line when feasible.
    415    let args: Vec<_> = errors
    416        .iter()
    417        .map(|unit_err| format!("    `{}`", unit_err.cli_args(ws, opts)))
    418        .collect();
    419    let message = match errors.len() {
    420        0 => return Ok(()),
    421        1 => format!("1 target failed:\n{}", args.join("\n")),
    422        n => format!("{n} targets failed:\n{}", args.join("\n")),
    423    };
    424    Err(anyhow::Error::msg(message).into())
    425}
    426
    427/// Displays an error on the console about a test failure.
    428fn report_test_error(
    429    ws: &Workspace<'_>,
    430    test_args: &[&str],
    431    opts: &ops::CompileOptions,
    432    unit_err: &UnitTestError,
    433    test_error: anyhow::Error,
    434) {
    435    let which = match unit_err.kind {
    436        TestKind::Test => "test failed",
    437        TestKind::Bench => "bench failed",
    438        TestKind::Doctest => "doctest failed",
    439    };
    440
    441    let mut err = format_err!("{}, to rerun pass `{}`", which, unit_err.cli_args(ws, opts));
    442    // Don't show "process didn't exit successfully" for simple errors.
    443    // libtest exits with 101 for normal errors.
    444    let (is_simple, executed) = test_error
    445        .downcast_ref::<ProcessError>()
    446        .and_then(|proc_err| proc_err.code)
    447        .map_or((false, false), |code| (code == 101, true));
    448
    449    if !is_simple {
    450        err = test_error.context(err);
    451    }
    452
    453    crate::display_error(&err, &mut ws.gctx().shell());
    454
    455    let harness: bool = unit_err.unit.target.harness();
    456    let nocapture: bool = test_args.contains(&"--nocapture");
    457
    458    if !is_simple && executed && harness && !nocapture {
    459        drop(ws.gctx().shell().note(
    460            "test exited abnormally; to see the full output pass --nocapture to the harness.",
    461        ));
    462    }
    463}