diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index abf535869da..a6951355751 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -242,24 +242,24 @@ private void tryStart() { logger().info("Starting container with ID: {}", containerId); dockerClient.startContainerCmd(containerId).exec(); + logger().info("Container {} is starting: {}", dockerImageName, containerId); + // For all registered output consumers, start following as close to container startup as possible this.logConsumers.forEach(this::followOutput); - logger().info("Container {} is starting: {}", dockerImageName, containerId); - // Tell subclasses that we're starting containerInfo = dockerClient.inspectContainerCmd(containerId).exec(); containerName = containerInfo.getName(); containerIsStarting(containerInfo); - // Wait until the container is running (may not be fully started) - + // Wait until the container has reached the desired running state if (!this.startupCheckStrategy.waitUntilStartupSuccessful(dockerClient, containerId)) { // Bail out, don't wait for the port to start listening. // (Exception thrown here will be caught below and wrapped) throw new IllegalStateException("Container did not start correctly."); } + // Wait until the process within the container has become ready for use (e.g. listening on network, log message emitted, etc). waitUntilContainerStarted(); logger().info("Container {} started", dockerImageName); @@ -1118,6 +1118,21 @@ public ExecResult execInContainer(Charset outputCharset, String... command) return ExecInContainerPattern.execInContainer(getContainerInfo(), outputCharset, command); } + /** + * @return all log output from the container from start until the current instant (both stdout and stderr) + */ + public String getLogs() { + return LogUtils.getOutput(dockerClient, containerId); + } + + /** + * @param types log types to return + * @return all log output from the container from start until the current instant + */ + public String getLogs(OutputFrame.OutputType... types) { + return LogUtils.getOutput(dockerClient, containerId, types); + } + /** * Allow container startup to be attempted more than once if an error occurs. To be if containers are * 'flaky' but this flakiness is not something that should affect test outcomes. diff --git a/core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java b/core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java index 367581528c2..cbdd42bb156 100644 --- a/core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java +++ b/core/src/main/java/org/testcontainers/containers/output/WaitingConsumer.java @@ -113,8 +113,12 @@ private void waitUntil(Predicate predicate, long expiry, int times) /** * Wait until Docker closes the stream of output. */ - public void waitUntilEnd() throws TimeoutException { - waitUntilEnd(Long.MAX_VALUE); + public void waitUntilEnd() { + try { + waitUntilEnd(Long.MAX_VALUE); + } catch (TimeoutException ignored) { + // timeout condition can never occur in a realistic timeframe + } } /** diff --git a/core/src/main/java/org/testcontainers/utility/LogUtils.java b/core/src/main/java/org/testcontainers/utility/LogUtils.java index 100a79a8395..a0d5d647c75 100644 --- a/core/src/main/java/org/testcontainers/utility/LogUtils.java +++ b/core/src/main/java/org/testcontainers/utility/LogUtils.java @@ -2,15 +2,14 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.LogContainerCmd; -import com.github.dockerjava.api.model.AuthConfig; -import com.google.common.base.MoreObjects; import lombok.experimental.UtilityClass; import org.testcontainers.containers.output.FrameConsumerResultCallback; import org.testcontainers.containers.output.OutputFrame; +import org.testcontainers.containers.output.ToStringConsumer; +import org.testcontainers.containers.output.WaitingConsumer; import java.util.function.Consumer; -import static com.google.common.base.Strings.isNullOrEmpty; import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR; import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT; @@ -19,15 +18,81 @@ */ @UtilityClass public class LogUtils { + /** - * {@inheritDoc} + * Attach a log consumer to a container's log outputs in follow mode. The consumer will receive all previous + * and all future log frames of the specified type(s). + * + * @param dockerClient a Docker client + * @param containerId container ID to attach to + * @param consumer a consumer of {@link OutputFrame}s + * @param types types of {@link OutputFrame} to receive */ - public void followOutput(DockerClient dockerClient, String containerId, - Consumer consumer, OutputFrame.OutputType... types) { + public void followOutput(DockerClient dockerClient, + String containerId, + Consumer consumer, + OutputFrame.OutputType... types) { + + attachConsumer(dockerClient, containerId, consumer, true, types); + } + + /** + * Attach a log consumer to a container's log outputs in follow mode. The consumer will receive all previous + * and all future log frames (both stdout and stderr). + * + * @param dockerClient a Docker client + * @param containerId container ID to attach to + * @param consumer a consumer of {@link OutputFrame}s + */ + public void followOutput(DockerClient dockerClient, + String containerId, + Consumer consumer) { + + followOutput(dockerClient, containerId, consumer, STDOUT, STDERR); + } + + /** + * Retrieve all previous log outputs for a container of the specified type(s). + * + * @param dockerClient a Docker client + * @param containerId container ID to attach to + * @param types types of {@link OutputFrame} to receive + * @return all previous output frames (stdout/stderr being separated by newline characters) + */ + public String getOutput(DockerClient dockerClient, + String containerId, + OutputFrame.OutputType... types) { + + final ToStringConsumer consumer = new ToStringConsumer(); + final WaitingConsumer wait = new WaitingConsumer(); + attachConsumer(dockerClient, containerId, consumer.andThen(wait), false, types); + + wait.waitUntilEnd(); + return consumer.toUtf8String(); + } + + /** + * Retrieve all previous log outputs for a container (both stdout and stderr) + * + * @param dockerClient a Docker client + * @param containerId container ID to attach to + * @return all previous output frames (stdout/stderr being separated by newline characters) + */ + public String getOutput(DockerClient dockerClient, + String containerId) { + + return getOutput(dockerClient, containerId, STDOUT, STDERR); + } + + private static void attachConsumer(DockerClient dockerClient, + String containerId, + Consumer consumer, + boolean followStream, + OutputFrame.OutputType... types) { final LogContainerCmd cmd = dockerClient.logContainerCmd(containerId) - .withFollowStream(true) - .withSince(0); + .withFollowStream(followStream) + .withSince(0); final FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); for (OutputFrame.OutputType type : types) { @@ -38,9 +103,4 @@ public void followOutput(DockerClient dockerClient, String containerId, cmd.exec(callback); } - - public void followOutput(DockerClient dockerClient, String containerId, Consumer consumer) { - followOutput(dockerClient, containerId, consumer, STDOUT, STDERR); - } - } diff --git a/core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java b/core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java new file mode 100644 index 00000000000..e8d12853a1b --- /dev/null +++ b/core/src/test/java/org/testcontainers/containers/output/ContainerLogsTest.java @@ -0,0 +1,36 @@ +package org.testcontainers.containers.output; + +import org.junit.Rule; +import org.junit.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; + +import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; +import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR; +import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT; + +public class ContainerLogsTest { + + @Rule + public GenericContainer container = new GenericContainer("alpine:3.3") + .withCommand("/bin/sh", "-c", "echo -n 'stdout' && echo -n 'stderr' 1>&2") + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); + + @Test + public void getLogsReturnsAllLogsToDate() { + final String logs = container.getLogs(); + assertEquals("stdout and stderr are reflected in the returned logs", "stdout\nstderr", logs); + } + + @Test + public void getLogsReturnsStdOutToDate() { + final String logs = container.getLogs(STDOUT); + assertEquals("stdout and stderr are reflected in the returned logs", "stdout", logs); + } + + @Test + public void getLogsReturnsStdErrToDate() { + final String logs = container.getLogs(STDERR); + assertEquals("stdout and stderr are reflected in the returned logs", "stderr", logs); + } +} diff --git a/core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java b/core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java index 1118302f701..30230cc12ff 100644 --- a/core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java +++ b/core/src/test/java/org/testcontainers/junit/WorkingDirectoryTest.java @@ -3,13 +3,8 @@ import org.junit.ClassRule; import org.junit.Test; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.OutputFrame; -import org.testcontainers.containers.output.ToStringConsumer; -import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; -import java.util.function.Consumer; - import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; /** @@ -17,20 +12,15 @@ */ public class WorkingDirectoryTest { - private static WaitingConsumer waitingConsumer = new WaitingConsumer(); - private static ToStringConsumer toStringConsumer = new ToStringConsumer(); - private static Consumer compositeConsumer = waitingConsumer.andThen(toStringConsumer); - @ClassRule public static GenericContainer container = new GenericContainer("alpine:3.2") .withWorkingDirectory("/etc") .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) - .withCommand("ls", "-al") - .withLogConsumer(compositeConsumer); + .withCommand("ls", "-al"); @Test public void checkOutput() { - String listing = toStringConsumer.toUtf8String(); + String listing = container.getLogs(); assertTrue("Directory listing contains expected /etc content", listing.contains("hostname")); assertTrue("Directory listing contains expected /etc content", listing.contains("init.d")); diff --git a/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java index b73bfeed46e..3789a654c20 100644 --- a/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java +++ b/core/src/test/java/org/testcontainers/utility/DirectoryTarResourceTest.java @@ -5,15 +5,11 @@ import org.hamcrest.core.IsCollectionContaining; import org.junit.Test; import org.testcontainers.containers.GenericContainer; -import org.testcontainers.containers.output.ToStringConsumer; -import org.testcontainers.containers.output.WaitingConsumer; import org.testcontainers.containers.startupcheck.OneShotStartupCheckStrategy; import org.testcontainers.images.builder.ImageFromDockerfile; import java.io.File; import java.util.Arrays; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.containsString; @@ -23,12 +19,7 @@ public class DirectoryTarResourceTest { @Test - public void simpleRecursiveFileTest() throws TimeoutException { - - WaitingConsumer wait = new WaitingConsumer(); - - final ToStringConsumer toString = new ToStringConsumer(); - + public void simpleRecursiveFileTest() { // 'src' is expected to be the project base directory, so all source code/resources should be copied in File directory = new File("src"); @@ -40,24 +31,17 @@ public void simpleRecursiveFileTest() throws TimeoutException { .cmd("cat /foo/test/resources/test-recursive-file.txt") .build() ).withFileFromFile("/tmp/foo", directory)) - .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) - .withLogConsumer(wait.andThen(toString)); + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); - wait.waitUntilEnd(60, TimeUnit.SECONDS); - final String results = toString.toUtf8String(); + final String results = container.getLogs(); assertTrue("The container has a file that was copied in via a recursive copy", results.contains("Used for DirectoryTarResourceTest")); } @Test - public void simpleRecursiveFileWithPermissionTest() throws TimeoutException { - - WaitingConsumer wait = new WaitingConsumer(); - - final ToStringConsumer toString = new ToStringConsumer(); - + public void simpleRecursiveFileWithPermissionTest() { GenericContainer container = new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> @@ -67,13 +51,10 @@ public void simpleRecursiveFileWithPermissionTest() throws TimeoutException { .build() ).withFileFromFile("/tmp/foo", new File("/mappable-resource/test-resource.txt"), 0754)) - .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) - .withLogConsumer(wait.andThen(toString)); + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); - wait.waitUntilEnd(60, TimeUnit.SECONDS); - - String listing = toString.toUtf8String(); + String listing = container.getLogs(); assertThat("Listing shows that file is copied with mode requested.", Arrays.asList(listing.split("\\n")), @@ -81,13 +62,9 @@ public void simpleRecursiveFileWithPermissionTest() throws TimeoutException { } @Test - public void simpleRecursiveClasspathResourceTest() throws TimeoutException { + public void simpleRecursiveClasspathResourceTest() { // This test combines the copying of classpath resources from JAR files with the recursive TAR approach, to allow JARed classpath resources to be copied in to an image - WaitingConsumer wait = new WaitingConsumer(); - - final ToStringConsumer toString = new ToStringConsumer(); - GenericContainer container = new GenericContainer( new ImageFromDockerfile() .withDockerfileFromBuilder(builder -> @@ -96,13 +73,11 @@ public void simpleRecursiveClasspathResourceTest() throws TimeoutException { .cmd("ls -lRt /foo") .build() ).withFileFromClasspath("/tmp/foo", "/recursive/dir")) // here we use /org/junit as a directory that really should exist on the classpath - .withStartupCheckStrategy(new OneShotStartupCheckStrategy()) - .withLogConsumer(wait.andThen(toString)); + .withStartupCheckStrategy(new OneShotStartupCheckStrategy()); container.start(); - wait.waitUntilEnd(60, TimeUnit.SECONDS); - final String results = toString.toUtf8String(); + final String results = container.getLogs(); // ExternalResource.class is known to exist in a subdirectory of /org/junit so should be successfully copied in assertTrue("The container has a file that was copied in via a recursive copy from a JAR resource", results.contains("content.txt"));