diff --git a/cargo-rbmt/README.md b/cargo-rbmt/README.md index 679154b..fc0dea0 100644 --- a/cargo-rbmt/README.md +++ b/cargo-rbmt/README.md @@ -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. @@ -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 diff --git a/cargo-rbmt/src/integration.rs b/cargo-rbmt/src/integration.rs new file mode 100644 index 0000000..3b12e47 --- /dev/null +++ b/cargo-rbmt/src/integration.rs @@ -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, + + /// 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>, +} + +impl IntegrationConfig { + /// Load integration configuration from a crate directory. + fn load(crate_dir: &Path) -> Result> { + 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> { + 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 = 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, Box> { + 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) +} diff --git a/cargo-rbmt/src/main.rs b/cargo-rbmt/src/main.rs index 5a0cf07..4c6cbe9 100644 --- a/cargo-rbmt/src/main.rs +++ b/cargo-rbmt/src/main.rs @@ -1,6 +1,7 @@ mod bench; mod docs; mod environment; +mod integration; mod lint; mod lock; mod test; @@ -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, } @@ -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); @@ -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);