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