Skip to content

Commit

Permalink
feat: expose follow flag for stdout and stderr (#640)
Browse files Browse the repository at this point in the history
It allows to read logs only until the moment of the call (i.e read all
currently accessible logs)
  • Loading branch information
DDtKey committed May 26, 2024
1 parent b4780a9 commit 032cc12
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 23 deletions.
35 changes: 23 additions & 12 deletions testcontainers/src/core/containers/async_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,22 @@ where
}

/// Returns an asynchronous reader for stdout.
pub fn stdout(&self) -> Pin<Box<dyn AsyncBufRead + Send>> {
let stdout = self.docker_client.stdout_logs(&self.id, true);
///
/// Accepts a boolean parameter to follow the logs:
/// - pass `true` to read logs from the moment the container starts until it stops (returns I/O error with kind [`UnexpectedEof`](std::io::ErrorKind::UnexpectedEof) if container removed).
/// - pass `false` to read logs from startup to present.
pub fn stdout(&self, follow: bool) -> Pin<Box<dyn AsyncBufRead + Send>> {
let stdout = self.docker_client.stdout_logs(&self.id, follow);
Box::pin(tokio_util::io::StreamReader::new(stdout.into_inner()))
}

/// Returns an asynchronous reader for stderr.
pub fn stderr(&self) -> Pin<Box<dyn AsyncBufRead + Send>> {
let stderr = self.docker_client.stderr_logs(&self.id, true);
///
/// Accepts a boolean parameter to follow the logs:
/// - pass `true` to read logs from the moment the container starts until it stops (returns I/O error with [`UnexpectedEof`](std::io::ErrorKind::UnexpectedEof) if container removed).
/// - pass `false` to read logs from startup to present.
pub fn stderr(&self, follow: bool) -> Pin<Box<dyn AsyncBufRead + Send>> {
let stderr = self.docker_client.stderr_logs(&self.id, follow);
Box::pin(tokio_util::io::StreamReader::new(stderr.into_inner()))
}

Expand Down Expand Up @@ -383,7 +391,7 @@ mod tests {
let image = GenericImage::new("testcontainers/helloworld", "1.1.0");
let container = RunnableImage::from(image).start().await?;

let stderr = container.stderr();
let stderr = container.stderr(true);

// it's possible to send logs into background task
let log_follower_task = tokio::spawn(async move {
Expand Down Expand Up @@ -417,24 +425,23 @@ mod tests {

// stdout is empty
let mut stdout = String::new();
container.stdout().read_to_string(&mut stdout).await?;
container.stdout(true).read_to_string(&mut stdout).await?;
assert_eq!(stdout, "");
// stderr contains 6 lines
let mut stderr = String::new();
container.stderr().read_to_string(&mut stderr).await?;
container.stderr(true).read_to_string(&mut stderr).await?;
assert_eq!(
stderr.lines().count(),
6,
"unexpected stderr size: {}",
stderr
"unexpected stderr size: {stderr}",
);

// start again to test eof on drop
container.start().await?;

// create logger task which reads logs from container up to EOF
let container_id = container.id().to_string();
let stderr = container.stderr();
let stderr = container.stderr(true);
let logger_task = tokio::spawn(async move {
let mut stderr_lines = stderr.lines();
while let Some(result) = stderr_lines.next_line().await.transpose() {
Expand All @@ -453,9 +460,13 @@ mod tests {
});

drop(container);
logger_task
let res = logger_task
.await
.map_err(|_| anyhow::anyhow!("failed to join log follower task"))??;
.map_err(|_| anyhow::anyhow!("failed to join log follower task"))?;
assert!(
res.is_ok(),
"UnexpectedEof is handled after dropping the container"
);

Ok(())
}
Expand Down
4 changes: 2 additions & 2 deletions testcontainers/src/core/containers/async_container/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ impl ExecResult {
Ok(res.exit_code)
}

/// Returns an asynchronous reader for stdout.
/// Returns an asynchronous reader for stdout. It follows log stream until the command exits.
pub fn stdout<'b>(&'b mut self) -> Pin<Box<dyn AsyncBufRead + 'b>> {
Box::pin(tokio_util::io::StreamReader::new(&mut self.stdout))
}

/// Returns an asynchronous reader for stderr.
/// Returns an asynchronous reader for stderr. It follows log stream until the command exits.
pub fn stderr<'b>(&'b mut self) -> Pin<Box<dyn AsyncBufRead + 'b>> {
Box::pin(tokio_util::io::StreamReader::new(&mut self.stderr))
}
Expand Down
22 changes: 15 additions & 7 deletions testcontainers/src/core/containers/sync_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,25 @@ where
}

/// Returns a reader for stdout.
pub fn stdout(&self) -> Box<dyn BufRead + Send> {
///
/// Accepts a boolean parameter to follow the logs:
/// - pass `true` to read logs from the moment the container starts until it stops (returns I/O error with kind `UnexpectedEof` if container removed).
/// - pass `false` to read logs from startup to present.
pub fn stdout(&self, follow: bool) -> Box<dyn BufRead + Send> {
Box::new(sync_reader::SyncReadBridge::new(
self.async_impl().stdout(),
self.async_impl().stdout(follow),
self.rt().clone(),
))
}

/// Returns a reader for stderr.
pub fn stderr(&self) -> Box<dyn BufRead + Send> {
///
/// Accepts a boolean parameter to follow the logs:
/// - pass `true` to read logs from the moment the container starts until it stops (returns I/O error with kind `UnexpectedEof` if container removed).
/// - pass `false` to read logs from startup to present.
pub fn stderr(&self, follow: bool) -> Box<dyn BufRead + Send> {
Box::new(sync_reader::SyncReadBridge::new(
self.async_impl().stderr(),
self.async_impl().stderr(follow),
self.rt().clone(),
))
}
Expand Down Expand Up @@ -226,7 +234,7 @@ mod test {
let image = GenericImage::new("testcontainers/helloworld", "1.1.0");
let container = RunnableImage::from(image).start()?;

let stderr = container.stderr();
let stderr = container.stderr(true);

// it's possible to send logs to another thread
let log_follower_thread = std::thread::spawn(move || {
Expand Down Expand Up @@ -260,11 +268,11 @@ mod test {

// stdout is empty
let mut stdout = String::new();
container.stdout().read_to_string(&mut stdout)?;
container.stdout(true).read_to_string(&mut stdout)?;
assert_eq!(stdout, "");
// stderr contains 6 lines
let mut stderr = String::new();
container.stderr().read_to_string(&mut stderr)?;
container.stderr(true).read_to_string(&mut stderr)?;
assert_eq!(
stderr.lines().count(),
6,
Expand Down
13 changes: 11 additions & 2 deletions testcontainers/src/core/containers/sync_container/exec.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use std::{fmt, io::BufRead, sync::Arc};

use crate::{core::sync_container::sync_reader, TestcontainersError};
use crate::{
core::{async_container, sync_container::sync_reader},
TestcontainersError,
};

/// Represents the result of an executed command in a container.
pub struct SyncExecResult {
pub(super) inner: crate::core::async_container::exec::ExecResult,
pub(super) inner: async_container::exec::ExecResult,
pub(super) runtime: Arc<tokio::runtime::Runtime>,
}

Expand Down Expand Up @@ -32,11 +35,17 @@ impl SyncExecResult {
}

/// Returns stdout as a vector of bytes.
/// Keep in mind that this will block until the command exits.
///
/// If you want to read stderr in chunks, use [`SyncExecResult::stdout`] instead.
pub fn stdout_to_vec(&mut self) -> Result<Vec<u8>, TestcontainersError> {
self.runtime.block_on(self.inner.stdout_to_vec())
}

/// Returns stderr as a vector of bytes.
/// Keep in mind that this will block until the command exits.
///
/// If you want to read stderr in chunks, use [`SyncExecResult::stderr`] instead.
pub fn stderr_to_vec(&mut self) -> Result<Vec<u8>, TestcontainersError> {
self.runtime.block_on(self.inner.stderr_to_vec())
}
Expand Down

0 comments on commit 032cc12

Please sign in to comment.