From 277967a4b727a91f99fb052506d2835ca56e5889 Mon Sep 17 00:00:00 2001 From: Josh Rotenberg Date: Mon, 5 Jan 2026 16:45:07 -0800 Subject: [PATCH 1/2] docs: add comprehensive rustdoc for testing module Adds detailed module-level documentation for the testing utilities with examples covering all features and common patterns. Documentation includes: - Why use this module (benefits overview) - Quick start example - Configuration options: - Lifecycle control (stop/remove on drop) - Debugging failed tests (keep_on_panic, capture_logs) - Ready checks (wait_for_ready) - Container reuse for faster local dev - Network support - Fast cleanup with stop timeout - Multi-container tests with ContainerGuardSet - Accessing container information - Common patterns: - Reusable test fixtures - Unique container names for parallel tests - Manual cleanup - Feature flag requirements Closes #224 --- src/testing.rs | 289 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 281 insertions(+), 8 deletions(-) diff --git a/src/testing.rs b/src/testing.rs index a0eaf59..a924dcf 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -1,28 +1,301 @@ -//! Testing utilities for container lifecycle management. +//! # Testing Utilities //! -//! This module provides [`ContainerGuard`], an RAII wrapper that automatically -//! manages container lifecycle. When the guard goes out of scope, containers -//! are stopped and removed automatically. +//! RAII-style container lifecycle management for integration tests. //! -//! # Example +//! This module provides [`ContainerGuard`] and [`ContainerGuardSet`] for automatic +//! container lifecycle management. Containers are automatically stopped and removed +//! when guards go out of scope, ensuring clean test environments. +//! +//! ## Why Use This? +//! +//! - **Automatic cleanup**: No more forgotten containers cluttering your Docker +//! - **Panic-safe**: Containers are cleaned up even if your test panics +//! - **Debug-friendly**: Keep containers alive on failure for inspection +//! - **Network support**: Automatic network creation for multi-container tests +//! - **Ready checks**: Wait for services to be ready before running tests +//! +//! ## Quick Start //! //! ```rust,no_run //! use docker_wrapper::testing::ContainerGuard; //! use docker_wrapper::RedisTemplate; //! //! #[tokio::test] -//! async fn test_redis() -> Result<(), Box> { +//! async fn test_with_redis() -> Result<(), Box> { +//! // Container starts and waits for Redis to be ready //! let guard = ContainerGuard::new(RedisTemplate::new("test-redis")) +//! .wait_for_ready(true) //! .start() //! .await?; //! -//! // Use the container... -//! let url = guard.template().connection_url(); +//! // Get connection string directly from guard +//! let url = guard.connection_string(); +//! // Use Redis at: redis://localhost:6379 //! //! Ok(()) //! // Container automatically stopped and removed here //! } //! ``` +//! +//! ## Configuration Options +//! +//! ### Lifecycle Control +//! +//! ```rust,no_run +//! # use docker_wrapper::testing::ContainerGuard; +//! # use docker_wrapper::RedisTemplate; +//! # async fn example() -> Result<(), Box> { +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) +//! .stop_on_drop(true) // Stop container on drop (default: true) +//! .remove_on_drop(true) // Remove container on drop (default: true) +//! .start() +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Debugging Failed Tests +//! +//! Keep containers running when tests fail for debugging: +//! +//! ```rust,no_run +//! # use docker_wrapper::testing::ContainerGuard; +//! # use docker_wrapper::RedisTemplate; +//! # async fn example() -> Result<(), Box> { +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) +//! .keep_on_panic(true) // Keep container if test panics +//! .capture_logs(true) // Print container logs on panic +//! .start() +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Ready Checks +//! +//! Wait for the service to be ready before proceeding: +//! +//! ```rust,no_run +//! # use docker_wrapper::testing::ContainerGuard; +//! # use docker_wrapper::RedisTemplate; +//! # async fn example() -> Result<(), Box> { +//! // Automatic wait during start +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) +//! .wait_for_ready(true) +//! .start() +//! .await?; +//! // Redis is guaranteed ready here +//! +//! // Or wait manually later +//! let guard2 = ContainerGuard::new(RedisTemplate::new("redis2")) +//! .start() +//! .await?; +//! guard2.wait_for_ready().await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Container Reuse +//! +//! Speed up local development by reusing running containers: +//! +//! ```rust,no_run +//! # use docker_wrapper::testing::ContainerGuard; +//! # use docker_wrapper::RedisTemplate; +//! # async fn example() -> Result<(), Box> { +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) +//! .reuse_if_running(true) // Reuse existing container if found +//! .remove_on_drop(false) // Keep it for next test run +//! .stop_on_drop(false) +//! .start() +//! .await?; +//! +//! if guard.was_reused() { +//! println!("Reused existing container"); +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Network Support +//! +//! Attach containers to custom networks: +//! +//! ```rust,no_run +//! # use docker_wrapper::testing::ContainerGuard; +//! # use docker_wrapper::RedisTemplate; +//! # async fn example() -> Result<(), Box> { +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) +//! .with_network("my-test-network") // Create and attach to network +//! .remove_network_on_drop(true) // Clean up network after test +//! .start() +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ### Fast Cleanup +//! +//! Use a short stop timeout for faster test cleanup: +//! +//! ```rust,no_run +//! # use docker_wrapper::testing::ContainerGuard; +//! # use docker_wrapper::RedisTemplate; +//! # use std::time::Duration; +//! # async fn example() -> Result<(), Box> { +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) +//! .stop_timeout(Duration::from_secs(1)) // 1 second graceful shutdown +//! .start() +//! .await?; +//! +//! // Or immediate SIGKILL +//! let guard2 = ContainerGuard::new(RedisTemplate::new("redis2")) +//! .stop_timeout(Duration::ZERO) +//! .start() +//! .await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Multi-Container Tests +//! +//! Use [`ContainerGuardSet`] for tests requiring multiple services: +//! +//! ```rust,no_run +//! use docker_wrapper::testing::ContainerGuardSet; +//! use docker_wrapper::RedisTemplate; +//! +//! #[tokio::test] +//! async fn test_multi_container() -> Result<(), Box> { +//! let guards = ContainerGuardSet::new() +//! .with_network("test-network") // Shared network for all containers +//! .add(RedisTemplate::new("redis-primary").port(6379)) +//! .add(RedisTemplate::new("redis-replica").port(6380)) +//! .keep_on_panic(true) +//! .start_all() +//! .await?; +//! +//! assert!(guards.contains("redis-primary")); +//! assert!(guards.contains("redis-replica")); +//! assert_eq!(guards.len(), 2); +//! +//! // All containers cleaned up together +//! Ok(()) +//! } +//! ``` +//! +//! ## Accessing Container Information +//! +//! ```rust,no_run +//! # use docker_wrapper::testing::ContainerGuard; +//! # use docker_wrapper::RedisTemplate; +//! # async fn example() -> Result<(), Box> { +//! let guard = ContainerGuard::new(RedisTemplate::new("redis").port(6379)) +//! .start() +//! .await?; +//! +//! // Connection string (for templates that support it) +//! let conn = guard.connection_string(); +//! +//! // Access underlying template +//! let template = guard.template(); +//! +//! // Get container ID +//! if let Some(id) = guard.container_id() { +//! println!("Container ID: {}", id); +//! } +//! +//! // Query host port for a container port +//! let host_port = guard.host_port(6379).await?; +//! +//! // Get container logs +//! let logs = guard.logs().await?; +//! +//! // Check if running +//! let running = guard.is_running().await?; +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Common Patterns +//! +//! ### Test Fixtures +//! +//! Create reusable test fixtures: +//! +//! ```rust,no_run +//! use docker_wrapper::testing::ContainerGuard; +//! use docker_wrapper::RedisTemplate; +//! use docker_wrapper::template::TemplateError; +//! +//! async fn redis_fixture(name: &str) -> Result, TemplateError> { +//! ContainerGuard::new(RedisTemplate::new(name)) +//! .wait_for_ready(true) +//! .keep_on_panic(true) +//! .capture_logs(true) +//! .start() +//! .await +//! } +//! +//! #[tokio::test] +//! async fn test_using_fixture() -> Result<(), Box> { +//! let redis = redis_fixture("test-redis").await?; +//! // Use redis... +//! Ok(()) +//! } +//! ``` +//! +//! ### Unique Container Names +//! +//! Use UUIDs to avoid name conflicts in parallel tests: +//! +//! ```rust,no_run +//! # use docker_wrapper::testing::ContainerGuard; +//! # use docker_wrapper::RedisTemplate; +//! fn unique_name(prefix: &str) -> String { +//! format!("{}-{}", prefix, uuid::Uuid::new_v4()) +//! } +//! +//! #[tokio::test] +//! async fn test_parallel_safe() -> Result<(), Box> { +//! let name = unique_name("redis"); +//! let guard = ContainerGuard::new(RedisTemplate::new(&name)) +//! .start() +//! .await?; +//! Ok(()) +//! } +//! ``` +//! +//! ### Manual Cleanup +//! +//! Trigger cleanup explicitly when needed: +//! +//! ```rust,no_run +//! # use docker_wrapper::testing::ContainerGuard; +//! # use docker_wrapper::RedisTemplate; +//! # async fn example() -> Result<(), Box> { +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) +//! .start() +//! .await?; +//! +//! // Do some work... +//! +//! // Explicitly cleanup (idempotent - safe to call multiple times) +//! guard.cleanup().await?; +//! +//! // Drop will not try to clean up again +//! # Ok(()) +//! # } +//! ``` +//! +//! ## Feature Flag +//! +//! This module requires the `testing` feature: +//! +//! ```toml +//! [dev-dependencies] +//! docker-wrapper = { version = "0.10", features = ["testing", "template-redis"] } +//! ``` use crate::command::DockerCommand; use crate::template::{HasConnectionString, Template, TemplateError}; From fff81f43df45db2a7530f5bffc4bd5d0179e449f Mon Sep 17 00:00:00 2001 From: Josh Rotenberg Date: Mon, 5 Jan 2026 16:56:21 -0800 Subject: [PATCH 2/2] docs: add testing_utilities example demonstrating ContainerGuard usage Comprehensive example showing: - Basic ContainerGuard usage with automatic cleanup - Connection strings for service access - Configuration options (stop timeout, debug mode, reuse) - ContainerGuardSet for multi-container tests - Container info access (logs, IDs) - Test fixture patterns All tests use unique ports for parallel safety. --- examples/testing_utilities.rs | 373 ++++++++++++++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 examples/testing_utilities.rs diff --git a/examples/testing_utilities.rs b/examples/testing_utilities.rs new file mode 100644 index 0000000..d5ccfbd --- /dev/null +++ b/examples/testing_utilities.rs @@ -0,0 +1,373 @@ +//! # Testing Utilities Example +//! +//! This example demonstrates how to use docker-wrapper's testing utilities +//! for integration testing with containers. These utilities provide RAII-style +//! lifecycle management, ensuring containers are automatically cleaned up. +//! +//! ## Features Demonstrated +//! +//! - `ContainerGuard`: Single container lifecycle management +//! - `ContainerGuardSet`: Multi-container test environments +//! - Automatic cleanup on drop (even on panic) +//! - Ready checks and connection strings +//! - Debug mode for failed tests +//! +//! ## Running the Examples +//! +//! Run specific tests: +//! ```bash +//! cargo test --example testing_utilities --features testing +//! ``` +//! +//! Run with output: +//! ```bash +//! cargo test --example testing_utilities --features testing -- --nocapture +//! ``` + +#[cfg(test)] +use docker_wrapper::testing::{ContainerGuard, ContainerGuardSet}; +#[cfg(test)] +use docker_wrapper::{RedisTemplate, Template}; +#[cfg(test)] +use std::sync::atomic::{AtomicU16, Ordering}; +#[cfg(test)] +use std::time::Duration; + +/// Generate unique container names for parallel test safety. +#[cfg(test)] +fn unique_name(prefix: &str) -> String { + format!("{}-{}", prefix, uuid::Uuid::new_v4().simple()) +} + +/// Generate unique ports for parallel test safety. +/// Starts at 40000 and increments for each call. +#[cfg(test)] +fn unique_port() -> u16 { + static PORT: AtomicU16 = AtomicU16::new(40000); + PORT.fetch_add(1, Ordering::SeqCst) +} + +// ============================================================================ +// Basic Usage +// ============================================================================ + +/// Basic container guard usage with automatic cleanup. +/// +/// The container is automatically stopped and removed when the guard +/// goes out of scope, even if the test panics. +#[tokio::test] +async fn test_basic_container_guard() { + let name = unique_name("basic-redis"); + let port = unique_port(); + + // Create and start a Redis container with automatic lifecycle management + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) + .wait_for_ready(true) // Wait for Redis to accept connections + .start() + .await + .expect("Failed to start Redis container"); + + // Container is ready - verify it's running + assert!( + guard.is_running().await.expect("Failed to check status"), + "Container should be running" + ); + + // Access the underlying template if needed + let template = guard.template(); + assert_eq!(template.config().name, name); + + // Container is automatically stopped and removed here +} + +/// Using connection strings for easy service access. +#[tokio::test] +async fn test_connection_string() { + let name = unique_name("conn-redis"); + let port = unique_port(); + + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) + .wait_for_ready(true) + .start() + .await + .expect("Failed to start Redis"); + + // Get the connection string directly from the guard + let conn = guard.connection_string(); + assert!( + conn.starts_with("redis://"), + "Should be a Redis connection URL" + ); + assert!( + conn.contains(&port.to_string()), + "Should include the configured port" + ); + + println!("Redis available at: {}", conn); +} + +// ============================================================================ +// Configuration Options +// ============================================================================ + +/// Fast cleanup with custom stop timeout. +/// +/// By default Docker waits 10 seconds for graceful shutdown. Use a shorter +/// timeout for faster test cleanup when you don't need graceful shutdown. +#[tokio::test] +async fn test_fast_cleanup() { + let name = unique_name("fast-redis"); + let port = unique_port(); + + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) + .wait_for_ready(true) + .stop_timeout(Duration::from_secs(1)) // Fast 1-second timeout + .start() + .await + .expect("Failed to start Redis"); + + assert!(guard.is_running().await.unwrap()); + + // Cleanup will be fast due to short timeout +} + +/// Keep containers running for debugging failed tests. +/// +/// When `keep_on_panic(true)` is set, the container won't be removed if +/// the test panics, allowing you to inspect its state for debugging. +#[tokio::test] +async fn test_debug_mode() { + let name = unique_name("debug-redis"); + let port = unique_port(); + + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) + .keep_on_panic(true) // Keep container if test panics + .capture_logs(true) // Print logs on panic + .wait_for_ready(true) + .start() + .await + .expect("Failed to start Redis"); + + // If this test panicked, the container would stay running + // and logs would be printed to stderr for debugging + assert!(guard.is_running().await.unwrap()); + + // Normal exit - container is cleaned up +} + +/// Container reuse for faster local development. +/// +/// With `reuse_if_running(true)`, if a container with the same name is +/// already running, it will be reused instead of starting a new one. +#[tokio::test] +async fn test_container_reuse() { + // Use a fixed name and port for reuse demonstration + let name = unique_name("reuse-redis"); + let port = unique_port(); + + // First run - starts a new container + let guard1 = ContainerGuard::new(RedisTemplate::new(&name).port(port)) + .reuse_if_running(true) + .stop_on_drop(false) // Don't stop so next test can reuse + .remove_on_drop(false) // Don't remove so next test can reuse + .wait_for_ready(true) + .start() + .await + .expect("Failed to start Redis"); + + let was_reused_first = guard1.was_reused(); + println!("First guard - was_reused: {}", was_reused_first); + + // Second guard - should reuse the running container + let guard2 = ContainerGuard::new(RedisTemplate::new(&name).port(port)) + .reuse_if_running(true) + .wait_for_ready(true) + .start() + .await + .expect("Failed to start/reuse Redis"); + + // This time it should be reused (if container was still running) + println!("Second guard - was_reused: {}", guard2.was_reused()); + assert!(guard2.was_reused(), "Second guard should reuse container"); + + // guard2 will clean up since we didn't disable cleanup +} + +/// Manual cleanup when you need explicit control. +#[tokio::test] +async fn test_manual_cleanup() { + let name = unique_name("manual-redis"); + let port = unique_port(); + + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) + .wait_for_ready(true) + .start() + .await + .expect("Failed to start Redis"); + + // Do some work... + assert!(guard.is_running().await.unwrap()); + + // Explicitly clean up before the guard goes out of scope + guard.cleanup().await.expect("Failed to cleanup"); + + // Guard will not try to clean up again on drop (idempotent) +} + +// ============================================================================ +// Multi-Container Tests +// ============================================================================ + +/// Using ContainerGuardSet for multi-container test environments. +/// +/// This is useful when your test needs multiple services that may need +/// to communicate with each other. +#[tokio::test] +async fn test_multi_container_set() { + let network = unique_name("test-net"); + let redis1_name = unique_name("redis-primary"); + let redis2_name = unique_name("redis-replica"); + let port1 = unique_port(); + let port2 = unique_port(); + + // Create a set with multiple containers on a shared network + let guards = ContainerGuardSet::new() + .with_network(&network) // Containers can communicate via this network + .add(RedisTemplate::new(&redis1_name).port(port1)) + .add(RedisTemplate::new(&redis2_name).port(port2)) + .keep_on_panic(true) // Keep all containers for debugging + .start_all() + .await + .expect("Failed to start container set"); + + // Verify both containers are in the set + assert!(guards.contains(&redis1_name)); + assert!(guards.contains(&redis2_name)); + assert_eq!(guards.len(), 2); + + // Get the shared network + assert_eq!(guards.network(), Some(network.as_str())); + + // List all container names + println!("Running containers:"); + for name in guards.names() { + println!(" - {}", name); + } + + // All containers and the network are cleaned up when guards is dropped +} + +/// ContainerGuardSet with custom options. +#[tokio::test] +async fn test_guard_set_options() { + let network = unique_name("options-net"); + let port = unique_port(); + + let guards = ContainerGuardSet::new() + .with_network(&network) + .create_network(true) // Create network if it doesn't exist (default) + .remove_network_on_drop(true) // Clean up network after test (default) + .wait_for_ready(true) // Wait for all containers to be ready (default) + .keep_on_panic(false) // Clean up even on panic + .add(RedisTemplate::new(&unique_name("opt-redis")).port(port)) + .start_all() + .await + .expect("Failed to start"); + + assert_eq!(guards.len(), 1); + assert!(!guards.is_empty()); +} + +// ============================================================================ +// Accessing Container Information +// ============================================================================ + +/// Accessing container details and logs. +#[tokio::test] +async fn test_container_info() { + let name = unique_name("info-redis"); + let port = unique_port(); + + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) + .wait_for_ready(true) + .start() + .await + .expect("Failed to start Redis"); + + // Get container ID (if available) + if let Some(id) = guard.container_id() { + println!("Container ID: {}", id); + assert!(!id.is_empty()); + } + + // Get connection string (includes the port) + let conn = guard.connection_string(); + println!("Connection string: {}", conn); + assert!(conn.contains(&port.to_string())); + + // Get container logs + let logs = guard.logs().await.expect("Failed to get logs"); + println!("Container logs:\n{}", logs); + assert!(logs.contains("Ready to accept connections")); + + // Check running status + assert!(guard.is_running().await.expect("Failed to check status")); +} + +// ============================================================================ +// Test Fixtures Pattern +// ============================================================================ + +/// Create reusable test fixtures for common setups. +#[cfg(test)] +async fn redis_fixture(name: &str, port: u16) -> ContainerGuard { + ContainerGuard::new(RedisTemplate::new(name).port(port)) + .wait_for_ready(true) + .keep_on_panic(true) + .capture_logs(true) + .stop_timeout(Duration::from_secs(2)) + .start() + .await + .expect("Failed to create Redis fixture") +} + +/// Using the test fixture pattern. +#[tokio::test] +async fn test_with_fixture() { + let name = unique_name("fixture-redis"); + let port = unique_port(); + let redis = redis_fixture(&name, port).await; + + // Fixture is ready to use + let conn = redis.connection_string(); + println!("Fixture ready at: {}", conn); + + // Clean up is automatic +} + +// ============================================================================ +// Main +// ============================================================================ + +fn main() { + println!("Testing Utilities Example"); + println!("========================="); + println!(); + println!("This example demonstrates docker-wrapper's testing utilities"); + println!("for integration testing with automatic container lifecycle management."); + println!(); + println!("Run the tests with:"); + println!(" cargo test --example testing_utilities --features testing"); + println!(); + println!("Run with output:"); + println!(" cargo test --example testing_utilities --features testing -- --nocapture"); + println!(); + println!("Features demonstrated:"); + println!(" - ContainerGuard: Single container RAII management"); + println!(" - ContainerGuardSet: Multi-container test environments"); + println!(" - Automatic cleanup on drop and panic"); + println!(" - Ready checks and connection strings"); + println!(" - Debug mode for failed tests"); + println!(" - Container reuse for faster development"); + println!(" - Test fixture patterns"); +}