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 feature flag automatically includes it in all test
5//! 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. `"test + otel"`).
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 `"otel,test"` and `"otel + test"` resolve to the same
66/// entry.  An unknown label after normalisation is an error.
67///
68/// # Errors
69///
70/// Returns an error when a label is provided but no matrix entry matches,
71/// listing all available labels.
72fn filter<'a>(
73    entries: &'a [matrix::MatrixEntry],
74    label: Option<&str>,
75) -> Result<Vec<&'a matrix::MatrixEntry>> {
76    let Some(raw) = label else {
77        return Ok(entries.iter().collect());
78    };
79
80    let normalised = matrix::normalize_features_arg(raw);
81    let matched: Vec<&matrix::MatrixEntry> =
82        entries.iter().filter(|e| e.label == normalised).collect();
83
84    if matched.is_empty() {
85        let available: Vec<&str> = entries
86            .iter()
87            .map(|e| e.label.as_str())
88            .collect::<std::collections::BTreeSet<_>>()
89            .into_iter()
90            .collect();
91        bail!(
92            "unknown feature combination {:?} (normalised to {:?})\n\nAvailable labels:\n  {}",
93            raw,
94            normalised,
95            available.join("\n  ")
96        );
97    }
98
99    Ok(matched)
100}