diff --git a/core/src/main/java/org/testcontainers/containers/wait/strategy/HostPortWaitStrategy.java b/core/src/main/java/org/testcontainers/containers/wait/strategy/HostPortWaitStrategy.java index 5f7eb92818c..f4204db973e 100644 --- a/core/src/main/java/org/testcontainers/containers/wait/strategy/HostPortWaitStrategy.java +++ b/core/src/main/java/org/testcontainers/containers/wait/strategy/HostPortWaitStrategy.java @@ -82,6 +82,7 @@ protected void waitUntilReady() { Instant now = Instant.now(); Awaitility .await() + .dontCatchUncaughtExceptions() // don't replace the global uncaught exception handler, see #11483 .pollInSameThread() .pollInterval(Duration.ofMillis(100)) .pollDelay(Duration.ZERO) diff --git a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java index 7b0aaafc169..1189501060e 100644 --- a/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java +++ b/core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java @@ -208,6 +208,7 @@ protected boolean test() { try (Socket socket = socketProvider.call()) { Awaitility .await() + .dontCatchUncaughtExceptions() // don't replace the global uncaught exception handler, see #11483 .atMost(TestcontainersConfiguration.getInstance().getClientPingTimeout(), TimeUnit.SECONDS) // timeout after configured duration .pollInterval(Duration.ofMillis(200)) // check state every 200ms .pollDelay(Duration.ofSeconds(0)) // start checking immediately diff --git a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java index 9d669e46b07..d25ee9861a5 100644 --- a/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java +++ b/core/src/main/java/org/testcontainers/images/RemoteDockerImage.java @@ -102,6 +102,7 @@ protected final String resolve() { Awaitility .await() + .dontCatchUncaughtExceptions() // don't replace the global uncaught exception handler, see #11483 .pollInSameThread() .pollDelay(Duration.ZERO) // start checking immediately .atMost(PULL_RETRY_TIME_LIMIT) diff --git a/core/src/test/java/org/testcontainers/dockerclient/DockerClientProviderStrategyUncaughtExceptionHandlerTest.java b/core/src/test/java/org/testcontainers/dockerclient/DockerClientProviderStrategyUncaughtExceptionHandlerTest.java new file mode 100644 index 00000000000..f4a137e5399 --- /dev/null +++ b/core/src/test/java/org/testcontainers/dockerclient/DockerClientProviderStrategyUncaughtExceptionHandlerTest.java @@ -0,0 +1,72 @@ +package org.testcontainers.dockerclient; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.testcontainers.utility.MockTestcontainersConfigurationExtension; +import org.testcontainers.utility.TestcontainersConfiguration; + +import java.net.ServerSocket; +import java.net.URI; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockTestcontainersConfigurationExtension.class) +class DockerClientProviderStrategyUncaughtExceptionHandlerTest { + + @Test + void doesNotReplaceGlobalUncaughtExceptionHandler() throws Exception { + // Keep the strategy test window short, but long enough to observe. + Mockito.doReturn(1).when(TestcontainersConfiguration.getInstance()).getClientPingTimeout(); + + // A port where nothing is listening, so the strategy keeps polling until it times out. + int closedPort; + try (ServerSocket serverSocket = new ServerSocket(0)) { + closedPort = serverSocket.getLocalPort(); + } + + DockerClientProviderStrategy strategy = new DockerClientProviderStrategy() { + @Override + public TransportConfig getTransportConfig() { + return TransportConfig.builder().dockerHost(URI.create("tcp://localhost:" + closedPort)).build(); + } + + @Override + public String getDescription() { + return "strategy pointing at a closed port"; + } + }; + + Thread.UncaughtExceptionHandler original = Thread.getDefaultUncaughtExceptionHandler(); + Thread.UncaughtExceptionHandler sentinel = (thread, throwable) -> {}; + Thread.setDefaultUncaughtExceptionHandler(sentinel); + + // Awaitility installs its own default uncaught exception handler for the duration of the await and + // restores the previous one afterwards, so the replacement is only observable while the await runs. + AtomicBoolean handlerReplaced = new AtomicBoolean(false); + AtomicBoolean stopWatching = new AtomicBoolean(false); + Thread watcher = new Thread(() -> { + while (!stopWatching.get()) { + if (Thread.getDefaultUncaughtExceptionHandler() != sentinel) { + handlerReplaced.set(true); + return; + } + } + }); + watcher.setDaemon(true); + + try { + watcher.start(); + strategy.test(); + } finally { + stopWatching.set(true); + watcher.join(); + Thread.setDefaultUncaughtExceptionHandler(original); + } + + assertThat(handlerReplaced) + .as("Testcontainers must not replace the global uncaught exception handler") + .isFalse(); + } +}