mod attach_provider; pub mod clients; pub mod orchestrator; pub mod ports; pub mod readiness; pub mod setup; use std::marker::PhantomData; use async_trait::async_trait; use testing_framework_core::scenario::{ AttachSource, CleanupGuard, Deployer, DynError, FeedHandle, ObservabilityCapabilityProvider, RequiresNodeControl, Runner, Scenario, }; use crate::{env::ComposeDeployEnv, errors::ComposeRunnerError, lifecycle::cleanup::RunnerCleanup}; /// Docker Compose-based deployer for test scenarios. #[derive(Clone, Copy)] pub struct ComposeDeployer { readiness_checks: bool, _env: PhantomData, } /// Compose deployment metadata returned by compose-specific deployment APIs. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ComposeDeploymentMetadata { /// Docker Compose project name used for this deployment when available. pub project_name: Option, } #[derive(Debug, thiserror::Error)] enum ComposeMetadataError { #[error("compose deployment metadata has no project name")] MissingProjectName, } impl ComposeDeploymentMetadata { /// Returns project name when deployment is bound to a specific compose /// project. #[must_use] pub fn project_name(&self) -> Option<&str> { self.project_name.as_deref() } /// Builds an attach source for the same compose project using deployer /// discovery to resolve services. pub fn attach_source(&self) -> Result { let project_name = self .project_name() .ok_or(ComposeMetadataError::MissingProjectName)?; Ok(AttachSource::compose(Vec::new()).with_project(project_name.to_owned())) } /// Builds an attach source for the same compose project. pub fn attach_source_for_services( &self, services: Vec, ) -> Result { let project_name = self .project_name() .ok_or(ComposeMetadataError::MissingProjectName)?; Ok(AttachSource::compose(services).with_project(project_name.to_owned())) } } impl Default for ComposeDeployer { fn default() -> Self { Self::new() } } impl ComposeDeployer { #[must_use] pub const fn new() -> Self { Self { readiness_checks: true, _env: PhantomData, } } #[must_use] pub const fn with_readiness(mut self, enabled: bool) -> Self { self.readiness_checks = enabled; self } /// Deploy and return compose-specific metadata alongside the generic /// runner. pub async fn deploy_with_metadata( &self, scenario: &Scenario, ) -> Result<(Runner, ComposeDeploymentMetadata), ComposeRunnerError> where Caps: RequiresNodeControl + ObservabilityCapabilityProvider + Send + Sync, { let deployer = Self { readiness_checks: self.readiness_checks, _env: PhantomData, }; orchestrator::DeploymentOrchestrator::new(deployer) .deploy_with_metadata(scenario) .await } } #[async_trait] impl Deployer for ComposeDeployer where Caps: RequiresNodeControl + ObservabilityCapabilityProvider + Send + Sync, E: ComposeDeployEnv, { type Error = ComposeRunnerError; async fn deploy(&self, scenario: &Scenario) -> Result, Self::Error> { let deployer = Self { readiness_checks: self.readiness_checks, _env: PhantomData, }; orchestrator::DeploymentOrchestrator::new(deployer) .deploy(scenario) .await } } pub(super) struct ComposeCleanupGuard { environment: RunnerCleanup, block_feed: Option, } impl ComposeCleanupGuard { const fn new(environment: RunnerCleanup, block_feed: FeedHandle) -> Self { Self { environment, block_feed: Some(block_feed), } } } impl CleanupGuard for ComposeCleanupGuard { fn cleanup(mut self: Box) { if let Some(block_feed) = self.block_feed.take() { CleanupGuard::cleanup(Box::new(block_feed)); } CleanupGuard::cleanup(Box::new(self.environment)); } } pub(super) fn make_cleanup_guard( environment: RunnerCleanup, block_feed: FeedHandle, ) -> Box { Box::new(ComposeCleanupGuard::new(environment, block_feed)) } #[cfg(test)] mod tests {}