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