Skip to main content

bake_test_rust/
lib.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Shared test tasks for Rust projects using Bake.
5//!
6//! The crate provides `test`, `test:coverage`, and `test:external`. Each task
7//! calls the optional project task `test:before` before running Cargo tests.
8mod cargo;
9mod coverage;
10mod external;
11mod manifest;
12mod metadata;
13#[cfg(test)]
14#[path = "tests/support.rs"]
15mod test_support;
16
17use bake::{Context, Error, Result};
18
19fn run_before_test_hook(context: &mut Context) -> Result<()> {
20    context.call_if_registered("test:before", &[])?;
21    Ok(())
22}
23
24/// Run the current Cargo workspace tests with the lockfile enforced.
25#[bake::task(name = "test")]
26fn run(
27    context: &mut Context,
28    #[bake(named, default = false, help = "Also test examples and benchmarks.")] all_targets: bool,
29) -> Result<String> {
30    run_before_test_hook(context)?;
31
32    let mut arguments = vec![
33        "test".to_owned(),
34        "--workspace".to_owned(),
35        "--locked".to_owned(),
36    ];
37    if all_targets {
38        arguments.push("--all-targets".to_owned());
39    }
40
41    cargo::run(context, &arguments)?;
42    Ok("Cargo workspace tests passed".to_owned())
43}
44
45/// Run workspace tests and require complete source-region coverage.
46///
47/// Documentation tests run through Cargo because `cargo-llvm-cov` does not
48/// currently support stable doctest coverage. The remaining test targets run
49/// under `cargo-llvm-cov`; source regions inside `unreachable!()` calls are
50/// excluded from the coverage gate.
51#[bake::task(name = "test:coverage")]
52fn run_coverage(
53    context: &mut Context,
54    #[bake(named, default = false, help = "Test every feature together.")] all_features: bool,
55    #[bake(help = "Repeat to select features.")] features: Vec<String>,
56    #[bake(help = "Limit coverage to one workspace package.")] package: Option<String>,
57    #[bake(named, default = false, help = "Also include examples and benchmarks.")]
58    all_targets: bool,
59) -> Result<String> {
60    if all_features && !features.is_empty() {
61        return Err(Error::new(
62            "choose either --all-features true or one or more --features values",
63        ));
64    }
65
66    run_before_test_hook(context)?;
67
68    let mut doc_arguments = vec!["test".to_owned()];
69    append_package_arguments(&mut doc_arguments, package.as_deref());
70    doc_arguments.extend(["--locked".to_owned(), "--doc".to_owned()]);
71    append_feature_arguments(&mut doc_arguments, all_features, &features);
72    cargo::run(context, &doc_arguments)?;
73
74    let report = coverage::CoverageReport::new()?;
75    let mut coverage_arguments = vec!["llvm-cov".to_owned()];
76    append_package_arguments(&mut coverage_arguments, package.as_deref());
77    coverage_arguments.push("--locked".to_owned());
78    if all_targets {
79        coverage_arguments.push("--all-targets".to_owned());
80    }
81    append_feature_arguments(&mut coverage_arguments, all_features, &features);
82    cargo::run_with_output_path(context, &coverage_arguments, report.path())?;
83
84    let summary = report.read(context.root())?;
85    if !summary.is_complete() {
86        return Err(Error::new(summary.failure_message()));
87    }
88
89    Ok(summary.success_message())
90}
91
92fn append_feature_arguments(arguments: &mut Vec<String>, all_features: bool, features: &[String]) {
93    if all_features {
94        arguments.push("--all-features".to_owned());
95    } else if !features.is_empty() {
96        arguments.push("--features".to_owned());
97        arguments.push(features.join(","));
98    }
99}
100
101fn append_package_arguments(arguments: &mut Vec<String>, package: Option<&str>) {
102    if let Some(package) = package {
103        arguments.push("--package".to_owned());
104        arguments.push(package.to_owned());
105    } else {
106        arguments.push("--workspace".to_owned());
107    }
108}
109
110/// Test configured downstream repositories against this workspace's local packages.
111#[bake::task(name = "test:external")]
112fn run_external(
113    context: &mut Context,
114    #[bake(named, default = false, help = "Also test examples and benchmarks.")] all_targets: bool,
115) -> Result<String> {
116    let workspace = metadata::read_workspace(context)?;
117    if workspace.external_repositories.is_empty() {
118        return Ok("No downstream repositories are configured for external tests".to_owned());
119    }
120    if workspace.packages.is_empty() {
121        return Err(Error::new(
122            "the current workspace has no crates.io packages to patch into downstream tests",
123        ));
124    }
125
126    let mut checkouts = Vec::with_capacity(workspace.external_repositories.len());
127    for repository in &workspace.external_repositories {
128        let checkout = external::checkout(context, repository)?;
129        manifest::patch_checkout(&checkout, &workspace.packages)?;
130        cargo::update_in(&checkout)?;
131        external::ensure_local_patches_are_selected(&checkout, &workspace.packages)?;
132        checkouts.push(checkout);
133    }
134
135    run_before_test_hook(context)?;
136
137    for checkout in &checkouts {
138        let mut arguments = vec!["test".to_owned(), "--workspace".to_owned()];
139        if all_targets {
140            arguments.push("--all-targets".to_owned());
141        }
142        external::run_cargo(checkout, &arguments)?;
143    }
144
145    let repository_count = checkouts.len();
146    let repository_label = if repository_count == 1 {
147        "repository"
148    } else {
149        "repositories"
150    };
151    Ok(format!(
152        "External tests passed for {repository_count} downstream {repository_label}"
153    ))
154}
155
156#[cfg(test)]
157#[path = "tests/tasks.rs"]
158mod tests;