Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,12 @@ private void waitUntil(Predicate<OutputFrame> 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
}
}

/**
Expand Down
86 changes: 73 additions & 13 deletions core/src/main/java/org/testcontainers/utility/LogUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<OutputFrame> consumer, OutputFrame.OutputType... types) {
public void followOutput(DockerClient dockerClient,
String containerId,
Consumer<OutputFrame> 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<OutputFrame> 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<OutputFrame> 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) {
Expand All @@ -38,9 +103,4 @@ public void followOutput(DockerClient dockerClient, String containerId,

cmd.exec(callback);
}

public void followOutput(DockerClient dockerClient, String containerId, Consumer<OutputFrame> consumer) {
followOutput(dockerClient, containerId, consumer, STDOUT, STDERR);
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,24 @@
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;

/**
* Created by rnorth on 26/07/2016.
*/
public class WorkingDirectoryTest {

private static WaitingConsumer waitingConsumer = new WaitingConsumer();
private static ToStringConsumer toStringConsumer = new ToStringConsumer();
private static Consumer<OutputFrame> 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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");

Expand All @@ -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 ->
Expand All @@ -67,27 +51,20 @@ 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")),
exactlyNItems(1, allOf(containsString("-rwxr-xr--"), containsString("foo"))));
}

@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 ->
Expand All @@ -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"));
Expand Down