Skip to content

Commit f3d7689

Browse files
committed
feat: add Container*::exit_code
1 parent 547e24e commit f3d7689

File tree

5 files changed

+69
-3
lines changed

5 files changed

+69
-3
lines changed

testcontainers/src/core/client.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use std::{
77
use bollard::{
88
auth::DockerCredentials,
99
container::{
10-
Config, CreateContainerOptions, ListContainersOptions, LogOutput, LogsOptions,
11-
RemoveContainerOptions, UploadToContainerOptions,
10+
Config, CreateContainerOptions, InspectContainerOptions, ListContainersOptions, LogOutput,
11+
LogsOptions, RemoveContainerOptions, UploadToContainerOptions,
1212
},
1313
errors::Error as BollardError,
1414
exec::{CreateExecOptions, StartExecOptions, StartExecResults},
@@ -340,6 +340,24 @@ impl Client {
340340
.map_err(ClientError::UploadToContainerError)
341341
}
342342

343+
pub(crate) async fn container_exit_code(
344+
&self,
345+
container_id: &str,
346+
) -> Result<Option<i64>, ClientError> {
347+
let container_info = self
348+
.bollard
349+
.inspect_container(container_id, Some(InspectContainerOptions { size: false }))
350+
.await
351+
.map_err(ClientError::InspectContainer)?;
352+
let Some(state) = container_info.state else {
353+
return Ok(None);
354+
};
355+
if state.running == Some(true) {
356+
return Ok(None);
357+
}
358+
Ok(state.exit_code)
359+
}
360+
343361
pub(crate) async fn pull_image(&self, descriptor: &str) -> Result<(), ClientError> {
344362
let pull_options = Some(CreateImageOptions {
345363
from_image: descriptor,

testcontainers/src/core/containers/async_container.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,12 @@ where
369369
Ok(stderr)
370370
}
371371

372+
/// Returns `Some(exit_code)` when the container is finished and `None` when the container is still running.
373+
pub async fn exit_code(&self) -> Result<Option<i64>> {
374+
let exit_code = self.docker_client.container_exit_code(&self.id).await?;
375+
Ok(exit_code)
376+
}
377+
372378
pub(crate) async fn block_until_ready(&self, ready_conditions: Vec<WaitFor>) -> Result<()> {
373379
log::debug!("Waiting for container {} to be ready", self.id);
374380
let id = self.id();

testcontainers/src/core/containers/sync_container.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ where
209209
Ok(stderr)
210210
}
211211

212+
/// Returns `Some(exit_code)` when the container is finished and `None` when the container is still running.
213+
pub fn exit_code(&self) -> Result<Option<i64>> {
214+
self.rt().block_on(self.async_impl().exit_code())
215+
}
216+
212217
/// Returns reference to inner `Runtime`. It's safe to unwrap because it's `Some` until `Container` is dropped.
213218
fn rt(&self) -> &Arc<tokio::runtime::Runtime> {
214219
&self.inner.as_ref().unwrap().runtime

testcontainers/tests/async_runner.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,3 +267,22 @@ async fn async_copy_files_to_container() -> anyhow::Result<()> {
267267

268268
Ok(())
269269
}
270+
271+
#[tokio::test]
272+
async fn async_container_exit_code() -> anyhow::Result<()> {
273+
let _ = pretty_env_logger::try_init();
274+
275+
// Container that should run for 1 second and exit with a specific doe
276+
let container = GenericImage::new("alpine", "latest")
277+
.with_cmd(vec!["/bin/sh", "-c", "sleep 1 && exit 4"])
278+
.start()
279+
.await?;
280+
281+
assert_eq!(container.exit_code().await?, None);
282+
283+
// After waiting for two seconds it shouldn't be running anymore
284+
tokio::time::sleep(Duration::from_secs(2)).await;
285+
assert_eq!(container.exit_code().await?, Some(4));
286+
287+
Ok(())
288+
}

testcontainers/tests/sync_runner.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#![cfg(feature = "blocking")]
22

3-
use std::time::Instant;
3+
use std::time::{Duration, Instant};
44

55
use testcontainers::{
66
core::{
@@ -287,3 +287,21 @@ fn sync_copy_files_to_container() -> anyhow::Result<()> {
287287

288288
Ok(())
289289
}
290+
291+
#[test]
292+
fn sync_container_exit_code() -> anyhow::Result<()> {
293+
let _ = pretty_env_logger::try_init();
294+
295+
// Container that should run for 1 second and exit with a specific doe
296+
let container = GenericImage::new("alpine", "latest")
297+
.with_cmd(vec!["/bin/sh", "-c", "sleep 1 && exit 4"])
298+
.start()?;
299+
300+
assert_eq!(container.exit_code()?, None);
301+
302+
// After waiting for two seconds it shouldn't be running anymore
303+
std::thread::sleep(Duration::from_secs(2));
304+
assert_eq!(container.exit_code()?, Some(4));
305+
306+
Ok(())
307+
}

0 commit comments

Comments
 (0)