Skip to main content

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