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..7eb1d6b8f0a 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,22 @@ 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") + .hasStackTraceContaining("Container did not start correctly."); + } + } + private void assertHasCorrectExposedAndLivenessCheckPorts(PostgreSQLContainer postgres) { assertThat(postgres.getExposedPorts()).containsExactly(PostgreSQLContainer.POSTGRESQL_PORT); assertThat(postgres.getLivenessCheckPortNumbers())