diff --git a/plugin/trino-accumulo/pom.xml b/plugin/trino-accumulo/pom.xml
index 1f1c1697b4f7..8cf27ac319e8 100644
--- a/plugin/trino-accumulo/pom.xml
+++ b/plugin/trino-accumulo/pom.xml
@@ -267,6 +267,12 @@
+
+ io.trino
+ trino-testing-containers
+ test
+
+
io.trino
trino-testing-services
diff --git a/plugin/trino-accumulo/src/test/java/io/trino/plugin/accumulo/TestingAccumuloServer.java b/plugin/trino-accumulo/src/test/java/io/trino/plugin/accumulo/TestingAccumuloServer.java
index f8a6049019c4..9854ac0966c0 100644
--- a/plugin/trino-accumulo/src/test/java/io/trino/plugin/accumulo/TestingAccumuloServer.java
+++ b/plugin/trino-accumulo/src/test/java/io/trino/plugin/accumulo/TestingAccumuloServer.java
@@ -14,6 +14,7 @@
package io.trino.plugin.accumulo;
import io.trino.testing.TestingProperties;
+import io.trino.testing.containers.junit.ReportLeakedContainers;
import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.AccumuloSecurityException;
import org.apache.accumulo.core.client.Connector;
@@ -63,6 +64,7 @@ private TestingAccumuloServer()
// TODO Change this class to not be a singleton
// https://github.com/trinodb/trino/issues/5842
accumuloContainer.start();
+ ReportLeakedContainers.ignoreContainerId(accumuloContainer.getContainerId());
}
public String getInstanceName()
diff --git a/testing/trino-testing-containers/pom.xml b/testing/trino-testing-containers/pom.xml
index 1541f026e585..76607d376919 100644
--- a/testing/trino-testing-containers/pom.xml
+++ b/testing/trino-testing-containers/pom.xml
@@ -61,6 +61,12 @@
trino-testing-services
+
+ org.junit.platform
+ junit-platform-launcher
+ true
+
+
org.rnorth.duct-tape
duct-tape
diff --git a/testing/trino-testing-containers/src/main/java/io/trino/testing/containers/junit/ReportLeakedContainers.java b/testing/trino-testing-containers/src/main/java/io/trino/testing/containers/junit/ReportLeakedContainers.java
new file mode 100644
index 000000000000..e41a54bea4ec
--- /dev/null
+++ b/testing/trino-testing-containers/src/main/java/io/trino/testing/containers/junit/ReportLeakedContainers.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.trino.testing.containers.junit;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.model.Container;
+import io.airlift.log.Logger;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestPlan;
+import org.testcontainers.DockerClientFactory;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.lang.Boolean.getBoolean;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+
+public final class ReportLeakedContainers
+{
+ private ReportLeakedContainers() {}
+
+ private static final Logger log = Logger.get(ReportLeakedContainers.class);
+ private static final boolean DISABLED = getBoolean("ReportLeakedContainers.disabled");
+
+ private static final Set ignoredIds = Collections.synchronizedSet(new HashSet<>());
+
+ public static void ignoreContainerId(String containerId)
+ {
+ ignoredIds.add(requireNonNull(containerId, "containerId is null"));
+ }
+
+ // Separate class so that ReportLeakedContainers.ignoreContainerId can be called without pulling junit platform onto classpath
+ public static class Listener
+ implements TestExecutionListener
+ {
+ @Override
+ public void testPlanExecutionFinished(TestPlan testPlan)
+ {
+ if (DISABLED) {
+ log.info("ReportLeakedContainers disabled");
+ return;
+ }
+ log.info("Checking for leaked containers");
+
+ @SuppressWarnings("resource") // Throws when close is attempted, as this is a global instance.
+ DockerClient dockerClient = DockerClientFactory.lazyClient();
+
+ List containers = dockerClient.listContainersCmd()
+ .withLabelFilter(Map.of(DockerClientFactory.TESTCONTAINERS_SESSION_ID_LABEL, DockerClientFactory.SESSION_ID))
+ .exec()
+ .stream()
+ .filter(container -> !ignoredIds.contains(container.getId()))
+ .collect(toImmutableList());
+
+ if (!containers.isEmpty()) {
+ log.error("Leaked containers: %s", containers.stream()
+ .map(container -> toStringHelper("container")
+ .add("id", container.getId())
+ .add("image", container.getImage())
+ .add("imageId", container.getImageId())
+ .toString())
+ .collect(joining(", ", "[", "]")));
+
+ // JUnit does not fail on a listener exception.
+ System.err.println("JVM will be terminated");
+ System.exit(1);
+ }
+ }
+ }
+}
diff --git a/testing/trino-testing-containers/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener b/testing/trino-testing-containers/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener
new file mode 100644
index 000000000000..c80b71364750
--- /dev/null
+++ b/testing/trino-testing-containers/src/main/resources/META-INF/services/org.junit.platform.launcher.TestExecutionListener
@@ -0,0 +1 @@
+io.trino.testing.containers.junit.ReportLeakedContainers$Listener