zakura 1.0.4

Zakura, an independent, consensus-compatible implementation of a Zcash node
//! `zakurad` launch-specific shared code for the `zakurad` acceptance tests.
//!
//! # Warning
//!
//! Test functions in this file will not be run.
//! This file is only for test library code.

use std::{
    fmt::Debug,
    net::SocketAddr,
    path::{Path, PathBuf},
    time::Duration,
};

use tempfile::TempDir;

use zakura_chain::parameters::Network::{self, *};
use zakura_network::CacheDir;
use zakura_test::{
    args,
    command::{Arguments, TestDirExt},
    prelude::*,
};
use zakurad::config::ZakuradConfig;

use crate::common::{
    config::testdir, lightwalletd::zakura_skip_lightwalletd_tests,
    sync::FINISH_PARTIAL_SYNC_TIMEOUT, test_type::TestType,
};

/// After we launch `zakurad`, 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 `zakurad`, 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 `zakurad` instance
/// it is using for its RPCs.
pub const LIGHTWALLETD_DELAY: Duration = Duration::from_secs(60);

/// The amount of time we wait for lightwalletd to update to the tip.
///
/// `lightwalletd` takes about 60-120 minutes to fully sync,
/// and `zakurad` 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 `zakurad`.
pub trait ZakuradTestDirExt
where
    Self: AsRef<Path> + Sized,
{
    // Zebra methods

    /// Spawn `zakurad` 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 `zakurad`.
    fn spawn_child(self, args: Arguments) -> Result<TestChild<Self>>;

    /// Create a config file and use it for all subsequently spawned `zakurad` 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 ZakuradConfig) -> Result<Self>;

    /// Create a config file with the exact contents of `config`, and use it for
    /// all subsequently spawned `zakurad` 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: &ZakuradConfig) -> Result<Self>;

    /// Overwrite any existing `zakurad` 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 ZakuradConfig) -> Result<Self>;

    /// `cache_dir` config update helper for `zakurad`.
    ///
    /// If needed:
    ///   - set the cache_dir in the config.
    fn cache_config_update_helper(self, config: &mut ZakuradConfig) -> Result<Self>;

    /// Config writing helper for `zakurad`.
    ///
    /// If needed:
    ///   - recursively create directories for the config and state,
    ///
    /// Then write out the config.
    fn write_config_helper(self, config: &ZakuradConfig) -> Result<Self>;
}

impl<T> ZakuradTestDirExt for T
where
    Self: TestDirExt + AsRef<Path> + Sized,
{
    #[allow(clippy::unwrap_in_result)]
    fn spawn_child(self, extra_args: Arguments) -> Result<TestChild<Self>> {
        let dir = self.as_ref();
        let default_config_path = dir.join("zakura.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(&super::zakurad_exe_path(), args)
    }

    fn with_config(self, config: &mut ZakuradConfig) -> Result<Self> {
        self.cache_config_update_helper(config)?
            .write_config_helper(config)
    }

    fn with_exact_config(self, config: &ZakuradConfig) -> Result<Self> {
        self.write_config_helper(config)
    }

    fn replace_config(self, config: &mut ZakuradConfig) -> Result<Self> {
        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("zakura.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 ZakuradConfig) -> Result<Self> {
        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: &ZakuradConfig) -> Result<Self> {
        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("zakura.toml");
        fs::File::create(config_file)?.write_all(toml::to_string(&config)?.as_bytes())?;

        Ok(self)
    }
}

/// Spawns a zakurad instance on `network` to test lightwalletd with `test_type`.
///
/// See [`spawn_zakurad_for_rpc_with_opts`] for more details.
#[tracing::instrument]
pub fn spawn_zakurad_for_rpc<S: AsRef<str> + Debug>(
    network: Network,
    test_name: S,
    test_type: TestType,
    use_internet_connection: bool,
) -> Result<Option<(TestChild<TempDir>, Option<SocketAddr>)>> {
    spawn_zakurad_for_rpc_with_opts(network, test_name, test_type, use_internet_connection, true)
}

/// Spawns a zakurad 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, set `ZAKURA_STATE__CACHE_DIR`
/// to provide an initial state to the zakurad instance.
///
/// If `use_non_finalized_backup` is `false` then configure the spawned zakurad instance
/// not to cache a backup of its non-finalized state on disk.
///
/// Returns:
/// - `Ok(Some(zakurad, zakura_rpc_address))` on success,
/// - `Ok(None)` if the test doesn't have the required network or cached state, and
/// - `Err(_)` if spawning zakurad fails.
///
/// `zakura_rpc_address` is `None` if the test type doesn't need an RPC port.
#[tracing::instrument]
pub fn spawn_zakurad_for_rpc_with_opts<S: AsRef<str> + Debug>(
    network: Network,
    test_name: S,
    test_type: TestType,
    use_internet_connection: bool,
    use_non_finalized_backup: bool,
) -> Result<Option<(TestChild<TempDir>, Option<SocketAddr>)>> {
    let test_name = test_name.as_ref();

    // Skip the test unless the user specifically asked for it
    if !can_spawn_zakurad_for_test_type(test_name, test_type, use_internet_connection) {
        return Ok(None);
    }

    // Get the zakurad config
    let mut config = test_type
        .zakurad_config(test_name, use_internet_connection, None, &network)
        .expect("already checked config")?;
    config.state.should_backup_non_finalized_state = use_non_finalized_backup;

    let (zakurad_failure_messages, zakurad_ignore_messages) = test_type.zakurad_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 zakurad = testdir()?
        .with_exact_config(&config)?
        .spawn_child(args!["start"])?
        .bypass_test_capture(true)
        .with_timeout(test_type.zakurad_timeout())
        .with_failure_regex_iter(zakurad_failure_messages, zakurad_ignore_messages);

    Ok(Some((zakurad, config.rpc.listen_addr)))
}

/// Spawns a zakurad 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, set `ZAKURA_STATE__CACHE_DIR`
/// to provide an initial state to the zakurad instance.
///
/// Returns:
/// - `Ok(Some(zakurad))` on success,
/// - `Ok(None)` if the test doesn't have the required network or cached state, and
/// - `Err(_)` if spawning zakurad fails.
#[tracing::instrument]
pub fn spawn_zakurad_without_rpc<Str, Dir>(
    network: Network,
    test_name: Str,
    use_cached_state: bool,
    ephemeral: bool,
    reuse_state_path: Dir,
    use_internet_connection: bool,
) -> Result<Option<TestChild<TempDir>>>
where
    Str: AsRef<str> + Debug,
    Dir: Into<Option<TempDir>> + 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_zakurad_for_test_type(test_name, test_type, use_internet_connection) {
        return Ok(None);
    }

    // Get the zakurad config
    let config = test_type
        .zakurad_config(
            test_name,
            use_internet_connection,
            replace_cache_dir,
            &network,
        )
        .expect("already checked config")?;

    let (zakurad_failure_messages, zakurad_ignore_messages) = test_type.zakurad_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 zakurad = testdir
        .with_exact_config(&config)?
        .spawn_child(args!["start"])?
        .bypass_test_capture(true)
        .with_timeout(test_type.zakurad_timeout())
        .with_failure_regex_iter(zakurad_failure_messages, zakurad_ignore_messages);

    Ok(Some(zakurad))
}

/// Returns `true` if a zakurad test for `test_type` has everything it needs to run.
#[tracing::instrument]
pub fn can_spawn_zakurad_for_test_type<S: AsRef<str> + Debug>(
    test_name: S,
    test_type: TestType,
    use_internet_connection: bool,
) -> bool {
    if use_internet_connection && zakura_test::net::zebra_skip_network_tests() {
        return false;
    }

    // Skip the test unless the user specifically asked for it
    //
    // TODO: pass test_type to zakura_skip_lightwalletd_tests() and check for lightwalletd launch in there
    if test_type.launches_lightwalletd() && zakura_skip_lightwalletd_tests() {
        return false;
    }

    // Check if we have any necessary cached states for the zakurad config.
    // The cache_dir and network values don't matter here.
    test_type
        .zakurad_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 zakura_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 zakura_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);
        }
    };
}