Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions cargo-rbmt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Maintainer tools for Rust-based projects in the Bitcoin domain. Built with [xshell](https://github.com/matklad/xshell).

## Environment Variables

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

## Configuration

Configuration for `rbmt` is stored in `rbmt.toml`. The file can live at both the workspace root (e.g. `$ROOT/rbmt.toml`) as well as per-package (e.g. `$ROOT/$PACKAGE/rbmt.toml`) within a repository.
Expand Down Expand Up @@ -57,9 +61,17 @@ exact_features = [
features_with_no_std = ["serde", "rand"]
```

### Environment Variables
### Integration

* `RBMT_LOG_LEVEL=quiet` - Suppress verbose output and reduce cargo noise.
The `integration` command is designed to work with the [`corepc`](https://github.com/rust-bitcoin/corepc) integration testing framework, which provides Bitcoin Core binaries and testing infrastructure.

```toml
[integration]
# Integration tests package name, defaults to "bitcoind-tests".
package = "bitcoind-tests"
# Versions to test. If omitted, tests all discovered versions from Cargo.toml.
versions = ["29_0", "28_2", "27_2"]
```

## Lock Files

Expand Down
147 changes: 147 additions & 0 deletions cargo-rbmt/src/integration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//! Integration test tasks for packages with bitcoind-tests or similar test packages.

use crate::environment::{get_crate_dirs, quiet_println, CONFIG_FILE_PATH};
use crate::quiet_cmd;
use serde::Deserialize;
use std::path::{Path, PathBuf};
use xshell::{cmd, Shell};

/// Integration test configuration loaded from rbmt.toml.
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
struct Config {
integration: IntegrationConfig,
}

/// Integration-specific configuration.
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
struct IntegrationConfig {
/// Package name containing integration tests (defaults to "bitcoind-tests").
package: Option<String>,

/// Bitcoind versions to test (runs each individually).
/// If not specified, discovers all version features from Cargo.toml.
///
/// # Examples
///
/// `["29_0", "28_2", "27_2"]`
versions: Option<Vec<String>>,
}

impl IntegrationConfig {
/// Load integration configuration from a crate directory.
fn load(crate_dir: &Path) -> Result<Self, Box<dyn std::error::Error>> {
let config_path = crate_dir.join(CONFIG_FILE_PATH);

if !config_path.exists() {
return Ok(IntegrationConfig::default());
}

let contents = std::fs::read_to_string(&config_path)?;
let config: Config = toml::from_str(&contents)?;
Ok(config.integration)
}

/// Get the package name (defaults to "bitcoind-tests").
fn package_name(&self) -> &str {
self.package.as_deref().unwrap_or("bitcoind-tests")
}
}

/// Run integration tests for all crates with integration test packages.
///
/// # Arguments
///
/// * `packages` - Optional filter for specific package names.
pub fn run(sh: &Shell, packages: &[String]) -> Result<(), Box<dyn std::error::Error>> {
let crate_dirs = get_crate_dirs(sh, packages)?;
quiet_println(&format!(
"Looking for integration tests in {} crate(s)",
crate_dirs.len()
));

for crate_dir in &crate_dirs {
let config = IntegrationConfig::load(Path::new(crate_dir))?;
let integration_dir = PathBuf::from(crate_dir).join(config.package_name());

if !integration_dir.exists() {
continue;
}

if !integration_dir.join("Cargo.toml").exists() {
continue;
}

quiet_println(&format!(
"Running integration tests for crate: {}",
crate_dir
));

let _dir = sh.push_dir(&integration_dir);

let available_versions = discover_version_features(sh, &integration_dir)?;
if available_versions.is_empty() {
quiet_println(" No version features found in Cargo.toml");
continue;
}

let versions_to_test: Vec<String> = if let Some(config_versions) = &config.versions {
// Filter available versions by config.
let mut filtered = Vec::new();
for requested in config_versions {
if available_versions.contains(requested) {
filtered.push(requested.clone());
} else {
return Err(format!(
"Requested version '{}' not found in available versions: {}",
requested,
available_versions.join(", ")
)
.into());
}
}
filtered
} else {
// No config, test all available versions.
available_versions
};

// Run tests for each version.
for version in &versions_to_test {
quiet_println(&format!(" Testing with version: {}", version));
quiet_cmd!(sh, "cargo test --features={version}").run()?;
}
}

Ok(())
}

/// Discover all features from the integration package using cargo metadata.
fn discover_version_features(
sh: &Shell,
integration_dir: &Path,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let _dir = sh.push_dir(integration_dir);
let metadata = cmd!(sh, "cargo metadata --format-version 1 --no-deps").read()?;
let json: serde_json::Value = serde_json::from_str(&metadata)?;

let mut features = Vec::new();

// Find the package in the metadata and extract its features.
if let Some(packages) = json["packages"].as_array() {
// Should only be one package since we're in the integration test directory.
if let Some(package) = packages.first() {
if let Some(package_features) = package["features"].as_object() {
for feature_name in package_features.keys() {
features.push(feature_name.clone());
}
}
}
}

// Sort for consistent output.
features.sort();

Ok(features)
}
14 changes: 12 additions & 2 deletions cargo-rbmt/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod bench;
mod docs;
mod environment;
mod integration;
mod lint;
mod lock;
mod test;
Expand Down Expand Up @@ -46,6 +47,8 @@ enum Commands {
#[arg(value_enum)]
toolchain: Toolchain,
},
/// Run bitcoin core integration tests.
Integration,
/// Update Cargo-minimal.lock and Cargo-recent.lock files.
Lock,
}
Expand All @@ -64,9 +67,10 @@ fn main() {
configure_log_level(&sh);
change_to_repo_root(&sh);

// Restore the specified lock file before running any command (except Lock itself).
// Restore the specified lock file before running any command (except Lock and Integration).
// Integration tests use their own lock files in the integration package directory.
if let Some(lock_file) = cli.lock_file {
if !matches!(cli.command, Commands::Lock) {
if !matches!(cli.command, Commands::Lock | Commands::Integration) {
if let Err(e) = lock::restore_lock_file(&sh, lock_file) {
eprintln!("Error restoring lock file: {}", e);
process::exit(1);
Expand Down Expand Up @@ -105,6 +109,12 @@ fn main() {
process::exit(1);
}
}
Commands::Integration => {
if let Err(e) = integration::run(&sh, &cli.packages) {
eprintln!("Error running integration tests: {}", e);
process::exit(1);
}
}
Commands::Lock => {
if let Err(e) = lock::run(&sh) {
eprintln!("Error updating lock files: {}", e);
Expand Down