//! `zebrad` launch-specific shared code for the `zebrad` acceptance tests. //! //! # Warning //! //! Test functions in this file will not be run. //! This file is only for test library code. use std::{ env, fmt::Debug, net::SocketAddr, path::{Path, PathBuf}, time::Duration, }; use color_eyre::eyre::Result; use tempfile::TempDir; use zebra_chain::parameters::Network::{self, *}; use zebra_network::CacheDir; use zebra_test::{ args, command::{Arguments, TestDirExt}, prelude::*, }; use zebrad::config::ZebradConfig; use crate::common::{ config::testdir, lightwalletd::zebra_skip_lightwalletd_tests, sync::FINISH_PARTIAL_SYNC_TIMEOUT, test_type::TestType, }; /// After we launch `zebrad`, wait this long for the command to start up, /// take the actions expected by the tests, and log the expected logs. /// /// Previously, this value was 3 seconds, which caused rare /// metrics or tracing test failures in Windows CI. pub const LAUNCH_DELAY: Duration = Duration::from_secs(20); /// After we launch `zebrad`, wait this long in extended tests. /// See [`LAUNCH_DELAY`] for details. pub const EXTENDED_LAUNCH_DELAY: Duration = Duration::from_secs(45); /// After we launch `lightwalletd`, wait this long for the command to start up, /// take the actions expected by the quick tests, and log the expected logs. /// /// `lightwalletd`'s actions also depend on the actions of the `zebrad` instance /// it is using for its RPCs. pub const LIGHTWALLETD_DELAY: Duration = Duration::from_secs(60); /// The amount of time we wait between launching two conflicting nodes. /// /// We use a longer time to make sure the first node has launched before the second starts, /// even if CI is under load. pub const BETWEEN_NODES_DELAY: Duration = Duration::from_secs(20); /// The amount of time we wait for lightwalletd to update to the tip. /// /// `lightwalletd` takes about 60-120 minutes to fully sync, /// and `zebrad` can take hours to update to the tip under load. pub const LIGHTWALLETD_UPDATE_TIP_DELAY: Duration = FINISH_PARTIAL_SYNC_TIMEOUT; /// The amount of time we wait for lightwalletd to do a full sync to the tip. /// /// See [`LIGHTWALLETD_UPDATE_TIP_DELAY`] for details. pub const LIGHTWALLETD_FULL_SYNC_TIP_DELAY: Duration = FINISH_PARTIAL_SYNC_TIMEOUT; /// Extension trait for methods on `tempfile::TempDir` for using it as a test /// directory for `zebrad`. pub trait ZebradTestDirExt where Self: AsRef + Sized, { // Zebra methods /// Spawn `zebrad` with `args` as a child process in this test directory, /// potentially taking ownership of the tempdir for the duration of the /// child process. /// /// If there is a config in the test directory, pass it to `zebrad`. fn spawn_child(self, args: Arguments) -> Result>; /// Create a config file and use it for all subsequently spawned `zebrad` processes. /// Returns an error if the config already exists. /// /// If needed: /// - recursively create directories for the config and state /// - set `config.cache_dir` based on `self` fn with_config(self, config: &mut ZebradConfig) -> Result; /// Create a config file with the exact contents of `config`, and use it for /// all subsequently spawned `zebrad` processes. Returns an error if the config /// already exists. /// /// If needed: /// - recursively create directories for the config and state fn with_exact_config(self, config: &ZebradConfig) -> Result; /// Overwrite any existing `zebrad` config file, and use the newly written config for /// all subsequently spawned processes. /// /// If needed: /// - recursively create directories for the config and state /// - set `config.cache_dir` based on `self` fn replace_config(self, config: &mut ZebradConfig) -> Result; /// `cache_dir` config update helper for `zebrad`. /// /// If needed: /// - set the cache_dir in the config. fn cache_config_update_helper(self, config: &mut ZebradConfig) -> Result; /// Config writing helper for `zebrad`. /// /// If needed: /// - recursively create directories for the config and state, /// /// Then write out the config. fn write_config_helper(self, config: &ZebradConfig) -> Result; } impl ZebradTestDirExt for T where Self: TestDirExt + AsRef + Sized, { #[allow(clippy::unwrap_in_result)] fn spawn_child(self, extra_args: Arguments) -> Result> { let dir = self.as_ref(); let default_config_path = dir.join("zebrad.toml"); let mut args = Arguments::new(); if default_config_path.exists() { args.set_parameter( "-c", default_config_path .as_path() .to_str() .expect("Path is valid Unicode"), ); } args.merge_with(extra_args); self.spawn_child_with_command(env!("CARGO_BIN_EXE_zebrad"), args) } fn with_config(self, config: &mut ZebradConfig) -> Result { self.cache_config_update_helper(config)? .write_config_helper(config) } fn with_exact_config(self, config: &ZebradConfig) -> Result { self.write_config_helper(config) } fn replace_config(self, config: &mut ZebradConfig) -> Result { use std::fs; use std::io::ErrorKind; // Remove any existing config before writing a new one let dir = self.as_ref(); let config_file = dir.join("zebrad.toml"); match fs::remove_file(config_file) { Ok(()) => {} // If the config file doesn't exist, that's ok Err(e) if e.kind() == ErrorKind::NotFound => {} Err(e) => Err(e)?, } self.cache_config_update_helper(config)? .write_config_helper(config) } fn cache_config_update_helper(self, config: &mut ZebradConfig) -> Result { let dir = self.as_ref(); let cache_dir = PathBuf::from(dir); // If the peer cache has already been disabled, don't re-enable it if config.network.cache_dir.is_enabled() { config.network.cache_dir = CacheDir::custom_path(&cache_dir); } // Only replace the state cache directory if it's going to be used if !config.state.ephemeral { config.state.cache_dir = cache_dir; } Ok(self) } fn write_config_helper(self, config: &ZebradConfig) -> Result { use std::fs; use std::io::Write; let dir = self.as_ref(); if !config.state.ephemeral { let cache_dir = dir.join("state"); fs::create_dir_all(cache_dir)?; } else { fs::create_dir_all(dir)?; } let config_file = dir.join("zebrad.toml"); fs::File::create(config_file)?.write_all(toml::to_string(&config)?.as_bytes())?; Ok(self) } } /// Spawns a zebrad instance on `network` to test lightwalletd with `test_type`. /// /// If `use_internet_connection` is `false` then spawn, but without any peers. /// This prevents it from downloading blocks. Instead, use the `ZEBRA_CACHED_STATE_DIR` /// environmental variable to provide an initial state to the zebrad instance. /// /// Returns: /// - `Ok(Some(zebrad, zebra_rpc_address))` on success, /// - `Ok(None)` if the test doesn't have the required network or cached state, and /// - `Err(_)` if spawning zebrad fails. /// /// `zebra_rpc_address` is `None` if the test type doesn't need an RPC port. #[tracing::instrument] pub fn spawn_zebrad_for_rpc + Debug>( network: Network, test_name: S, test_type: TestType, use_internet_connection: bool, ) -> Result, Option)>> { let test_name = test_name.as_ref(); // Skip the test unless the user specifically asked for it if !can_spawn_zebrad_for_test_type(test_name, test_type, use_internet_connection) { return Ok(None); } // Get the zebrad config let config = test_type .zebrad_config(test_name, use_internet_connection, None, &network) .expect("already checked config")?; let (zebrad_failure_messages, zebrad_ignore_messages) = test_type.zebrad_failure_messages(); // Writes a configuration that has RPC listen_addr set (if needed). // If the state path env var is set, uses it in the config. let zebrad = testdir()? .with_exact_config(&config)? .spawn_child(args!["start"])? .bypass_test_capture(true) .with_timeout(test_type.zebrad_timeout()) .with_failure_regex_iter(zebrad_failure_messages, zebrad_ignore_messages); Ok(Some((zebrad, config.rpc.listen_addr))) } /// Spawns a zebrad instance on `network` without RPCs or `lightwalletd`. /// /// If `use_cached_state` is `true`, then update the cached state to the tip. /// If `ephemeral` is `true`, then use an ephemeral state path. /// If `reuse_state_path` is `Some(path)`, then use the state at that path, and take ownership of /// the temporary directory, so it isn't deleted until the test ends. /// Otherwise, just create an empty state in this test's new temporary directory. /// /// If `use_internet_connection` is `false` then spawn, but without any peers. /// This prevents it from downloading blocks. Instead, use the `ZEBRA_CACHED_STATE_DIR` /// environmental variable to provide an initial state to the zebrad instance. /// /// Returns: /// - `Ok(Some(zebrad))` on success, /// - `Ok(None)` if the test doesn't have the required network or cached state, and /// - `Err(_)` if spawning zebrad fails. #[tracing::instrument] pub fn spawn_zebrad_without_rpc( network: Network, test_name: Str, use_cached_state: bool, ephemeral: bool, reuse_state_path: Dir, use_internet_connection: bool, ) -> Result>> where Str: AsRef + Debug, Dir: Into> + Debug, { use TestType::*; let test_name = test_name.as_ref(); let reuse_state_path = reuse_state_path.into(); let testdir = reuse_state_path .unwrap_or_else(|| testdir().expect("failed to create test temporary directory")); let (test_type, replace_cache_dir) = if use_cached_state { (UpdateZebraCachedStateNoRpc, None) } else if ephemeral { ( LaunchWithEmptyState { launches_lightwalletd: false, }, None, ) } else { (UseAnyState, Some(testdir.path())) }; // Skip the test unless the user specifically asked for it if !can_spawn_zebrad_for_test_type(test_name, test_type, use_internet_connection) { return Ok(None); } // Get the zebrad config let config = test_type .zebrad_config( test_name, use_internet_connection, replace_cache_dir, &network, ) .expect("already checked config")?; let (zebrad_failure_messages, zebrad_ignore_messages) = test_type.zebrad_failure_messages(); // Writes a configuration that does not have RPC listen_addr set. // If the state path env var is set, uses it in the config. let zebrad = testdir .with_exact_config(&config)? .spawn_child(args!["start"])? .bypass_test_capture(true) .with_timeout(test_type.zebrad_timeout()) .with_failure_regex_iter(zebrad_failure_messages, zebrad_ignore_messages); Ok(Some(zebrad)) } /// Returns `true` if a zebrad test for `test_type` has everything it needs to run. #[tracing::instrument] pub fn can_spawn_zebrad_for_test_type + Debug>( test_name: S, test_type: TestType, use_internet_connection: bool, ) -> bool { if use_internet_connection && zebra_test::net::zebra_skip_network_tests() { return false; } // Skip the test unless the user specifically asked for it // // TODO: pass test_type to zebra_skip_lightwalletd_tests() and check for lightwalletd launch in there if test_type.launches_lightwalletd() && zebra_skip_lightwalletd_tests() { return false; } // Check if we have any necessary cached states for the zebrad config. // The cache_dir and network values don't matter here. test_type .zebrad_config(test_name, true, None, &Mainnet) .is_some() } /// Panics if `$pred` is false, with an error report containing: /// * context from `$source`, and /// * an optional wrapper error, using `$fmt_arg`+ as a format string and /// arguments. #[macro_export] macro_rules! assert_with_context { ($pred:expr, $source:expr) => { if !$pred { use color_eyre::Section as _; use color_eyre::SectionExt as _; use zebra_test::command::ContextFrom as _; let report = color_eyre::eyre::eyre!("failed assertion") .section(stringify!($pred).header("Predicate:")) .context_from($source); panic!("Error: {:?}", report); } }; ($pred:expr, $source:expr, $($fmt_arg:tt)+) => { if !$pred { use color_eyre::Section as _; use color_eyre::SectionExt as _; use zebra_test::command::ContextFrom as _; let report = color_eyre::eyre::eyre!("failed assertion") .section(stringify!($pred).header("Predicate:")) .context_from($source) .wrap_err(format!($($fmt_arg)+)); panic!("Error: {:?}", report); } }; }