Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.testcontainers.DockerClientFactory;
import org.testcontainers.UnstableAPI;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
import org.testcontainers.containers.startupcheck.MinimumDurationRunningStartupCheckStrategy;
import org.testcontainers.containers.startupcheck.StartupCheckStrategy;
Expand Down Expand Up @@ -184,6 +185,11 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>

private List<Consumer<OutputFrame>> logConsumers = new ArrayList<>();

/**
* In-memory log buffer used by {@link #withLogCapture()}.
*/
private ToStringConsumer capturedLogsConsumer;

private static final Set<String> AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>();

@Nullable
Expand Down Expand Up @@ -1362,6 +1368,34 @@ public SELF withLogConsumer(Consumer<OutputFrame> consumer) {
return self();
}

/**
* Enables in-memory capture of container logs.
* <p>
* This is useful for debugging startup failures or containers that exit quickly,
* where {@link #getLogs()} may return empty output.
*
* @return this container instance
*/
public SELF withLogCapture() {
if (capturedLogsConsumer == null) {
capturedLogsConsumer = new ToStringConsumer();
withLogConsumer(capturedLogsConsumer);
}
return self();
}

/**
* Returns logs captured in memory when {@link #withLogCapture()} is enabled.
* <p>
* This is useful for debugging startup failures or containers that exit quickly,
* where {@link #getLogs()} may return empty output.
*
* @return captured container output (stdout and stderr), or an empty string if log capture is not enabled
*/
public String getCapturedLogs() {
return capturedLogsConsumer != null ? capturedLogsConsumer.toUtf8String() : "";
}

/**
* {@inheritDoc}
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package org.testcontainers.containers;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;

import java.time.Duration;

/**
* Tests for in-memory log capture in {@link GenericContainer}.
* <p>
* These tests verify that container logs can be captured even when container startup fails or the
* container exits immediately after producing output.
*/
class GenericContainerLogCaptureTest {

private static final DockerImageName TEST_IMAGE =
DockerImageName.parse("busybox:1.36");

private static final String CONTAINER_LOG_MESSAGE =
"container startup output";

/**
* A log pattern that is intentionally never produced by the container. Used to force startup
* failure via {@link Wait#forLogMessage(String, int)}.
*/
private static final String NON_MATCHING_LOG_PATTERN =
"this-log-message-will-never-appear";

private static final Duration STARTUP_TIMEOUT =
Duration.ofSeconds(2);

@Test
void shouldNotCaptureLogsByDefaultWhenStartupFails() {
try (GenericContainer<?> container = createFailingContainer()) {

Assertions.assertThrows(
ContainerLaunchException.class,
container::start,
"Container startup should fail due to unmet wait condition"
);

Assertions.assertTrue(
container.getCapturedLogs().isEmpty(),
"Captured logs should be empty when log capture is not enabled"
);
}
}

@Test
void shouldCaptureLogsWhenEnabledEvenIfStartupFails() {
try (GenericContainer<?> container =
createFailingContainer().withLogCapture()) {

Assertions.assertThrows(
ContainerLaunchException.class,
container::start,
"Container startup should fail due to unmet wait condition"
);

String capturedLogs = container.getCapturedLogs();

Assertions.assertFalse(
capturedLogs.isEmpty(),
"Captured logs should not be empty when log capture is enabled"
);

Assertions.assertTrue(
capturedLogs.contains(CONTAINER_LOG_MESSAGE),
"Captured logs should contain the container output"
);
}
}

/**
* Creates a container configuration that:
* <ul>
* <li>Produces a single line of output</li>
* <li>Exits immediately</li>
* <li>Fails startup due to an unmet log-based wait condition</li>
* </ul>
*
* @return a configured {@link GenericContainer} instance
*/
private static GenericContainer<?> createFailingContainer() {
return new GenericContainer<>(TEST_IMAGE)
.withStartupCheckStrategy(new OneShotStartupCheckStrategy())
.waitingFor(
Wait.forLogMessage(NON_MATCHING_LOG_PATTERN, 1)
.withStartupTimeout(STARTUP_TIMEOUT)
)
.withCommand("sh", "-c", "echo \"" + CONTAINER_LOG_MESSAGE + "\"");
}
}