rigtest 0.4.0

Runtime library for the cargo-rigtest test framework
Documentation
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

/// A pinned, heap-allocated [`Future`] that is [`Send`] and bounded by lifetime `'a`.
///
/// This is the return type stored in [`TestCase::test_fn`],
/// [`GlobalSetupEntry::setup_fn`], and [`GlobalTeardownEntry::teardown_fn`].
/// You will not normally need to use it directly — the `#[testcase]`,
/// `#[global_setup]`, and `#[global_teardown]` proc macros generate the correct
/// wrappers automatically.
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

/// Boxed error type returned from test functions, setup closures, and teardown
/// closures. Equivalent to `Box<dyn std::error::Error + Send + Sync>`.
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;

/// The concrete type of the function pointer stored in [`TestCase::test_fn`].
///
/// It accepts a shared [`TestContext`][crate::context::TestContext] and returns
/// a [`BoxFuture`] that resolves to `Ok(())` on success or an error on failure.
/// You will not normally construct values of this type directly.
pub type TestFn = fn(Arc<crate::context::TestContext>) -> BoxFuture<'static, Result<(), BoxError>>;

/// A single test case registered at compile time via `#[testcase]`.
///
/// The `#[testcase]` attribute macro fills all fields automatically from the
/// annotated function and its attribute arguments. You should not construct
/// `TestCase` values manually.
pub struct TestCase {
    /// The test name as it appears in output and filter expressions.
    ///
    /// Set to the Rust function name (e.g. `"my_test"`) by the `#[testcase]` macro.
    pub name: &'static str,
    /// The Rust module path where the test function was declared.
    ///
    /// Set by `module_path!()` inside the macro expansion — for example
    /// `"my_crate::integration"`.
    pub module: &'static str,
    /// The source file path where the test function was declared.
    ///
    /// Set by `file!()` inside the macro expansion — for example
    /// `"tests/integration.rs"`.
    pub file: &'static str,
    /// When `true` this test must not run concurrently with any other test.
    ///
    /// Set by `#[testcase(serial)]`.
    pub serial: bool,
    /// Kill the subprocess and record a failure if the test exceeds this duration.
    ///
    /// `None` means no timeout. Set by `#[testcase(timeout = Duration::from_secs(N))]`.
    pub timeout: Option<std::time::Duration>,
    /// Retry a failed test up to this many additional times before reporting failure.
    ///
    /// `0` means no retries (the test runs exactly once). Set by
    /// `#[testcase(retries = N)]`.
    pub retries: u32,
    /// The test function, receiving a shared [`TestContext`][crate::context::TestContext].
    ///
    /// Generated by the `#[testcase]` macro as a thin wrapper that boxes the
    /// user's `async fn` into a [`BoxFuture`].
    pub test_fn: TestFn,
}

// linkme requires Sync
unsafe impl Sync for TestCase {}

/// A global setup function registered at compile time via `#[global_setup]`.
///
/// At most one entry may exist per test binary. The runtime calls [`setup_fn`]
/// once before running any tests, then uses [`serialize_fn`] to hand the state
/// to each test subprocess via an environment variable and [`deserialize_fn`]
/// to reconstruct it inside each subprocess.
///
/// [`setup_fn`]: GlobalSetupEntry::setup_fn
/// [`serialize_fn`]: GlobalSetupEntry::serialize_fn
/// [`deserialize_fn`]: GlobalSetupEntry::deserialize_fn
pub struct GlobalSetupEntry {
    /// Invokes the user's `#[global_setup]` function and returns its value as a
    /// type-erased `Box<dyn Any + Send + Sync>`.
    pub setup_fn: fn() -> BoxFuture<'static, Box<dyn std::any::Any + Send + Sync>>,
    /// Serializes the state value to a JSON string for subprocess handoff.
    ///
    /// The concrete type must implement `serde::Serialize`.
    pub serialize_fn: fn(&(dyn std::any::Any + Send + Sync)) -> String,
    /// Deserializes state from the JSON string produced by [`GlobalSetupEntry::serialize_fn`].
    ///
    /// The concrete type must implement `serde::de::DeserializeOwned`.
    pub deserialize_fn: fn(&str) -> Box<dyn std::any::Any + Send + Sync>,
}

unsafe impl Sync for GlobalSetupEntry {}

/// A global teardown function registered at compile time via `#[global_teardown]`.
///
/// At most one entry may exist per test binary. The runtime calls [`teardown_fn`]
/// once after all tests have run, passing the state produced by `#[global_setup]`.
///
/// [`teardown_fn`]: GlobalTeardownEntry::teardown_fn
pub struct GlobalTeardownEntry {
    /// Invokes the user's `#[global_teardown]` function with the type-erased global
    /// state, consuming it.
    pub teardown_fn: fn(Box<dyn std::any::Any + Send + Sync>) -> BoxFuture<'static, ()>,
}

unsafe impl Sync for GlobalTeardownEntry {}

/// All test cases discovered at compile time via `#[testcase]`.
///
/// Populated by [`linkme`] distributed slices; the coordinator iterates this
/// slice to build the list of tests to run.
#[linkme::distributed_slice]
pub static RIG_TEST_CASES: [TestCase];

/// At most one global setup entry, registered via `#[global_setup]`.
///
/// The runtime asserts at startup that this slice contains zero or one element.
#[linkme::distributed_slice]
pub static RIG_GLOBAL_SETUP: [GlobalSetupEntry];

/// At most one global teardown entry, registered via `#[global_teardown]`.
///
/// The runtime asserts at startup that this slice contains zero or one element.
#[linkme::distributed_slice]
pub static RIG_GLOBAL_TEARDOWN: [GlobalTeardownEntry];

/// An HTTP client configurator registered at compile time via
/// `#[rigtest::main(http_client = fn_name)]`.
///
/// At most one entry may exist per test binary. When present, the runtime calls
/// [`configure_fn`] with a fresh [`reqwest::ClientBuilder`] before each test
/// subprocess runs, and uses the returned builder to construct `ctx.client`.
///
/// [`configure_fn`]: HttpClientConfiguratorEntry::configure_fn
#[cfg(feature = "http-client")]
pub struct HttpClientConfiguratorEntry {
    /// Calls the user's configure function, returning a modified builder or an error.
    ///
    /// On error the test subprocess exits with a descriptive message before
    /// running any test logic.
    pub configure_fn: fn(reqwest::ClientBuilder) -> Result<reqwest::ClientBuilder, BoxError>,
}

// linkme requires Sync
#[cfg(feature = "http-client")]
unsafe impl Sync for HttpClientConfiguratorEntry {}

/// At most one HTTP client configurator, registered via
/// `#[rigtest::main(http_client = fn_name)]`.
///
/// The runtime asserts at startup that this slice contains zero or one element.
#[cfg(feature = "http-client")]
#[linkme::distributed_slice]
pub static RIG_HTTP_CLIENT_CONFIGURATOR: [HttpClientConfiguratorEntry];

/// An SSH client configurator registered at compile time via
/// `#[rigtest::main(ssh_client = fn_name)]`.
///
/// At most one entry may exist per test binary. When present, the runtime calls
/// [`configure_fn`] with the destination string and a fresh [`openssh::SessionBuilder`]
/// before establishing each SSH connection, and uses the returned builder to connect.
///
/// # Platform support
///
/// This type is only available on Unix. The `ssh-client` feature depends on
/// [`openssh`], which requires the system `ssh` binary and is not supported on
/// non-Unix targets.
///
/// [`configure_fn`]: SshClientConfiguratorEntry::configure_fn
#[cfg(all(feature = "ssh-client", unix))]
pub struct SshClientConfiguratorEntry {
    /// Calls the user's configure function with the destination and a fresh builder,
    /// returning a modified builder or an error.
    pub configure_fn:
        fn(&str, openssh::SessionBuilder) -> Result<openssh::SessionBuilder, BoxError>,
}

// linkme requires Sync
#[cfg(all(feature = "ssh-client", unix))]
unsafe impl Sync for SshClientConfiguratorEntry {}

/// At most one SSH client configurator, registered via
/// `#[rigtest::main(ssh_client = fn_name)]`.
///
/// The runtime asserts at startup that this slice contains zero or one element.
#[cfg(all(feature = "ssh-client", unix))]
#[linkme::distributed_slice]
pub static RIG_SSH_CLIENT_CONFIGURATOR: [SshClientConfiguratorEntry];