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 @@ -2,12 +2,19 @@

import com.github.dockerjava.api.command.LogContainerCmd;
import lombok.SneakyThrows;
import org.awaitility.Awaitility;
import org.awaitility.core.TerminalFailureException;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.output.FrameConsumerResultCallback;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.WaitingConsumer;

import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
Expand All @@ -19,7 +26,7 @@ public class LogMessageWaitStrategy extends AbstractWaitStrategy {
private int times = 1;

@Override
@SneakyThrows(IOException.class)
@SneakyThrows({ IOException.class, InterruptedException.class })
protected void waitUntilReady() {
WaitingConsumer waitingConsumer = new WaitingConsumer();

Expand All @@ -41,9 +48,43 @@ protected void waitUntilReady() {
// (?s) enables line terminator matching (equivalent to Pattern.DOTALL)
return outputFrame.getUtf8String().matches("(?s)" + regEx);
};
try {

// Run the log wait in the background, so that this thread can abort as soon as the
// container exits, instead of waiting for the whole startup timeout.
final Future<Boolean> logWaitFuture = EXECUTOR.submit(() -> {
waitingConsumer.waitUntil(waitPredicate, startupTimeout.getSeconds(), TimeUnit.SECONDS, times);
} catch (TimeoutException e) {
return true;
});

final Callable<Boolean> logWaitComplete = logWaitFuture::isDone;
try {
Awaitility
.await()
.pollInSameThread()
.pollInterval(Duration.ofMillis(100))
.pollDelay(Duration.ZERO)
.failFast("container is no longer running", () -> !waitStrategyTarget.isRunning())
.forever()
.until(logWaitComplete);
} catch (TerminalFailureException e) {
// The container exited while we were still waiting. Give the log wait a brief grace
// period to match any output that was buffered right before the container exited,
// before failing fast.
try {
logWaitFuture.get(1, TimeUnit.SECONDS);
return;
} catch (TimeoutException | ExecutionException | CancellationException graceException) {
throw new IllegalStateException(
"Container stopped before the expected log output matching '" + regEx + "' was produced"
);
} finally {
logWaitFuture.cancel(true);
}
}

try {
logWaitFuture.get(0, TimeUnit.SECONDS);
} catch (ExecutionException | TimeoutException | CancellationException e) {
throw new ContainerLaunchException("Timed out waiting for log output matching '" + regEx + "'");
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.testcontainers.containers.wait.strategy;

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;

import java.time.Duration;
import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;

class LogMessageWaitStrategyTest {

@Test
void shouldFailFastWhenContainerExitsBeforeExpectedLogOutput() {
GenericContainer<?> container = new GenericContainer<>("alpine:3.20")
.withCommand("sh", "-c", "echo started; sleep 2; exit 1")
.waitingFor(Wait.forLogMessage(".*never.*", 1).withStartupTimeout(Duration.ofSeconds(60)));

Instant start = Instant.now();
Throwable thrown = catchThrowable(container::start);
Duration elapsed = Duration.between(start, Instant.now());

assertThat(thrown)
.as("Container launch fails when the container exits before producing the expected log output")
.isInstanceOf(ContainerLaunchException.class)
.hasRootCauseMessage("Container stopped before the expected log output matching '.*never.*' was produced");
assertThat(elapsed)
.as("Fails fast instead of waiting for the whole startup timeout")
.isLessThan(Duration.ofSeconds(20));
}

@Test
void shouldStillTimeOutWhenContainerKeepsRunningWithoutExpectedLogOutput() {
GenericContainer<?> container = new GenericContainer<>("alpine:3.20")
.withCommand("sh", "-c", "sleep 30")
.waitingFor(Wait.forLogMessage(".*never.*", 1).withStartupTimeout(Duration.ofSeconds(3)));

Throwable thrown = catchThrowable(container::start);

assertThat(thrown)
.as("Timeout semantics for a still running container are unchanged")
.isInstanceOf(ContainerLaunchException.class)
.hasRootCauseMessage("Timed out waiting for log output matching '.*never.*'");
}

@Test
void shouldSucceedWhenExpectedLogOutputAppearsWhileContainerIsRunning() {
GenericContainer<?> container = new GenericContainer<>("alpine:3.20")
.withCommand("sh", "-c", "echo ready; sleep 5")
.waitingFor(Wait.forLogMessage(".*ready.*", 1).withStartupTimeout(Duration.ofSeconds(30)));

container.start();

assertThat(container.isRunning()).isTrue();
}
}
Loading