Skip to content

Commit 5f2bc94

Browse files
committed
tasks: add tests task
1 parent 2ae860e commit 5f2bc94

File tree

4 files changed

+320
-4
lines changed

4 files changed

+320
-4
lines changed

tasks/README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ Maintainer tools for Rust-based projects in the Bitcoin domain. Built with [xshe
44

55
## Configuration
66

7-
Configuration for `rbmt` is stored in `contrib/rbmt.toml`.
7+
Configuration for `rbmt` is stored in `contrib/rbmt.toml`. The file can live at both the workspace root (e.g. `$ROOT/contrib/rbmt.toml`) as well as per-crate (e.g. `$ROOT/$CRATE/contrib/rbmt.toml`) within a repository.
88

99
### Lint
1010

11-
The `lint` command detects duplicate dependencies, but some may be unavoidable (e.g., during dependency updates where transitive dependencies haven't caught up). Configure the `[lint]` section to whitelist specific duplicates.
11+
The `lint` command detects duplicate dependencies, but some may be unavoidable (e.g., during dependency updates where transitive dependencies haven't caught up). Configure the `[lint]` section to whitelist specific duplicates for a workspace (or a crate if only one crate in a repository).
1212

1313
```toml
1414
[lint]
@@ -18,6 +18,45 @@ allowed_duplicates = [
1818
]
1919
```
2020

21+
### Test
22+
23+
The `test` command can be configured to run feature matrix testing for your crate. Configure with the `contrib/rbmt.toml` file at the crate level.
24+
25+
```toml
26+
[test]
27+
# Examples to run with specific features enabled.
28+
# Format: "example_name:feature1 feature2"
29+
examples = [
30+
"example1:serde",
31+
"example2:serde rand",
32+
]
33+
34+
# Features to test with the conventional `std` feature enabled.
35+
# Tests each feature alone with std, all pairs, and all together.
36+
# Example: ["serde", "rand"] tests: std+serde, std+rand, std+serde+rand
37+
features_with_std = ["serde", "rand"]
38+
39+
# Features to test without the `std` feature.
40+
# Tests each feature alone, all pairs, and all together.
41+
# Example: ["serde", "rand"] tests: serde, rand, serde+rand
42+
features_without_std = ["serde", "rand"]
43+
44+
# Exact feature combinations to test.
45+
# Use for crates that don't follow conventional `std` patterns.
46+
# Each inner array is tested as-is with no automatic combinations.
47+
# Example: [["serde", "rand"], ["rand"]] tests exactly those two combinations
48+
exact_features = [
49+
["serde", "rand"],
50+
["rand"],
51+
]
52+
53+
# Features to test with an explicit `no-std` feature enabled.
54+
# Only use if your crate has a `no-std` feature (rust-miniscript pattern).
55+
# Tests each feature with no-std, all pairs, and all together.
56+
# Example: ["serde", "rand"] tests: no-std+serde, no-std+rand, no-std+serde+rand
57+
features_with_no_std = ["serde", "rand"]
58+
```
59+
2160
### Environment Variables
2261

2362
* `RBMT_LOG_LEVEL=quiet` - Suppress verbose output and reduce cargo noise.

tasks/src/main.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ mod bench;
22
mod docs;
33
mod environment;
44
mod lint;
5+
mod test;
56
mod toolchain;
67

78
use clap::{Parser, Subcommand};
89
use std::process;
910
use xshell::Shell;
1011

1112
use environment::{change_to_repo_root, configure_log_level};
13+
use toolchain::Toolchain;
1214

1315
#[derive(Parser)]
1416
#[command(name = "rbmt")]
@@ -28,6 +30,12 @@ enum Commands {
2830
Docsrs,
2931
/// Run benchmark tests for all crates.
3032
Bench,
33+
/// Run tests with specified toolchain.
34+
Test {
35+
/// Toolchain to use: stable, nightly, or msrv.
36+
#[arg(value_enum)]
37+
toolchain: Toolchain,
38+
},
3139
}
3240

3341
fn main() {
@@ -61,5 +69,11 @@ fn main() {
6169
process::exit(1);
6270
}
6371
}
72+
Commands::Test { toolchain } => {
73+
if let Err(e) = test::run(&sh, toolchain) {
74+
eprintln!("Error running tests: {}", e);
75+
process::exit(1);
76+
}
77+
}
6478
}
6579
}

tasks/src/test.rs

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
//! Build and test tasks with feature matrix testing.
2+
3+
use crate::environment::{get_crate_dirs, quiet_println, CONFIG_FILE_PATH};
4+
use crate::quiet_cmd;
5+
use crate::toolchain::{check_toolchain, Toolchain};
6+
use serde::Deserialize;
7+
use std::path::Path;
8+
use xshell::Shell;
9+
10+
/// Test configuration loaded from contrib/rbmt.toml.
11+
#[derive(Debug, Deserialize)]
12+
struct Config {
13+
test: TestConfig,
14+
}
15+
16+
/// Test-specific configuration.
17+
#[derive(Debug, Deserialize)]
18+
struct TestConfig {
19+
/// Examples to run with the format "name:feature1 feature2".
20+
///
21+
/// # Examples
22+
///
23+
/// `["example1:serde", "example2:serde rand"]`
24+
examples: Vec<String>,
25+
26+
/// List of individual features to test with the conventional `std` feature enabled.
27+
/// Automatically tests feature combinations, alone with `std`, all pairs, and all together.
28+
///
29+
/// # Examples
30+
///
31+
/// `["serde", "rand"]` tests `std+serde`, `std+rand`, `std+serde+rand`.
32+
features_with_std: Vec<String>,
33+
34+
/// List of individual features to test without the `std` feature.
35+
/// Automatically tests features combinations, each feature alone,
36+
/// all pairs, and all together.
37+
///
38+
/// # Examples
39+
///
40+
/// `["serde", "rand"]` tests `serde`, `rand`, `serde+rand`.
41+
features_without_std: Vec<String>,
42+
43+
/// Exact feature combinations to test.
44+
/// Use for crates that don't follow the conventional `std` feature pattern.
45+
/// Each inner vector is a list of features to test together. There is
46+
/// no automatic combinations of features tests.
47+
///
48+
/// # Examples
49+
///
50+
/// `[["serde", "rand"], ["rand"]]` tests exactly those two combinations.
51+
exact_features: Vec<Vec<String>>,
52+
53+
/// List of individual features to test with the `no-std` feature enabled.
54+
/// Only use if your crate has an explicit `no-std` feature (rust-miniscript pattern).
55+
/// Automatically tests each feature alone with `no-std`, all pairs, and all together.
56+
///
57+
/// # Examples
58+
///
59+
/// `["serde", "rand"]` tests `no-std+serde`, `no-std+serde`, `no-std+serde+rand`.
60+
features_with_no_std: Vec<String>,
61+
}
62+
63+
impl TestConfig {
64+
/// Load test configuration from a crate directory.
65+
fn load(crate_dir: &Path) -> Result<Self, Box<dyn std::error::Error>> {
66+
let config_path = crate_dir.join(CONFIG_FILE_PATH);
67+
68+
if !config_path.exists() {
69+
// Return empty config if file doesn't exist.
70+
return Ok(TestConfig {
71+
examples: Vec::new(),
72+
features_with_std: Vec::new(),
73+
features_without_std: Vec::new(),
74+
exact_features: Vec::new(),
75+
features_with_no_std: Vec::new(),
76+
});
77+
}
78+
79+
let contents = std::fs::read_to_string(&config_path)?;
80+
let config: Config = toml::from_str(&contents)?;
81+
Ok(config.test)
82+
}
83+
}
84+
85+
/// Run build and test for all crates with the specified toolchain.
86+
pub fn run(sh: &Shell, toolchain: Toolchain) -> Result<(), Box<dyn std::error::Error>> {
87+
check_toolchain(sh, toolchain)?;
88+
89+
let crate_dirs = get_crate_dirs(sh)?;
90+
quiet_println(&format!("Testing {} crates", crate_dirs.len()));
91+
92+
for crate_dir in &crate_dirs {
93+
quiet_println(&format!("Testing crate: {}", crate_dir));
94+
95+
let _dir = sh.push_dir(crate_dir);
96+
let config = TestConfig::load(Path::new(crate_dir))?;
97+
98+
do_test(sh, &config)?;
99+
do_feature_matrix(sh, &config)?;
100+
}
101+
102+
Ok(())
103+
}
104+
105+
/// Run basic build, test, and examples.
106+
fn do_test(sh: &Shell, config: &TestConfig) -> Result<(), Box<dyn std::error::Error>> {
107+
quiet_println("Running basic tests");
108+
109+
// Basic build and test.
110+
quiet_cmd!(sh, "cargo build").run()?;
111+
quiet_cmd!(sh, "cargo test").run()?;
112+
113+
// Run examples.
114+
for example in &config.examples {
115+
let parts: Vec<&str> = example.split(':').collect();
116+
if parts.len() != 2 {
117+
return Err(format!(
118+
"Invalid example format: {}, expected 'name:features'",
119+
example
120+
)
121+
.into());
122+
}
123+
124+
let name = parts[0];
125+
let features = parts[1];
126+
127+
quiet_println(&format!(
128+
"Running example {} with features: {}",
129+
name, features
130+
));
131+
quiet_cmd!(sh, "cargo run --example {name} --features={features}").run()?;
132+
}
133+
134+
Ok(())
135+
}
136+
137+
/// Run feature matrix tests.
138+
fn do_feature_matrix(sh: &Shell, config: &TestConfig) -> Result<(), Box<dyn std::error::Error>> {
139+
quiet_println("Running feature matrix tests");
140+
141+
// Handle exact features (for unusual crates).
142+
if !config.exact_features.is_empty() {
143+
for features in &config.exact_features {
144+
let features_str = features.join(" ");
145+
quiet_println(&format!("Testing exact features: {}", features_str));
146+
quiet_cmd!(
147+
sh,
148+
"cargo build --no-default-features --features={features_str}"
149+
)
150+
.run()?;
151+
quiet_cmd!(
152+
sh,
153+
"cargo test --no-default-features --features={features_str}"
154+
)
155+
.run()?;
156+
}
157+
return Ok(());
158+
}
159+
160+
// Handle no-std pattern (rust-miniscript).
161+
if !config.features_with_no_std.is_empty() {
162+
quiet_println("Testing no-std");
163+
quiet_cmd!(sh, "cargo build --no-default-features --features=no-std").run()?;
164+
quiet_cmd!(sh, "cargo test --no-default-features --features=no-std").run()?;
165+
166+
loop_features(sh, "no-std", &config.features_with_no_std)?;
167+
} else {
168+
quiet_println("Testing no-default-features");
169+
quiet_cmd!(sh, "cargo build --no-default-features").run()?;
170+
quiet_cmd!(sh, "cargo test --no-default-features").run()?;
171+
}
172+
173+
// Test all features.
174+
quiet_println("Testing all-features");
175+
quiet_cmd!(sh, "cargo build --all-features").run()?;
176+
quiet_cmd!(sh, "cargo test --all-features").run()?;
177+
178+
// Test features with std.
179+
if !config.features_with_std.is_empty() {
180+
loop_features(sh, "std", &config.features_with_std)?;
181+
}
182+
183+
// Test features without std.
184+
if !config.features_without_std.is_empty() {
185+
loop_features(sh, "", &config.features_without_std)?;
186+
}
187+
188+
Ok(())
189+
}
190+
191+
/// Test each feature individually and all combinations of two features.
192+
///
193+
/// This implements three feature matrix testing strategies.
194+
/// 1. All features together.
195+
/// 2. Each feature individually (only if more than one feature).
196+
/// 3. All unique pairs of features.
197+
///
198+
/// The pair testing catches feature interaction bugs (where two features work
199+
/// independently, but conflict when combined) while keeping test time manageable.
200+
fn loop_features(
201+
sh: &Shell,
202+
base: &str,
203+
features: &[String],
204+
) -> Result<(), Box<dyn std::error::Error>> {
205+
let base_flag = if base.is_empty() {
206+
String::new()
207+
} else {
208+
format!("{} ", base)
209+
};
210+
211+
// Test all features together.
212+
let all_features = format!("{}{}", base_flag, features.join(" "));
213+
quiet_println(&format!("Testing features: {}", all_features.trim()));
214+
quiet_cmd!(
215+
sh,
216+
"cargo build --no-default-features --features={all_features}"
217+
)
218+
.run()?;
219+
quiet_cmd!(
220+
sh,
221+
"cargo test --no-default-features --features={all_features}"
222+
)
223+
.run()?;
224+
225+
// Test each feature individually and all pairs (only if more than one feature).
226+
if features.len() > 1 {
227+
for i in 0..features.len() {
228+
let feature_combo = format!("{}{}", base_flag, features[i]);
229+
quiet_println(&format!("Testing features: {}", feature_combo.trim()));
230+
quiet_cmd!(
231+
sh,
232+
"cargo build --no-default-features --features={feature_combo}"
233+
)
234+
.run()?;
235+
quiet_cmd!(
236+
sh,
237+
"cargo test --no-default-features --features={feature_combo}"
238+
)
239+
.run()?;
240+
241+
// Test all pairs with features[i].
242+
for j in (i + 1)..features.len() {
243+
let feature_combo = format!("{}{} {}", base_flag, features[i], features[j]);
244+
quiet_println(&format!("Testing features: {}", feature_combo.trim()));
245+
quiet_cmd!(
246+
sh,
247+
"cargo build --no-default-features --features={feature_combo}"
248+
)
249+
.run()?;
250+
quiet_cmd!(
251+
sh,
252+
"cargo test --no-default-features --features={feature_combo}"
253+
)
254+
.run()?;
255+
}
256+
}
257+
}
258+
259+
Ok(())
260+
}

tasks/src/toolchain.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::path::Path;
22
use xshell::{cmd, Shell};
33

44
/// Toolchain requirement for a task.
5-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5+
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
66
pub enum Toolchain {
77
/// Nightly toolchain.
88
Nightly,
@@ -92,7 +92,10 @@ fn get_msrv_from_manifest(manifest_path: &Path) -> Result<String, Box<dyn std::e
9292
}
9393

9494
/// Extract version number from rustc --version output.
95-
/// Example: "rustc 1.74.0 (79e9716c9 2023-11-13)" -> Some("1.74.0")
95+
///
96+
/// # Examples
97+
///
98+
/// `"rustc 1.74.0 (79e9716c9 2023-11-13)"` -> `Some("1.74.0")`
9699
fn extract_version(rustc_version: &str) -> Option<&str> {
97100
rustc_version.split_whitespace().find_map(|part| {
98101
// Split off any suffix like "-nightly" or "-beta".

0 commit comments

Comments
 (0)