|
| 1 | +//! # Testing Utilities Example |
| 2 | +//! |
| 3 | +//! This example demonstrates how to use docker-wrapper's testing utilities |
| 4 | +//! for integration testing with containers. These utilities provide RAII-style |
| 5 | +//! lifecycle management, ensuring containers are automatically cleaned up. |
| 6 | +//! |
| 7 | +//! ## Features Demonstrated |
| 8 | +//! |
| 9 | +//! - `ContainerGuard`: Single container lifecycle management |
| 10 | +//! - `ContainerGuardSet`: Multi-container test environments |
| 11 | +//! - Automatic cleanup on drop (even on panic) |
| 12 | +//! - Ready checks and connection strings |
| 13 | +//! - Debug mode for failed tests |
| 14 | +//! |
| 15 | +//! ## Running the Examples |
| 16 | +//! |
| 17 | +//! Run specific tests: |
| 18 | +//! ```bash |
| 19 | +//! cargo test --example testing_utilities --features testing |
| 20 | +//! ``` |
| 21 | +//! |
| 22 | +//! Run with output: |
| 23 | +//! ```bash |
| 24 | +//! cargo test --example testing_utilities --features testing -- --nocapture |
| 25 | +//! ``` |
| 26 | +
|
| 27 | +#[cfg(test)] |
| 28 | +use docker_wrapper::testing::{ContainerGuard, ContainerGuardSet}; |
| 29 | +#[cfg(test)] |
| 30 | +use docker_wrapper::{RedisTemplate, Template}; |
| 31 | +#[cfg(test)] |
| 32 | +use std::sync::atomic::{AtomicU16, Ordering}; |
| 33 | +#[cfg(test)] |
| 34 | +use std::time::Duration; |
| 35 | + |
| 36 | +/// Generate unique container names for parallel test safety. |
| 37 | +#[cfg(test)] |
| 38 | +fn unique_name(prefix: &str) -> String { |
| 39 | + format!("{}-{}", prefix, uuid::Uuid::new_v4().simple()) |
| 40 | +} |
| 41 | + |
| 42 | +/// Generate unique ports for parallel test safety. |
| 43 | +/// Starts at 40000 and increments for each call. |
| 44 | +#[cfg(test)] |
| 45 | +fn unique_port() -> u16 { |
| 46 | + static PORT: AtomicU16 = AtomicU16::new(40000); |
| 47 | + PORT.fetch_add(1, Ordering::SeqCst) |
| 48 | +} |
| 49 | + |
| 50 | +// ============================================================================ |
| 51 | +// Basic Usage |
| 52 | +// ============================================================================ |
| 53 | + |
| 54 | +/// Basic container guard usage with automatic cleanup. |
| 55 | +/// |
| 56 | +/// The container is automatically stopped and removed when the guard |
| 57 | +/// goes out of scope, even if the test panics. |
| 58 | +#[tokio::test] |
| 59 | +async fn test_basic_container_guard() { |
| 60 | + let name = unique_name("basic-redis"); |
| 61 | + let port = unique_port(); |
| 62 | + |
| 63 | + // Create and start a Redis container with automatic lifecycle management |
| 64 | + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) |
| 65 | + .wait_for_ready(true) // Wait for Redis to accept connections |
| 66 | + .start() |
| 67 | + .await |
| 68 | + .expect("Failed to start Redis container"); |
| 69 | + |
| 70 | + // Container is ready - verify it's running |
| 71 | + assert!( |
| 72 | + guard.is_running().await.expect("Failed to check status"), |
| 73 | + "Container should be running" |
| 74 | + ); |
| 75 | + |
| 76 | + // Access the underlying template if needed |
| 77 | + let template = guard.template(); |
| 78 | + assert_eq!(template.config().name, name); |
| 79 | + |
| 80 | + // Container is automatically stopped and removed here |
| 81 | +} |
| 82 | + |
| 83 | +/// Using connection strings for easy service access. |
| 84 | +#[tokio::test] |
| 85 | +async fn test_connection_string() { |
| 86 | + let name = unique_name("conn-redis"); |
| 87 | + let port = unique_port(); |
| 88 | + |
| 89 | + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) |
| 90 | + .wait_for_ready(true) |
| 91 | + .start() |
| 92 | + .await |
| 93 | + .expect("Failed to start Redis"); |
| 94 | + |
| 95 | + // Get the connection string directly from the guard |
| 96 | + let conn = guard.connection_string(); |
| 97 | + assert!( |
| 98 | + conn.starts_with("redis://"), |
| 99 | + "Should be a Redis connection URL" |
| 100 | + ); |
| 101 | + assert!( |
| 102 | + conn.contains(&port.to_string()), |
| 103 | + "Should include the configured port" |
| 104 | + ); |
| 105 | + |
| 106 | + println!("Redis available at: {}", conn); |
| 107 | +} |
| 108 | + |
| 109 | +// ============================================================================ |
| 110 | +// Configuration Options |
| 111 | +// ============================================================================ |
| 112 | + |
| 113 | +/// Fast cleanup with custom stop timeout. |
| 114 | +/// |
| 115 | +/// By default Docker waits 10 seconds for graceful shutdown. Use a shorter |
| 116 | +/// timeout for faster test cleanup when you don't need graceful shutdown. |
| 117 | +#[tokio::test] |
| 118 | +async fn test_fast_cleanup() { |
| 119 | + let name = unique_name("fast-redis"); |
| 120 | + let port = unique_port(); |
| 121 | + |
| 122 | + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) |
| 123 | + .wait_for_ready(true) |
| 124 | + .stop_timeout(Duration::from_secs(1)) // Fast 1-second timeout |
| 125 | + .start() |
| 126 | + .await |
| 127 | + .expect("Failed to start Redis"); |
| 128 | + |
| 129 | + assert!(guard.is_running().await.unwrap()); |
| 130 | + |
| 131 | + // Cleanup will be fast due to short timeout |
| 132 | +} |
| 133 | + |
| 134 | +/// Keep containers running for debugging failed tests. |
| 135 | +/// |
| 136 | +/// When `keep_on_panic(true)` is set, the container won't be removed if |
| 137 | +/// the test panics, allowing you to inspect its state for debugging. |
| 138 | +#[tokio::test] |
| 139 | +async fn test_debug_mode() { |
| 140 | + let name = unique_name("debug-redis"); |
| 141 | + let port = unique_port(); |
| 142 | + |
| 143 | + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) |
| 144 | + .keep_on_panic(true) // Keep container if test panics |
| 145 | + .capture_logs(true) // Print logs on panic |
| 146 | + .wait_for_ready(true) |
| 147 | + .start() |
| 148 | + .await |
| 149 | + .expect("Failed to start Redis"); |
| 150 | + |
| 151 | + // If this test panicked, the container would stay running |
| 152 | + // and logs would be printed to stderr for debugging |
| 153 | + assert!(guard.is_running().await.unwrap()); |
| 154 | + |
| 155 | + // Normal exit - container is cleaned up |
| 156 | +} |
| 157 | + |
| 158 | +/// Container reuse for faster local development. |
| 159 | +/// |
| 160 | +/// With `reuse_if_running(true)`, if a container with the same name is |
| 161 | +/// already running, it will be reused instead of starting a new one. |
| 162 | +#[tokio::test] |
| 163 | +async fn test_container_reuse() { |
| 164 | + // Use a fixed name and port for reuse demonstration |
| 165 | + let name = unique_name("reuse-redis"); |
| 166 | + let port = unique_port(); |
| 167 | + |
| 168 | + // First run - starts a new container |
| 169 | + let guard1 = ContainerGuard::new(RedisTemplate::new(&name).port(port)) |
| 170 | + .reuse_if_running(true) |
| 171 | + .stop_on_drop(false) // Don't stop so next test can reuse |
| 172 | + .remove_on_drop(false) // Don't remove so next test can reuse |
| 173 | + .wait_for_ready(true) |
| 174 | + .start() |
| 175 | + .await |
| 176 | + .expect("Failed to start Redis"); |
| 177 | + |
| 178 | + let was_reused_first = guard1.was_reused(); |
| 179 | + println!("First guard - was_reused: {}", was_reused_first); |
| 180 | + |
| 181 | + // Second guard - should reuse the running container |
| 182 | + let guard2 = ContainerGuard::new(RedisTemplate::new(&name).port(port)) |
| 183 | + .reuse_if_running(true) |
| 184 | + .wait_for_ready(true) |
| 185 | + .start() |
| 186 | + .await |
| 187 | + .expect("Failed to start/reuse Redis"); |
| 188 | + |
| 189 | + // This time it should be reused (if container was still running) |
| 190 | + println!("Second guard - was_reused: {}", guard2.was_reused()); |
| 191 | + assert!(guard2.was_reused(), "Second guard should reuse container"); |
| 192 | + |
| 193 | + // guard2 will clean up since we didn't disable cleanup |
| 194 | +} |
| 195 | + |
| 196 | +/// Manual cleanup when you need explicit control. |
| 197 | +#[tokio::test] |
| 198 | +async fn test_manual_cleanup() { |
| 199 | + let name = unique_name("manual-redis"); |
| 200 | + let port = unique_port(); |
| 201 | + |
| 202 | + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) |
| 203 | + .wait_for_ready(true) |
| 204 | + .start() |
| 205 | + .await |
| 206 | + .expect("Failed to start Redis"); |
| 207 | + |
| 208 | + // Do some work... |
| 209 | + assert!(guard.is_running().await.unwrap()); |
| 210 | + |
| 211 | + // Explicitly clean up before the guard goes out of scope |
| 212 | + guard.cleanup().await.expect("Failed to cleanup"); |
| 213 | + |
| 214 | + // Guard will not try to clean up again on drop (idempotent) |
| 215 | +} |
| 216 | + |
| 217 | +// ============================================================================ |
| 218 | +// Multi-Container Tests |
| 219 | +// ============================================================================ |
| 220 | + |
| 221 | +/// Using ContainerGuardSet for multi-container test environments. |
| 222 | +/// |
| 223 | +/// This is useful when your test needs multiple services that may need |
| 224 | +/// to communicate with each other. |
| 225 | +#[tokio::test] |
| 226 | +async fn test_multi_container_set() { |
| 227 | + let network = unique_name("test-net"); |
| 228 | + let redis1_name = unique_name("redis-primary"); |
| 229 | + let redis2_name = unique_name("redis-replica"); |
| 230 | + let port1 = unique_port(); |
| 231 | + let port2 = unique_port(); |
| 232 | + |
| 233 | + // Create a set with multiple containers on a shared network |
| 234 | + let guards = ContainerGuardSet::new() |
| 235 | + .with_network(&network) // Containers can communicate via this network |
| 236 | + .add(RedisTemplate::new(&redis1_name).port(port1)) |
| 237 | + .add(RedisTemplate::new(&redis2_name).port(port2)) |
| 238 | + .keep_on_panic(true) // Keep all containers for debugging |
| 239 | + .start_all() |
| 240 | + .await |
| 241 | + .expect("Failed to start container set"); |
| 242 | + |
| 243 | + // Verify both containers are in the set |
| 244 | + assert!(guards.contains(&redis1_name)); |
| 245 | + assert!(guards.contains(&redis2_name)); |
| 246 | + assert_eq!(guards.len(), 2); |
| 247 | + |
| 248 | + // Get the shared network |
| 249 | + assert_eq!(guards.network(), Some(network.as_str())); |
| 250 | + |
| 251 | + // List all container names |
| 252 | + println!("Running containers:"); |
| 253 | + for name in guards.names() { |
| 254 | + println!(" - {}", name); |
| 255 | + } |
| 256 | + |
| 257 | + // All containers and the network are cleaned up when guards is dropped |
| 258 | +} |
| 259 | + |
| 260 | +/// ContainerGuardSet with custom options. |
| 261 | +#[tokio::test] |
| 262 | +async fn test_guard_set_options() { |
| 263 | + let network = unique_name("options-net"); |
| 264 | + let port = unique_port(); |
| 265 | + |
| 266 | + let guards = ContainerGuardSet::new() |
| 267 | + .with_network(&network) |
| 268 | + .create_network(true) // Create network if it doesn't exist (default) |
| 269 | + .remove_network_on_drop(true) // Clean up network after test (default) |
| 270 | + .wait_for_ready(true) // Wait for all containers to be ready (default) |
| 271 | + .keep_on_panic(false) // Clean up even on panic |
| 272 | + .add(RedisTemplate::new(&unique_name("opt-redis")).port(port)) |
| 273 | + .start_all() |
| 274 | + .await |
| 275 | + .expect("Failed to start"); |
| 276 | + |
| 277 | + assert_eq!(guards.len(), 1); |
| 278 | + assert!(!guards.is_empty()); |
| 279 | +} |
| 280 | + |
| 281 | +// ============================================================================ |
| 282 | +// Accessing Container Information |
| 283 | +// ============================================================================ |
| 284 | + |
| 285 | +/// Accessing container details and logs. |
| 286 | +#[tokio::test] |
| 287 | +async fn test_container_info() { |
| 288 | + let name = unique_name("info-redis"); |
| 289 | + let port = unique_port(); |
| 290 | + |
| 291 | + let guard = ContainerGuard::new(RedisTemplate::new(&name).port(port)) |
| 292 | + .wait_for_ready(true) |
| 293 | + .start() |
| 294 | + .await |
| 295 | + .expect("Failed to start Redis"); |
| 296 | + |
| 297 | + // Get container ID (if available) |
| 298 | + if let Some(id) = guard.container_id() { |
| 299 | + println!("Container ID: {}", id); |
| 300 | + assert!(!id.is_empty()); |
| 301 | + } |
| 302 | + |
| 303 | + // Get connection string (includes the port) |
| 304 | + let conn = guard.connection_string(); |
| 305 | + println!("Connection string: {}", conn); |
| 306 | + assert!(conn.contains(&port.to_string())); |
| 307 | + |
| 308 | + // Get container logs |
| 309 | + let logs = guard.logs().await.expect("Failed to get logs"); |
| 310 | + println!("Container logs:\n{}", logs); |
| 311 | + assert!(logs.contains("Ready to accept connections")); |
| 312 | + |
| 313 | + // Check running status |
| 314 | + assert!(guard.is_running().await.expect("Failed to check status")); |
| 315 | +} |
| 316 | + |
| 317 | +// ============================================================================ |
| 318 | +// Test Fixtures Pattern |
| 319 | +// ============================================================================ |
| 320 | + |
| 321 | +/// Create reusable test fixtures for common setups. |
| 322 | +#[cfg(test)] |
| 323 | +async fn redis_fixture(name: &str, port: u16) -> ContainerGuard<RedisTemplate> { |
| 324 | + ContainerGuard::new(RedisTemplate::new(name).port(port)) |
| 325 | + .wait_for_ready(true) |
| 326 | + .keep_on_panic(true) |
| 327 | + .capture_logs(true) |
| 328 | + .stop_timeout(Duration::from_secs(2)) |
| 329 | + .start() |
| 330 | + .await |
| 331 | + .expect("Failed to create Redis fixture") |
| 332 | +} |
| 333 | + |
| 334 | +/// Using the test fixture pattern. |
| 335 | +#[tokio::test] |
| 336 | +async fn test_with_fixture() { |
| 337 | + let name = unique_name("fixture-redis"); |
| 338 | + let port = unique_port(); |
| 339 | + let redis = redis_fixture(&name, port).await; |
| 340 | + |
| 341 | + // Fixture is ready to use |
| 342 | + let conn = redis.connection_string(); |
| 343 | + println!("Fixture ready at: {}", conn); |
| 344 | + |
| 345 | + // Clean up is automatic |
| 346 | +} |
| 347 | + |
| 348 | +// ============================================================================ |
| 349 | +// Main |
| 350 | +// ============================================================================ |
| 351 | + |
| 352 | +fn main() { |
| 353 | + println!("Testing Utilities Example"); |
| 354 | + println!("========================="); |
| 355 | + println!(); |
| 356 | + println!("This example demonstrates docker-wrapper's testing utilities"); |
| 357 | + println!("for integration testing with automatic container lifecycle management."); |
| 358 | + println!(); |
| 359 | + println!("Run the tests with:"); |
| 360 | + println!(" cargo test --example testing_utilities --features testing"); |
| 361 | + println!(); |
| 362 | + println!("Run with output:"); |
| 363 | + println!(" cargo test --example testing_utilities --features testing -- --nocapture"); |
| 364 | + println!(); |
| 365 | + println!("Features demonstrated:"); |
| 366 | + println!(" - ContainerGuard: Single container RAII management"); |
| 367 | + println!(" - ContainerGuardSet: Multi-container test environments"); |
| 368 | + println!(" - Automatic cleanup on drop and panic"); |
| 369 | + println!(" - Ready checks and connection strings"); |
| 370 | + println!(" - Debug mode for failed tests"); |
| 371 | + println!(" - Container reuse for faster development"); |
| 372 | + println!(" - Test fixture patterns"); |
| 373 | +} |
0 commit comments