#
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}