diff --git a/core/src/main/java/org/testcontainers/containers/Container.java b/core/src/main/java/org/testcontainers/containers/Container.java index abea7fef576..ed684c14260 100644 --- a/core/src/main/java/org/testcontainers/containers/Container.java +++ b/core/src/main/java/org/testcontainers/containers/Container.java @@ -375,6 +375,13 @@ SELF withClasspathResourceMapping( */ SELF withWorkingDirectory(String workDir); + /** + * Set the container alias to distinguish between multiple containers. + * + * @param alias name + */ + SELF withContainerAlias(String alias); + /** * Resolve Docker image and set it. * diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 0fe944433ae..bc3ff9f2c59 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -138,6 +138,9 @@ public class GenericContainer> @Nullable private String workingDirectory = null; + @Nullable + private String containerAlias = null; + /** * The shared memory size to use when starting the container. * This value is in bytes. @@ -343,7 +346,14 @@ protected void doStart() { } ); } catch (Exception e) { - throw new ContainerLaunchException("Container startup failed for image " + getDockerImageName(), e); + String containerAliasStr = ""; + if (StringUtils.isNotBlank(containerAlias)) { + containerAliasStr = " (containerAlias='" + containerAlias.trim() + "')"; + } + throw new ContainerLaunchException( + "Container startup failed for image " + getDockerImageName() + containerAliasStr, + e + ); } } @@ -663,7 +673,11 @@ public void stop() { * @return a logger that references the docker image name */ protected Logger logger() { - return DockerLoggerFactory.getLogger(this.getDockerImageName()); + String loggerName = this.getDockerImageName(); + if (StringUtils.isNotBlank(containerAlias)) { + loggerName = loggerName + "--" + containerAlias.trim(); + } + return DockerLoggerFactory.getLogger(loggerName); } /** @@ -1256,6 +1270,12 @@ public SELF withWorkingDirectory(String workDir) { return self(); } + @Override + public SELF withContainerAlias(String alias) { + this.setContainerAlias(alias); + return self(); + } + /** * {@inheritDoc} */ diff --git a/core/src/test/java/org/testcontainers/junit/ContainerAliasTest.java b/core/src/test/java/org/testcontainers/junit/ContainerAliasTest.java new file mode 100644 index 00000000000..69a28fdd72c --- /dev/null +++ b/core/src/test/java/org/testcontainers/junit/ContainerAliasTest.java @@ -0,0 +1,102 @@ +package org.testcontainers.junit; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; +import org.testcontainers.TestImages; +import org.testcontainers.containers.ContainerLaunchException; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; + +import java.util.List; + +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 ContainerAliasTest { + + private final TestLogger testLogger = new TestLogger(); + + public GenericContainer container1 = new GenericContainer(TestImages.ALPINE_IMAGE) + .withContainerAlias("potato") + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) + .withCommand("ls", "-al"); + + public GenericContainer container2 = new GenericContainer(TestImages.ALPINE_IMAGE) + .withContainerAlias("monkey") + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) + .withCommand("non-existing-command"); + + @BeforeEach + void setUp() { + testLogger.startCapturing(); + } + + @AfterEach + void tearDown() { + testLogger.stopCapturing(); + } + + @Test + void checkOutput() { + assertThatNoException() + .isThrownBy(() -> { + container1.start(); + }); + + assertThatThrownBy(() -> { + container2.start(); + }) + .isInstanceOf(ContainerLaunchException.class) + .hasMessage("Container startup failed for image alpine:3.17 (containerAlias='monkey')"); + + List container1PotatoLogs = testLogger + .getLogs() + .stream() + .filter(it -> "tc.alpine:3.17--potato".equals(it.getLoggerName())) + .map(ILoggingEvent::getFormattedMessage) + .toList(); + assertThat(container1PotatoLogs) + .anyMatch(it -> it.equals("Creating container for image: alpine:3.17")) + .anyMatch(it -> it.startsWith("Container alpine:3.17 is starting: ")) + .anyMatch(it -> it.startsWith("Container alpine:3.17 started in P")); + + List container2MonkeyLogs = testLogger + .getLogs() + .stream() + .filter(it -> "tc.alpine:3.17--monkey".equals(it.getLoggerName())) + .map(ILoggingEvent::getFormattedMessage) + .toList(); + assertThat(container2MonkeyLogs) + .anyMatch(it -> it.equals("Creating container for image: alpine:3.17")) + .anyMatch(it -> it.startsWith("Container alpine:3.17 is starting: ")) + .anyMatch(it -> it.equals("Could not start container")) + .anyMatch(it -> it.equals("There are no stdout/stderr logs available for the failed container")); + } +} + +class TestLogger { + + private final ListAppender listAppender = new ListAppender<>(); + + public void startCapturing() { + Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + listAppender.start(); + rootLogger.addAppender(listAppender); + } + + public void stopCapturing() { + Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + rootLogger.detachAppender(listAppender); + listAppender.stop(); + } + + public List getLogs() { + return listAppender.list; + } +}