Skip to content

Commit 5c26e47

Browse files
committed
tasks: add tests task
1 parent 28603a7 commit 5c26e47

File tree

3 files changed

+272
-2
lines changed

3 files changed

+272
-2
lines changed

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

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)