diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index fa5711807e7..758feeb4ae8 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -495,7 +495,10 @@ private void tryStart() { } if (inspectContainerResponse == null) { - throw new IllegalStateException("Wait strategy failed. Container is removed", e); + throw new IllegalStateException( + "Wait strategy failed. Container " + containerId + " is removed", + e + ); } InspectContainerResponse.ContainerState state = inspectContainerResponse.getState(); @@ -538,14 +541,23 @@ private void tryStart() { if (containerId != null) { // Log output if startup failed, either due to a container failure or exception (including timeout) - final String containerLogs = getLogs(); + try { + final String containerLogs = getLogs(); - if (containerLogs.length() > 0) { - logger().error("Log output from the failed container:\n{}", getLogs()); - } else { - logger().error("There are no stdout/stderr logs available for the failed container"); + if (containerLogs.length() > 0) { + logger().error("Log output from the failed container:\n{}", getLogs()); + } else { + logger().error("There are no stdout/stderr logs available for the failed container"); + } + } catch (NotFoundException e2) { + logger().error("Could not retrieve logs for container {}: container not found", containerId); + } + + try { + stop(); + } catch (Exception e2) { + logger().debug("Failed to stop container {}", containerId, e2); } - stop(); } throw new ContainerLaunchException("Could not create/start container", e); diff --git a/core/src/main/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategy.java b/core/src/main/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategy.java index f161a8e158e..69691533779 100644 --- a/core/src/main/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategy.java @@ -11,13 +11,35 @@ */ public class IsRunningStartupCheckStrategy extends StartupCheckStrategy { - @SuppressWarnings("deprecation") @Override + @SuppressWarnings("deprecation") public boolean waitUntilStartupSuccessful(GenericContainer container) { - // Optimization: container already has the initial "after start" state, check it first - if (checkState(container.getContainerInfo().getState()) == StartupStatus.SUCCESSFUL) { - return true; + InspectContainerResponse.ContainerState cachedState = container.getContainerInfo().getState(); + StartupStatus cachedStatus = checkState(cachedState); + + if (cachedStatus == StartupStatus.SUCCESSFUL) { + // Cached state shows the container as running/exited-success — verify with + // one live Docker inspect to detect stale state (e.g., container crashed + // between the port-mapping check and this startup check). + try { + if ( + checkStartupState(container.getDockerClient(), container.getContainerId()) == + StartupStatus.SUCCESSFUL + ) { + return true; + } + // Live state doesn't match cached — container may have crashed. + // Fall through to full rate-limited polling. + } catch (Exception e) { + // Live inspect failed (e.g., Docker timeout on slow CI) — trust + // the cached state as the best available information. + return true; + } + } else if (cachedStatus == StartupStatus.FAILED) { + // Container already exited with a non-zero exit code + return false; } + return super.waitUntilStartupSuccessful(container); } diff --git a/core/src/test/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategyTest.java b/core/src/test/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategyTest.java index fc7a60c2115..a4fcd3867cc 100644 --- a/core/src/test/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategyTest.java +++ b/core/src/test/java/org/testcontainers/containers/startupcheck/IsRunningStartupCheckStrategyTest.java @@ -1,6 +1,5 @@ package org.testcontainers.containers.startupcheck; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.testcontainers.TestImages; import org.testcontainers.containers.GenericContainer; @@ -17,7 +16,6 @@ void testCommandQuickExitSuccess() { } @Test - @Disabled("This test can fail to throw an AssertionError if the container doesn't fail quickly enough") void testCommandQuickExitFailure() { try (GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE).withCommand("/bin/false")) { assertThatThrownBy(container::start) @@ -34,4 +32,16 @@ void testCommandStaysRunning() { container.start(); // should start with no Exception } } + + @Test + void testQuickExitWithDifferentExitCode() { + try ( + GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) + .withCommand("/bin/sh", "-c", "exit 42") + ) { + assertThatThrownBy(container::start) + .hasStackTraceContaining("Container startup failed") + .hasStackTraceContaining("Container did not start correctly"); + } + } } diff --git a/modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLContainerTest.java b/modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLContainerTest.java index beefa3776b1..a7e8001e8e4 100644 --- a/modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLContainerTest.java +++ b/modules/postgresql/src/test/java/org/testcontainers/postgresql/PostgreSQLContainerTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.Test; import org.testcontainers.PostgreSQLTestImages; +import org.testcontainers.containers.ContainerLaunchException; import org.testcontainers.db.AbstractContainerDatabaseTest; import java.sql.ResultSet; @@ -11,6 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class PostgreSQLContainerTest extends AbstractContainerDatabaseTest { static { @@ -122,6 +124,21 @@ void testWithAdditionalUrlParamInJdbcUrl() { } } + @Test + void testContainerExitBeforeStartupCheck() { + // Regression test for #11860: verify that a PostgreSQLContainer which exits before/during + // startup produces a handled ContainerLaunchException from the startup check, not a + // suppressed or cascading error from cleanup (getLogs/stop throwing NotFoundException). + try ( + PostgreSQLContainer postgres = new PostgreSQLContainer(PostgreSQLTestImages.POSTGRES_TEST_IMAGE) +.withCommand("/bin/false") + ) { + assertThatThrownBy(postgres::start) + .isInstanceOf(ContainerLaunchException.class) + .hasStackTraceContaining("Container startup failed"); + } + } + private void assertHasCorrectExposedAndLivenessCheckPorts(PostgreSQLContainer postgres) { assertThat(postgres.getExposedPorts()).containsExactly(PostgreSQLContainer.POSTGRESQL_PORT); assertThat(postgres.getLivenessCheckPortNumbers())