#
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 color = gctx.shell().color_choice();
    180
    181    for doctest_info in &compilation.to_doc_test {
    182        let Doctest {
    183            args,
    184            unstable_opts,
    185            unit,
    186            linker,
    187            script_meta,
    188            env,
    189        } = doctest_info;
    190
    191        gctx.shell().status("Doc-tests", unit.target.name())?;
    192        let mut p = compilation.rustdoc_process(unit, *script_meta)?;
    193
    194        for (var, value) in env {
    195            p.env(var, value);
    196        }
    197
    198        let color_arg = match color {
    199            ColorChoice::Always => "always",
    200            ColorChoice::Never => "never",
    201            ColorChoice::CargoAuto => "auto",
    202        };
    203        p.arg("--color").arg(color_arg);
    204
    205        p.arg("--crate-name").arg(&unit.target.crate_name());
    206        p.arg("--test");
    207
    208        add_path_args(ws, unit, &mut p);
    209        p.arg("--test-run-directory").arg(unit.pkg.root());
    210
    211        if let CompileKind::Target(target) = unit.kind {
    212            // use `rustc_target()` to properly handle JSON target paths
    213            p.arg("--target").arg(target.rustc_target());
    214        }
    215
    216        if let Some((runtool, runtool_args)) = compilation.target_runner(unit.kind) {
    217            p.arg("--test-runtool").arg(runtool);
    218            for arg in runtool_args {
    219                p.arg("--test-runtool-arg").arg(arg);
    220            }
    221        }
    222        if let Some(linker) = linker {
    223            let mut joined = OsString::from("linker=");
    224            joined.push(linker);
    225            p.arg("-C").arg(joined);
    226        }
    227
    228        if unit.profile.panic != PanicStrategy::Unwind {
    229            p.arg("-C").arg(format!("panic={}", unit.profile.panic));
    230        }
    231
    232        for &rust_dep in &[
    233            &compilation.deps_output[&unit.kind],
    234            &compilation.deps_output[&CompileKind::Host],
    235        ] {
    236            let mut arg = OsString::from("dependency=");
    237            arg.push(rust_dep);
    238            p.arg("-L").arg(arg);
    239        }
    240
    241        for native_dep in compilation.native_dirs.iter() {
    242            p.arg("-L").arg(native_dep);
    243        }
    244
    245        for arg in test_args {
    246            p.arg("--test-args").arg(arg);
    247        }
    248
    249        if gctx.shell().verbosity() == Verbosity::Quiet {
    250            p.arg("--test-args").arg("--quiet");
    251        }
    252
    253        p.args(unit.pkg.manifest().lint_rustflags());
    254
    255        p.args(args);
    256
    257        if *unstable_opts {
    258            p.arg("-Zunstable-options");
    259        }
    260
    261        if gctx.extra_verbose() {
    262            p.display_env_vars();
    263        }
    264
    265        gctx.shell()
    266            .verbose(|shell| shell.status("Running", p.to_string()))?;
    267
    268        if let Err(e) = p.exec() {
    269            let code = fail_fast_code(&e);
    270            let unit_err = UnitTestError {
    271                unit: unit.clone(),
    272                kind: TestKind::Doctest,
    273            };
    274            report_test_error(ws, test_args, &options.compile_opts, &unit_err, e);
    275            errors.push(unit_err);
    276            if !options.no_fail_fast {
    277                return Err(CliError::code(code));
    278            }
    279        }
    280    }
    281    Ok(errors)
    282}
    283
    284/// Displays human-readable descriptions of the test executables.
    285///
    286/// This is used when `cargo test --no-run` is used.
    287fn display_no_run_information(
    288    ws: &Workspace<'_>,
    289    test_args: &[&str],
    290    compilation: &Compilation<'_>,
    291    exec_type: &str,
    292) -> CargoResult<()> {
    293    let gctx = ws.gctx();
    294    let cwd = gctx.cwd();
    295    for UnitOutput {
    296        unit,
    297        path,
    298        script_meta,
    299    } in compilation.tests.iter()
    300    {
    301        let (exe_display, cmd) = cmd_builds(
    302            gctx,
    303            cwd,
    304            unit,
    305            path,
    306            script_meta,
    307            test_args,
    308            compilation,
    309            exec_type,
    310        )?;
    311        gctx.shell()
    312            .concise(|shell| shell.status("Executable", &exe_display))?;
    313        gctx.shell()
    314            .verbose(|shell| shell.status("Executable", &cmd))?;
    315    }
    316
    317    return Ok(());
    318}
    319
    320/// Creates a [`ProcessBuilder`] for executing a single test.
    321///
    322/// Returns a tuple `(exe_display, process)` where `exe_display` is a string
    323/// to display that describes the executable path in a human-readable form.
    324/// `process` is the `ProcessBuilder` to use for executing the test.
    325fn cmd_builds(
    326    gctx: &GlobalContext,
    327    cwd: &Path,
    328    unit: &Unit,
    329    path: &PathBuf,
    330    script_meta: &Option<UnitHash>,
    331    test_args: &[&str],
    332    compilation: &Compilation<'_>,
    333    exec_type: &str,
    334) -> CargoResult<(String, ProcessBuilder)> {
    335    let test_path = unit.target.src_path().path().unwrap();
    336    let short_test_path = test_path
    337        .strip_prefix(unit.pkg.root())
    338        .unwrap_or(test_path)
    339        .display();
    340
    341    let exe_display = match unit.target.kind() {
    342        TargetKind::Test | TargetKind::Bench => format!(
    343            "{} ({})",
    344            short_test_path,
    345            path.strip_prefix(cwd).unwrap_or(path).display()
    346        ),
    347        _ => format!(
    348            "{} {} ({})",
    349            exec_type,
    350            short_test_path,
    351            path.strip_prefix(cwd).unwrap_or(path).display()
    352        ),
    353    };
    354
    355    let mut cmd = compilation.target_process(path, unit.kind, &unit.pkg, *script_meta)?;
    356    cmd.args(test_args);
    357    if unit.target.harness() && gctx.shell().verbosity() == Verbosity::Quiet {
    358        cmd.arg("--quiet");
    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");
    431
    432    if !is_simple && executed && harness && !nocapture {
    433        drop(ws.gctx().shell().note(
    434            "test exited abnormally; to see the full output pass --nocapture to the harness.",
    435        ));
    436    }
    437}