Skip to main content

xtask_lib/tasks/
test.rs

1//! `cargo xtask test` — run tests across the full feature matrix.
2//!
3//! The feature matrix is derived dynamically from the workspace `Cargo.toml`
4//! files, so adding a new non-aliased feature flag automatically includes it
5//! in all test runs without any manual update.
6//!
7//! Each matrix entry runs two passes:
8//! 1. `cargo nextest run` — parallel test execution with per-test output.
9//! 2. `cargo test --doc` — doctests, which nextest does not execute.
10
11use anyhow::{Result, bail};
12use clap::Args;
13
14use super::util::{cmd, matrix, workspace};
15
16/// Arguments for `cargo xtask test`.
17#[derive(Debug, Args)]
18pub struct TestArgs {
19    /// Run only the named feature combination (e.g. `"telemetry + test"`).
20    ///
21    /// When omitted, all matrix entries are run in sequence.
22    #[arg(long)]
23    pub features: Option<String>,
24}
25
26/// Runs `cargo nextest run` and `cargo test --doc` across the feature matrix
27/// (or a single entry).
28///
29/// The `TEST_ROOT_DIR` environment variable is set to the workspace root so
30/// that integration tests that read fixture files can locate them regardless
31/// of the working directory.
32///
33/// # Errors
34///
35/// Returns the first test failure encountered.
36pub fn run(args: TestArgs) -> Result<()> {
37    let root = workspace::root()?;
38    let root_str = root.to_string_lossy().into_owned();
39    let env = [("TEST_ROOT_DIR", root_str.as_str())];
40
41    let entries = matrix::scan(&root, &["local"])?;
42    let entries = filter(&entries, args.features.as_deref())?;
43
44    for entry in entries {
45        println!("\n==> nextest ({})", entry.label);
46
47        let mut nextest_args = vec!["nextest", "run", "--all-targets"];
48        nextest_args.extend_from_slice(&entry.cargo_args());
49        cmd::run("cargo", &nextest_args, &root, &env)?;
50
51        println!("\n==> doctest ({})", entry.label);
52
53        let mut doctest_args = vec!["test", "--doc"];
54        doctest_args.extend_from_slice(&entry.cargo_args());
55        cmd::run("cargo", &doctest_args, &root, &env)?;
56    }
57
58    Ok(())
59}
60
61/// Returns the matrix entries to run, optionally filtered by label.
62///
63/// When `label` is `None` all entries are returned.  When a label is
64/// provided it is normalised via [`matrix::normalize_features_arg`] before
65/// matching, so both `"telemetry,test"` and `"telemetry + test"` resolve to
66/// the same
67/// entry.  An unknown label after normalisation is an error.
68///
69/// # Errors
70///
71/// Returns an error when a label is provided but no matrix entry matches,
72/// listing all available labels.
73fn filter<'a>(
74    entries: &'a [matrix::MatrixEntry],
75    label: Option<&str>,
76) -> Result<Vec<&'a matrix::MatrixEntry>> {
77    let Some(raw) = label else {
78        return Ok(entries.iter().collect());
79    };
80
81    let normalised = matrix::normalize_features_arg(raw);
82    let matched: Vec<&matrix::MatrixEntry> =
83        entries.iter().filter(|e| e.label == normalised).collect();
84
85    if matched.is_empty() {
86        let available: Vec<&str> = entries
87            .iter()
88            .map(|e| e.label.as_str())
89            .collect::<std::collections::BTreeSet<_>>()
90            .into_iter()
91            .collect();
92        bail!(
93            "unknown feature combination {:?} (normalised to {:?})\n\nAvailable labels:\n  {}",
94            raw,
95            normalised,
96            available.join("\n  ")
97        );
98    }
99
100    Ok(matched)
101}