Skip to content

Commit f497dd2

Browse files
feat: add wait_for_ready() method and builder option to ContainerGuard (#225)
- Add wait_for_ready() convenience method on ContainerGuard that delegates to the underlying template's readiness check - Add wait_for_ready builder option to automatically wait during start() - Works for both new and reused containers Closes #218
1 parent 282f735 commit f497dd2

2 files changed

Lines changed: 129 additions & 4 deletions

File tree

src/testing.rs

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ pub struct GuardOptions {
4444
pub capture_logs: bool,
4545
/// Reuse existing container if already running (default: false)
4646
pub reuse_if_running: bool,
47+
/// Automatically wait for container to be ready after start (default: false)
48+
pub wait_for_ready: bool,
4749
}
4850

4951
impl Default for GuardOptions {
@@ -54,6 +56,7 @@ impl Default for GuardOptions {
5456
keep_on_panic: false,
5557
capture_logs: false,
5658
reuse_if_running: false,
59+
wait_for_ready: false,
5760
}
5861
}
5962
}
@@ -118,38 +121,83 @@ impl<T: Template> ContainerGuardBuilder<T> {
118121
self
119122
}
120123

124+
/// Set whether to automatically wait for the container to be ready after starting (default: false).
125+
///
126+
/// When enabled, `start()` will not return until the container passes its
127+
/// readiness check. This is useful for tests that need to immediately connect
128+
/// to the service.
129+
///
130+
/// # Example
131+
///
132+
/// ```rust,no_run
133+
/// # use docker_wrapper::testing::ContainerGuard;
134+
/// # use docker_wrapper::RedisTemplate;
135+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
136+
/// let guard = ContainerGuard::new(RedisTemplate::new("test"))
137+
/// .wait_for_ready(true)
138+
/// .start()
139+
/// .await?;
140+
/// // Container is guaranteed ready at this point
141+
/// # Ok(())
142+
/// # }
143+
/// ```
144+
#[must_use]
145+
pub fn wait_for_ready(mut self, wait: bool) -> Self {
146+
self.options.wait_for_ready = wait;
147+
self
148+
}
149+
121150
/// Start the container and return a guard that manages its lifecycle.
122151
///
123152
/// If `reuse_if_running` is enabled and a container is already running,
124153
/// it will be reused instead of starting a new one.
125154
///
155+
/// If `wait_for_ready` is enabled, this method will block until the
156+
/// container passes its readiness check.
157+
///
126158
/// # Errors
127159
///
128160
/// Returns an error if the container fails to start or the readiness check times out.
129161
pub async fn start(self) -> Result<ContainerGuard<T>, TemplateError> {
162+
let wait_for_ready = self.options.wait_for_ready;
163+
130164
// Check if we should reuse an existing container
131165
if self.options.reuse_if_running {
132166
if let Ok(true) = self.template.is_running().await {
133-
return Ok(ContainerGuard {
167+
let guard = ContainerGuard {
134168
template: self.template,
135169
container_id: None, // We don't have the ID for reused containers
136170
options: self.options,
137171
was_reused: true,
138172
cleaned_up: Arc::new(AtomicBool::new(false)),
139-
});
173+
};
174+
175+
// Wait for ready if configured (even for reused containers)
176+
if wait_for_ready {
177+
guard.wait_for_ready().await?;
178+
}
179+
180+
return Ok(guard);
140181
}
141182
}
142183

143184
// Start the container
144185
let container_id = self.template.start_and_wait().await?;
145186

146-
Ok(ContainerGuard {
187+
let guard = ContainerGuard {
147188
template: self.template,
148189
container_id: Some(container_id),
149190
options: self.options,
150191
was_reused: false,
151192
cleaned_up: Arc::new(AtomicBool::new(false)),
152-
})
193+
};
194+
195+
// Wait for ready if configured
196+
if wait_for_ready {
197+
guard.wait_for_ready().await?;
198+
}
199+
200+
Ok(guard)
153201
}
154202
}
155203

@@ -223,6 +271,35 @@ impl<T: Template> ContainerGuard<T> {
223271
self.template.is_running().await
224272
}
225273

274+
/// Wait for the container to be ready.
275+
///
276+
/// This calls the underlying template's readiness check. The exact behavior
277+
/// depends on the template implementation - for example, Redis templates
278+
/// wait for a successful PING response.
279+
///
280+
/// # Errors
281+
///
282+
/// Returns an error if the readiness check times out or the Docker command fails.
283+
///
284+
/// # Example
285+
///
286+
/// ```rust,no_run
287+
/// # use docker_wrapper::testing::ContainerGuard;
288+
/// # use docker_wrapper::RedisTemplate;
289+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
290+
/// let guard = ContainerGuard::new(RedisTemplate::new("test"))
291+
/// .start()
292+
/// .await?;
293+
///
294+
/// // Wait for Redis to be ready to accept connections
295+
/// guard.wait_for_ready().await?;
296+
/// # Ok(())
297+
/// # }
298+
/// ```
299+
pub async fn wait_for_ready(&self) -> Result<(), TemplateError> {
300+
self.template.wait_for_ready().await
301+
}
302+
226303
/// Get the host port mapped to a container port.
227304
///
228305
/// This is useful when using dynamic port allocation - Docker assigns
@@ -417,6 +494,7 @@ mod tests {
417494
assert!(!opts.keep_on_panic);
418495
assert!(!opts.capture_logs);
419496
assert!(!opts.reuse_if_running);
497+
assert!(!opts.wait_for_ready);
420498
}
421499

422500
#[test]

tests/container_guard_integration.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,50 @@ async fn test_container_guard_manual_cleanup() {
170170
"Container should be stopped after cleanup"
171171
);
172172
}
173+
174+
#[tokio::test]
175+
async fn test_container_guard_wait_for_ready_method() {
176+
let name = unique_name("guard-wait-ready");
177+
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(next_port()))
178+
.start()
179+
.await
180+
.expect("Failed to start container");
181+
182+
// Explicitly call wait_for_ready
183+
guard
184+
.wait_for_ready()
185+
.await
186+
.expect("Failed to wait for ready");
187+
188+
// Container should definitely be running and ready now
189+
assert!(
190+
guard.is_running().await.expect("Failed to check running"),
191+
"Container should be running"
192+
);
193+
194+
// Container cleanup happens on drop
195+
}
196+
197+
#[tokio::test]
198+
async fn test_container_guard_auto_wait_for_ready() {
199+
let name = unique_name("guard-auto-wait");
200+
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(next_port()))
201+
.wait_for_ready(true) // Enable auto-wait
202+
.start()
203+
.await
204+
.expect("Failed to start container");
205+
206+
// Container should be immediately ready - no separate wait_for_ready call needed
207+
assert!(
208+
guard.is_running().await.expect("Failed to check running"),
209+
"Container should be running"
210+
);
211+
212+
// Calling wait_for_ready again should succeed immediately since already ready
213+
guard
214+
.wait_for_ready()
215+
.await
216+
.expect("wait_for_ready should succeed on already-ready container");
217+
218+
// Container cleanup happens on drop
219+
}

0 commit comments

Comments
 (0)