diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 0fe944433ae..ba0c4e9004b 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -447,6 +447,7 @@ private void tryStart() { // Wait until inspect container returns the mapped ports containerInfo = await() + .dontCatchUncaughtExceptions() .atMost(5, TimeUnit.SECONDS) .pollInterval(DynamicPollInterval.ofMillis(50)) .pollInSameThread() 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..9d270bd74fa 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() .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..b1293962212 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() .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..864b184d3f4 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() .pollInSameThread() .pollDelay(Duration.ZERO) // start checking immediately .atMost(PULL_RETRY_TIME_LIMIT) diff --git a/core/src/test/java/org/testcontainers/utility/DockerClientProviderStrategyUncaughtExceptionHandlerTest.java b/core/src/test/java/org/testcontainers/utility/DockerClientProviderStrategyUncaughtExceptionHandlerTest.java new file mode 100644 index 00000000000..7f264238c0c --- /dev/null +++ b/core/src/test/java/org/testcontainers/utility/DockerClientProviderStrategyUncaughtExceptionHandlerTest.java @@ -0,0 +1,94 @@ +package org.testcontainers.utility; + +import org.junit.jupiter.api.Test; +import org.awaitility.Awaitility; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; + +import static org.assertj.core.api.Assertions.assertThat; + +class DockerClientProviderStrategyUncaughtExceptionHandlerTest { + + @Test + void dockerClientProviderStrategyTestShouldUseDontCatchUncaughtExceptions() throws Exception { + byte[] bytes; + try (InputStream is = getClass() + .getClassLoader() + .getResourceAsStream("org/testcontainers/dockerclient/DockerClientProviderStrategy.class")) { + assertThat(is).isNotNull(); + bytes = is.readAllBytes(); + } + + // Use ISO_8859_1 to preserve a stable 1:1 mapping of bytes to chars for a lightweight constant-pool substring check. + String content = new String(bytes, StandardCharsets.ISO_8859_1); + assertThat(content).contains("dontCatchUncaughtExceptions"); + } + + @Test + void shouldNotInterceptUncaughtExceptionsFromOtherThreads() throws Exception { + Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler(); + AtomicReference observedException = new AtomicReference<>(); + CountDownLatch observedExceptionLatch = new CountDownLatch(1); + Thread.UncaughtExceptionHandler expectedHandler = (t, e) -> { + if (observedException.compareAndSet(null, e)) { + observedExceptionLatch.countDown(); + } + }; + + Thread.setDefaultUncaughtExceptionHandler(expectedHandler); + try { + AtomicReference changedHandler = new AtomicReference<>(); + AtomicBoolean monitorStop = new AtomicBoolean(false); + Thread monitor = new Thread(() -> { + while (!monitorStop.get()) { + Thread.UncaughtExceptionHandler current = Thread.getDefaultUncaughtExceptionHandler(); + if (current != expectedHandler) { + changedHandler.compareAndSet(null, current); + break; + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1)); + } + }, "docker-strategy-uncaught-handler-monitor"); + + Thread thrower = new Thread(() -> { + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + return; + } + throw new RuntimeException("boom"); + }, "docker-strategy-uncaught-handler-thrower"); + + monitor.start(); + thrower.start(); + + Awaitility + .await() + .dontCatchUncaughtExceptions() + .pollDelay(Duration.ZERO) + .pollInterval(Duration.ofMillis(25)) + .atMost(Duration.ofSeconds(2)) + .until(() -> { + Thread.sleep(300); + return true; + }); + + monitorStop.set(true); + monitor.join(TimeUnit.SECONDS.toMillis(5)); + thrower.join(TimeUnit.SECONDS.toMillis(5)); + + assertThat(changedHandler.get()).isNull(); + assertThat(observedExceptionLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(observedException.get()).isInstanceOf(RuntimeException.class).hasMessage("boom"); + } finally { + Thread.setDefaultUncaughtExceptionHandler(originalHandler); + } + } +} diff --git a/modules/solace/src/main/java/org/testcontainers/solace/SolaceContainer.java b/modules/solace/src/main/java/org/testcontainers/solace/SolaceContainer.java index 2ab22c8ffa6..d0125f63e28 100644 --- a/modules/solace/src/main/java/org/testcontainers/solace/SolaceContainer.java +++ b/modules/solace/src/main/java/org/testcontainers/solace/SolaceContainer.java @@ -222,6 +222,7 @@ private void updateConfigScript(StringBuilder scriptBuilder, String command) { private void waitOnCommandResult(String waitingFor, String... command) { Awaitility .await() + .dontCatchUncaughtExceptions() .pollInterval(Duration.ofMillis(500)) .timeout(Duration.ofSeconds(30)) .until(() -> {