diff --git a/.github/workflows/backward-compatibility.yml b/.github/workflows/backward-compatibility.yml index c14dbe236c2..222ca30b016 100644 --- a/.github/workflows/backward-compatibility.yml +++ b/.github/workflows/backward-compatibility.yml @@ -158,9 +158,12 @@ jobs: --signed-entity-types ${{ inputs.signed-entity-types }} EXIT_CODE=$? if [ $EXIT_CODE -eq 0 ]; then - echo "SUCCESS=true" >> $GITHUB_ENV + echo "RESULT=success" >> $GITHUB_ENV + elif [ $EXIT_CODE -eq 3 ]; then + echo "RESULT=incompatible" >> $GITHUB_ENV + exit 0 else - echo "SUCCESS=false" >> $GITHUB_ENV + echo "RESULT=failure" >> $GITHUB_ENV fi exit $EXIT_CODE @@ -195,8 +198,8 @@ jobs: --arg AGGREGATOR "$AGGREGATOR_TAG" \ --arg SIGNER "$SIGNER_TAG" \ --arg CLIENT "$CLIENT_TAG" \ - --argjson SUCCESS "${{ env.SUCCESS }}" \ - '{tag: $TAG, node: $NODE, mithril_signer: $SIGNER, mithril_aggregator: $AGGREGATOR, mithril_client: $CLIENT, cardano_node_version: $CARDANO_NODE, success: $SUCCESS}' \ + --arg RESULT "${{ env.RESULT }}" \ + '{tag: $TAG, node: $NODE, mithril_signer: $SIGNER, mithril_aggregator: $AGGREGATOR, mithril_client: $CLIENT, cardano_node_version: $CARDANO_NODE, result: $RESULT}' \ > ./${{ env.RESULT_FILE_NAME }}.json - name: Upload test result JSON @@ -241,6 +244,7 @@ jobs: shell: bash run: | CHECK_MARK=":heavy_check_mark:" + WARN_MARK=":warning:" CROSS_MARK=":no_entry:" echo "## Distributions backward compatibility" >> $GITHUB_STEP_SUMMARY @@ -250,22 +254,22 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "**Signed entity types**: ${{ inputs.signed-entity-types }}" >> $GITHUB_STEP_SUMMARY - echo "**Cardano nodes**: ${{ inputs.cardano_node_version }}" >> $GITHUB_STEP_SUMMARY + echo "**Cardano nodes**: ${{ inputs.cardano-node-version }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Compatibility | mithril-signer | mithril-aggregator | mithril-client |" >> $GITHUB_STEP_SUMMARY echo "| --- | :---: | :---: | :---: |" >> $GITHUB_STEP_SUMMARY # Transform summary.json into Markdown table rows - jq -r --arg CHECK_MARK "$CHECK_MARK" --arg CROSS_MARK "$CROSS_MARK" \ - 'group_by(.tag) | - sort_by(.[0].tag | sub("-[a-z]+$"; "") | tonumber) | reverse | + jq -r --arg CHECK_MARK "$CHECK_MARK" --arg WARN_MARK "$WARN_MARK" --arg CROSS_MARK "$CROSS_MARK" \ + 'def parseresult(result): if result == "success" then $CHECK_MARK elif result == "incompatible" then $WARN_MARK else $CROSS_MARK end; + group_by(.tag) | sort_by(.[0].tag | sub("-[a-z]+$"; "") | tonumber) | reverse | .[] | { tag: .[0].tag, - signer: (map(select(.node == "mithril-signer") | if .success then $CHECK_MARK else $CROSS_MARK end) | join("")), - aggregator: (map(select(.node == "mithril-aggregator") | if .success then $CHECK_MARK else $CROSS_MARK end) | join("")), - client: (map(select(.node == "mithril-client") | if .success then $CHECK_MARK else $CROSS_MARK end) | join("")) + signer: (map(select(.node == "mithril-signer") | parseresult(.result)) | join("")), + aggregator: (map(select(.node == "mithril-aggregator") | parseresult(.result)) | join("")), + client: (map(select(.node == "mithril-client") | parseresult(.result)) | join("")) } | "| `\(.tag)` | \(.signer) | \(.aggregator) | \(.client) |"' "./test-results/summary.json" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/nightly-dispatcher.yml b/.github/workflows/nightly-dispatcher.yml index 3a7b2d32477..c479dc4683f 100644 --- a/.github/workflows/nightly-dispatcher.yml +++ b/.github/workflows/nightly-dispatcher.yml @@ -22,6 +22,12 @@ jobs: notify-on-failure: uses: ./.github/workflows/test-notify-on-failure.yml - needs: [docker-builds, aggregator-stress-test, test-client] + needs: + [ + backward-compatibility, + docker-builds, + aggregator-stress-test, + test-client, + ] if: failure() secrets: inherit diff --git a/Cargo.lock b/Cargo.lock index 394d0316aa0..7ce406b9b49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4080,7 +4080,7 @@ dependencies = [ [[package]] name = "mithril-end-to-end" -version = "0.4.108" +version = "0.4.109" dependencies = [ "anyhow", "async-recursion", @@ -4091,6 +4091,7 @@ dependencies = [ "mithril-common", "mithril-doc", "reqwest", + "semver", "serde", "serde_json", "slog", diff --git a/flake.nix b/flake.nix index cba6cabbf60..407586f2ac6 100644 --- a/flake.nix +++ b/flake.nix @@ -91,7 +91,7 @@ cargoArtifacts = buildDeps cargoToml baseCargoArtifacts; } // { - cargoTestCommand = "cargo test"; + cargoTestCommand = "RUST_BACKTRACE=1 cargo test --profile release"; } // args); diff --git a/mithril-client/src/aggregator_client.rs b/mithril-client/src/aggregator_client.rs index 263eff69591..485496bfc37 100644 --- a/mithril-client/src/aggregator_client.rs +++ b/mithril-client/src/aggregator_client.rs @@ -512,7 +512,7 @@ mod tests { macro_rules! assert_error_eq { ($left:expr, $right:expr) => { - assert_eq!(format!("{:?}", &$left), format!("{:?}", &$right),); + assert_eq!(format!("{}", &$left), format!("{}", &$right),); }; } diff --git a/mithril-test-lab/mithril-end-to-end/Cargo.toml b/mithril-test-lab/mithril-end-to-end/Cargo.toml index 7c0fb2af052..11d7d23b4ea 100644 --- a/mithril-test-lab/mithril-end-to-end/Cargo.toml +++ b/mithril-test-lab/mithril-end-to-end/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mithril-end-to-end" -version = "0.4.108" +version = "0.4.109" authors = { workspace = true } edition = { workspace = true } documentation = { workspace = true } @@ -29,6 +29,7 @@ mithril-cardano-node-internal-database = { path = "../../internal/cardano-node/m mithril-common = { path = "../../mithril-common" } mithril-doc = { path = "../../internal/mithril-doc" } reqwest = { workspace = true, features = ["default"] } +semver = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } slog = { workspace = true, features = [ diff --git a/mithril-test-lab/mithril-end-to-end/backward-compatibility.md b/mithril-test-lab/mithril-end-to-end/backward-compatibility.md new file mode 100644 index 00000000000..e7c8d980acb --- /dev/null +++ b/mithril-test-lab/mithril-end-to-end/backward-compatibility.md @@ -0,0 +1,68 @@ +# Backward compatibility + +The end-to-end tests scenarios are backward compatible with previous nodes versions. + +This is achieved by differentiating the scenarios based on the version of the nodes they are testing. + +There are multiple ways to do this: + +- disabling part of the scenarios +- adding or removing arguments to the binaries (e.g. `--backend` flag for the client-cli) + +> [!TIP] +> Node versions and distribution versions below are the ones that first included the changes, as node versions evolve +> faster than distribution versions the actual node version included in the distribution may be higher. +> +> e.g. `--origin-tag` flag for the client-cli was introduced in version `0.11.13`, the first distribution that included +> that change is `2517.0`, which include client-cli `0.12.0`. + +--- + +## Supported changes + +List of breaking changes that are supported by the end-to-end tests. + +format is: `- **since 'X.Y.Z' (distribution version) [to 'X.Y.Z' (distribution version) (optional)]**: supported change` + +### Mithril client + +- **since `0.12.34` (2543.0)**: test of new `--epoch` filter to `cardano-db list` (disabled on lower versions) +- **since `0.12.11` (2524.0)**: removal of `cardano-db-v2` replaced with `cardano-db [command] --backend [v1,v2]` +- **since `0.11.14` (2517.0)**: addition of `--include-ancillary` flag to `cardano-db download` +- **since `0.11.13` (2517.0)**: addition of global `--origin-tag` parameter + +### Mithril aggregator + +- **since `0.7.94` (next to 2543.1)**: only the leader aggregator must be restarted when updating protocol parameters + +### Mithril signer + +--- + +## Unsupported changes + +List of breaking changes that are NOT supported by the end-to-end tests, running the tests with nodes versions +violating at least one of the case below will result in an error and an exit code of `3`. + +format is: + +- for an incompatibility of a node below a specific version: + `- **min supported version is 'X.Y.Z' (distribution version)**: explanatory message` +- for an incompatibility between two nodes: + `- **below 'X.Y.Z' (distribution version) with {other node} `X.Y.Z` (other distribution version) and up**: explanatory message` + +### Mithril client + +- **below `0.11.14` (2517.0) with aggregator `0.7.31` (2517.0) and up**: split ancillary files are not correctly supported + for older clients, causing the verification to fail because the incomplete immutable files trio is missing. + +### Mithril aggregator + +- **below `0.7.91` (next to 2543.1) with signer `0.2.277` (next to 2543.1) and up**: new `/protocol-configuration/{epoch}`aggregator + route to update network parameters (required by signer`0.2.277` and up) +- **below `0.7.55` (2524.0) with cardano-node version `10.4.1` and up**: support of UTxO-HD was added only on aggregator `0.7.55` and up + +### Mithril signer + +- **min supported version is `0.2.221` (2450.0)**: addition of the `CardanoDatabase` signed entity type, previous signers + are not able to handle unknown signed entities diff --git a/mithril-test-lab/mithril-end-to-end/src/assertions/check.rs b/mithril-test-lab/mithril-end-to-end/src/assertions/check.rs index f36a2da86f5..1837941e71f 100644 --- a/mithril-test-lab/mithril-end-to-end/src/assertions/check.rs +++ b/mithril-test-lab/mithril-end-to-end/src/assertions/check.rs @@ -3,7 +3,7 @@ use std::time::Duration; use anyhow::{Context, anyhow}; use reqwest::StatusCode; use serde::de::DeserializeOwned; -use slog_scope::info; +use slog_scope::{info, warn}; use mithril_common::{ StdResult, @@ -558,13 +558,21 @@ pub async fn assert_client_can_verify_cardano_database( client .run(ClientCommand::CardanoDbV2(CardanoDbV2Command::List)) .await?; - client - .run(ClientCommand::CardanoDbV2( - CardanoDbV2Command::ListPerEpoch { - epoch_specifier: EpochSpecifier::LatestMinusOffset(5), - }, - )) - .await?; + + if client.version().is_above_or_equal("0.12.34") { + client + .run(ClientCommand::CardanoDbV2( + CardanoDbV2Command::ListPerEpoch { + epoch_specifier: EpochSpecifier::LatestMinusOffset(5), + }, + )) + .await?; + } else { + warn!( + "Client version is below 0.12.34, skipping `cardano-db snapshot list --epoch latest-5` check" + ); + } + client .run(ClientCommand::CardanoDbV2(CardanoDbV2Command::Show { hash: hash.to_string(), diff --git a/mithril-test-lab/mithril-end-to-end/src/devnet/runner.rs b/mithril-test-lab/mithril-end-to-end/src/devnet/runner.rs index 84553acacf1..e8f4da6a5e2 100644 --- a/mithril-test-lab/mithril-end-to-end/src/devnet/runner.rs +++ b/mithril-test-lab/mithril-end-to-end/src/devnet/runner.rs @@ -1,6 +1,4 @@ use anyhow::{Context, anyhow}; -use mithril_common::StdResult; -use mithril_common::entities::{PartyId, TransactionHash}; use slog_scope::info; use std::fs::{self, File, read_to_string}; use std::io::Read; @@ -9,6 +7,11 @@ use std::process::Stdio; use thiserror::Error; use tokio::process::Command; +use mithril_common::StdResult; +use mithril_common::entities::{PartyId, TransactionHash}; + +use crate::utils::file_utils; + #[derive(Error, Debug, PartialEq, Eq)] #[error("Retryable devnet error: `{0}`")] pub struct RetryableDevnetError(pub String); @@ -68,7 +71,7 @@ pub struct DevnetBootstrapArgs { pub number_of_full_nodes: u8, pub cardano_slot_length: f64, pub cardano_epoch_length: f64, - pub cardano_node_version: String, + pub cardano_node_version: semver::Version, pub cardano_hard_fork_latest_era_at_epoch: u16, pub skip_cardano_bin_download: bool, } @@ -76,17 +79,8 @@ pub struct DevnetBootstrapArgs { impl Devnet { pub async fn bootstrap(bootstrap_args: &DevnetBootstrapArgs) -> StdResult { let bootstrap_script = "devnet-mkfiles.sh"; - let bootstrap_script_path = bootstrap_args - .devnet_scripts_dir - .canonicalize() - .with_context(|| { - format!( - "Can't find bootstrap script '{}' in {}", - bootstrap_script, - bootstrap_args.devnet_scripts_dir.display(), - ) - })? - .join(bootstrap_script); + let bootstrap_script_path = + file_utils::get_process_path(bootstrap_script, &bootstrap_args.devnet_scripts_dir)?; if bootstrap_args.artifacts_target_dir.exists() { fs::remove_dir_all(&bootstrap_args.artifacts_target_dir) @@ -118,7 +112,10 @@ impl Devnet { "EPOCH_LENGTH", bootstrap_args.cardano_epoch_length.to_string(), ); - bootstrap_command.env("CARDANO_NODE_VERSION", &bootstrap_args.cardano_node_version); + bootstrap_command.env( + "CARDANO_NODE_VERSION", + bootstrap_args.cardano_node_version.to_string(), + ); bootstrap_command.env( "CARDANO_HARD_FORK_LATEST_ERA_AT_EPOCH", bootstrap_args.cardano_hard_fork_latest_era_at_epoch.to_string(), diff --git a/mithril-test-lab/mithril-end-to-end/src/end_to_end_spec.rs b/mithril-test-lab/mithril-end-to-end/src/end_to_end_spec.rs index 04baacad1d0..cf173a3ffb6 100644 --- a/mithril-test-lab/mithril-end-to-end/src/end_to_end_spec.rs +++ b/mithril-test-lab/mithril-end-to-end/src/end_to_end_spec.rs @@ -136,7 +136,8 @@ impl Spec { "epoch after which the protocol parameters will change".to_string(), ) .await?; - if aggregator.is_first() { + + if aggregator.is_first() || aggregator.version().is_below("0.7.94") { assertions::update_protocol_parameters(aggregator).await?; } diff --git a/mithril-test-lab/mithril-end-to-end/src/lib.rs b/mithril-test-lab/mithril-end-to-end/src/lib.rs index 81800c156b1..5e1e35e0993 100644 --- a/mithril-test-lab/mithril-end-to-end/src/lib.rs +++ b/mithril-test-lab/mithril-end-to-end/src/lib.rs @@ -10,6 +10,7 @@ pub use devnet::*; pub use end_to_end_spec::Spec; pub use mithril::*; pub use run_only::RunOnly; +pub use utils::{CompatibilityChecker, CompatibilityCheckerError, NodeVersion}; use clap::ValueEnum; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] diff --git a/mithril-test-lab/mithril-end-to-end/src/main.rs b/mithril-test-lab/mithril-end-to-end/src/main.rs index 26021dd6dde..c80f46d0b50 100644 --- a/mithril-test-lab/mithril-end-to-end/src/main.rs +++ b/mithril-test-lab/mithril-end-to-end/src/main.rs @@ -3,6 +3,7 @@ use clap::{CommandFactory, Parser, Subcommand}; use slog::{Drain, Level, Logger}; use slog_scope::{error, info}; use std::{ + collections::BTreeMap, fmt, fs, path::{Path, PathBuf}, process::{ExitCode, Termination}, @@ -19,8 +20,9 @@ use tokio::{ use mithril_common::StdResult; use mithril_doc::GenerateDocCommands; use mithril_end_to_end::{ - Devnet, DevnetBootstrapArgs, DmqNodeFlavor, MithrilInfrastructure, MithrilInfrastructureConfig, - RetryableDevnetError, RunOnly, Spec, + Aggregator, Client, CompatibilityChecker, CompatibilityCheckerError, Devnet, + DevnetBootstrapArgs, DmqNodeFlavor, MithrilInfrastructure, MithrilInfrastructureConfig, + NodeVersion, RelaySigner, RetryableDevnetError, RunOnly, Signer, Spec, }; /// Tests args @@ -67,9 +69,9 @@ pub struct Args { #[clap(long, default_value_t = 30.0)] cardano_epoch_length: f64, - /// Cardano node version + /// Cardano node version, must be a valid semver version #[clap(long, default_value = "10.5.1")] - cardano_node_version: String, + cardano_node_version: semver::Version, /// Epoch at which hard fork to the latest Cardano era will be made (starts with the latest era by default) #[clap(long, default_value_t = 0)] @@ -203,19 +205,24 @@ async fn main_exec() -> StdResult<()> { return cmd.execute(&mut Args::command()).map_err(|message| anyhow!(message)); } - let work_dir = match &args.work_directory { - Some(path) => { - create_workdir_if_not_exist_clean_otherwise(path); - path.canonicalize()? - } - None => { - #[cfg(target_os = "macos")] - let work_dir = PathBuf::from("./mithril_end_to_end"); - #[cfg(not(target_os = "macos"))] - let work_dir = std::env::temp_dir().join("mithril_end_to_end"); - create_workdir_if_not_exist_clean_otherwise(&work_dir); - work_dir.canonicalize()? - } + let work_dir = { + let dir = match &args.work_directory { + Some(path) => path.to_owned(), + None => { + if cfg!(target_os = "macos") { + PathBuf::from("./mithril_end_to_end") + } else { + std::env::temp_dir().join("mithril_end_to_end") + } + } + }; + create_workdir_if_not_exist_clean_otherwise(&dir); + std::path::absolute(&dir).with_context(|| { + format!( + "Failed to resolve absolute work directory path: {}", + &dir.display() + ) + })? }; let artifacts_dir = { let path = work_dir.join("artifacts"); @@ -253,14 +260,16 @@ enum AppResult { UnretryableError(anyhow::Error), RetryableError(anyhow::Error), Cancelled(anyhow::Error), + IncompatibleNode(anyhow::Error), } impl AppResult { fn exit_code(&self) -> ExitCode { match self { AppResult::Success() => ExitCode::SUCCESS, - AppResult::UnretryableError(_) | AppResult::Cancelled(_) => ExitCode::FAILURE, - AppResult::RetryableError(_) => ExitCode::from(2), + AppResult::UnretryableError(..) | AppResult::Cancelled(..) => ExitCode::FAILURE, + AppResult::RetryableError(..) => ExitCode::from(2), + AppResult::IncompatibleNode(..) => ExitCode::from(3), } } } @@ -272,6 +281,7 @@ impl fmt::Display for AppResult { AppResult::UnretryableError(error) => write!(f, "Error(Unretryable): {error:?}"), AppResult::RetryableError(error) => write!(f, "Error(Retryable): {error:?}"), AppResult::Cancelled(error) => write!(f, "Cancelled: {error:?}"), + AppResult::IncompatibleNode(error) => write!(f, "{error:?}"), } } } @@ -298,6 +308,8 @@ impl From> for AppResult { AppResult::RetryableError(error) } else if error.is::() { AppResult::Cancelled(error) + } else if error.is::() { + AppResult::IncompatibleNode(error) } else { AppResult::UnretryableError(error) } @@ -352,6 +364,26 @@ impl App { let use_p2p_passive_relays = args.use_p2p_passive_relays; + CompatibilityChecker::default().check(BTreeMap::from([ + ( + Aggregator::BIN_NAME, + NodeVersion::fetch_semver(Aggregator::BIN_NAME, &args.bin_directory)?, + ), + ( + Signer::BIN_NAME, + NodeVersion::fetch_semver(Signer::BIN_NAME, &args.bin_directory)?, + ), + ( + Client::BIN_NAME, + NodeVersion::fetch_semver(Client::BIN_NAME, &args.bin_directory)?, + ), + ( + RelaySigner::BIN_NAME, + NodeVersion::fetch_semver(RelaySigner::BIN_NAME, &args.bin_directory)?, + ), + ("cardano-node", args.cardano_node_version.to_owned()), + ]))?; + let devnet = Devnet::bootstrap(&DevnetBootstrapArgs { devnet_scripts_dir: args.devnet_scripts_directory, artifacts_target_dir: work_dir.join("devnet"), diff --git a/mithril-test-lab/mithril-end-to-end/src/mithril/aggregator.rs b/mithril-test-lab/mithril-end-to-end/src/mithril/aggregator.rs index 6dc8ea06081..9556e184327 100644 --- a/mithril-test-lab/mithril-end-to-end/src/mithril/aggregator.rs +++ b/mithril-test-lab/mithril-end-to-end/src/mithril/aggregator.rs @@ -13,7 +13,7 @@ use tokio::sync::RwLock; use mithril_cardano_node_chain::chain_observer::{ChainObserver, PallasChainObserver}; use mithril_common::{CardanoNetwork, StdResult, entities}; -use crate::utils::MithrilCommand; +use crate::utils::{MithrilCommand, NodeVersion}; use crate::{ ANCILLARY_MANIFEST_SECRET_KEY, DEVNET_DMQ_MAGIC_ID, DEVNET_MAGIC_ID, DmqNodeFlavor, ERA_MARKERS_SECRET_KEY, ERA_MARKERS_VERIFICATION_KEY, FullNode, GENESIS_SECRET_KEY, @@ -31,7 +31,7 @@ pub struct AggregatorConfig<'a> { pub store_dir: &'a Path, pub artifacts_dir: &'a Path, pub bin_dir: &'a Path, - pub cardano_node_version: &'a str, + pub cardano_node_version: &'a semver::Version, pub mithril_run_interval: u32, pub mithril_era: &'a str, pub mithril_era_reader_adapter: &'a str, @@ -50,13 +50,18 @@ pub struct Aggregator { server_port: u64, db_directory: PathBuf, mithril_run_interval: u32, + version: NodeVersion, command: Arc>, process: RwLock>, chain_observer: Arc, } impl Aggregator { + pub const BIN_NAME: &'static str = "mithril-aggregator"; + pub fn new(aggregator_config: &AggregatorConfig) -> StdResult { + let version = NodeVersion::fetch(Self::BIN_NAME, aggregator_config.bin_dir)?; + let magic_id = DEVNET_MAGIC_ID.to_string(); let dmq_magic_id = DEVNET_DMQ_MAGIC_ID.to_string(); let server_port_parameter = aggregator_config.server_port.to_string(); @@ -77,6 +82,7 @@ impl Aggregator { let signed_entity_types = aggregator_config.signed_entity_types.join(","); let mithril_run_interval = format!("{}", aggregator_config.mithril_run_interval); let public_server_url = format!("http://localhost:{server_port_parameter}/aggregator"); + let cardano_node_version = aggregator_config.cardano_node_version.to_string(); let mut env = HashMap::from([ ("NETWORK", "devnet"), ("NETWORK_MAGIC", &magic_id), @@ -120,10 +126,7 @@ impl Aggregator { "AGGREGATE_SIGNATURE_TYPE", aggregator_config.aggregate_signature_type, ), - ( - "CARDANO_NODE_VERSION", - aggregator_config.cardano_node_version, - ), + ("CARDANO_NODE_VERSION", &cardano_node_version), ("CHAIN_OBSERVER_TYPE", aggregator_config.chain_observer_type), ("CARDANO_TRANSACTIONS_PROVER_CACHE_POOL_SIZE", "5"), ("CARDANO_TRANSACTIONS_DATABASE_CONNECTION_POOL_SIZE", "5"), @@ -172,7 +175,7 @@ impl Aggregator { ]; let command = MithrilCommand::new( - "mithril-aggregator", + Self::BIN_NAME, aggregator_config.work_dir, aggregator_config.bin_dir, env, @@ -189,6 +192,7 @@ impl Aggregator { server_port: aggregator_config.server_port, db_directory: aggregator_config.full_node.db_path.clone(), mithril_run_interval: aggregator_config.mithril_run_interval, + version, command: Arc::new(RwLock::new(command)), process: RwLock::new(None), chain_observer, @@ -206,6 +210,7 @@ impl Aggregator { server_port: other.server_port, db_directory: other.db_directory.clone(), mithril_run_interval: other.mithril_run_interval, + version: other.version.clone(), command: other.command.clone(), process: RwLock::new(None), chain_observer: other.chain_observer.clone(), @@ -240,6 +245,11 @@ impl Aggregator { self.chain_observer.clone() } + /// Get the version of the mithril-aggregator binary. + pub fn version(&self) -> &NodeVersion { + &self.version + } + pub async fn serve(&self) -> StdResult<()> { let mut command = self.command.write().await; command.set_log_name(&format!("mithril-aggregator-{}", self.name_suffix)); diff --git a/mithril-test-lab/mithril-end-to-end/src/mithril/client.rs b/mithril-test-lab/mithril-end-to-end/src/mithril/client.rs index 832eb654154..b0494e55601 100644 --- a/mithril-test-lab/mithril-end-to-end/src/mithril/client.rs +++ b/mithril-test-lab/mithril-end-to-end/src/mithril/client.rs @@ -1,16 +1,18 @@ use anyhow::{Context, anyhow}; +use slog_scope::warn; use std::collections::HashMap; use std::path::{Path, PathBuf}; use mithril_common::StdResult; use mithril_common::entities::{EpochSpecifier, TransactionHash}; -use crate::utils::MithrilCommand; +use crate::utils::{MithrilCommand, NodeVersion}; use crate::{ANCILLARY_MANIFEST_VERIFICATION_KEY, GENESIS_VERIFICATION_KEY}; #[derive(Debug)] pub struct Client { command: MithrilCommand, + version: NodeVersion, } #[derive(Debug)] @@ -29,7 +31,7 @@ impl CardanoDbCommand { } } - fn cli_arg(&self) -> Vec { + fn cli_arg(&self, client_version: &NodeVersion) -> Vec { match self { CardanoDbCommand::List() => { vec!["snapshot".to_string(), "list".to_string()] @@ -38,13 +40,25 @@ impl CardanoDbCommand { vec!["snapshot".to_string(), "show".to_string(), digest.clone()] } CardanoDbCommand::Download { digest } => { - vec![ - "download".to_string(), - "--include-ancillary".to_string(), - "--download-dir".to_string(), - "v1".to_string(), - digest.clone(), - ] + if client_version.is_below("0.11.14") { + warn!( + "client version is below 0.11.14, skip unsupported `--include-ancillary` flag for `cardano-db download`" + ); + vec![ + "download".to_string(), + "--download-dir".to_string(), + "v1".to_string(), + digest.clone(), + ] + } else { + vec![ + "download".to_string(), + "--include-ancillary".to_string(), + "--download-dir".to_string(), + "v1".to_string(), + digest.clone(), + ] + } } } } @@ -70,7 +84,7 @@ impl CardanoDbV2Command { } } - fn cli_arg(&self) -> Vec { + fn cli_arg(&self, _client_version: &NodeVersion) -> Vec { match self { CardanoDbV2Command::List => { vec!["snapshot".to_string(), "list".to_string()] @@ -113,7 +127,7 @@ impl MithrilStakeDistributionCommand { } } - fn cli_arg(&self) -> Vec { + fn cli_arg(&self, _client_version: &NodeVersion) -> Vec { match self { MithrilStakeDistributionCommand::List => { vec!["list".to_string()] @@ -147,7 +161,7 @@ impl CardanoTransactionCommand { } } - fn cli_arg(&self) -> Vec { + fn cli_arg(&self, _client_version: &NodeVersion) -> Vec { match self { CardanoTransactionCommand::ListSnapshot => { vec!["snapshot".to_string(), "list".to_string()] @@ -178,7 +192,7 @@ impl CardanoStakeDistributionCommand { } } - fn cli_arg(&self) -> Vec { + fn cli_arg(&self, _client_version: &NodeVersion) -> Vec { match self { CardanoStakeDistributionCommand::List => { vec!["list".to_string()] @@ -218,29 +232,51 @@ impl ClientCommand { } } - fn cli_arg(&self) -> Vec { + fn cli_arg(&self, client_version: &NodeVersion) -> Vec { let mut args = match self { - ClientCommand::CardanoDb(cmd) => [ - vec!["cardano-db".to_string()], - cmd.cli_arg(), - vec!["--backend".to_string(), "v1".to_string()], + ClientCommand::CardanoDb(cmd) => { + if client_version.is_below("0.12.11") { + warn!( + "client version is below 0.12.11, skip unsupported `--backend` flag for `cardano-db`" + ); + [vec!["cardano-db".to_string()], cmd.cli_arg(client_version)].concat() + } else { + [ + vec!["cardano-db".to_string()], + cmd.cli_arg(client_version), + vec!["--backend".to_string(), "v1".to_string()], + ] + .concat() + } + } + ClientCommand::MithrilStakeDistribution(cmd) => [ + vec!["mithril-stake-distribution".to_string()], + cmd.cli_arg(client_version), ] .concat(), - ClientCommand::MithrilStakeDistribution(cmd) => { - [vec!["mithril-stake-distribution".to_string()], cmd.cli_arg()].concat() - } ClientCommand::CardanoTransaction(cmd) => { - [vec!["cardano-transaction".to_string()], cmd.cli_arg()].concat() - } - ClientCommand::CardanoStakeDistribution(cmd) => { - [vec!["cardano-stake-distribution".to_string()], cmd.cli_arg()].concat() + [vec!["cardano-transaction".to_string()], cmd.cli_arg(client_version)].concat() } - ClientCommand::CardanoDbV2(cmd) => [ - vec!["cardano-db".to_string()], - cmd.cli_arg(), - vec!["--backend".to_string(), "v2".to_string()], + ClientCommand::CardanoStakeDistribution(cmd) => [ + vec!["cardano-stake-distribution".to_string()], + cmd.cli_arg(client_version), ] .concat(), + ClientCommand::CardanoDbV2(cmd) => { + if client_version.is_below("0.12.11") { + warn!( + "client version is below 0.12.11, fallback to `cardano-db-v2` command instead of unsupported `cardano-db --backend v2`" + ); + [vec!["cardano-db-v2".to_string()], cmd.cli_arg(client_version)].concat() + } else { + [ + vec!["cardano-db".to_string()], + cmd.cli_arg(client_version), + vec!["--backend".to_string(), "v2".to_string()], + ] + .concat() + } + } }; args.push("--json".to_string()); @@ -249,6 +285,8 @@ impl ClientCommand { } impl Client { + pub const BIN_NAME: &'static str = "mithril-client"; + pub fn new(aggregator_endpoint: String, work_dir: &Path, bin_dir: &Path) -> StdResult { let env = HashMap::from([ ("GENESIS_VERIFICATION_KEY", GENESIS_VERIFICATION_KEY), @@ -258,17 +296,26 @@ impl Client { ANCILLARY_MANIFEST_VERIFICATION_KEY, ), ]); - let args = vec!["-vvv", "--origin-tag", "E2E"]; - let command = MithrilCommand::new("mithril-client", work_dir, bin_dir, env, &args)?; + let version = NodeVersion::fetch(Self::BIN_NAME, bin_dir)?; - Ok(Self { command }) + // Always use the unstable flag as the e2e tests are not meant to check the coherence of the client commands + let mut args = vec!["-vvv", "--unstable"]; + if version.is_above_or_equal("0.11.13") { + args.extend_from_slice(&["--origin-tag", "E2E"]); + } else { + warn!("client version is below 0.11.13, skip unsupported `--origin-tag` flag"); + } + + let command = MithrilCommand::new(Self::BIN_NAME, work_dir, bin_dir, env, &args)?; + + Ok(Self { command, version }) } pub async fn run(&mut self, command: ClientCommand) -> StdResult { let output_path = self .command .set_output_filename(&format!("mithril-client-{}", command.name())); - let args = command.cli_arg(); + let args = command.cli_arg(self.version()); let exit_status = self .command @@ -290,4 +337,9 @@ impl Client { }) } } + + /// Get the version of the mithril-client binary. + pub fn version(&self) -> &NodeVersion { + &self.version + } } diff --git a/mithril-test-lab/mithril-end-to-end/src/mithril/infrastructure.rs b/mithril-test-lab/mithril-end-to-end/src/mithril/infrastructure.rs index 935630d153f..1181b607272 100644 --- a/mithril-test-lab/mithril-end-to-end/src/mithril/infrastructure.rs +++ b/mithril-test-lab/mithril-end-to-end/src/mithril/infrastructure.rs @@ -25,7 +25,7 @@ pub struct MithrilInfrastructureConfig { pub store_dir: PathBuf, pub artifacts_dir: PathBuf, pub bin_dir: PathBuf, - pub cardano_node_version: String, + pub cardano_node_version: semver::Version, pub mithril_run_interval: u32, pub mithril_era: String, pub mithril_era_reader_adapter: String, @@ -62,7 +62,7 @@ impl MithrilInfrastructureConfig { store_dir: PathBuf::from("/tmp/store"), artifacts_dir: PathBuf::from("/tmp/artifacts"), bin_dir: PathBuf::from("/tmp/bin"), - cardano_node_version: "1.0.0".to_string(), + cardano_node_version: semver::Version::new(1, 0, 0), mithril_run_interval: 10, mithril_era: "era1".to_string(), mithril_era_reader_adapter: "adapter1".to_string(), diff --git a/mithril-test-lab/mithril-end-to-end/src/mithril/relay_aggregator.rs b/mithril-test-lab/mithril-end-to-end/src/mithril/relay_aggregator.rs index d50cbd86fd0..30b7438273f 100644 --- a/mithril-test-lab/mithril-end-to-end/src/mithril/relay_aggregator.rs +++ b/mithril-test-lab/mithril-end-to-end/src/mithril/relay_aggregator.rs @@ -1,19 +1,24 @@ -use crate::utils::MithrilCommand; -use crate::{Aggregator, DEVNET_DMQ_MAGIC_ID}; -use mithril_common::StdResult; use std::collections::HashMap; use std::path::Path; use tokio::process::Child; +use mithril_common::StdResult; + +use crate::utils::{MithrilCommand, NodeVersion}; +use crate::{Aggregator, DEVNET_DMQ_MAGIC_ID}; + #[derive(Debug)] pub struct RelayAggregator { name_suffix: String, listen_port: u64, command: MithrilCommand, process: Option, + version: NodeVersion, } impl RelayAggregator { + pub const BIN_NAME: &'static str = "mithril-relay"; + pub fn new( index: usize, listen_port: u64, @@ -23,6 +28,7 @@ impl RelayAggregator { bin_dir: &Path, use_dmq: bool, ) -> StdResult { + let version = NodeVersion::fetch(Self::BIN_NAME, bin_dir)?; let name = Aggregator::name_suffix(index); let listen_port_str = format!("{listen_port}"); let dmq_magic_id = DEVNET_DMQ_MAGIC_ID.to_string(); @@ -44,7 +50,7 @@ impl RelayAggregator { } let args = vec!["-vvv", "aggregator"]; - let mut command = MithrilCommand::new("mithril-relay", work_dir, bin_dir, env, &args)?; + let mut command = MithrilCommand::new(Self::BIN_NAME, work_dir, bin_dir, env, &args)?; command.set_log_name(&format!("mithril-relay-aggregator-{name}",)); Ok(Self { @@ -52,6 +58,7 @@ impl RelayAggregator { listen_port, command, process: None, + version, }) } @@ -63,6 +70,11 @@ impl RelayAggregator { self.name_suffix.clone() } + /// Get the version of the mithril-relay binary. + pub fn version(&self) -> &NodeVersion { + &self.version + } + pub fn start(&mut self) -> StdResult<()> { self.process = Some(self.command.start(&[])?); Ok(()) diff --git a/mithril-test-lab/mithril-end-to-end/src/mithril/relay_passive.rs b/mithril-test-lab/mithril-end-to-end/src/mithril/relay_passive.rs index 6529c018421..341037337c3 100644 --- a/mithril-test-lab/mithril-end-to-end/src/mithril/relay_passive.rs +++ b/mithril-test-lab/mithril-end-to-end/src/mithril/relay_passive.rs @@ -1,18 +1,23 @@ -use crate::utils::MithrilCommand; -use mithril_common::StdResult; use std::collections::HashMap; use std::path::Path; use tokio::process::Child; +use mithril_common::StdResult; + +use crate::utils::{MithrilCommand, NodeVersion}; + #[derive(Debug)] pub struct RelayPassive { listen_port: u64, relay_id: String, command: MithrilCommand, process: Option, + version: NodeVersion, } impl RelayPassive { + pub const BIN_NAME: &'static str = "mithril-relay"; + pub fn new( listen_port: u64, dial_to: Option, @@ -20,6 +25,8 @@ impl RelayPassive { work_dir: &Path, bin_dir: &Path, ) -> StdResult { + let version = NodeVersion::fetch(Self::BIN_NAME, bin_dir)?; + let listen_port_str = format!("{listen_port}"); let mut env = HashMap::from([("LISTEN_PORT", listen_port_str.as_str())]); if let Some(dial_to) = &dial_to { @@ -35,6 +42,7 @@ impl RelayPassive { relay_id, command, process: None, + version, }) } @@ -42,6 +50,11 @@ impl RelayPassive { format!("/ip4/127.0.0.1/tcp/{}", self.listen_port) } + /// Get the version of the mithril-relay binary. + pub fn version(&self) -> &NodeVersion { + &self.version + } + pub fn start(&mut self) -> StdResult<()> { self.process = Some(self.command.start(&[])?); Ok(()) diff --git a/mithril-test-lab/mithril-end-to-end/src/mithril/relay_signer.rs b/mithril-test-lab/mithril-end-to-end/src/mithril/relay_signer.rs index a034500e483..b1c53706070 100644 --- a/mithril-test-lab/mithril-end-to-end/src/mithril/relay_signer.rs +++ b/mithril-test-lab/mithril-end-to-end/src/mithril/relay_signer.rs @@ -1,11 +1,13 @@ -use crate::DEVNET_DMQ_MAGIC_ID; -use crate::utils::MithrilCommand; -use mithril_common::StdResult; -use mithril_common::entities::PartyId; use std::collections::HashMap; use std::path::Path; use tokio::process::Child; +use mithril_common::StdResult; +use mithril_common::entities::PartyId; + +use crate::DEVNET_DMQ_MAGIC_ID; +use crate::utils::{MithrilCommand, NodeVersion}; + pub struct RelaySignerConfiguration<'a> { pub signer_number: usize, pub listen_port: u64, @@ -27,10 +29,15 @@ pub struct RelaySigner { party_id: PartyId, command: MithrilCommand, process: Option, + version: NodeVersion, } impl RelaySigner { + pub const BIN_NAME: &'static str = "mithril-relay"; + pub fn new(configuration: &RelaySignerConfiguration) -> StdResult { + let version = NodeVersion::fetch(Self::BIN_NAME, configuration.bin_dir)?; + let party_id = configuration.party_id.to_owned(); let listen_port_str = format!("{}", configuration.listen_port); let server_port_str = format!("{}", configuration.server_port); @@ -85,6 +92,7 @@ impl RelaySigner { party_id, command, process: None, + version, }) } @@ -96,6 +104,11 @@ impl RelaySigner { format!("http://localhost:{}", &self.server_port) } + /// Get the version of the mithril-relay binary. + pub fn version(&self) -> &NodeVersion { + &self.version + } + pub fn start(&mut self) -> StdResult<()> { self.process = Some(self.command.start(&[])?); Ok(()) diff --git a/mithril-test-lab/mithril-end-to-end/src/mithril/signer.rs b/mithril-test-lab/mithril-end-to-end/src/mithril/signer.rs index b271303b975..2737a2d56c2 100644 --- a/mithril-test-lab/mithril-end-to-end/src/mithril/signer.rs +++ b/mithril-test-lab/mithril-end-to-end/src/mithril/signer.rs @@ -10,7 +10,7 @@ use tokio::process::Child; use tokio::sync::RwLock; use crate::devnet::PoolNode; -use crate::utils::MithrilCommand; +use crate::utils::{MithrilCommand, NodeVersion}; use crate::{DEVNET_DMQ_MAGIC_ID, DEVNET_MAGIC_ID, DmqNodeFlavor, ERA_MARKERS_VERIFICATION_KEY}; #[derive(Debug)] @@ -38,10 +38,15 @@ pub struct Signer { party_id: PartyId, command: Arc>, process: RwLock>, + version: NodeVersion, } impl Signer { + pub const BIN_NAME: &'static str = "mithril-signer"; + pub fn new(signer_config: &SignerConfig) -> StdResult { + let version = NodeVersion::fetch(Self::BIN_NAME, signer_config.bin_dir)?; + let party_id = signer_config.pool_node.party_id()?; let magic_id = DEVNET_MAGIC_ID.to_string(); let dmq_magic_id = DEVNET_DMQ_MAGIC_ID.to_string(); @@ -151,9 +156,15 @@ impl Signer { party_id, command: Arc::new(RwLock::new(command)), process: RwLock::new(None), + version, }) } + /// Get the version of the mithril-signer binary. + pub fn version(&self) -> &NodeVersion { + &self.version + } + pub async fn start(&self) -> StdResult<()> { let mut command = self.command.write().await; let mut process = self.process.write().await; @@ -164,8 +175,7 @@ impl Signer { pub async fn stop(&self) -> StdResult<()> { let mut process_option = self.process.write().await; if let Some(process) = process_option.as_mut() { - let name = self.name.as_str(); - info!("Stopping {name}"); + info!("Stopping {}", &self.name); process.kill().await.with_context(|| "Could not kill signer")?; *process_option = None; } diff --git a/mithril-test-lab/mithril-end-to-end/src/stress_test/aggregator_helpers.rs b/mithril-test-lab/mithril-end-to-end/src/stress_test/aggregator_helpers.rs index 60e52c24895..aaffc4dda6e 100644 --- a/mithril-test-lab/mithril-end-to-end/src/stress_test/aggregator_helpers.rs +++ b/mithril-test-lab/mithril-end-to-end/src/stress_test/aggregator_helpers.rs @@ -28,7 +28,7 @@ pub async fn bootstrap_aggregator( store_dir: &args.work_dir.join("aggregator_store"), artifacts_dir: &args.work_dir.join("aggregator_artifacts"), bin_dir: &args.bin_dir, - cardano_node_version: "1.2.3", + cardano_node_version: &semver::Version::new(1, 2, 3), mithril_run_interval: 1000, mithril_era: &args.mithril_era, mithril_era_marker_address: "", diff --git a/mithril-test-lab/mithril-end-to-end/src/stress_test/entities.rs b/mithril-test-lab/mithril-end-to-end/src/stress_test/entities.rs index 15015715403..5e754b79c7d 100644 --- a/mithril-test-lab/mithril-end-to-end/src/stress_test/entities.rs +++ b/mithril-test-lab/mithril-end-to-end/src/stress_test/entities.rs @@ -92,7 +92,12 @@ impl AggregatorParameters { std::fs::create_dir_all(&tmp_dir) .with_context(|| format!("Could not create temp directory '{}'.", tmp_dir.display()))?; - let tmp_dir = tmp_dir.canonicalize().unwrap(); + let tmp_dir = std::path::absolute(&tmp_dir).with_context(|| { + format!( + "Could not get absolute path to the temp directory '{}'.", + tmp_dir.display() + ) + })?; let cardano_cli_path = { if !opts.cardano_cli_path.exists() { @@ -102,9 +107,9 @@ impl AggregatorParameters { ))? } - opts.cardano_cli_path.canonicalize().with_context(|| { + std::path::absolute(&opts.cardano_cli_path).with_context(|| { format!( - "Could not canonicalize path to the cardano-cli, path: '{}'", + "Could not get absolute path to the cardano-cli; path: '{}'", opts.cardano_cli_path.display() ) })? diff --git a/mithril-test-lab/mithril-end-to-end/src/utils/compatibility_checker.rs b/mithril-test-lab/mithril-end-to-end/src/utils/compatibility_checker.rs new file mode 100644 index 00000000000..07dadc787b9 --- /dev/null +++ b/mithril-test-lab/mithril-end-to-end/src/utils/compatibility_checker.rs @@ -0,0 +1,316 @@ +use std::collections::BTreeMap; +use std::fmt::{Display, Formatter}; + +/// Tool to check if the end-to-end runner can be launched with the given nodes versions +pub struct CompatibilityChecker { + rules: Vec, +} + +impl Default for CompatibilityChecker { + fn default() -> Self { + Self::new(vec![ + incompatibility_rule!( + "mithril-client", below_version: semver::Version::new(0, 11, 14), + is_incompatible_with: "mithril-aggregator", starting_version: semver::Version::new(0, 7, 31), + context: "below versions doesn't support properly cardano db verification without ancillary files" + ), + incompatibility_rule!( + "mithril-aggregator", below_version: semver::Version::new(0, 7, 91), + is_incompatible_with: "mithril-signer", starting_version: semver::Version::new(0, 2, 277), + context: "signers starting `0.2.277` needs the `/protocol-parameters/{epoch}` route which is not available in aggregator older versions" + ), + incompatibility_rule!( + "mithril-aggregator", below_version: semver::Version::new(0, 7, 55), + is_incompatible_with: "cardano-node", starting_version: semver::Version::new(10, 4, 1), + context: "older aggregator doesn't support UTxO-HD ledgers" + ), + incompatibility_rule!( + "mithril-signer", min_supported_version: semver::Version::new(0, 2, 221), + context: "older signers raise errors when an aggregator propagate a signed entity types that they don't know (i.e. CardanoDatabase signed entity type)" + ), + ]) + } +} + +/// Error returned by the compatibility checker when the nodes are incompatible with one or more rules +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompatibilityCheckerError { + nodes_with_version: BTreeMap<&'static str, semver::Version>, + detected_incompatibilities: Vec, +} + +impl Display for CompatibilityCheckerError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "Incompatible nodes detected:")?; + for rule in &self.detected_incompatibilities { + writeln!(f, "- {rule}")?; + } + writeln!(f)?; + writeln!(f, "Actual nodes versions:")?; + for (node_name, version) in &self.nodes_with_version { + writeln!(f, "- {node_name}: `{version}`")?; + } + + Ok(()) + } +} + +impl std::error::Error for CompatibilityCheckerError {} + +/// A rule defining an incompatibility of the end-to-end runner with a given node version, or of +/// two node versions between each others +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompatibilityRule { + node_name: &'static str, + is_incompatible_with: IncompatibilityReason, + additional_context: Option<&'static str>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IncompatibilityReason { + /// Two nodes are incompatible with each others + IncompatibleWithNode { + min_compatible_version: semver::Version, + other_node_name: &'static str, + other_node_min_compatible_version: semver::Version, + }, + /// The end-to-end runner is not compatible with the given node version + NotSupportedVersion { + min_supported_version: semver::Version, + }, +} + +impl CompatibilityRule { + /// Check if the given nodes versions are incompatible with this rule. + pub fn has_incompatibility( + &self, + nodes_with_version: &BTreeMap<&'static str, semver::Version>, + ) -> bool { + if !nodes_with_version.contains_key(self.node_name) { + return false; + } + let node_version = nodes_with_version.get(self.node_name).unwrap(); + + // Note: unwrap on the comparator is safe because the comparator is constructed from a valid semver object. + match &self.is_incompatible_with { + IncompatibilityReason::NotSupportedVersion { + min_supported_version, + } => { + let comparator = + semver::Comparator::parse(&format!("<{min_supported_version}")).unwrap(); + comparator.matches(node_version) + } + IncompatibilityReason::IncompatibleWithNode { + min_compatible_version, + other_node_name: incompatible_node_name, + other_node_min_compatible_version: incompatible_node_version, + } => { + if let Some(other_node_version) = nodes_with_version.get(incompatible_node_name) { + let node_comparator = + semver::Comparator::parse(&format!("<{min_compatible_version}")).unwrap(); + let other_node_comparator = + semver::Comparator::parse(&format!(">={incompatible_node_version}")) + .unwrap(); + + node_comparator.matches(node_version) + && other_node_comparator.matches(other_node_version) + } else { + false + } + } + } + } +} + +impl Display for CompatibilityRule { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let context: String = self + .additional_context + .map(|ctx| format!(", {ctx}")) + .unwrap_or_default(); + + match &self.is_incompatible_with { + IncompatibilityReason::NotSupportedVersion { + min_supported_version, + } => { + write!( + f, + "minimum supported {name} version is `{min_supported_version}`{context}", + name = self.node_name, + ) + } + IncompatibilityReason::IncompatibleWithNode { + min_compatible_version, + other_node_name, + other_node_min_compatible_version, + } => { + write!( + f, + "{other_node_name} starting version `{other_node_min_compatible_version}` is incompatible with {name} with a version below `{min_compatible_version}`{context}", + name = self.node_name, + ) + } + } + } +} + +impl CompatibilityChecker { + /// Create a new compatibility checker with the given rules. + pub fn new(rules: Vec) -> Self { + CompatibilityChecker { rules } + } + + /// Check if the given nodes versions are compatible with the parametrized rules. + pub fn check( + &self, + nodes_with_version: BTreeMap<&'static str, semver::Version>, + ) -> Result<(), CompatibilityCheckerError> { + slog_scope::debug!( + "Checking nodes compatibility"; + "nodes" => #?nodes_with_version.iter().map(|(k, v)| format!("{k}: {v}")).collect::>(), + "rules" => #?self.rules.iter().map(|r| format!("{r}")).collect::>() + ); + let detected_incompatibilities: Vec<_> = self + .rules + .iter() + .filter(|r| r.has_incompatibility(&nodes_with_version)) + .cloned() + .collect(); + + if detected_incompatibilities.is_empty() { + Ok(()) + } else { + Err(CompatibilityCheckerError { + detected_incompatibilities, + nodes_with_version, + }) + } + } +} + +/// Define a compatibility rule that will be checked against the nodes with their versions +/// to ensure that the end-to-end runner can be executed +macro_rules! incompatibility_rule { + ($node:literal, min_supported_version:$min_version:expr, context:$context:literal) => { + $crate::utils::CompatibilityRule { + node_name: $node, + is_incompatible_with: $crate::utils::IncompatibilityReason::NotSupportedVersion { + min_supported_version: $min_version, + }, + additional_context: (!$context.is_empty()).then_some($context), + } + }; + ($node:literal, min_supported_version:$min_version:expr) => { + $crate::utils::incompatibility_rule!($node, min_supported_version:$min_version, context:"") + }; + ($node:literal, below_version:$version:expr, is_incompatible_with:$incompatible_node:literal, starting_version:$min_version:expr, context:$context:literal) => { + $crate::utils::CompatibilityRule { + node_name: $node, + is_incompatible_with: $crate::utils::IncompatibilityReason::IncompatibleWithNode { + min_compatible_version: $version, + other_node_name: $incompatible_node, + other_node_min_compatible_version: $min_version, + }, + additional_context: (!$context.is_empty()).then_some($context), + } + }; + ($node:literal, below_version:$version:expr, is_incompatible_with:$incompatible_node:literal, starting_version:$min_version:expr) => { + $crate::utils::incompatibility_rule!($node, below_version:$version, is_incompatible_with:$incompatible_node, starting_version:$min_version, context:"") + }; +} +pub(crate) use incompatibility_rule; + +#[cfg(test)] +mod tests { + use semver::Version; + + use super::*; + + #[test] + fn check_min_compatible_version() { + let rule = incompatibility_rule!("TestNode", min_supported_version: Version::new(10, 2, 1)); + + assert!(!rule.has_incompatibility(&BTreeMap::from([]))); + assert!(!rule.has_incompatibility(&BTreeMap::from([("TestNode", Version::new(10, 2, 1))]))); + assert!(rule.has_incompatibility(&BTreeMap::from([("TestNode", Version::new(10, 2, 0))]))); + } + + #[test] + fn check_incompatible_nodes_versions() { + let rule = incompatibility_rule!("node alpha", below_version: Version::new(3, 2, 1), is_incompatible_with: "node beta", starting_version: Version::new(6, 4, 1)); + + assert!(!rule.has_incompatibility(&BTreeMap::from([]))); + assert!(!rule.has_incompatibility(&BTreeMap::from([ + ("node alpha", Version::new(3, 2, 1)), + ("node beta", Version::new(6, 4, 1)), + ]))); + // Even if node alpha is far below the minimum version of the rule, there is no incompatibility if node beta is missing + assert!( + !rule.has_incompatibility(&BTreeMap::from([("node alpha", Version::new(1, 0, 0))])) + ); + // Incompatible if Node beta right at the threshold and Node alpha right below the minimum version threshold + assert!(rule.has_incompatibility(&BTreeMap::from([ + ("node alpha", Version::new(3, 2, 0)), + ("node beta", Version::new(6, 4, 1)), + ]))); + } + + #[test] + fn checker_can_detect_multiple_incompatible_rule() { + let checker = CompatibilityChecker::new(vec![ + incompatibility_rule!("node alpha", min_supported_version: Version::new(2, 2, 1)), + incompatibility_rule!("node alpha", min_supported_version: Version::new(3, 2, 1)), + incompatibility_rule!("node alpha", below_version: Version::new(3, 2, 1), is_incompatible_with: "node beta", starting_version: Version::new(1, 0, 1)), + ]); + + let error = checker + .check(BTreeMap::from([ + ("node alpha", Version::new(3, 1, 5)), + ("node beta", Version::new(1, 0, 9)), + ])) + .unwrap_err(); + + assert_eq!( + error.detected_incompatibilities, + vec![ + incompatibility_rule!("node alpha", min_supported_version: Version::new(3, 2, 1)), + incompatibility_rule!("node alpha", below_version: Version::new(3, 2, 1), is_incompatible_with: "node beta", starting_version: Version::new(1, 0, 1)), + ] + ); + } + + #[test] + fn display_compat_error() { + let error = CompatibilityCheckerError { + nodes_with_version: BTreeMap::from([ + ("node alpha", Version::new(1, 1, 1)), + ("node beta", Version::new(5, 5, 5)), + ("node gamma", Version::new(6, 6, 6)), + ("node zeta", Version::new(8, 8, 8)), + ]), + detected_incompatibilities: vec![ + incompatibility_rule!("node alpha", min_supported_version: Version::new(2, 0, 0), context: "first error context"), + incompatibility_rule!( + "node alpha", below_version: Version::new(2, 0, 0), + is_incompatible_with: "node beta", starting_version: Version::new(5, 0, 0), + context: "second error context" + ), + incompatibility_rule!("node gamma", min_supported_version: Version::new(3, 0, 0)), + ], + }; + + assert_eq!( + "Incompatible nodes detected:\ + \n- minimum supported node alpha version is `2.0.0`, first error context\ + \n- node beta starting version `5.0.0` is incompatible with node alpha with a version below `2.0.0`, second error context\ + \n- minimum supported node gamma version is `3.0.0`\ + \n\ + \nActual nodes versions:\ + \n- node alpha: `1.1.1`\ + \n- node beta: `5.5.5`\ + \n- node gamma: `6.6.6`\ + \n- node zeta: `8.8.8`\n", + format!("{error}") + ); + } +} diff --git a/mithril-test-lab/mithril-end-to-end/src/utils/file_utils.rs b/mithril-test-lab/mithril-end-to-end/src/utils/file_utils.rs index c1efc7dd20a..db9d668216d 100644 --- a/mithril-test-lab/mithril-end-to-end/src/utils/file_utils.rs +++ b/mithril-test-lab/mithril-end-to-end/src/utils/file_utils.rs @@ -1,8 +1,9 @@ use anyhow::Context; -use mithril_common::StdResult; use std::{path::Path, process::Stdio}; use tokio::process::Command; +use mithril_common::StdResult; + /// Tail a file into into the given stream /// /// For the sake of simplicity it use internally the tail command so be sure to have it on @@ -63,6 +64,28 @@ pub async fn last_errors(file_path: &Path, number_of_error: u64) -> StdResult StdResult { + use anyhow::Context; + + let process_path = std::path::absolute(bin_dir) + .with_context(|| { + format!( + "failed to get absolute path for '{bin_dir}/{bin_name}'", + bin_dir = bin_dir.display(), + ) + })? + .join(bin_name); + + if !process_path.exists() { + anyhow::bail!( + "cannot find '{bin_name}' executable in expected location '{bin_dir}'", + bin_dir = bin_dir.display(), + ); + } + Ok(process_path) +} + #[cfg(test)] mod tests { use crate::utils::file_utils; diff --git a/mithril-test-lab/mithril-end-to-end/src/utils/mithril_command.rs b/mithril-test-lab/mithril-end-to-end/src/utils/mithril_command.rs index bbae3b8df57..e72ffdc91b9 100644 --- a/mithril-test-lab/mithril-end-to-end/src/utils/mithril_command.rs +++ b/mithril-test-lab/mithril-end-to-end/src/utils/mithril_command.rs @@ -1,11 +1,13 @@ -use crate::utils::{LogGroup, file_utils}; use anyhow::{Context, anyhow}; -use mithril_common::StdResult; use slog_scope::info; use std::collections::HashMap; use std::path::{Path, PathBuf}; use tokio::process::{Child, Command}; +use mithril_common::StdResult; + +use crate::utils::{LogGroup, file_utils}; + #[derive(Debug, Clone)] pub struct MithrilCommand { name: String, @@ -25,17 +27,7 @@ impl MithrilCommand { env_vars: HashMap<&str, &str>, default_args: &[&str], ) -> StdResult { - let current_dir = std::env::current_dir().unwrap(); - let process_path = bin_dir - .canonicalize() - .unwrap_or_else(|_| { - panic!( - "expected '{}/{name}' to be an existing executable. Current dir: {}", - bin_dir.display(), - current_dir.display(), - ) - }) - .join(name); + let process_path = file_utils::get_process_path(name, bin_dir)?; let log_path = work_dir.join(format!("{name}.log")); // ugly but it's far easier for callers to manipulate string literals @@ -45,14 +37,6 @@ impl MithrilCommand { env_vars.insert("RUST_BACKTRACE".to_string(), "full".to_string()); - if !process_path.exists() { - return Err(anyhow!( - "cannot find {} executable in expected location \"{}\"", - name, - bin_dir.display() - )); - } - Ok(MithrilCommand { name: name.to_string(), process_path, diff --git a/mithril-test-lab/mithril-end-to-end/src/utils/mod.rs b/mithril-test-lab/mithril-end-to-end/src/utils/mod.rs index acdd6db8084..9520cd0aa0f 100644 --- a/mithril-test-lab/mithril-end-to-end/src/utils/mod.rs +++ b/mithril-test-lab/mithril-end-to-end/src/utils/mod.rs @@ -1,12 +1,16 @@ mod mithril_command; #[macro_use] mod spec_utils; -mod file_utils; +mod compatibility_checker; +pub(crate) mod file_utils; mod formatting; +mod version_req; +pub use compatibility_checker::*; pub use formatting::*; pub use mithril_command::MithrilCommand; pub use spec_utils::AttemptResult; +pub use version_req::NodeVersion; pub fn is_running_in_github_actions() -> bool { std::env::var("GITHUB_ACTIONS").is_ok() diff --git a/mithril-test-lab/mithril-end-to-end/src/utils/version_req.rs b/mithril-test-lab/mithril-end-to-end/src/utils/version_req.rs new file mode 100644 index 00000000000..3c3271fa948 --- /dev/null +++ b/mithril-test-lab/mithril-end-to-end/src/utils/version_req.rs @@ -0,0 +1,173 @@ +use anyhow::Context; +use std::path::Path; + +use mithril_common::StdResult; + +use crate::utils::file_utils; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct NodeVersion { + semver_version: semver::Version, +} + +impl NodeVersion { + /// `NodeVersion` factory + pub fn new(semver_version: semver::Version) -> Self { + Self { semver_version } + } + + /// Fetch the `NodeVersion` of a binary + /// + /// The binary must have a `--version` command which returns the version in the following forms: + /// `string x.y.z` where `x.y.z` is a semver version. + pub fn fetch(bin_name: &str, bin_dir: &Path) -> StdResult { + Self::fetch_semver(bin_name, bin_dir).map(NodeVersion::new) + } + + /// Fetch the semver version of a binary + /// + /// The binary must have a `--version` command which returns the version in the following forms: + /// `string x.y.z` where `x.y.z` is a semver version. + pub fn fetch_semver(bin_name: &str, bin_dir: &Path) -> StdResult { + let process_path = file_utils::get_process_path(bin_name, bin_dir)?; + // Note: usage of blocking std::process::Command instead of tokio::process::Command to avoid making this method async + // example output: mithril-client 0.12.33+3063c3e + let output = std::process::Command::new(&process_path) + .args(["--version"]) + .output() + .with_context(|| { + format!( + "failed to run `{bin_name} --version`; bin_dir: `{}`; expanded process_path: `{}`", + bin_dir.display(), + process_path.display() + ) + })?; + + if output.status.success() { + let raw_output = String::from_utf8(output.stdout) + .with_context(|| format!("failed to parse `{bin_name}` raw version to uft8"))?; + let version_string = raw_output.split_whitespace().nth(1).with_context(|| { + format!("could not find `{bin_name}` semver version; output: `{raw_output}`",) + })?; + + semver::Version::parse(version_string).with_context(|| { + format!("failed to parse `{bin_name}` semver version; input: `{version_string}`") + }) + } else { + let stdout = String::from_utf8(output.stdout).ok(); + let stderr = String::from_utf8(output.stderr).ok(); + anyhow::bail!( + "`failed to fetch `{bin_name}` version; stdout: `{stdout:?}`; stderr: `{stderr:?}`" + ); + } + } + + /// Checks if the node version is strictly below the given version. + /// + /// Panics if `version` is not a valid semver version + pub fn is_below(&self, version: &'static str) -> bool { + let version_req = semver::VersionReq::parse(&format!("<{version}")).unwrap(); + version_req.matches(&self.semver_version) + } + + /// Checks if the node version is equal or above the given version. + /// + /// Panics if `version` is not a valid semver version + pub fn is_above_or_equal(&self, version: &'static str) -> bool { + let version_req = semver::VersionReq::parse(&format!(">={version}")).unwrap(); + version_req.matches(&self.semver_version) + } + + /// Checks if the node version is between the given min and max version + /// + /// Check against the min version is superior or equal, check against the max version is + /// strictly below. + /// + /// Panics if either `min_version` or `max_version` are not a valid semver version + pub fn is_between(&self, min_version: &'static str, max_version: &'static str) -> bool { + let version_req = + semver::VersionReq::parse(&format!(">={min_version}, <{max_version}")).unwrap(); + version_req.matches(&self.semver_version) + } +} + +impl From<&NodeVersion> for semver::Version { + fn from(value: &NodeVersion) -> Self { + value.semver_version.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_version_below() { + let version = NodeVersion::new(semver::Version::new(1, 2, 3)); + + assert!(version.is_below("1.2.4")); + assert!(!version.is_below("1.2.3")); + assert!(!version.is_below("1.2.2")); + } + + #[test] + fn test_version_equal_or_above() { + let version = NodeVersion::new(semver::Version::new(5, 7, 1)); + + assert!(version.is_above_or_equal("4.6.0")); + assert!(version.is_above_or_equal("5.7.1")); + assert!(!version.is_above_or_equal("5.7.2")); + } + + #[test] + fn test_version_between() { + let version = NodeVersion::new(semver::Version::new(2, 4, 3)); + + assert!(version.is_between("2.3.0", "2.5.0")); + assert!(version.is_between("2.4.3", "2.5.0")); + assert!(version.is_between("2.4.3", "2.4.4")); + + assert!(!version.is_between("2.3.0", "2.4.3")); + assert!(!version.is_between("2.4.4", "2.5.0")); + } + + // Unix only has those tests leverage shell scripts and unix permissions + #[cfg(all(test, unix))] + mod unix_only { + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + + use mithril_common::temp_dir_create; + + use super::*; + + fn write_shell_script(file_name: &str, folder: &Path, content: &str) { + let script_path = folder.join(file_name); + let mut file = std::fs::OpenOptions::new() + .create_new(true) + .write(true) + .mode(0o755) + .open(&script_path) + .unwrap(); + file.write_all(format!("#!/bin/bash\n\n{content}\n").as_ref()) + .unwrap(); + } + + #[test] + fn fetch_version() { + let temp_dir = temp_dir_create!(); + write_shell_script( + "test-program", + &temp_dir, + r#"echo "test-program 1.24.109+2f7e87""#, + ); + + let version = NodeVersion::fetch("test-program", &temp_dir).unwrap(); + + assert_eq!( + NodeVersion::new(semver::Version::parse("1.24.109+2f7e87").unwrap()), + version + ); + } + } +}