|
1 | | -//! Testing utilities for container lifecycle management. |
| 1 | +//! # Testing Utilities |
2 | 2 | //! |
3 | | -//! This module provides [`ContainerGuard`], an RAII wrapper that automatically |
4 | | -//! manages container lifecycle. When the guard goes out of scope, containers |
5 | | -//! are stopped and removed automatically. |
| 3 | +//! RAII-style container lifecycle management for integration tests. |
6 | 4 | //! |
7 | | -//! # Example |
| 5 | +//! This module provides [`ContainerGuard`] and [`ContainerGuardSet`] for automatic |
| 6 | +//! container lifecycle management. Containers are automatically stopped and removed |
| 7 | +//! when guards go out of scope, ensuring clean test environments. |
| 8 | +//! |
| 9 | +//! ## Why Use This? |
| 10 | +//! |
| 11 | +//! - **Automatic cleanup**: No more forgotten containers cluttering your Docker |
| 12 | +//! - **Panic-safe**: Containers are cleaned up even if your test panics |
| 13 | +//! - **Debug-friendly**: Keep containers alive on failure for inspection |
| 14 | +//! - **Network support**: Automatic network creation for multi-container tests |
| 15 | +//! - **Ready checks**: Wait for services to be ready before running tests |
| 16 | +//! |
| 17 | +//! ## Quick Start |
8 | 18 | //! |
9 | 19 | //! ```rust,no_run |
10 | 20 | //! use docker_wrapper::testing::ContainerGuard; |
11 | 21 | //! use docker_wrapper::RedisTemplate; |
12 | 22 | //! |
13 | 23 | //! #[tokio::test] |
14 | | -//! async fn test_redis() -> Result<(), Box<dyn std::error::Error>> { |
| 24 | +//! async fn test_with_redis() -> Result<(), Box<dyn std::error::Error>> { |
| 25 | +//! // Container starts and waits for Redis to be ready |
15 | 26 | //! let guard = ContainerGuard::new(RedisTemplate::new("test-redis")) |
| 27 | +//! .wait_for_ready(true) |
16 | 28 | //! .start() |
17 | 29 | //! .await?; |
18 | 30 | //! |
19 | | -//! // Use the container... |
20 | | -//! let url = guard.template().connection_url(); |
| 31 | +//! // Get connection string directly from guard |
| 32 | +//! let url = guard.connection_string(); |
| 33 | +//! // Use Redis at: redis://localhost:6379 |
21 | 34 | //! |
22 | 35 | //! Ok(()) |
23 | 36 | //! // Container automatically stopped and removed here |
24 | 37 | //! } |
25 | 38 | //! ``` |
| 39 | +//! |
| 40 | +//! ## Configuration Options |
| 41 | +//! |
| 42 | +//! ### Lifecycle Control |
| 43 | +//! |
| 44 | +//! ```rust,no_run |
| 45 | +//! # use docker_wrapper::testing::ContainerGuard; |
| 46 | +//! # use docker_wrapper::RedisTemplate; |
| 47 | +//! # async fn example() -> Result<(), Box<dyn std::error::Error>> { |
| 48 | +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) |
| 49 | +//! .stop_on_drop(true) // Stop container on drop (default: true) |
| 50 | +//! .remove_on_drop(true) // Remove container on drop (default: true) |
| 51 | +//! .start() |
| 52 | +//! .await?; |
| 53 | +//! # Ok(()) |
| 54 | +//! # } |
| 55 | +//! ``` |
| 56 | +//! |
| 57 | +//! ### Debugging Failed Tests |
| 58 | +//! |
| 59 | +//! Keep containers running when tests fail for debugging: |
| 60 | +//! |
| 61 | +//! ```rust,no_run |
| 62 | +//! # use docker_wrapper::testing::ContainerGuard; |
| 63 | +//! # use docker_wrapper::RedisTemplate; |
| 64 | +//! # async fn example() -> Result<(), Box<dyn std::error::Error>> { |
| 65 | +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) |
| 66 | +//! .keep_on_panic(true) // Keep container if test panics |
| 67 | +//! .capture_logs(true) // Print container logs on panic |
| 68 | +//! .start() |
| 69 | +//! .await?; |
| 70 | +//! # Ok(()) |
| 71 | +//! # } |
| 72 | +//! ``` |
| 73 | +//! |
| 74 | +//! ### Ready Checks |
| 75 | +//! |
| 76 | +//! Wait for the service to be ready before proceeding: |
| 77 | +//! |
| 78 | +//! ```rust,no_run |
| 79 | +//! # use docker_wrapper::testing::ContainerGuard; |
| 80 | +//! # use docker_wrapper::RedisTemplate; |
| 81 | +//! # async fn example() -> Result<(), Box<dyn std::error::Error>> { |
| 82 | +//! // Automatic wait during start |
| 83 | +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) |
| 84 | +//! .wait_for_ready(true) |
| 85 | +//! .start() |
| 86 | +//! .await?; |
| 87 | +//! // Redis is guaranteed ready here |
| 88 | +//! |
| 89 | +//! // Or wait manually later |
| 90 | +//! let guard2 = ContainerGuard::new(RedisTemplate::new("redis2")) |
| 91 | +//! .start() |
| 92 | +//! .await?; |
| 93 | +//! guard2.wait_for_ready().await?; |
| 94 | +//! # Ok(()) |
| 95 | +//! # } |
| 96 | +//! ``` |
| 97 | +//! |
| 98 | +//! ### Container Reuse |
| 99 | +//! |
| 100 | +//! Speed up local development by reusing running containers: |
| 101 | +//! |
| 102 | +//! ```rust,no_run |
| 103 | +//! # use docker_wrapper::testing::ContainerGuard; |
| 104 | +//! # use docker_wrapper::RedisTemplate; |
| 105 | +//! # async fn example() -> Result<(), Box<dyn std::error::Error>> { |
| 106 | +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) |
| 107 | +//! .reuse_if_running(true) // Reuse existing container if found |
| 108 | +//! .remove_on_drop(false) // Keep it for next test run |
| 109 | +//! .stop_on_drop(false) |
| 110 | +//! .start() |
| 111 | +//! .await?; |
| 112 | +//! |
| 113 | +//! if guard.was_reused() { |
| 114 | +//! println!("Reused existing container"); |
| 115 | +//! } |
| 116 | +//! # Ok(()) |
| 117 | +//! # } |
| 118 | +//! ``` |
| 119 | +//! |
| 120 | +//! ### Network Support |
| 121 | +//! |
| 122 | +//! Attach containers to custom networks: |
| 123 | +//! |
| 124 | +//! ```rust,no_run |
| 125 | +//! # use docker_wrapper::testing::ContainerGuard; |
| 126 | +//! # use docker_wrapper::RedisTemplate; |
| 127 | +//! # async fn example() -> Result<(), Box<dyn std::error::Error>> { |
| 128 | +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) |
| 129 | +//! .with_network("my-test-network") // Create and attach to network |
| 130 | +//! .remove_network_on_drop(true) // Clean up network after test |
| 131 | +//! .start() |
| 132 | +//! .await?; |
| 133 | +//! # Ok(()) |
| 134 | +//! # } |
| 135 | +//! ``` |
| 136 | +//! |
| 137 | +//! ### Fast Cleanup |
| 138 | +//! |
| 139 | +//! Use a short stop timeout for faster test cleanup: |
| 140 | +//! |
| 141 | +//! ```rust,no_run |
| 142 | +//! # use docker_wrapper::testing::ContainerGuard; |
| 143 | +//! # use docker_wrapper::RedisTemplate; |
| 144 | +//! # use std::time::Duration; |
| 145 | +//! # async fn example() -> Result<(), Box<dyn std::error::Error>> { |
| 146 | +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) |
| 147 | +//! .stop_timeout(Duration::from_secs(1)) // 1 second graceful shutdown |
| 148 | +//! .start() |
| 149 | +//! .await?; |
| 150 | +//! |
| 151 | +//! // Or immediate SIGKILL |
| 152 | +//! let guard2 = ContainerGuard::new(RedisTemplate::new("redis2")) |
| 153 | +//! .stop_timeout(Duration::ZERO) |
| 154 | +//! .start() |
| 155 | +//! .await?; |
| 156 | +//! # Ok(()) |
| 157 | +//! # } |
| 158 | +//! ``` |
| 159 | +//! |
| 160 | +//! ## Multi-Container Tests |
| 161 | +//! |
| 162 | +//! Use [`ContainerGuardSet`] for tests requiring multiple services: |
| 163 | +//! |
| 164 | +//! ```rust,no_run |
| 165 | +//! use docker_wrapper::testing::ContainerGuardSet; |
| 166 | +//! use docker_wrapper::RedisTemplate; |
| 167 | +//! |
| 168 | +//! #[tokio::test] |
| 169 | +//! async fn test_multi_container() -> Result<(), Box<dyn std::error::Error>> { |
| 170 | +//! let guards = ContainerGuardSet::new() |
| 171 | +//! .with_network("test-network") // Shared network for all containers |
| 172 | +//! .add(RedisTemplate::new("redis-primary").port(6379)) |
| 173 | +//! .add(RedisTemplate::new("redis-replica").port(6380)) |
| 174 | +//! .keep_on_panic(true) |
| 175 | +//! .start_all() |
| 176 | +//! .await?; |
| 177 | +//! |
| 178 | +//! assert!(guards.contains("redis-primary")); |
| 179 | +//! assert!(guards.contains("redis-replica")); |
| 180 | +//! assert_eq!(guards.len(), 2); |
| 181 | +//! |
| 182 | +//! // All containers cleaned up together |
| 183 | +//! Ok(()) |
| 184 | +//! } |
| 185 | +//! ``` |
| 186 | +//! |
| 187 | +//! ## Accessing Container Information |
| 188 | +//! |
| 189 | +//! ```rust,no_run |
| 190 | +//! # use docker_wrapper::testing::ContainerGuard; |
| 191 | +//! # use docker_wrapper::RedisTemplate; |
| 192 | +//! # async fn example() -> Result<(), Box<dyn std::error::Error>> { |
| 193 | +//! let guard = ContainerGuard::new(RedisTemplate::new("redis").port(6379)) |
| 194 | +//! .start() |
| 195 | +//! .await?; |
| 196 | +//! |
| 197 | +//! // Connection string (for templates that support it) |
| 198 | +//! let conn = guard.connection_string(); |
| 199 | +//! |
| 200 | +//! // Access underlying template |
| 201 | +//! let template = guard.template(); |
| 202 | +//! |
| 203 | +//! // Get container ID |
| 204 | +//! if let Some(id) = guard.container_id() { |
| 205 | +//! println!("Container ID: {}", id); |
| 206 | +//! } |
| 207 | +//! |
| 208 | +//! // Query host port for a container port |
| 209 | +//! let host_port = guard.host_port(6379).await?; |
| 210 | +//! |
| 211 | +//! // Get container logs |
| 212 | +//! let logs = guard.logs().await?; |
| 213 | +//! |
| 214 | +//! // Check if running |
| 215 | +//! let running = guard.is_running().await?; |
| 216 | +//! # Ok(()) |
| 217 | +//! # } |
| 218 | +//! ``` |
| 219 | +//! |
| 220 | +//! ## Common Patterns |
| 221 | +//! |
| 222 | +//! ### Test Fixtures |
| 223 | +//! |
| 224 | +//! Create reusable test fixtures: |
| 225 | +//! |
| 226 | +//! ```rust,no_run |
| 227 | +//! use docker_wrapper::testing::ContainerGuard; |
| 228 | +//! use docker_wrapper::RedisTemplate; |
| 229 | +//! use docker_wrapper::template::TemplateError; |
| 230 | +//! |
| 231 | +//! async fn redis_fixture(name: &str) -> Result<ContainerGuard<RedisTemplate>, TemplateError> { |
| 232 | +//! ContainerGuard::new(RedisTemplate::new(name)) |
| 233 | +//! .wait_for_ready(true) |
| 234 | +//! .keep_on_panic(true) |
| 235 | +//! .capture_logs(true) |
| 236 | +//! .start() |
| 237 | +//! .await |
| 238 | +//! } |
| 239 | +//! |
| 240 | +//! #[tokio::test] |
| 241 | +//! async fn test_using_fixture() -> Result<(), Box<dyn std::error::Error>> { |
| 242 | +//! let redis = redis_fixture("test-redis").await?; |
| 243 | +//! // Use redis... |
| 244 | +//! Ok(()) |
| 245 | +//! } |
| 246 | +//! ``` |
| 247 | +//! |
| 248 | +//! ### Unique Container Names |
| 249 | +//! |
| 250 | +//! Use UUIDs to avoid name conflicts in parallel tests: |
| 251 | +//! |
| 252 | +//! ```rust,no_run |
| 253 | +//! # use docker_wrapper::testing::ContainerGuard; |
| 254 | +//! # use docker_wrapper::RedisTemplate; |
| 255 | +//! fn unique_name(prefix: &str) -> String { |
| 256 | +//! format!("{}-{}", prefix, uuid::Uuid::new_v4()) |
| 257 | +//! } |
| 258 | +//! |
| 259 | +//! #[tokio::test] |
| 260 | +//! async fn test_parallel_safe() -> Result<(), Box<dyn std::error::Error>> { |
| 261 | +//! let name = unique_name("redis"); |
| 262 | +//! let guard = ContainerGuard::new(RedisTemplate::new(&name)) |
| 263 | +//! .start() |
| 264 | +//! .await?; |
| 265 | +//! Ok(()) |
| 266 | +//! } |
| 267 | +//! ``` |
| 268 | +//! |
| 269 | +//! ### Manual Cleanup |
| 270 | +//! |
| 271 | +//! Trigger cleanup explicitly when needed: |
| 272 | +//! |
| 273 | +//! ```rust,no_run |
| 274 | +//! # use docker_wrapper::testing::ContainerGuard; |
| 275 | +//! # use docker_wrapper::RedisTemplate; |
| 276 | +//! # async fn example() -> Result<(), Box<dyn std::error::Error>> { |
| 277 | +//! let guard = ContainerGuard::new(RedisTemplate::new("redis")) |
| 278 | +//! .start() |
| 279 | +//! .await?; |
| 280 | +//! |
| 281 | +//! // Do some work... |
| 282 | +//! |
| 283 | +//! // Explicitly cleanup (idempotent - safe to call multiple times) |
| 284 | +//! guard.cleanup().await?; |
| 285 | +//! |
| 286 | +//! // Drop will not try to clean up again |
| 287 | +//! # Ok(()) |
| 288 | +//! # } |
| 289 | +//! ``` |
| 290 | +//! |
| 291 | +//! ## Feature Flag |
| 292 | +//! |
| 293 | +//! This module requires the `testing` feature: |
| 294 | +//! |
| 295 | +//! ```toml |
| 296 | +//! [dev-dependencies] |
| 297 | +//! docker-wrapper = { version = "0.10", features = ["testing", "template-redis"] } |
| 298 | +//! ``` |
26 | 299 |
|
27 | 300 | use crate::command::DockerCommand; |
28 | 301 | use crate::template::{HasConnectionString, Template, TemplateError}; |
|
0 commit comments