Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 82 additions & 4 deletions src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub struct GuardOptions {
pub capture_logs: bool,
/// Reuse existing container if already running (default: false)
pub reuse_if_running: bool,
/// Automatically wait for container to be ready after start (default: false)
pub wait_for_ready: bool,
}

impl Default for GuardOptions {
Expand All @@ -54,6 +56,7 @@ impl Default for GuardOptions {
keep_on_panic: false,
capture_logs: false,
reuse_if_running: false,
wait_for_ready: false,
}
}
}
Expand Down Expand Up @@ -118,38 +121,83 @@ impl<T: Template> ContainerGuardBuilder<T> {
self
}

/// Set whether to automatically wait for the container to be ready after starting (default: false).
///
/// When enabled, `start()` will not return until the container passes its
/// readiness check. This is useful for tests that need to immediately connect
/// to the service.
///
/// # Example
///
/// ```rust,no_run
/// # use docker_wrapper::testing::ContainerGuard;
/// # use docker_wrapper::RedisTemplate;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let guard = ContainerGuard::new(RedisTemplate::new("test"))
/// .wait_for_ready(true)
/// .start()
/// .await?;
/// // Container is guaranteed ready at this point
/// # Ok(())
/// # }
/// ```
#[must_use]
pub fn wait_for_ready(mut self, wait: bool) -> Self {
self.options.wait_for_ready = wait;
self
}

/// Start the container and return a guard that manages its lifecycle.
///
/// If `reuse_if_running` is enabled and a container is already running,
/// it will be reused instead of starting a new one.
///
/// If `wait_for_ready` is enabled, this method will block until the
/// container passes its readiness check.
///
/// # Errors
///
/// Returns an error if the container fails to start or the readiness check times out.
pub async fn start(self) -> Result<ContainerGuard<T>, TemplateError> {
let wait_for_ready = self.options.wait_for_ready;

// Check if we should reuse an existing container
if self.options.reuse_if_running {
if let Ok(true) = self.template.is_running().await {
return Ok(ContainerGuard {
let guard = ContainerGuard {
template: self.template,
container_id: None, // We don't have the ID for reused containers
options: self.options,
was_reused: true,
cleaned_up: Arc::new(AtomicBool::new(false)),
});
};

// Wait for ready if configured (even for reused containers)
if wait_for_ready {
guard.wait_for_ready().await?;
}

return Ok(guard);
}
}

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

Ok(ContainerGuard {
let guard = ContainerGuard {
template: self.template,
container_id: Some(container_id),
options: self.options,
was_reused: false,
cleaned_up: Arc::new(AtomicBool::new(false)),
})
};

// Wait for ready if configured
if wait_for_ready {
guard.wait_for_ready().await?;
}

Ok(guard)
}
}

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

/// Wait for the container to be ready.
///
/// This calls the underlying template's readiness check. The exact behavior
/// depends on the template implementation - for example, Redis templates
/// wait for a successful PING response.
///
/// # Errors
///
/// Returns an error if the readiness check times out or the Docker command fails.
///
/// # Example
///
/// ```rust,no_run
/// # use docker_wrapper::testing::ContainerGuard;
/// # use docker_wrapper::RedisTemplate;
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let guard = ContainerGuard::new(RedisTemplate::new("test"))
/// .start()
/// .await?;
///
/// // Wait for Redis to be ready to accept connections
/// guard.wait_for_ready().await?;
/// # Ok(())
/// # }
/// ```
pub async fn wait_for_ready(&self) -> Result<(), TemplateError> {
self.template.wait_for_ready().await
}

/// Get the host port mapped to a container port.
///
/// This is useful when using dynamic port allocation - Docker assigns
Expand Down Expand Up @@ -417,6 +494,7 @@ mod tests {
assert!(!opts.keep_on_panic);
assert!(!opts.capture_logs);
assert!(!opts.reuse_if_running);
assert!(!opts.wait_for_ready);
}

#[test]
Expand Down
47 changes: 47 additions & 0 deletions tests/container_guard_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,50 @@ async fn test_container_guard_manual_cleanup() {
"Container should be stopped after cleanup"
);
}

#[tokio::test]
async fn test_container_guard_wait_for_ready_method() {
let name = unique_name("guard-wait-ready");
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(next_port()))
.start()
.await
.expect("Failed to start container");

// Explicitly call wait_for_ready
guard
.wait_for_ready()
.await
.expect("Failed to wait for ready");

// Container should definitely be running and ready now
assert!(
guard.is_running().await.expect("Failed to check running"),
"Container should be running"
);

// Container cleanup happens on drop
}

#[tokio::test]
async fn test_container_guard_auto_wait_for_ready() {
let name = unique_name("guard-auto-wait");
let guard = ContainerGuard::new(RedisTemplate::new(&name).port(next_port()))
.wait_for_ready(true) // Enable auto-wait
.start()
.await
.expect("Failed to start container");

// Container should be immediately ready - no separate wait_for_ready call needed
assert!(
guard.is_running().await.expect("Failed to check running"),
"Container should be running"
);

// Calling wait_for_ready again should succeed immediately since already ready
guard
.wait_for_ready()
.await
.expect("wait_for_ready should succeed on already-ready container");

// Container cleanup happens on drop
}