cargo-rbmt 0.5.3

Maintainer tools for rust-bitcoin projects
// SPDX-License-Identifier: MIT AND Apache-2.0

//! Integration test tasks for packages with bitcoind-tests or similar test packages.

use std::path::Path;

use serde::Deserialize;
use xshell::Shell;

use crate::environment::{
    cargo_cmd, get_workspace_packages, CmdExt, PackageManifest, ProgressGuard,
};
use crate::toolchain::{prepare_toolchain, Toolchain};

/// Integration-specific configuration, read from `[package.metadata.rbmt.integration]` in `Cargo.toml`.
#[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 `[package.metadata.rbmt.integration]` in the package's `Cargo.toml`.
    fn load(crate_dir: &Path) -> Result<Self, Box<dyn std::error::Error>> {
        #[derive(serde::Deserialize, Default)]
        struct RbmtTable {
            #[serde(default)]
            integration: IntegrationConfig,
        }

        let path = crate_dir.join("Cargo.toml");
        if !path.exists() {
            return Ok(Self::default());
        }
        let contents = std::fs::read_to_string(&path)?;
        Ok(toml::from_str::<PackageManifest<RbmtTable>>(&contents)?
            .package
            .metadata
            .rbmt
            .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 packages that contain an integration tests subpackage.
///
/// # Arguments
///
/// * `packages` - Optional filter for specific package names.
pub fn run(sh: &Shell, packages: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let packages = get_workspace_packages(sh, packages)?;
    let _progress = ProgressGuard::new();
    rbmt_eprintln!("Looking for integration tests in {} crate(s)", packages.len());

    for package in packages {
        let config = IntegrationConfig::load(&package.dir)?;
        let integration_dir = package.dir.join(config.package_name());

        if !integration_dir.exists() {
            continue;
        }

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

        rbmt_eprintln!("Running integration tests for {}", package.name);

        let _dir = sh.push_dir(&integration_dir);
        // The integration package must defines its own [package.metadata.rbmt.toolchains].
        prepare_toolchain(sh, Toolchain::Stable)?;
        let integration_packages = get_workspace_packages(sh, &[])?;
        let integration_package = integration_packages
            .iter()
            .find(|p| p.name == config.package_name())
            .ok_or_else(|| {
                format!(
                    "Integration package '{}' not found in {}",
                    config.package_name(),
                    integration_dir.display()
                )
            })?;
        let available_versions = &integration_package.features;
        if available_versions.is_empty() {
            rbmt_eprintln!("  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.clone()
        };

        // Run tests for each version.
        for version in &versions_to_test {
            rbmt_eprintln!("  Testing with version: {}", version);
            cargo_cmd(sh).arg("test").arg("--features").arg(version).run_with_capture()?;
        }
    }

    Ok(())
}