From 9a8ab9468df14a56aa80392bb3be654ec00cbd0e Mon Sep 17 00:00:00 2001 From: teor Date: Sat, 19 Mar 2022 02:02:22 +1000 Subject: [PATCH] T0. refactor(test): split zebrad acceptance tests into sub-modules (#3901) * Improve launch delay docs * Initial split of zebrad acceptance tests into modules * Split shared lightwalletd test code into a module --- zebra-test/src/net.rs | 13 + zebrad/tests/acceptance.rs | 783 ++-------------------------- zebrad/tests/common/check.rs | 35 ++ zebrad/tests/common/config.rs | 82 +++ zebrad/tests/common/launch.rs | 199 +++++++ zebrad/tests/common/lightwalletd.rs | 157 ++++++ zebrad/tests/common/mod.rs | 16 + zebrad/tests/common/sync.rs | 345 ++++++++++++ 8 files changed, 886 insertions(+), 744 deletions(-) create mode 100644 zebrad/tests/common/check.rs create mode 100644 zebrad/tests/common/config.rs create mode 100644 zebrad/tests/common/launch.rs create mode 100644 zebrad/tests/common/lightwalletd.rs create mode 100644 zebrad/tests/common/mod.rs create mode 100644 zebrad/tests/common/sync.rs diff --git a/zebra-test/src/net.rs b/zebra-test/src/net.rs index 46415af4a..a12398c78 100644 --- a/zebra-test/src/net.rs +++ b/zebra-test/src/net.rs @@ -79,3 +79,16 @@ pub fn random_known_port() -> u16 { rand::thread_rng().gen_range(53500..60999) } + +/// Returns the "magic" port number that tells the operating system to +/// choose a random unallocated port. +/// +/// The OS chooses a different port each time it opens a connection or +/// listener with this magic port number. +/// +/// ## Usage +/// +/// See the usage note for `random_known_port`. +pub fn random_unallocated_port() -> u16 { + 0 +} diff --git a/zebrad/tests/acceptance.rs b/zebrad/tests/acceptance.rs index d5827c759..d7937ac54 100644 --- a/zebrad/tests/acceptance.rs +++ b/zebrad/tests/acceptance.rs @@ -21,16 +21,12 @@ //! or you have poor network connectivity, //! skip all the network tests by setting the `ZEBRA_SKIP_NETWORK_TESTS` environmental variable. +use std::{collections::HashSet, convert::TryInto, env, path::PathBuf, time::Duration}; + use color_eyre::{ eyre::{Result, WrapErr}, Help, }; -use tempfile::TempDir; - -use std::{ - collections::HashSet, convert::TryInto, env, net::SocketAddr, path::Path, path::PathBuf, - time::Duration, -}; use zebra_chain::{ block::Height, @@ -38,312 +34,25 @@ use zebra_chain::{ }; use zebra_network::constants::PORT_IN_USE_ERROR; use zebra_state::constants::LOCK_FILE_ERROR; -use zebra_test::{ - command::{ContextFrom, TestDirExt}, - net::random_known_port, - prelude::*, + +use zebra_test::{command::ContextFrom, net::random_known_port, prelude::*}; + +mod common; + +use common::{ + check::{is_zebrad_version, EphemeralCheck, EphemeralConfig}, + config::{default_test_config, persistent_test_config, testdir}, + launch::{ZebradTestDirExt, BETWEEN_NODES_DELAY, LAUNCH_DELAY, LIGHTWALLETD_DELAY}, + lightwalletd::{ + random_known_rpc_port_config, zebra_skip_lightwalletd_tests, LightWalletdTestDirExt, + }, + sync::{ + create_cached_database_height, sync_until, MempoolBehavior, LARGE_CHECKPOINT_TEST_HEIGHT, + LARGE_CHECKPOINT_TIMEOUT, MEDIUM_CHECKPOINT_TEST_HEIGHT, STOP_AT_HEIGHT_REGEX, + STOP_ON_LOAD_TIMEOUT, SYNC_FINISHED_REGEX, TINY_CHECKPOINT_TEST_HEIGHT, + TINY_CHECKPOINT_TIMEOUT, + }, }; -use zebrad::{ - components::{mempool, sync}, - config::{SyncSection, TracingSection, ZebradConfig}, -}; - -/// The amount of time we wait after launching `zebrad`. -/// -/// Previously, this value was 3 seconds, which caused rare -/// metrics or tracing test failures in Windows CI. -const LAUNCH_DELAY: Duration = Duration::from_secs(15); - -/// The amount of time we wait after launching `lightwalletd`, -/// and between expected `lightwalletd` log messages. -const LIGHTWALLETD_DELAY: Duration = Duration::from_secs(60); - -/// The amount of time we wait between launching two -/// conflicting nodes. -const BETWEEN_NODES_DELAY: Duration = Duration::from_secs(2); - -/// Returns a config with: -/// - a Zcash listener on an unused port on IPv4 localhost, and -/// - an ephemeral state, -/// - the minimum syncer lookahead limit, and -/// - shorter task intervals, to improve test coverage. -fn default_test_config() -> Result { - const TEST_DURATION: Duration = Duration::from_secs(30); - - let network = zebra_network::Config { - // The OS automatically chooses an unused port. - listen_addr: "127.0.0.1:0".parse()?, - crawl_new_peer_interval: TEST_DURATION, - ..zebra_network::Config::default() - }; - - let sync = SyncSection { - // Avoid downloading unnecessary blocks. - lookahead_limit: sync::MIN_LOOKAHEAD_LIMIT, - ..SyncSection::default() - }; - - let mempool = mempool::Config { - eviction_memory_time: TEST_DURATION, - ..mempool::Config::default() - }; - - let consensus = zebra_consensus::Config { - debug_skip_parameter_preload: true, - ..zebra_consensus::Config::default() - }; - - let force_use_color = !matches!( - env::var("ZEBRA_FORCE_USE_COLOR"), - Err(env::VarError::NotPresent) - ); - let tracing = TracingSection { - force_use_color, - ..TracingSection::default() - }; - - let config = ZebradConfig { - network, - state: zebra_state::Config::ephemeral(), - sync, - mempool, - consensus, - tracing, - ..ZebradConfig::default() - }; - - Ok(config) -} - -fn persistent_test_config() -> Result { - let mut config = default_test_config()?; - config.state.ephemeral = false; - Ok(config) -} - -fn testdir() -> Result { - tempfile::Builder::new() - .prefix("zebrad_tests") - .tempdir() - .map_err(Into::into) -} - -/// Extension trait for methods on `tempfile::TempDir` for using it as a test -/// directory for `zebrad`. -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: &[&str]) -> 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; - - // lightwalletd methods - - /// Spawn `lightwalletd` with `args` as a child process in this test directory, - /// potentially taking ownership of the tempdir for the duration of the - /// child process. - /// - /// By default, launch a working test instance with logging, and avoid port conflicts. - /// - /// # Panics - /// - /// If there is no lightwalletd config in the test directory. - fn spawn_lightwalletd_child(self, args: &[&str]) -> Result>; - - /// Create a config file and use it for all subsequently spawned `lightwalletd` processes. - /// Returns an error if the config already exists. - /// - /// If needed: - /// - recursively create directories for the config - fn with_lightwalletd_config(self, zebra_rpc_listener: SocketAddr) -> Result; -} - -impl ZebradTestDirExt for T -where - Self: TestDirExt + AsRef + Sized, -{ - fn spawn_child(self, args: &[&str]) -> Result> { - let dir = self.as_ref(); - let default_config_path = dir.join("zebrad.toml"); - - if default_config_path.exists() { - let mut extra_args: Vec<_> = vec![ - "-c", - default_config_path - .as_path() - .to_str() - .expect("Path is valid Unicode"), - ]; - extra_args.extend_from_slice(args); - self.spawn_child_with_command(env!("CARGO_BIN_EXE_zebrad"), &extra_args) - } else { - 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 { - if !config.state.ephemeral { - let dir = self.as_ref(); - let cache_dir = dir.join("state"); - 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) - } - - fn spawn_lightwalletd_child(self, extra_args: &[&str]) -> Result> { - let dir = self.as_ref().to_owned(); - let default_config_path = dir.join("lightwalletd-zcash.conf"); - - assert!( - default_config_path.exists(), - "lightwalletd requires a config" - ); - - // By default, launch a working test instance with logging, - // and avoid port conflicts. - let mut args: Vec<_> = vec![ - // the fake zcashd conf we just wrote - "--zcash-conf-path", - default_config_path - .as_path() - .to_str() - .expect("Path is valid Unicode"), - // the lightwalletd cache directory - // - // TODO: create a sub-directory for lightwalletd - "--data-dir", - dir.to_str().expect("Path is valid Unicode"), - // log to standard output - // - // TODO: if lightwalletd needs to run on Windows, - // work out how to log to the terminal on all platforms - "--log-file", - "/dev/stdout", - // let the OS choose a random available wallet client port - "--grpc-bind-addr", - "127.0.0.1:0", - "--http-bind-addr", - "127.0.0.1:0", - // don't require a TLS certificate for the HTTP server - "--no-tls-very-insecure", - ]; - args.extend_from_slice(extra_args); - - self.spawn_child_with_command("lightwalletd", &args) - } - - fn with_lightwalletd_config(self, zebra_rpc_listener: SocketAddr) -> Result { - use std::fs; - - let lightwalletd_config = format!( - "\ - rpcbind={}\n\ - rpcport={}\n\ - ", - zebra_rpc_listener.ip(), - zebra_rpc_listener.port(), - ); - - let dir = self.as_ref(); - fs::create_dir_all(dir)?; - - let config_file = dir.join("lightwalletd-zcash.conf"); - fs::write(config_file, lightwalletd_config.as_bytes())?; - - Ok(self) - } -} #[test] fn generate_no_args() -> Result<()> { @@ -362,38 +71,6 @@ fn generate_no_args() -> Result<()> { Ok(()) } -/// 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_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); - } - }; -} - #[test] fn generate_args() -> Result<()> { zebra_test::init(); @@ -440,17 +117,6 @@ fn generate_args() -> Result<()> { Ok(()) } -/// Is `s` a valid `zebrad` version string? -/// -/// Trims whitespace before parsing the version. -/// -/// Returns false if the version is invalid, or if there is anything else on the -/// line that contains the version. In particular, this check will fail if `s` -/// includes any terminal formatting. -fn is_zebrad_version(s: &str) -> bool { - semver::Version::parse(s.replace("zebrad", "").trim()).is_ok() -} - #[test] fn help_no_args() -> Result<()> { zebra_test::init(); @@ -576,24 +242,6 @@ fn persistent_mode() -> Result<()> { Ok(()) } -/// The cache_dir config used in the ephemeral mode tests -#[derive(Debug, PartialEq, Eq)] -enum EphemeralConfig { - /// the cache_dir config is left at its default value - Default, - /// the cache_dir config is set to a path in the tempdir - MisconfiguredCacheDir, -} - -/// The check performed by the ephemeral mode tests -#[derive(Debug, PartialEq, Eq)] -enum EphemeralCheck { - /// an existing directory is not deleted - ExistingDirectory, - /// a missing directory is not created - MissingDirectory, -} - #[test] fn ephemeral_existing_directory() -> Result<()> { ephemeral(EphemeralConfig::Default, EphemeralCheck::ExistingDirectory) @@ -827,45 +475,6 @@ fn valid_generated_config(command: &str, expect_stdout_line_contains: &str) -> R Ok(()) } -const TINY_CHECKPOINT_TEST_HEIGHT: Height = Height(0); -const MEDIUM_CHECKPOINT_TEST_HEIGHT: Height = - Height(zebra_consensus::MAX_CHECKPOINT_HEIGHT_GAP as u32); -const LARGE_CHECKPOINT_TEST_HEIGHT: Height = - Height((zebra_consensus::MAX_CHECKPOINT_HEIGHT_GAP * 2) as u32); - -const STOP_AT_HEIGHT_REGEX: &str = "stopping at configured height"; - -/// The text that should be logged when the initial sync finishes at the estimated chain tip. -/// -/// This message is only logged if: -/// - we have reached the estimated chain tip, -/// - we have synced all known checkpoints, -/// - the syncer has stopped downloading lots of blocks, and -/// - we are regularly downloading some blocks via the syncer or block gossip. -const SYNC_FINISHED_REGEX: &str = "finished initial sync to chain tip, using gossiped blocks"; - -/// The maximum amount of time Zebra should take to reload after shutting down. -/// -/// This should only take a second, but sometimes CI VMs or RocksDB can be slow. -const STOP_ON_LOAD_TIMEOUT: Duration = Duration::from_secs(10); - -/// The maximum amount of time Zebra should take to sync a few hundred blocks. -/// -/// Usually the small checkpoint is much shorter than this. -const TINY_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(120); - -/// The maximum amount of time Zebra should take to sync a thousand blocks. -const LARGE_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(180); - -/// The test sync height where we switch to using the default lookahead limit. -/// -/// Most tests only download a few blocks. So tests default to the minimum lookahead limit, -/// to avoid downloading extra blocks, and slowing down the test. -/// -/// But if we're going to be downloading lots of blocks, we use the default lookahead limit, -/// so that the sync is faster. This can increase the RAM needed for tests. -const MIN_HEIGHT_FOR_DEFAULT_LOOKAHEAD: Height = Height(3 * sync::DEFAULT_LOOKAHEAD_LIMIT as u32); - /// Test if `zebrad` can sync the first checkpoint on mainnet. /// /// The first checkpoint contains a single genesis block. @@ -1083,229 +692,6 @@ fn full_sync_test(network: Network, timeout_argument_name: &'static str) -> Resu } } -/// Sync on `network` until `zebrad` reaches `height`, or until it logs `stop_regex`. -/// -/// If `stop_regex` is encountered before the process exits, kills the -/// process, and mark the test as successful, even if `height` has not -/// been reached. To disable the height limit, and just stop at `stop_regex`, -/// use `Height::MAX` for the `height`. -/// -/// # Test Settings -/// -/// If `reuse_tempdir` is supplied, use it as the test's temporary directory. -/// -/// If `height` is higher than the mandatory checkpoint, -/// configures `zebrad` to preload the Zcash parameters. -/// If it is lower, skips the parameter preload. -/// -/// Configures `zebrad` to debug-enable the mempool based on `mempool_behavior`, -/// then check the logs for the expected `mempool_behavior`. -/// -/// If `checkpoint_sync` is true, configures `zebrad` to use as many checkpoints as possible. -/// If it is false, only use the mandatory checkpoints. -/// -/// If `check_legacy_chain` is true, make sure the logs contain the legacy chain check. -/// -/// If your test environment does not have network access, skip -/// this test by setting the `ZEBRA_SKIP_NETWORK_TESTS` env var. -/// -/// # Test Status -/// -/// On success, returns the associated `TempDir`. Returns an error if -/// the child exits or `timeout` elapses before `stop_regex` is found. -#[allow(clippy::too_many_arguments)] -fn sync_until( - height: Height, - network: Network, - stop_regex: &str, - timeout: Duration, - // Test Settings - // TODO: turn these into an argument struct - reuse_tempdir: impl Into>, - mempool_behavior: MempoolBehavior, - checkpoint_sync: bool, - check_legacy_chain: bool, -) -> Result { - zebra_test::init(); - - if zebra_test::net::zebra_skip_network_tests() { - return testdir(); - } - - let reuse_tempdir = reuse_tempdir.into(); - - // Use a persistent state, so we can handle large syncs - let mut config = persistent_test_config()?; - config.network.network = network; - config.state.debug_stop_at_height = Some(height.0); - config.mempool.debug_enable_at_height = mempool_behavior.enable_at_height(); - config.consensus.checkpoint_sync = checkpoint_sync; - - // Download the parameters at launch, if we're going to need them later. - if height > network.mandatory_checkpoint_height() { - config.consensus.debug_skip_parameter_preload = false; - } - - // Use the default lookahead limit if we're syncing lots of blocks. - // (Most tests use a smaller limit to minimise redundant block downloads.) - if height > MIN_HEIGHT_FOR_DEFAULT_LOOKAHEAD { - config.sync.lookahead_limit = sync::DEFAULT_LOOKAHEAD_LIMIT; - } - - let tempdir = if let Some(reuse_tempdir) = reuse_tempdir { - reuse_tempdir.replace_config(&mut config)? - } else { - testdir()?.with_config(&mut config)? - }; - - let mut child = tempdir.spawn_child(&["start"])?.with_timeout(timeout); - - let network = format!("network: {},", network); - - if mempool_behavior.require_activation() { - // require that the mempool activated, - // checking logs as they arrive - - child.expect_stdout_line_matches(&network)?; - - if check_legacy_chain { - child.expect_stdout_line_matches("starting legacy chain check")?; - child.expect_stdout_line_matches("no legacy chain found")?; - } - - // before the stop regex, expect mempool activation - if mempool_behavior.require_forced_activation() { - child.expect_stdout_line_matches("enabling mempool for debugging")?; - } - child.expect_stdout_line_matches("activating mempool")?; - - // then wait for the stop log, which must happen after the mempool becomes active - child.expect_stdout_line_matches(stop_regex)?; - - // make sure the child process is dead - // if it has already exited, ignore that error - let _ = child.kill(); - - Ok(child.dir) - } else { - // Require that the mempool didn't activate, - // checking the entire `zebrad` output after it exits. - // - // # Correctness - // - // Unlike the other mempool behaviours, `zebrad` must stop after logging the stop regex, - // without being killed by [`sync_until`] test harness. - // - // Since it needs to collect all the output, - // the test harness can't kill `zebrad` after it logs the `stop_regex`. - assert!( - height.0 < 2_000_000, - "zebrad must exit by itself, so we can collect all the output", - ); - let output = child.wait_with_output()?; - - output.stdout_line_contains(&network)?; - - if check_legacy_chain { - output.stdout_line_contains("starting legacy chain check")?; - output.stdout_line_contains("no legacy chain found")?; - } - - // check it did not activate or use the mempool - assert!(output.stdout_line_contains("activating mempool").is_err()); - assert!(output - .stdout_line_contains("sending mempool transaction broadcast") - .is_err()); - - // check it logged the stop regex before exiting - output.stdout_line_matches(stop_regex)?; - - // check exited by itself, successfully - output.assert_was_not_killed()?; - let output = output.assert_success()?; - - Ok(output.dir.expect("wait_with_output sets dir")) - } -} - -fn cached_mandatory_checkpoint_test_config() -> Result { - let mut config = persistent_test_config()?; - config.state.cache_dir = "/zebrad-cache".into(); - - // To get to the mandatory checkpoint, we need to sync lots of blocks. - // (Most tests use a smaller limit to minimise redundant block downloads.) - // - // If we're syncing past the checkpoint with cached state, we don't need the extra lookahead. - // But the extra downloaded blocks shouldn't slow down the test that much, - // and testing with the defaults gives us better test coverage. - config.sync.lookahead_limit = sync::DEFAULT_LOOKAHEAD_LIMIT; - - Ok(config) -} - -/// Create or update a cached state for `network`, stopping at `height`. -/// -/// # Test Settings -/// -/// If `debug_skip_parameter_preload` is true, configures `zebrad` to preload the Zcash parameters. -/// If it is false, skips the parameter preload. -/// -/// If `checkpoint_sync` is true, configures `zebrad` to use as many checkpoints as possible. -/// If it is false, only use the mandatory checkpoints. -/// -/// If `check_legacy_chain` is true, make sure the logs contain the legacy chain check. -/// -/// The test passes when `zebrad` logs the `stop_regex`. -/// Typically this is `STOP_AT_HEIGHT_REGEX`, -/// with an extra check for checkpoint or full validation. -/// -/// This test ignores the `ZEBRA_SKIP_NETWORK_TESTS` env var. -/// -/// # Test Status -/// -/// Returns an error if the child exits or the fixed timeout elapses -/// before `STOP_AT_HEIGHT_REGEX` is found. -#[allow(clippy::print_stderr)] -fn create_cached_database_height( - network: Network, - height: Height, - debug_skip_parameter_preload: bool, - checkpoint_sync: bool, - stop_regex: &str, -) -> Result<()> { - eprintln!("creating cached database"); - - // 16 hours - let timeout = Duration::from_secs(60 * 60 * 16); - - // Use a persistent state, so we can handle large syncs - let mut config = cached_mandatory_checkpoint_test_config()?; - // TODO: add convenience methods? - config.network.network = network; - config.state.debug_stop_at_height = Some(height.0); - config.consensus.debug_skip_parameter_preload = debug_skip_parameter_preload; - config.consensus.checkpoint_sync = checkpoint_sync; - - let dir = PathBuf::from("/zebrad-cache"); - let mut child = dir - .with_exact_config(&config)? - .spawn_child(&["start"])? - .with_timeout(timeout) - .bypass_test_capture(true); - - let network = format!("network: {},", network); - child.expect_stdout_line_matches(&network)?; - - child.expect_stdout_line_matches("starting legacy chain check")?; - child.expect_stdout_line_matches("no legacy chain found")?; - - child.expect_stdout_line_matches(stop_regex)?; - - child.kill()?; - - Ok(()) -} - fn create_cached_database(network: Network) -> Result<()> { let height = network.mandatory_checkpoint_height(); let checkpoint_stop_regex = format!("{}.*CommitFinalized request", STOP_AT_HEIGHT_REGEX); @@ -1387,20 +773,6 @@ fn sync_past_mandatory_checkpoint_testnet() { sync_past_mandatory_checkpoint(network).unwrap(); } -/// Returns the "magic" port number that tells the operating system to -/// choose a random unallocated port. -/// -/// The OS chooses a different port each time it opens a connection or -/// listener with this magic port number. -/// -/// ## Usage -/// -/// See the usage note for `random_known_port`. -#[allow(dead_code)] -fn random_unallocated_port() -> u16 { - 0 -} - #[tokio::test] async fn metrics_endpoint() -> Result<()> { use hyper::Client; @@ -1560,20 +932,18 @@ async fn rpc_endpoint() -> Result<()> { return Ok(()); } - // [Note on port conflict](#Note on port conflict) - let port = random_known_port(); - let endpoint = format!("127.0.0.1:{}", port); - let url = format!("http://{}", endpoint); - // Write a configuration that has RPC listen_addr set - let mut config = default_test_config()?; - config.rpc.listen_addr = Some(endpoint.parse().unwrap()); + // [Note on port conflict](#Note on port conflict) + let mut config = random_known_rpc_port_config()?; + let url = format!("http://{}", config.rpc.listen_addr.unwrap()); let dir = testdir()?.with_config(&mut config)?; let mut child = dir.spawn_child(&["start"])?; // Wait until port is open. - child.expect_stdout_line_matches(format!("Opened RPC endpoint at {}", endpoint).as_str())?; + child.expect_stdout_line_matches( + format!("Opened RPC endpoint at {}", config.rpc.listen_addr.unwrap()).as_str(), + )?; // Create an http client let client = Client::new(); @@ -1627,48 +997,32 @@ async fn rpc_endpoint() -> Result<()> { #[test] #[cfg(not(target_os = "windows"))] fn lightwalletd_integration() -> Result<()> { - use std::net::SocketAddr; - zebra_test::init(); - // Skip the test unless we specifically asked for it - // - // TODO: check if the lightwalletd binary is in the PATH? - // (this doesn't seem to be implemented in the standard library) - if env::var("ZEBRA_TEST_LIGHTWALLETD").is_err() { - tracing::info!( - "skipped lightwalletd integration test, \ - set the 'ZEBRA_TEST_LIGHTWALLETD' environmental variable to run the test", - ); - + // Skip the test unless the user specifically asked for it + if zebra_skip_lightwalletd_tests() { return Ok(()); } // Launch zebrad + // Write a configuration that has RPC listen_addr set // [Note on port conflict](#Note on port conflict) - let listen_port = random_known_port(); - let listen_ip = "127.0.0.1".parse().expect("hard-coded IP is valid"); - let zebra_rpc_listener = SocketAddr::new(listen_ip, listen_port); - - // Write a configuration that has the rpc listen_addr option set - // TODO: split this config into another function? - let mut config = default_test_config()?; - config.rpc.listen_addr = Some(zebra_rpc_listener); + let mut config = random_known_rpc_port_config()?; let zdir = testdir()?.with_config(&mut config)?; let mut zebrad = zdir.spawn_child(&["start"])?.with_timeout(LAUNCH_DELAY); // Wait until `zebrad` has opened the RPC endpoint zebrad.expect_stdout_line_matches( - format!("Opened RPC endpoint at {}", zebra_rpc_listener).as_str(), + format!("Opened RPC endpoint at {}", config.rpc.listen_addr.unwrap()).as_str(), )?; // Launch lightwalletd // Write a fake zcashd configuration that has the rpcbind and rpcport options set let ldir = testdir()?; - let ldir = ldir.with_lightwalletd_config(zebra_rpc_listener)?; + let ldir = ldir.with_lightwalletd_config(config.rpc.listen_addr.unwrap())?; // Launch the lightwalletd process let result = ldir.spawn_lightwalletd_child(&[]); @@ -1720,6 +1074,7 @@ fn lightwalletd_integration() -> Result<()> { // // zcash/lightwalletd exits by itself, but // adityapk00/lightwalletd keeps on going, so it gets killed by the test harness. + lightwalletd_output .assert_was_killed() .wrap_err("Possible port conflict. Are there other acceptance tests running?")?; @@ -1828,15 +1183,15 @@ fn zebra_rpc_conflict() -> Result<()> { return Ok(()); } + // Write a configuration that has RPC listen_addr set // [Note on port conflict](#Note on port conflict) - let port = random_known_port(); - let listen_addr = format!("127.0.0.1:{}", port); + let mut config = random_known_rpc_port_config()?; - // Write a configuration that has our created RPC listen_addr - let mut config = default_test_config()?; - config.rpc.listen_addr = Some(listen_addr.parse().unwrap()); let dir1 = testdir()?.with_config(&mut config)?; - let regex1 = regex::escape(&format!(r"Opened RPC endpoint at {}", listen_addr)); + let regex1 = regex::escape(&format!( + r"Opened RPC endpoint at {}", + config.rpc.listen_addr.unwrap(), + )); // From another folder create a configuration with the same endpoint. // `rpc.listen_addr` will be the same in the 2 nodes. @@ -1961,63 +1316,3 @@ where Ok(()) } - -/// What the expected behavior of the mempool is for a test that uses [`sync_until`]. -enum MempoolBehavior { - /// The mempool should be forced to activate at a certain height, for debug purposes. - /// - /// [`sync_until`] will kill `zebrad` after it logs mempool activation, - /// then the `stop_regex`. - ForceActivationAt(Height), - - /// The mempool should be automatically activated. - /// - /// [`sync_until`] will kill `zebrad` after it logs mempool activation, - /// then the `stop_regex`. - ShouldAutomaticallyActivate, - - /// The mempool should not become active during the test. - /// - /// # Correctness - /// - /// Unlike the other mempool behaviours, `zebrad` must stop after logging the stop regex, - /// without being killed by [`sync_until`] test harness. - /// - /// Since it needs to collect all the output, - /// the test harness can't kill `zebrad` after it logs the `stop_regex`. - ShouldNotActivate, -} - -impl MempoolBehavior { - /// Return the height value that the mempool should be enabled at, if available. - pub fn enable_at_height(&self) -> Option { - match self { - MempoolBehavior::ForceActivationAt(height) => Some(height.0), - MempoolBehavior::ShouldAutomaticallyActivate | MempoolBehavior::ShouldNotActivate => { - None - } - } - } - - /// Returns `true` if the mempool should activate, - /// either by forced or automatic activation. - pub fn require_activation(&self) -> bool { - self.require_forced_activation() || self.require_automatic_activation() - } - - /// Returns `true` if the mempool should be forcefully activated at a specified height. - pub fn require_forced_activation(&self) -> bool { - matches!(self, MempoolBehavior::ForceActivationAt(_)) - } - - /// Returns `true` if the mempool should automatically activate. - pub fn require_automatic_activation(&self) -> bool { - matches!(self, MempoolBehavior::ShouldAutomaticallyActivate) - } - - /// Returns `true` if the mempool should not activate. - #[allow(dead_code)] - pub fn require_no_activation(&self) -> bool { - matches!(self, MempoolBehavior::ShouldNotActivate) - } -} diff --git a/zebrad/tests/common/check.rs b/zebrad/tests/common/check.rs new file mode 100644 index 000000000..e9b7ce3e2 --- /dev/null +++ b/zebrad/tests/common/check.rs @@ -0,0 +1,35 @@ +//! Shared checks for the `zebrad` acceptance tests. +//! +//! # Warning +//! +//! Test functions in this file will not be run. +//! This file is only for test library code. + +/// The cache_dir config used in the ephemeral mode tests +#[derive(Debug, PartialEq, Eq)] +pub enum EphemeralConfig { + /// the cache_dir config is left at its default value + Default, + /// the cache_dir config is set to a path in the tempdir + MisconfiguredCacheDir, +} + +/// The check performed by the ephemeral mode tests +#[derive(Debug, PartialEq, Eq)] +pub enum EphemeralCheck { + /// an existing directory is not deleted + ExistingDirectory, + /// a missing directory is not created + MissingDirectory, +} + +/// Is `s` a valid `zebrad` version string? +/// +/// Trims whitespace before parsing the version. +/// +/// Returns false if the version is invalid, or if there is anything else on the +/// line that contains the version. In particular, this check will fail if `s` +/// includes any terminal formatting. +pub fn is_zebrad_version(s: &str) -> bool { + semver::Version::parse(s.replace("zebrad", "").trim()).is_ok() +} diff --git a/zebrad/tests/common/config.rs b/zebrad/tests/common/config.rs new file mode 100644 index 000000000..60913f005 --- /dev/null +++ b/zebrad/tests/common/config.rs @@ -0,0 +1,82 @@ +//! `zebrad` config-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, time::Duration}; + +use color_eyre::eyre::Result; +use tempfile::TempDir; + +use zebrad::{ + components::{mempool, sync}, + config::{SyncSection, TracingSection, ZebradConfig}, +}; + +/// Returns a config with: +/// - a Zcash listener on an unused port on IPv4 localhost, and +/// - an ephemeral state, +/// - the minimum syncer lookahead limit, and +/// - shorter task intervals, to improve test coverage. +pub fn default_test_config() -> Result { + const TEST_DURATION: Duration = Duration::from_secs(30); + + let network = zebra_network::Config { + // The OS automatically chooses an unused port. + listen_addr: "127.0.0.1:0".parse()?, + crawl_new_peer_interval: TEST_DURATION, + ..zebra_network::Config::default() + }; + + let sync = SyncSection { + // Avoid downloading unnecessary blocks. + lookahead_limit: sync::MIN_LOOKAHEAD_LIMIT, + ..SyncSection::default() + }; + + let mempool = mempool::Config { + eviction_memory_time: TEST_DURATION, + ..mempool::Config::default() + }; + + let consensus = zebra_consensus::Config { + debug_skip_parameter_preload: true, + ..zebra_consensus::Config::default() + }; + + let force_use_color = !matches!( + env::var("ZEBRA_FORCE_USE_COLOR"), + Err(env::VarError::NotPresent) + ); + let tracing = TracingSection { + force_use_color, + ..TracingSection::default() + }; + + let config = ZebradConfig { + network, + state: zebra_state::Config::ephemeral(), + sync, + mempool, + consensus, + tracing, + ..ZebradConfig::default() + }; + + Ok(config) +} + +pub fn persistent_test_config() -> Result { + let mut config = default_test_config()?; + config.state.ephemeral = false; + Ok(config) +} + +pub fn testdir() -> Result { + tempfile::Builder::new() + .prefix("zebrad_tests") + .tempdir() + .map_err(Into::into) +} diff --git a/zebrad/tests/common/launch.rs b/zebrad/tests/common/launch.rs new file mode 100644 index 000000000..82eae13b2 --- /dev/null +++ b/zebrad/tests/common/launch.rs @@ -0,0 +1,199 @@ +//! `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, path::Path, time::Duration}; + +use color_eyre::eyre::Result; + +use zebrad::config::ZebradConfig; + +use zebra_test::{command::TestDirExt, prelude::*}; + +/// 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(15); + +/// After we launch `lightwalletd`, wait this long for the command to start up, +/// take the actions expected by the 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. +pub const BETWEEN_NODES_DELAY: Duration = Duration::from_secs(2); + +/// 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: &[&str]) -> 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, +{ + fn spawn_child(self, args: &[&str]) -> Result> { + let dir = self.as_ref(); + let default_config_path = dir.join("zebrad.toml"); + + if default_config_path.exists() { + let mut extra_args: Vec<_> = vec![ + "-c", + default_config_path + .as_path() + .to_str() + .expect("Path is valid Unicode"), + ]; + extra_args.extend_from_slice(args); + self.spawn_child_with_command(env!("CARGO_BIN_EXE_zebrad"), &extra_args) + } else { + 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 { + if !config.state.ephemeral { + let dir = self.as_ref(); + let cache_dir = dir.join("state"); + 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) + } +} + +/// 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); + } + }; +} diff --git a/zebrad/tests/common/lightwalletd.rs b/zebrad/tests/common/lightwalletd.rs new file mode 100644 index 000000000..2b6c3cc1f --- /dev/null +++ b/zebrad/tests/common/lightwalletd.rs @@ -0,0 +1,157 @@ +//! `lightwalletd`-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, net::SocketAddr, path::Path}; + +use zebra_test::{ + command::{TestChild, TestDirExt}, + net::random_known_port, + prelude::*, +}; +use zebrad::config::ZebradConfig; + +use super::{config::default_test_config, launch::ZebradTestDirExt}; + +/// The name of the env var that enables Zebra lightwalletd integration tests. +/// These tests need a `lightwalletd` binary in the test machine's path. +/// +/// We use a constant so that the compiler detects typos. +/// +/// # Note +/// +/// This environmental variable is used to enable the lightwalletd tests. +/// But the network tests are *disabled* by their environmental variables. +const ZEBRA_TEST_LIGHTWALLETD: &str = "ZEBRA_TEST_LIGHTWALLETD"; + +/// Should we skip Zebra lightwalletd integration tests? +#[allow(clippy::print_stderr)] +pub fn zebra_skip_lightwalletd_tests() -> bool { + // TODO: check if the lightwalletd binary is in the PATH? + // (this doesn't seem to be implemented in the standard library) + // + // See is_command_available in zebra-test/tests/command.rs for one way to do this. + + if env::var_os(ZEBRA_TEST_LIGHTWALLETD).is_none() { + // This message is captured by the test runner, use + // `cargo test -- --nocapture` to see it. + eprintln!( + "Skipped lightwalletd integration test, \ + set the 'ZEBRA_TEST_LIGHTWALLETD' environmental variable to run the test", + ); + return true; + } + + false +} + +/// Returns a `zebrad` config with a random known RPC port. +pub fn random_known_rpc_port_config() -> Result { + // [Note on port conflict](#Note on port conflict) + let listen_port = random_known_port(); + let listen_ip = "127.0.0.1".parse().expect("hard-coded IP is valid"); + let zebra_rpc_listener = SocketAddr::new(listen_ip, listen_port); + + // Write a configuration that has the rpc listen_addr option set + // TODO: split this config into another function? + let mut config = default_test_config()?; + config.rpc.listen_addr = Some(zebra_rpc_listener); + + Ok(config) +} + +/// Extension trait for methods on `tempfile::TempDir` for using it as a test +/// directory for `zebrad`. +pub trait LightWalletdTestDirExt: ZebradTestDirExt +where + Self: AsRef + Sized, +{ + /// Spawn `lightwalletd` with `args` as a child process in this test directory, + /// potentially taking ownership of the tempdir for the duration of the + /// child process. + /// + /// By default, launch a working test instance with logging, and avoid port conflicts. + /// + /// # Panics + /// + /// If there is no lightwalletd config in the test directory. + fn spawn_lightwalletd_child(self, args: &[&str]) -> Result>; + + /// Create a config file and use it for all subsequently spawned `lightwalletd` processes. + /// Returns an error if the config already exists. + /// + /// If needed: + /// - recursively create directories for the config + fn with_lightwalletd_config(self, zebra_rpc_listener: SocketAddr) -> Result; +} + +impl LightWalletdTestDirExt for T +where + Self: TestDirExt + AsRef + Sized, +{ + fn spawn_lightwalletd_child(self, extra_args: &[&str]) -> Result> { + let dir = self.as_ref().to_owned(); + let default_config_path = dir.join("lightwalletd-zcash.conf"); + + assert!( + default_config_path.exists(), + "lightwalletd requires a config" + ); + + // By default, launch a working test instance with logging, + // and avoid port conflicts. + let mut args: Vec<_> = vec![ + // the fake zcashd conf we just wrote + "--zcash-conf-path", + default_config_path + .as_path() + .to_str() + .expect("Path is valid Unicode"), + // the lightwalletd cache directory + // + // TODO: create a sub-directory for lightwalletd + "--data-dir", + dir.to_str().expect("Path is valid Unicode"), + // log to standard output + // + // TODO: if lightwalletd needs to run on Windows, + // work out how to log to the terminal on all platforms + "--log-file", + "/dev/stdout", + // let the OS choose a random available wallet client port + "--grpc-bind-addr", + "127.0.0.1:0", + "--http-bind-addr", + "127.0.0.1:0", + // don't require a TLS certificate for the HTTP server + "--no-tls-very-insecure", + ]; + args.extend_from_slice(extra_args); + + self.spawn_child_with_command("lightwalletd", &args) + } + + fn with_lightwalletd_config(self, zebra_rpc_listener: SocketAddr) -> Result { + use std::fs; + + let lightwalletd_config = format!( + "\ + rpcbind={}\n\ + rpcport={}\n\ + ", + zebra_rpc_listener.ip(), + zebra_rpc_listener.port(), + ); + + let dir = self.as_ref(); + fs::create_dir_all(dir)?; + + let config_file = dir.join("lightwalletd-zcash.conf"); + fs::write(config_file, lightwalletd_config.as_bytes())?; + + Ok(self) + } +} diff --git a/zebrad/tests/common/mod.rs b/zebrad/tests/common/mod.rs new file mode 100644 index 000000000..b1b94397a --- /dev/null +++ b/zebrad/tests/common/mod.rs @@ -0,0 +1,16 @@ +//! Shared code for the `zebrad` acceptance tests. +//! +//! # Warning +//! +//! Test functions in this file and its submodules will not be run. +//! This file is only for test library code. +//! +//! This module uses the legacy directory structure, +//! to avoid compiling an empty "common" test binary: +//! https://doc.rust-lang.org/book/ch11-03-test-organization.html#submodules-in-integration-tests + +pub mod check; +pub mod config; +pub mod launch; +pub mod lightwalletd; +pub mod sync; diff --git a/zebrad/tests/common/sync.rs b/zebrad/tests/common/sync.rs new file mode 100644 index 000000000..de81628c8 --- /dev/null +++ b/zebrad/tests/common/sync.rs @@ -0,0 +1,345 @@ +//! `zebrad` sync-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::{path::PathBuf, time::Duration}; + +use color_eyre::eyre::Result; +use tempfile::TempDir; + +use zebra_chain::{block::Height, parameters::Network}; +use zebrad::{components::sync, config::ZebradConfig}; + +use zebra_test::prelude::*; + +use super::{ + config::{persistent_test_config, testdir}, + launch::ZebradTestDirExt, +}; + +pub const TINY_CHECKPOINT_TEST_HEIGHT: Height = Height(0); +pub const MEDIUM_CHECKPOINT_TEST_HEIGHT: Height = + Height(zebra_consensus::MAX_CHECKPOINT_HEIGHT_GAP as u32); +pub const LARGE_CHECKPOINT_TEST_HEIGHT: Height = + Height((zebra_consensus::MAX_CHECKPOINT_HEIGHT_GAP * 2) as u32); + +pub const STOP_AT_HEIGHT_REGEX: &str = "stopping at configured height"; + +/// The text that should be logged when the initial sync finishes at the estimated chain tip. +/// +/// This message is only logged if: +/// - we have reached the estimated chain tip, +/// - we have synced all known checkpoints, +/// - the syncer has stopped downloading lots of blocks, and +/// - we are regularly downloading some blocks via the syncer or block gossip. +pub const SYNC_FINISHED_REGEX: &str = "finished initial sync to chain tip, using gossiped blocks"; + +/// The maximum amount of time Zebra should take to reload after shutting down. +/// +/// This should only take a second, but sometimes CI VMs or RocksDB can be slow. +pub const STOP_ON_LOAD_TIMEOUT: Duration = Duration::from_secs(10); + +/// The maximum amount of time Zebra should take to sync a few hundred blocks. +/// +/// Usually the small checkpoint is much shorter than this. +pub const TINY_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(120); + +/// The maximum amount of time Zebra should take to sync a thousand blocks. +pub const LARGE_CHECKPOINT_TIMEOUT: Duration = Duration::from_secs(180); + +/// The test sync height where we switch to using the default lookahead limit. +/// +/// Most tests only download a few blocks. So tests default to the minimum lookahead limit, +/// to avoid downloading extra blocks, and slowing down the test. +/// +/// But if we're going to be downloading lots of blocks, we use the default lookahead limit, +/// so that the sync is faster. This can increase the RAM needed for tests. +pub const MIN_HEIGHT_FOR_DEFAULT_LOOKAHEAD: Height = + Height(3 * sync::DEFAULT_LOOKAHEAD_LIMIT as u32); + +/// What the expected behavior of the mempool is for a test that uses [`sync_until`]. +pub enum MempoolBehavior { + /// The mempool should be forced to activate at a certain height, for debug purposes. + /// + /// [`sync_until`] will kill `zebrad` after it logs mempool activation, + /// then the `stop_regex`. + ForceActivationAt(Height), + + /// The mempool should be automatically activated. + /// + /// [`sync_until`] will kill `zebrad` after it logs mempool activation, + /// then the `stop_regex`. + ShouldAutomaticallyActivate, + + /// The mempool should not become active during the test. + /// + /// # Correctness + /// + /// Unlike the other mempool behaviours, `zebrad` must stop after logging the stop regex, + /// without being killed by [`sync_until`] test harness. + /// + /// Since it needs to collect all the output, + /// the test harness can't kill `zebrad` after it logs the `stop_regex`. + ShouldNotActivate, +} + +impl MempoolBehavior { + /// Return the height value that the mempool should be enabled at, if available. + pub fn enable_at_height(&self) -> Option { + match self { + MempoolBehavior::ForceActivationAt(height) => Some(height.0), + MempoolBehavior::ShouldAutomaticallyActivate | MempoolBehavior::ShouldNotActivate => { + None + } + } + } + + /// Returns `true` if the mempool should activate, + /// either by forced or automatic activation. + pub fn require_activation(&self) -> bool { + self.require_forced_activation() || self.require_automatic_activation() + } + + /// Returns `true` if the mempool should be forcefully activated at a specified height. + pub fn require_forced_activation(&self) -> bool { + matches!(self, MempoolBehavior::ForceActivationAt(_)) + } + + /// Returns `true` if the mempool should automatically activate. + pub fn require_automatic_activation(&self) -> bool { + matches!(self, MempoolBehavior::ShouldAutomaticallyActivate) + } + + /// Returns `true` if the mempool should not activate. + #[allow(dead_code)] + pub fn require_no_activation(&self) -> bool { + matches!(self, MempoolBehavior::ShouldNotActivate) + } +} + +/// Sync on `network` until `zebrad` reaches `height`, or until it logs `stop_regex`. +/// +/// If `stop_regex` is encountered before the process exits, kills the +/// process, and mark the test as successful, even if `height` has not +/// been reached. To disable the height limit, and just stop at `stop_regex`, +/// use `Height::MAX` for the `height`. +/// +/// # Test Settings +/// +/// If `reuse_tempdir` is supplied, use it as the test's temporary directory. +/// +/// If `height` is higher than the mandatory checkpoint, +/// configures `zebrad` to preload the Zcash parameters. +/// If it is lower, skips the parameter preload. +/// +/// Configures `zebrad` to debug-enable the mempool based on `mempool_behavior`, +/// then check the logs for the expected `mempool_behavior`. +/// +/// If `checkpoint_sync` is true, configures `zebrad` to use as many checkpoints as possible. +/// If it is false, only use the mandatory checkpoints. +/// +/// If `check_legacy_chain` is true, make sure the logs contain the legacy chain check. +/// +/// If your test environment does not have network access, skip +/// this test by setting the `ZEBRA_SKIP_NETWORK_TESTS` env var. +/// +/// # Test Status +/// +/// On success, returns the associated `TempDir`. Returns an error if +/// the child exits or `timeout` elapses before `stop_regex` is found. +#[allow(clippy::too_many_arguments)] +pub fn sync_until( + height: Height, + network: Network, + stop_regex: &str, + timeout: Duration, + // Test Settings + // TODO: turn these into an argument struct + reuse_tempdir: impl Into>, + mempool_behavior: MempoolBehavior, + checkpoint_sync: bool, + check_legacy_chain: bool, +) -> Result { + zebra_test::init(); + + if zebra_test::net::zebra_skip_network_tests() { + return testdir(); + } + + let reuse_tempdir = reuse_tempdir.into(); + + // Use a persistent state, so we can handle large syncs + let mut config = persistent_test_config()?; + config.network.network = network; + config.state.debug_stop_at_height = Some(height.0); + config.mempool.debug_enable_at_height = mempool_behavior.enable_at_height(); + config.consensus.checkpoint_sync = checkpoint_sync; + + // Download the parameters at launch, if we're going to need them later. + if height > network.mandatory_checkpoint_height() { + config.consensus.debug_skip_parameter_preload = false; + } + + // Use the default lookahead limit if we're syncing lots of blocks. + // (Most tests use a smaller limit to minimise redundant block downloads.) + if height > MIN_HEIGHT_FOR_DEFAULT_LOOKAHEAD { + config.sync.lookahead_limit = sync::DEFAULT_LOOKAHEAD_LIMIT; + } + + let tempdir = if let Some(reuse_tempdir) = reuse_tempdir { + reuse_tempdir.replace_config(&mut config)? + } else { + testdir()?.with_config(&mut config)? + }; + + let mut child = tempdir.spawn_child(&["start"])?.with_timeout(timeout); + + let network = format!("network: {},", network); + + if mempool_behavior.require_activation() { + // require that the mempool activated, + // checking logs as they arrive + + child.expect_stdout_line_matches(&network)?; + + if check_legacy_chain { + child.expect_stdout_line_matches("starting legacy chain check")?; + child.expect_stdout_line_matches("no legacy chain found")?; + } + + // before the stop regex, expect mempool activation + if mempool_behavior.require_forced_activation() { + child.expect_stdout_line_matches("enabling mempool for debugging")?; + } + child.expect_stdout_line_matches("activating mempool")?; + + // then wait for the stop log, which must happen after the mempool becomes active + child.expect_stdout_line_matches(stop_regex)?; + + // make sure the child process is dead + // if it has already exited, ignore that error + let _ = child.kill(); + + Ok(child.dir) + } else { + // Require that the mempool didn't activate, + // checking the entire `zebrad` output after it exits. + // + // # Correctness + // + // Unlike the other mempool behaviours, `zebrad` must stop after logging the stop regex, + // without being killed by [`sync_until`] test harness. + // + // Since it needs to collect all the output, + // the test harness can't kill `zebrad` after it logs the `stop_regex`. + assert!( + height.0 < 2_000_000, + "zebrad must exit by itself, so we can collect all the output", + ); + let output = child.wait_with_output()?; + + output.stdout_line_contains(&network)?; + + if check_legacy_chain { + output.stdout_line_contains("starting legacy chain check")?; + output.stdout_line_contains("no legacy chain found")?; + } + + // check it did not activate or use the mempool + assert!(output.stdout_line_contains("activating mempool").is_err()); + assert!(output + .stdout_line_contains("sending mempool transaction broadcast") + .is_err()); + + // check it logged the stop regex before exiting + output.stdout_line_matches(stop_regex)?; + + // check exited by itself, successfully + output.assert_was_not_killed()?; + let output = output.assert_success()?; + + Ok(output.dir.expect("wait_with_output sets dir")) + } +} + +/// Returns a test config for caching Zebra's state up to the mandatory checkpoint. +pub fn cached_mandatory_checkpoint_test_config() -> Result { + let mut config = persistent_test_config()?; + config.state.cache_dir = "/zebrad-cache".into(); + + // To get to the mandatory checkpoint, we need to sync lots of blocks. + // (Most tests use a smaller limit to minimise redundant block downloads.) + // + // If we're syncing past the checkpoint with cached state, we don't need the extra lookahead. + // But the extra downloaded blocks shouldn't slow down the test that much, + // and testing with the defaults gives us better test coverage. + config.sync.lookahead_limit = sync::DEFAULT_LOOKAHEAD_LIMIT; + + Ok(config) +} + +/// Create or update a cached state for `network`, stopping at `height`. +/// +/// # Test Settings +/// +/// If `debug_skip_parameter_preload` is true, configures `zebrad` to preload the Zcash parameters. +/// If it is false, skips the parameter preload. +/// +/// If `checkpoint_sync` is true, configures `zebrad` to use as many checkpoints as possible. +/// If it is false, only use the mandatory checkpoints. +/// +/// If `check_legacy_chain` is true, make sure the logs contain the legacy chain check. +/// +/// The test passes when `zebrad` logs the `stop_regex`. +/// Typically this is `STOP_AT_HEIGHT_REGEX`, +/// with an extra check for checkpoint or full validation. +/// +/// This test ignores the `ZEBRA_SKIP_NETWORK_TESTS` env var. +/// +/// # Test Status +/// +/// Returns an error if the child exits or the fixed timeout elapses +/// before `STOP_AT_HEIGHT_REGEX` is found. +#[allow(clippy::print_stderr)] +pub fn create_cached_database_height( + network: Network, + height: Height, + debug_skip_parameter_preload: bool, + checkpoint_sync: bool, + stop_regex: &str, +) -> Result<()> { + eprintln!("creating cached database"); + + // 16 hours + let timeout = Duration::from_secs(60 * 60 * 16); + + // Use a persistent state, so we can handle large syncs + let mut config = cached_mandatory_checkpoint_test_config()?; + // TODO: add convenience methods? + config.network.network = network; + config.state.debug_stop_at_height = Some(height.0); + config.consensus.debug_skip_parameter_preload = debug_skip_parameter_preload; + config.consensus.checkpoint_sync = checkpoint_sync; + + let dir = PathBuf::from("/zebrad-cache"); + let mut child = dir + .with_exact_config(&config)? + .spawn_child(&["start"])? + .with_timeout(timeout) + .bypass_test_capture(true); + + let network = format!("network: {},", network); + child.expect_stdout_line_matches(&network)?; + + child.expect_stdout_line_matches("starting legacy chain check")?; + child.expect_stdout_line_matches("no legacy chain found")?; + + child.expect_stdout_line_matches(stop_regex)?; + + child.kill()?; + + Ok(()) +}