From 463c6acdcbe80f0ba1bb2fd8e56b184117f02145 Mon Sep 17 00:00:00 2001 From: Tomas Hanley Date: Fri, 26 Jan 2018 16:18:16 +0000 Subject: [PATCH 1/2] Support multiple Docker annotations (containers) with networks. Fixes issue #19 --- .../github/junit5docker/ContainerInfo.java | 25 ++++ .../junit5docker/DefaultDockerClient.java | 56 +++++++- .../java/com/github/junit5docker/Docker.java | 8 ++ .../junit5docker/DockerClientAdapter.java | 11 +- .../github/junit5docker/DockerExtension.java | 65 ++++++--- .../java/com/github/junit5docker/Dockers.java | 22 +++ .../junit5docker/DefaultDockerClientIT.java | 13 +- .../junit5docker/DockerExtensionTest.java | 127 ++++++++++++------ .../StartMultipleDockerAnnotationsIT.java | 99 ++++++++++++++ .../fakes/FakeExtensionContext.java | 6 +- 10 files changed, 357 insertions(+), 75 deletions(-) create mode 100644 src/main/java/com/github/junit5docker/ContainerInfo.java create mode 100644 src/main/java/com/github/junit5docker/Dockers.java create mode 100644 src/test/java/com/github/junit5docker/StartMultipleDockerAnnotationsIT.java diff --git a/src/main/java/com/github/junit5docker/ContainerInfo.java b/src/main/java/com/github/junit5docker/ContainerInfo.java new file mode 100644 index 0000000..4fb4cd6 --- /dev/null +++ b/src/main/java/com/github/junit5docker/ContainerInfo.java @@ -0,0 +1,25 @@ +package com.github.junit5docker; + +import java.util.Collection; + +import static java.util.Collections.emptySet; +import static java.util.Collections.unmodifiableCollection; + +public class ContainerInfo { + + private final String containerId; + private final Collection networkIds; + + public ContainerInfo(String containerId, Collection networkIds) { + this.containerId = containerId; + this.networkIds = networkIds == null ? emptySet() : unmodifiableCollection(networkIds); + } + + public String getContainerId() { + return containerId; + } + + public Collection getNetworkIds() { + return networkIds; + } +} diff --git a/src/main/java/com/github/junit5docker/DefaultDockerClient.java b/src/main/java/com/github/junit5docker/DefaultDockerClient.java index 1f83271..e6774ca 100644 --- a/src/main/java/com/github/junit5docker/DefaultDockerClient.java +++ b/src/main/java/com/github/junit5docker/DefaultDockerClient.java @@ -3,12 +3,17 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.Network; import com.github.dockerjava.api.model.Ports; import com.github.dockerjava.core.DockerClientBuilder; import com.github.dockerjava.core.command.PullImageResultCallback; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Stream; import static com.github.dockerjava.api.model.ExposedPort.tcp; @@ -25,12 +30,59 @@ class DefaultDockerClient implements DockerClientAdapter { } @Override - public String startContainer(String wantedImage, Map environment, PortBinding... portBinding) { + public ContainerInfo startContainer(String wantedImage, Map environment, String[] networkNames, PortBinding... portBinding) { Ports bindings = createPortBindings(portBinding); List environmentStrings = createEnvironmentList(environment); String containerId = createContainer(wantedImage, bindings, environmentStrings); dockerClient.startContainerCmd(containerId).exec(); - return containerId; + Collection networkIds = joinNetworks(networkNames, containerId); + return new ContainerInfo(containerId, networkIds); + } + + private Collection joinNetworks(String[] networkNames, String containerId) { + if (networkNames == null || networkNames.length == 0) { + return Collections.emptySet(); + } + Collection networkIds = new HashSet<>(); + + for (String network : networkNames) { + String networkId = getNetworkId(network); + dockerClient.connectToNetworkCmd() + .withNetworkId(networkId) + .withContainerId(containerId) + .exec(); + networkIds.add(networkId); + } + return networkIds; + } + + private String getNetworkId(String networkName) { + return getExistingNetworkId(networkName) + .orElseGet(() -> dockerClient.createNetworkCmd().withName(networkName).exec().getId()); + } + + private Optional getExistingNetworkId(String networkName) { + return dockerClient.listNetworksCmd().exec().stream() + .filter(n -> n.getName().equals(networkName)) + .reduce((a, b) -> { + throw new IllegalStateException("Multiple networks found with the same name: " + a + ", " + b); + }) + .map(Network::getId); + } + + @Override + public void disconnectFromNetwork(String containerId, String networkId) { + dockerClient.disconnectFromNetworkCmd() + .withContainerId(containerId) + .withNetworkId(networkId) + .exec(); + } + + @Override + public void maybeRemoveNetwork(String networkId) { + if (dockerClient.inspectNetworkCmd().withNetworkId(networkId).exec().getContainers().isEmpty()) { + dockerClient.removeNetworkCmd(networkId).exec(); + } } @Override diff --git a/src/main/java/com/github/junit5docker/Docker.java b/src/main/java/com/github/junit5docker/Docker.java index f64e9be..3ee8942 100644 --- a/src/main/java/com/github/junit5docker/Docker.java +++ b/src/main/java/com/github/junit5docker/Docker.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @@ -20,6 +21,7 @@ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(DockerExtension.class) +@Repeatable(value = Dockers.class) public @interface Docker { /** @@ -51,4 +53,10 @@ * False if it should be created only once for the test class. */ boolean newForEachCase() default true; + + /** + * The names of the networks for the container to join. + */ + String[] networks() default {}; + } diff --git a/src/main/java/com/github/junit5docker/DockerClientAdapter.java b/src/main/java/com/github/junit5docker/DockerClientAdapter.java index 693d273..18ee36a 100644 --- a/src/main/java/com/github/junit5docker/DockerClientAdapter.java +++ b/src/main/java/com/github/junit5docker/DockerClientAdapter.java @@ -4,7 +4,16 @@ import java.util.stream.Stream; interface DockerClientAdapter { - String startContainer(String wantedImage, Map environment, PortBinding... portBinding); + + ContainerInfo startContainer(String wantedImage, Map environment, String[] networkNames, PortBinding... portBinding); + + void disconnectFromNetwork(String containerId, String networkId); + + /** + * Remove the specified network if no containers are connected to it. + * @param networkId + */ + void maybeRemoveNetwork(String networkId); void stopAndRemoveContainer(String containerId); diff --git a/src/main/java/com/github/junit5docker/DockerExtension.java b/src/main/java/com/github/junit5docker/DockerExtension.java index 0e8b30f..4cbd91c 100644 --- a/src/main/java/com/github/junit5docker/DockerExtension.java +++ b/src/main/java/com/github/junit5docker/DockerExtension.java @@ -6,12 +6,15 @@ import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Stream; @@ -21,7 +24,6 @@ class DockerExtension implements BeforeAllCallback, AfterAllCallback, BeforeEach private final DockerClientAdapter dockerClient; - private String containerId; DockerExtension() { this(new DefaultDockerClient()); @@ -32,31 +34,35 @@ class DockerExtension implements BeforeAllCallback, AfterAllCallback, BeforeEach } @Override - public void beforeAll(ExtensionContext containerExtensionContext) { - Docker dockerAnnotation = findDockerAnnotation(containerExtensionContext); - if (!dockerAnnotation.newForEachCase()) startContainer(dockerAnnotation); + public void beforeAll(ExtensionContext context) { + forEachDocker(context, d -> !d.newForEachCase(), this::startContainer); } @Override public void beforeEach(ExtensionContext context) { - Docker dockerAnnotation = findDockerAnnotation(context); - if (dockerAnnotation.newForEachCase()) startContainer(dockerAnnotation); + forEachDocker(context, Docker::newForEachCase, this::startContainer); } - private void startContainer(Docker dockerAnnotation) { + private static void forEachDocker(ExtensionContext context, Predicate predicate, BiConsumer action){ + Arrays.stream(findDockerAnnotations(context)).filter(predicate).forEach(d -> action.accept(context, d)); + } + + private void startContainer(ExtensionContext context, Docker dockerAnnotation) { PortBinding[] portBindings = createPortBindings(dockerAnnotation); Map environmentMap = createEnvironmentMap(dockerAnnotation); String imageReference = findImageName(dockerAnnotation); WaitFor waitFor = dockerAnnotation.waitFor(); - containerId = dockerClient.startContainer(imageReference, environmentMap, portBindings); - waitForLogAccordingTo(waitFor); + String[] networkNames = dockerAnnotation.networks(); + ContainerInfo containerInfo = dockerClient.startContainer(imageReference, environmentMap, networkNames, portBindings); + waitForLogAccordingTo(waitFor, containerInfo.getContainerId()); + getStore(context).put(dockerAnnotation, containerInfo); } - private void waitForLogAccordingTo(WaitFor waitFor) { + private void waitForLogAccordingTo(WaitFor waitFor, String containerId) { String expectedLog = waitFor.value(); if (!WaitFor.NOTHING.equals(expectedLog)) { ExecutorService executor = Executors.newSingleThreadExecutor(); - CompletableFuture logFound = supplyAsync(findFirstLogContaining(expectedLog), executor); + CompletableFuture logFound = supplyAsync(findFirstLogContaining(expectedLog, containerId), executor); executor.shutdown(); try { boolean termination = executor.awaitTermination(waitFor.timeoutInMillis(), TimeUnit.MILLISECONDS); @@ -72,7 +78,7 @@ private void waitForLogAccordingTo(WaitFor waitFor) { } } - private Supplier findFirstLogContaining(String logToFind) { + private Supplier findFirstLogContaining(String logToFind, String containerId) { return () -> { try (Stream logs = dockerClient.logs(containerId)) { return logs.anyMatch(log -> log.contains(logToFind)); @@ -80,16 +86,16 @@ private Supplier findFirstLogContaining(String logToFind) { }; } - private Docker findDockerAnnotation(ExtensionContext extensionContext) { + private static Docker[] findDockerAnnotations(ExtensionContext extensionContext) { Class testClass = extensionContext.getTestClass().get(); - return testClass.getAnnotation(Docker.class); + return testClass.getAnnotationsByType(Docker.class); } - private String findImageName(Docker dockerAnnotation) { + private static String findImageName(Docker dockerAnnotation) { return dockerAnnotation.image(); } - private Map createEnvironmentMap(Docker dockerAnnotation) { + private static Map createEnvironmentMap(Docker dockerAnnotation) { Map environmentMap = new HashMap<>(); Environment[] environments = dockerAnnotation.environments(); for (Environment environment : environments) { @@ -98,7 +104,7 @@ private Map createEnvironmentMap(Docker dockerAnnotation) { return environmentMap; } - private PortBinding[] createPortBindings(Docker dockerAnnotation) { + private static PortBinding[] createPortBindings(Docker dockerAnnotation) { Port[] ports = dockerAnnotation.ports(); PortBinding[] portBindings = new PortBinding[ports.length]; for (int i = 0; i < ports.length; i++) { @@ -109,14 +115,29 @@ private PortBinding[] createPortBindings(Docker dockerAnnotation) { } @Override - public void afterAll(ExtensionContext containerExtensionContext) { - Docker dockerAnnotation = findDockerAnnotation(containerExtensionContext); - if (!dockerAnnotation.newForEachCase()) dockerClient.stopAndRemoveContainer(containerId); + public void afterAll(ExtensionContext context) { + forEachDocker(context, d -> !d.newForEachCase(), this::stopAndRemove); } @Override public void afterEach(ExtensionContext context) { - Docker dockerAnnotation = findDockerAnnotation(context); - if (dockerAnnotation.newForEachCase()) dockerClient.stopAndRemoveContainer(containerId); + forEachDocker(context, Docker::newForEachCase, this::stopAndRemove); } + + private void stopAndRemove(ExtensionContext context, Docker docker) { + ContainerInfo containerInfo = getStore(context).remove(docker, ContainerInfo.class); + + String containerId = containerInfo.getContainerId(); + + containerInfo.getNetworkIds().forEach(c -> { + dockerClient.disconnectFromNetwork(containerId, c); + dockerClient.maybeRemoveNetwork(c); + }); + dockerClient.stopAndRemoveContainer(containerId); + } + + private static ExtensionContext.Store getStore(ExtensionContext context) { + return context.getStore(ExtensionContext.Namespace.GLOBAL); + } + } diff --git a/src/main/java/com/github/junit5docker/Dockers.java b/src/main/java/com/github/junit5docker/Dockers.java new file mode 100644 index 0000000..8d9fdad --- /dev/null +++ b/src/main/java/com/github/junit5docker/Dockers.java @@ -0,0 +1,22 @@ +package com.github.junit5docker; + + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(DockerExtension.class) +public @interface Dockers { + + /** + * @return the list of Docker containers to start + * @see Docker + */ + Docker[] value(); + +} diff --git a/src/test/java/com/github/junit5docker/DefaultDockerClientIT.java b/src/test/java/com/github/junit5docker/DefaultDockerClientIT.java index 4e8ae7f..dd153b4 100644 --- a/src/test/java/com/github/junit5docker/DefaultDockerClientIT.java +++ b/src/test/java/com/github/junit5docker/DefaultDockerClientIT.java @@ -51,7 +51,6 @@ public class DefaultDockerClientIT { @BeforeEach public void getExistingContainers() { existingContainers = dockerClient.listContainersCmd().exec(); - } @AfterEach @@ -90,7 +89,7 @@ public void ensureImageIsPulled() { @Test @DisplayName("start a container without ports") public void shouldStartContainer() { - String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap()); + String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap(), null).getContainerId(); assertThat(dockerClient.listContainersCmd().exec()).hasSize(existingContainers.size() + 1); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); assertThat(startedContainer.getConfig().getImage()).isEqualTo(WANTED_IMAGE); @@ -99,8 +98,8 @@ public void shouldStartContainer() { @Test @DisplayName("start a container with one port") public void shouldStartContainerWithOnePort() { - String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap(), - new PortBinding(8081, 8080)); + String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap(), null, + new PortBinding(8081, 8080)).getContainerId(); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); Ports ports = startedContainer.getHostConfig().getPortBindings(); assertThat(ports).isNotNull(); @@ -118,7 +117,7 @@ public void shouldStartContainerWithEnvironmentVariables() { Map environments = new HashMap<>(); environments.put("khaled", "souf"); environments.put("abdellah", "stagiaire"); - String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, environments); + String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, environments, null).getContainerId(); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); List envs = Arrays.asList(startedContainer.getConfig().getEnv()); assertThat(envs).hasSize(2 + DEFAULT_DOCKER_ENV_NUMBER) @@ -145,7 +144,7 @@ public void ensureContainerIsNotPresent() { @Test @DisplayName("start a container after pulling the image") public void shouldStartContainer() { - String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap()); + String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap(), null).getContainerId(); assertThat(dockerClient.listContainersCmd().exec()).hasSize(existingContainers.size() + 1); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); assertThat(startedContainer.getConfig().getImage()).isEqualTo(WANTED_IMAGE); @@ -158,7 +157,7 @@ class WithABugInDockerJava { @Test @DisplayName("add latest to the image name if none is given") public void shouldStartLatestContainer() { - String containerId = defaultDockerClient.startContainer("faustxvi/simple-two-ports", emptyMap()); + String containerId = defaultDockerClient.startContainer("faustxvi/simple-two-ports", emptyMap(), null).getContainerId(); List currentContainers = dockerClient.listContainersCmd().exec(); assertThat(currentContainers).hasSize(existingContainers.size() + 1); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); diff --git a/src/test/java/com/github/junit5docker/DockerExtensionTest.java b/src/test/java/com/github/junit5docker/DockerExtensionTest.java index 9ec1a09..70623cb 100644 --- a/src/test/java/com/github/junit5docker/DockerExtensionTest.java +++ b/src/test/java/com/github/junit5docker/DockerExtensionTest.java @@ -9,6 +9,8 @@ import org.mockito.Captor; import org.mockito.MockitoAnnotations; +import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; @@ -38,30 +40,34 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class DockerExtensionTest { +class DockerExtensionTest { static final String WAITED_LOG = "started"; - private DockerClientAdapter dockerClient = mock(DockerClientAdapter.class); + private static final String CONTAINER_ID = "CONTAINER_ID"; - private DockerExtension dockerExtension = new DockerExtension(dockerClient); + private final DockerClientAdapter dockerClient = mock(DockerClientAdapter.class); + + private final DockerExtension dockerExtension = new DockerExtension(dockerClient); @Nested class BeforeEachTestsShould { @Test - public void startContainerNotMarked() { + void startContainerNotMarked() { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); ExtensionContext context = new FakeExtensionContext(DefaultCreationContainerTest.class); dockerExtension.beforeEach(context); - verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), - eq(new PortBinding(8801, 8800))); + verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), any(), + eq(new PortBinding(8801, 8800))); } @Test - public void notStartContainerIfMarkedAsReused() { + void notStartContainerIfMarkedAsReused() { ExtensionContext context = new FakeExtensionContext(DoNotRecreateContainerTest.class); dockerExtension.beforeEach(context); verify(dockerClient, never()).startContainer(any(), anyMap(), any()); @@ -74,48 +80,55 @@ class BeforeAllTestsShould { @Captor private ArgumentCaptor> mapArgumentCaptor; + @Captor + private ArgumentCaptor stringArrayArgumentCaptor; + @BeforeEach - public void initMocks() { + void initMocks() { MockitoAnnotations.initMocks(this); } @Test - public void startContainerWithOnePort() { + void startContainerWithOnePort() { ExtensionContext context = new FakeExtensionContext(OnePortTest.class); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); dockerExtension.beforeAll(context); - verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), + verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), any(), eq(new PortBinding(8801, 8800))); } @Test - public void notStartContainerIfMarkedAsRecreated() { + void notStartContainerIfMarkedAsRecreated() { ExtensionContext context = new FakeExtensionContext(DefaultCreationContainerTest.class); dockerExtension.beforeAll(context); verify(dockerClient, never()).startContainer(any(), anyMap(), any()); } @Test - public void startContainerWithMultiplePorts() { + void startContainerWithMultiplePorts() { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); ExtensionContext context = new FakeExtensionContext(MultiplePortTest.class); dockerExtension.beforeAll(context); - verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), + verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), any(), eq(new PortBinding(8801, 8800)), eq(new PortBinding(9901, 9900))); } @Test - public void notWaitByDefault() { + void notWaitByDefault() { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); ExtensionContext context = new FakeExtensionContext(WaitForNothingTest.class); dockerExtension.beforeAll(context); verify(dockerClient, never()).logs(anyString()); } @Test - public void startContainerWithEnvironmentVariables() { + void startContainerWithEnvironmentVariables() { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); ExtensionContext context = new FakeExtensionContext(OneEnvironmentTest.class); dockerExtension.beforeAll(context); verify(dockerClient).startContainer(eq("wantedImage"), - mapArgumentCaptor.capture(), any()); + mapArgumentCaptor.capture(), any(), any()); Map environment = mapArgumentCaptor.getValue(); assertThat(environment) .hasSize(1) @@ -123,11 +136,23 @@ public void startContainerWithEnvironmentVariables() { .containsValues("myValue"); } + @Test + void startContainerWithNetworks() { + List networkIds = Arrays.asList("my-network-1", "my-network-2"); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, networkIds)); + ExtensionContext context = new FakeExtensionContext(TwoNetworksTest.class); + dockerExtension.beforeAll(context); + verify(dockerClient).startContainer(eq("wantedImage"), any(), stringArrayArgumentCaptor.capture(), any()); + String[] networks = stringArrayArgumentCaptor.getValue(); + assertThat(networks).containsExactly("my-network-1", "my-network-2"); + } + @Nested class BeThreadSafe { @Test - public void waitForLogToAppear() throws ExecutionException, InterruptedException { + void waitForLogToAppear() throws ExecutionException, InterruptedException { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); ExtensionContext context = new FakeExtensionContext(WaitForLogTest.class); long duration = sendLogAndTimeExecution(100, TimeUnit.MILLISECONDS, () -> dockerExtension.beforeAll(context)); @@ -138,7 +163,8 @@ public void waitForLogToAppear() throws ExecutionException, InterruptedException } @Test - public void closeLogStreamOnceFound() throws ExecutionException, InterruptedException { + void closeLogStreamOnceFound() { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); AtomicBoolean streamClosed = new AtomicBoolean(false); Stream logStream = Stream.of(WAITED_LOG).onClose(() -> streamClosed.set(true)); when(dockerClient.logs(argThat(argument -> true))).thenReturn(logStream); @@ -147,7 +173,8 @@ public void closeLogStreamOnceFound() throws ExecutionException, InterruptedExce } @Test - public void closeLogEvenWithExceptionOnRead() throws ExecutionException, InterruptedException { + void closeLogEvenWithExceptionOnRead() { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); AtomicBoolean streamClosed = new AtomicBoolean(false); Stream logStream = Stream.generate(() -> { throw new RuntimeException(); @@ -161,7 +188,8 @@ public void closeLogEvenWithExceptionOnRead() throws ExecutionException, Interru } @Test - public void timeoutIfLogDoesNotAppear() { + void timeoutIfLogDoesNotAppear() { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> { ExtensionContext context = new FakeExtensionContext(TimeoutTest.class); sendLogAndTimeExecution(1, TimeUnit.SECONDS, () -> dockerExtension.beforeAll(context)); @@ -169,7 +197,8 @@ public void timeoutIfLogDoesNotAppear() { } @Test - public void throwsExceptionIfLogNotFoundAndLogsEnded() { + void throwsExceptionIfLogNotFoundAndLogsEnded() { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> { ExtensionContext context = new FakeExtensionContext(WaitForNotPresentLogTest.class); @@ -179,7 +208,8 @@ public void throwsExceptionIfLogNotFoundAndLogsEnded() { } @Test - public void beInterruptible() throws ExecutionException, InterruptedException { + void beInterruptible() throws ExecutionException, InterruptedException { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); ExtensionContext context = new FakeExtensionContext(InterruptionTest.class); Thread mainThread = Thread.currentThread(); CountDownLatch logRequest = new CountDownLatch(1); @@ -236,25 +266,22 @@ private Future sendLogAfter(int waitingTime, TimeUnit timeUnit, ExecutorServi @Nested class AfterEachTestsShould { - private static final String CONTAINER_ID = "CONTAINER_ID"; + ExtensionContext defaultCreationContainerTest = new FakeExtensionContext(DefaultCreationContainerTest.class); @BeforeEach - public void callBefore() { - when(dockerClient.startContainer(anyString(), anyMap(), - any())) - .thenReturn(CONTAINER_ID); - dockerExtension.beforeEach(new FakeExtensionContext(DefaultCreationContainerTest.class)); + void callBefore() { + when (dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + dockerExtension.beforeEach(defaultCreationContainerTest); } @Test - public void stopContainer() { - ExtensionContext context = new FakeExtensionContext(DefaultCreationContainerTest.class); - dockerExtension.afterEach(context); + void stopContainer() { + dockerExtension.afterEach(defaultCreationContainerTest); verify(dockerClient).stopAndRemoveContainer(CONTAINER_ID); } @Test - public void notStopContainerNotMarkedAsRenewable() { + void notStopContainerNotMarkedAsRenewable() { ExtensionContext context = new FakeExtensionContext(OnePortTest.class); dockerExtension.afterEach(context); verify(dockerClient, never()).stopAndRemoveContainer(any()); @@ -265,29 +292,39 @@ public void notStopContainerNotMarkedAsRenewable() { @Nested class AfterAllTestsShould { - private static final String CONTAINER_ID = "CONTAINER_ID"; + private final FakeExtensionContext onePortExtensionContext = new FakeExtensionContext(OnePortTest.class); @BeforeEach - public void callBefore() { - when(dockerClient.startContainer(anyString(), anyMap(), - any())) - .thenReturn(CONTAINER_ID); - dockerExtension.beforeAll(new FakeExtensionContext(OnePortTest.class)); + void callBefore() { + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())) + .thenReturn(new ContainerInfo(CONTAINER_ID, null)); + dockerExtension.beforeAll(onePortExtensionContext); } @Test - public void stopContainer() { - ExtensionContext context = new FakeExtensionContext(OnePortTest.class); - dockerExtension.afterAll(context); + void stopContainer() { + dockerExtension.afterAll(onePortExtensionContext); verify(dockerClient).stopAndRemoveContainer(CONTAINER_ID); } @Test - public void notStopContainerMarkedAsRenewable() { + void notStopContainerMarkedAsRenewable() { ExtensionContext context = new FakeExtensionContext(DefaultCreationContainerTest.class); dockerExtension.afterAll(context); verify(dockerClient, never()).stopAndRemoveContainer(any()); } + @Test + void disconnectFromAndRemoveNetworks() { + ExtensionContext context = new FakeExtensionContext(TwoNetworksTest.class); + List networkIds = Arrays.asList("111", "222"); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())) + .thenReturn(new ContainerInfo(CONTAINER_ID, networkIds)); + dockerExtension.beforeAll(context); + dockerExtension.afterAll(context); + verify(dockerClient, times(2)).disconnectFromNetwork(eq(CONTAINER_ID), argThat(networkIds::contains)); + verify(dockerClient, times(2)).maybeRemoveNetwork(any()); + } + } @Docker(image = "wantedImage", ports = @Port(exposed = 8801, inner = 8800), newForEachCase = false) @@ -307,6 +344,12 @@ private static class OneEnvironmentTest { } + @Docker(image = "wantedImage", ports = @Port(exposed = 8801, inner = 8800), + networks = {"my-network-1", "my-network-2"}, newForEachCase = false) + private static class TwoNetworksTest { + + } + @Docker(image = "wantedImage", ports = @Port(exposed = 8801, inner = 8800), waitFor = @WaitFor(WAITED_LOG), newForEachCase = false) private static class WaitForLogTest { diff --git a/src/test/java/com/github/junit5docker/StartMultipleDockerAnnotationsIT.java b/src/test/java/com/github/junit5docker/StartMultipleDockerAnnotationsIT.java new file mode 100644 index 0000000..d59ad3c --- /dev/null +++ b/src/test/java/com/github/junit5docker/StartMultipleDockerAnnotationsIT.java @@ -0,0 +1,99 @@ +package com.github.junit5docker; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.model.Network; +import com.github.dockerjava.core.DockerClientBuilder; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import static com.github.dockerjava.core.DefaultDockerClientConfig.createDefaultConfigBuilder; +import static com.github.junit5docker.StartMultipleDockerAnnotationsIT.TEST_NETWORK_1; +import static com.github.junit5docker.StartMultipleDockerAnnotationsIT.TEST_NETWORK_2; +import static org.assertj.core.api.Assertions.assertThat; + +@Dockers({ + @Docker(image = "faustxvi/simple-two-ports", ports = @Port(exposed = 8081, inner = 8080), networks = {TEST_NETWORK_1, TEST_NETWORK_2}), + @Docker(image = "faustxvi/simple-two-ports", ports = @Port(exposed = 8082, inner = 8080), networks = {TEST_NETWORK_1, TEST_NETWORK_2}) +}) +@DisplayName("Multiple docker containers with multiple networks") +class StartMultipleDockerAnnotationsIT { + + static final String TEST_NETWORK_1 = "testNetwork1"; + static final String TEST_NETWORK_2 = "testNetwork2"; + + @BeforeAll + static void verifyCleanStateBefore() { + assertNothingRunning(); + } + + @BeforeEach + void verifyContainerIsReady() { + assertContainersRunning(); + } + + @Test + void verifyFirstContainerIsStarted() { + assertContainersRunning(); + } + + @AfterEach + void verifyContainerIsStillAlive() { + assertContainersRunning(); + } + + @AfterAll + static void verifyCleanStateAfter() { + assertNothingRunning(); + } + + private static void assertNothingRunning() { + assertThat(canConnectOnPort(8081)).isFalse(); + assertThat(canConnectOnPort(8082)).isFalse(); + assertThat(getNetworks(TEST_NETWORK_1)).isEmpty(); + assertThat(getNetworks(TEST_NETWORK_2)).isEmpty(); + } + + private static void assertContainersRunning() { + assertThat(canConnectOnPort(8081)).isTrue(); + assertThat(canConnectOnPort(8082)).isTrue(); + assertContainersConnectedToNetwork(TEST_NETWORK_1); + assertContainersConnectedToNetwork(TEST_NETWORK_2); + } + + private static void assertContainersConnectedToNetwork(String networkName) { + List networks1 = getNetworks(networkName); + + assertThat(networks1).size().isEqualTo(1); + Network network = networks1.get(0); + assertThat(network.getContainers()).size().isEqualTo(2); + assertThat(network.getName()).isEqualTo(networkName); + } + + private static List getNetworks(String networkName) { + DockerClient dockerClient = DockerClientBuilder.getInstance(createDefaultConfigBuilder().withApiVersion("1.22")).build(); + return dockerClient.listNetworksCmd().exec().stream().filter(n -> networkName.equals(n.getName())).collect(Collectors.toList()); + } + + private static boolean canConnectOnPort(int port) { + HttpGet request = new HttpGet("http://localhost:" + port + "/env"); + try (CloseableHttpClient httpClient = HttpClientBuilder.create().build(); + CloseableHttpResponse response = httpClient.execute(request)) { + return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK; + } catch (IOException e) { + return false; + } + } +} diff --git a/src/test/java/com/github/junit5docker/fakes/FakeExtensionContext.java b/src/test/java/com/github/junit5docker/fakes/FakeExtensionContext.java index 8b20bc5..2893337 100644 --- a/src/test/java/com/github/junit5docker/fakes/FakeExtensionContext.java +++ b/src/test/java/com/github/junit5docker/fakes/FakeExtensionContext.java @@ -1,6 +1,8 @@ package com.github.junit5docker.fakes; import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.engine.execution.ExtensionValuesStore; +import org.junit.jupiter.engine.execution.NamespaceAwareStore; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; @@ -10,9 +12,11 @@ public class FakeExtensionContext implements ExtensionContext { private final Class testSampleClass; + private final ExtensionValuesStore valuesStore; public FakeExtensionContext(Class testSampleClass) { this.testSampleClass = testSampleClass; + valuesStore = new ExtensionValuesStore(null); } @Override @@ -71,6 +75,6 @@ public void publishReportEntry(Map map) { @Override public Store getStore(Namespace namespace) { - return null; + return new NamespaceAwareStore(valuesStore, namespace); } } From 854ceb54050596377882be0e1003343e25b76bb9 Mon Sep 17 00:00:00 2001 From: Tomas Hanley Date: Fri, 26 Jan 2018 17:26:55 +0000 Subject: [PATCH 2/2] Fix checkstyle errors --- .../github/junit5docker/ContainerInfo.java | 9 +- .../junit5docker/DefaultDockerClient.java | 18 +++- .../java/com/github/junit5docker/Docker.java | 5 +- .../junit5docker/DockerClientAdapter.java | 8 +- .../github/junit5docker/DockerExtension.java | 63 ++++------- .../github/junit5docker/DockerLogChecker.java | 48 +++++++++ .../java/com/github/junit5docker/Dockers.java | 12 ++- .../junit5docker/DefaultDockerClientIT.java | 16 +-- .../junit5docker/DockerExtensionTest.java | 102 +++++++++--------- .../StartMultipleDockerAnnotationsIT.java | 17 ++- .../fakes/FakeExtensionContext.java | 2 + 11 files changed, 177 insertions(+), 123 deletions(-) create mode 100644 src/main/java/com/github/junit5docker/DockerLogChecker.java diff --git a/src/main/java/com/github/junit5docker/ContainerInfo.java b/src/main/java/com/github/junit5docker/ContainerInfo.java index 4fb4cd6..3d2f389 100644 --- a/src/main/java/com/github/junit5docker/ContainerInfo.java +++ b/src/main/java/com/github/junit5docker/ContainerInfo.java @@ -5,21 +5,22 @@ import static java.util.Collections.emptySet; import static java.util.Collections.unmodifiableCollection; -public class ContainerInfo { +class ContainerInfo { private final String containerId; + private final Collection networkIds; - public ContainerInfo(String containerId, Collection networkIds) { + ContainerInfo(String containerId, Collection networkIds) { this.containerId = containerId; this.networkIds = networkIds == null ? emptySet() : unmodifiableCollection(networkIds); } - public String getContainerId() { + String getContainerId() { return containerId; } - public Collection getNetworkIds() { + Collection getNetworkIds() { return networkIds; } } diff --git a/src/main/java/com/github/junit5docker/DefaultDockerClient.java b/src/main/java/com/github/junit5docker/DefaultDockerClient.java index e6774ca..f6f5fcc 100644 --- a/src/main/java/com/github/junit5docker/DefaultDockerClient.java +++ b/src/main/java/com/github/junit5docker/DefaultDockerClient.java @@ -30,7 +30,10 @@ class DefaultDockerClient implements DockerClientAdapter { } @Override - public ContainerInfo startContainer(String wantedImage, Map environment, String[] networkNames, PortBinding... portBinding) { + public ContainerInfo startContainer(String wantedImage, + Map environment, + String[] networkNames, + PortBinding... portBinding) { Ports bindings = createPortBindings(portBinding); List environmentStrings = createEnvironmentList(environment); String containerId = createContainer(wantedImage, bindings, environmentStrings); @@ -62,10 +65,15 @@ private String getNetworkId(String networkName) { } private Optional getExistingNetworkId(String networkName) { - return dockerClient.listNetworksCmd().exec().stream() - .filter(n -> n.getName().equals(networkName)) - .reduce((a, b) -> { - throw new IllegalStateException("Multiple networks found with the same name: " + a + ", " + b); + return dockerClient.listNetworksCmd() + .exec() + .stream() + .filter(name -> name.getName().equals(networkName)) + .reduce((name1, name2) -> { + String msg = String.format("Multiple networks found with the same name: %s and %s", + name1, + name2); + throw new IllegalStateException(msg); }) .map(Network::getId); } diff --git a/src/main/java/com/github/junit5docker/Docker.java b/src/main/java/com/github/junit5docker/Docker.java index 3ee8942..1f37229 100644 --- a/src/main/java/com/github/junit5docker/Docker.java +++ b/src/main/java/com/github/junit5docker/Docker.java @@ -11,7 +11,7 @@ import static com.github.junit5docker.WaitFor.NOTHING; /** - *

This annotation is the junit-docker entry point.

+ *

This annotation is a junit-docker entry point.

* *

Adding this annotation to a test's class will start a docker container before running the tests and will be stop * at the end of the tests. This is done once per class.

@@ -55,7 +55,8 @@ boolean newForEachCase() default true; /** - * The names of the networks for the container to join. + * @return the names of the networks for the container to join. + * Empty if the container has no networks to join. */ String[] networks() default {}; diff --git a/src/main/java/com/github/junit5docker/DockerClientAdapter.java b/src/main/java/com/github/junit5docker/DockerClientAdapter.java index 18ee36a..54a6e7a 100644 --- a/src/main/java/com/github/junit5docker/DockerClientAdapter.java +++ b/src/main/java/com/github/junit5docker/DockerClientAdapter.java @@ -5,13 +5,17 @@ interface DockerClientAdapter { - ContainerInfo startContainer(String wantedImage, Map environment, String[] networkNames, PortBinding... portBinding); + ContainerInfo startContainer(String wantedImage, + Map environment, + String[] networkNames, + PortBinding... portBinding); void disconnectFromNetwork(String containerId, String networkId); /** * Remove the specified network if no containers are connected to it. - * @param networkId + * + * @param networkId The id of the network to try remove */ void maybeRemoveNetwork(String networkId); diff --git a/src/main/java/com/github/junit5docker/DockerExtension.java b/src/main/java/com/github/junit5docker/DockerExtension.java index 4cbd91c..ec58978 100644 --- a/src/main/java/com/github/junit5docker/DockerExtension.java +++ b/src/main/java/com/github/junit5docker/DockerExtension.java @@ -9,21 +9,14 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Stream; - -import static java.util.concurrent.CompletableFuture.supplyAsync; class DockerExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback { private final DockerClientAdapter dockerClient; + private final DockerLogChecker dockerLogChecker; DockerExtension() { this(new DefaultDockerClient()); @@ -31,11 +24,12 @@ class DockerExtension implements BeforeAllCallback, AfterAllCallback, BeforeEach DockerExtension(DockerClientAdapter dockerClient) { this.dockerClient = dockerClient; + this.dockerLogChecker = new DockerLogChecker(dockerClient); } @Override public void beforeAll(ExtensionContext context) { - forEachDocker(context, d -> !d.newForEachCase(), this::startContainer); + forEachDocker(context, docker -> !docker.newForEachCase(), this::startContainer); } @Override @@ -43,8 +37,12 @@ public void beforeEach(ExtensionContext context) { forEachDocker(context, Docker::newForEachCase, this::startContainer); } - private static void forEachDocker(ExtensionContext context, Predicate predicate, BiConsumer action){ - Arrays.stream(findDockerAnnotations(context)).filter(predicate).forEach(d -> action.accept(context, d)); + private static void forEachDocker(ExtensionContext context, + Predicate predicate, + BiConsumer action) { + Arrays.stream(findDockerAnnotations(context)) + .filter(predicate) + .forEach(docker -> action.accept(context, docker)); } private void startContainer(ExtensionContext context, Docker dockerAnnotation) { @@ -53,39 +51,14 @@ private void startContainer(ExtensionContext context, Docker dockerAnnotation) { String imageReference = findImageName(dockerAnnotation); WaitFor waitFor = dockerAnnotation.waitFor(); String[] networkNames = dockerAnnotation.networks(); - ContainerInfo containerInfo = dockerClient.startContainer(imageReference, environmentMap, networkNames, portBindings); - waitForLogAccordingTo(waitFor, containerInfo.getContainerId()); + ContainerInfo containerInfo = dockerClient.startContainer(imageReference, + environmentMap, + networkNames, + portBindings); + dockerLogChecker.waitForLogAccordingTo(waitFor, containerInfo.getContainerId()); getStore(context).put(dockerAnnotation, containerInfo); } - private void waitForLogAccordingTo(WaitFor waitFor, String containerId) { - String expectedLog = waitFor.value(); - if (!WaitFor.NOTHING.equals(expectedLog)) { - ExecutorService executor = Executors.newSingleThreadExecutor(); - CompletableFuture logFound = supplyAsync(findFirstLogContaining(expectedLog, containerId), executor); - executor.shutdown(); - try { - boolean termination = executor.awaitTermination(waitFor.timeoutInMillis(), TimeUnit.MILLISECONDS); - if (!termination) { - throw new AssertionError("Timeout while waiting for log : \"" + expectedLog + "\""); - } - if (!logFound.getNow(false)) { - throw new AssertionError("\"" + expectedLog + "\" not found in logs and container stopped"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } - - private Supplier findFirstLogContaining(String logToFind, String containerId) { - return () -> { - try (Stream logs = dockerClient.logs(containerId)) { - return logs.anyMatch(log -> log.contains(logToFind)); - } - }; - } - private static Docker[] findDockerAnnotations(ExtensionContext extensionContext) { Class testClass = extensionContext.getTestClass().get(); return testClass.getAnnotationsByType(Docker.class); @@ -116,7 +89,7 @@ private static PortBinding[] createPortBindings(Docker dockerAnnotation) { @Override public void afterAll(ExtensionContext context) { - forEachDocker(context, d -> !d.newForEachCase(), this::stopAndRemove); + forEachDocker(context, docker -> !docker.newForEachCase(), this::stopAndRemove); } @Override @@ -129,9 +102,9 @@ private void stopAndRemove(ExtensionContext context, Docker docker) { String containerId = containerInfo.getContainerId(); - containerInfo.getNetworkIds().forEach(c -> { - dockerClient.disconnectFromNetwork(containerId, c); - dockerClient.maybeRemoveNetwork(c); + containerInfo.getNetworkIds().forEach(networkId -> { + dockerClient.disconnectFromNetwork(containerId, networkId); + dockerClient.maybeRemoveNetwork(networkId); }); dockerClient.stopAndRemoveContainer(containerId); } diff --git a/src/main/java/com/github/junit5docker/DockerLogChecker.java b/src/main/java/com/github/junit5docker/DockerLogChecker.java new file mode 100644 index 0000000..55a5de3 --- /dev/null +++ b/src/main/java/com/github/junit5docker/DockerLogChecker.java @@ -0,0 +1,48 @@ +package com.github.junit5docker; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static java.util.concurrent.CompletableFuture.supplyAsync; + +class DockerLogChecker { + + private final DockerClientAdapter dockerClient; + + DockerLogChecker(DockerClientAdapter dockerClient) { + this.dockerClient = dockerClient; + } + + void waitForLogAccordingTo(WaitFor waitFor, String containerId) { + String expectedLog = waitFor.value(); + if (!WaitFor.NOTHING.equals(expectedLog)) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + CompletableFuture logFound = supplyAsync(findFirstLogContaining(expectedLog, containerId), + executor); + executor.shutdown(); + try { + boolean termination = executor.awaitTermination(waitFor.timeoutInMillis(), TimeUnit.MILLISECONDS); + if (!termination) { + throw new AssertionError("Timeout while waiting for log : \"" + expectedLog + "\""); + } + if (!logFound.getNow(false)) { + throw new AssertionError("\"" + expectedLog + "\" not found in logs and container stopped"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + private Supplier findFirstLogContaining(String logToFind, String containerId) { + return () -> { + try (Stream logs = dockerClient.logs(containerId)) { + return logs.anyMatch(log -> log.contains(logToFind)); + } + }; + } +} diff --git a/src/main/java/com/github/junit5docker/Dockers.java b/src/main/java/com/github/junit5docker/Dockers.java index 8d9fdad..cba0c52 100644 --- a/src/main/java/com/github/junit5docker/Dockers.java +++ b/src/main/java/com/github/junit5docker/Dockers.java @@ -1,6 +1,5 @@ package com.github.junit5docker; - import org.junit.jupiter.api.extension.ExtendWith; import java.lang.annotation.ElementType; @@ -8,13 +7,22 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + *

This annotation is a junit-docker entry point.

+ *

It can be used to start multiple docker containers per test.

+ *

+ *

It must contain one or more {@link com.github.junit5docker.Docker} annotations.

+ * + * @see Docker + */ + @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @ExtendWith(DockerExtension.class) public @interface Dockers { /** - * @return the list of Docker containers to start + * @return the list of Docker containers to start. * @see Docker */ Docker[] value(); diff --git a/src/test/java/com/github/junit5docker/DefaultDockerClientIT.java b/src/test/java/com/github/junit5docker/DefaultDockerClientIT.java index dd153b4..5ef2947 100644 --- a/src/test/java/com/github/junit5docker/DefaultDockerClientIT.java +++ b/src/test/java/com/github/junit5docker/DefaultDockerClientIT.java @@ -89,7 +89,8 @@ public void ensureImageIsPulled() { @Test @DisplayName("start a container without ports") public void shouldStartContainer() { - String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap(), null).getContainerId(); + String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap(), null) + .getContainerId(); assertThat(dockerClient.listContainersCmd().exec()).hasSize(existingContainers.size() + 1); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); assertThat(startedContainer.getConfig().getImage()).isEqualTo(WANTED_IMAGE); @@ -117,11 +118,11 @@ public void shouldStartContainerWithEnvironmentVariables() { Map environments = new HashMap<>(); environments.put("khaled", "souf"); environments.put("abdellah", "stagiaire"); - String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, environments, null).getContainerId(); + String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, environments, null) + .getContainerId(); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); List envs = Arrays.asList(startedContainer.getConfig().getEnv()); - assertThat(envs).hasSize(2 + DEFAULT_DOCKER_ENV_NUMBER) - .contains("khaled=souf", "abdellah=stagiaire"); + assertThat(envs).hasSize(2 + DEFAULT_DOCKER_ENV_NUMBER).contains("khaled=souf", "abdellah=stagiaire"); } } @@ -144,7 +145,8 @@ public void ensureContainerIsNotPresent() { @Test @DisplayName("start a container after pulling the image") public void shouldStartContainer() { - String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap(), null).getContainerId(); + String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap(), null) + .getContainerId(); assertThat(dockerClient.listContainersCmd().exec()).hasSize(existingContainers.size() + 1); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); assertThat(startedContainer.getConfig().getImage()).isEqualTo(WANTED_IMAGE); @@ -157,7 +159,9 @@ class WithABugInDockerJava { @Test @DisplayName("add latest to the image name if none is given") public void shouldStartLatestContainer() { - String containerId = defaultDockerClient.startContainer("faustxvi/simple-two-ports", emptyMap(), null).getContainerId(); + String containerId = defaultDockerClient.startContainer("faustxvi/simple-two-ports", + emptyMap(), + null).getContainerId(); List currentContainers = dockerClient.listContainersCmd().exec(); assertThat(currentContainers).hasSize(existingContainers.size() + 1); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); diff --git a/src/test/java/com/github/junit5docker/DockerExtensionTest.java b/src/test/java/com/github/junit5docker/DockerExtensionTest.java index 70623cb..d8dee33 100644 --- a/src/test/java/com/github/junit5docker/DockerExtensionTest.java +++ b/src/test/java/com/github/junit5docker/DockerExtensionTest.java @@ -46,10 +46,12 @@ class DockerExtensionTest { - static final String WAITED_LOG = "started"; + private static final String WAITED_LOG = "started"; private static final String CONTAINER_ID = "CONTAINER_ID"; + private static final ContainerInfo CONTAINER_INFO = new ContainerInfo(CONTAINER_ID, null); + private final DockerClientAdapter dockerClient = mock(DockerClientAdapter.class); private final DockerExtension dockerExtension = new DockerExtension(dockerClient); @@ -59,11 +61,10 @@ class BeforeEachTestsShould { @Test void startContainerNotMarked() { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); ExtensionContext context = new FakeExtensionContext(DefaultCreationContainerTest.class); dockerExtension.beforeEach(context); - verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), any(), - eq(new PortBinding(8801, 8800))); + verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), any(), eq(new PortBinding(8801, 8800))); } @Test @@ -91,10 +92,9 @@ void initMocks() { @Test void startContainerWithOnePort() { ExtensionContext context = new FakeExtensionContext(OnePortTest.class); - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); dockerExtension.beforeAll(context); - verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), any(), - eq(new PortBinding(8801, 8800))); + verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), any(), eq(new PortBinding(8801, 8800))); } @Test @@ -106,17 +106,19 @@ void notStartContainerIfMarkedAsRecreated() { @Test void startContainerWithMultiplePorts() { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); ExtensionContext context = new FakeExtensionContext(MultiplePortTest.class); dockerExtension.beforeAll(context); - verify(dockerClient).startContainer(eq("wantedImage"), anyMap(), any(), - eq(new PortBinding(8801, 8800)), - eq(new PortBinding(9901, 9900))); + verify(dockerClient).startContainer(eq("wantedImage"), + anyMap(), + any(), + eq(new PortBinding(8801, 8800)), + eq(new PortBinding(9901, 9900))); } @Test void notWaitByDefault() { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); ExtensionContext context = new FakeExtensionContext(WaitForNothingTest.class); dockerExtension.beforeAll(context); verify(dockerClient, never()).logs(anyString()); @@ -124,22 +126,20 @@ void notWaitByDefault() { @Test void startContainerWithEnvironmentVariables() { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); ExtensionContext context = new FakeExtensionContext(OneEnvironmentTest.class); dockerExtension.beforeAll(context); - verify(dockerClient).startContainer(eq("wantedImage"), - mapArgumentCaptor.capture(), any(), any()); + verify(dockerClient).startContainer(eq("wantedImage"), mapArgumentCaptor.capture(), any(), any()); Map environment = mapArgumentCaptor.getValue(); - assertThat(environment) - .hasSize(1) - .containsKeys("toTest") - .containsValues("myValue"); + assertThat(environment).hasSize(1).containsKeys("toTest").containsValues("myValue"); } @Test void startContainerWithNetworks() { List networkIds = Arrays.asList("my-network-1", "my-network-2"); - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, networkIds)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo( + CONTAINER_ID, + networkIds)); ExtensionContext context = new FakeExtensionContext(TwoNetworksTest.class); dockerExtension.beforeAll(context); verify(dockerClient).startContainer(eq("wantedImage"), any(), stringArrayArgumentCaptor.capture(), any()); @@ -152,19 +152,20 @@ class BeThreadSafe { @Test void waitForLogToAppear() throws ExecutionException, InterruptedException { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); ExtensionContext context = new FakeExtensionContext(WaitForLogTest.class); long duration = sendLogAndTimeExecution(100, - TimeUnit.MILLISECONDS, () -> dockerExtension.beforeAll(context)); - assertThat(duration) - .overridingErrorMessage("Should have waited for log to appear during %d ms but waited %d ms", 100, - duration) - .isGreaterThanOrEqualTo(100); + TimeUnit.MILLISECONDS, + () -> dockerExtension.beforeAll(context)); + assertThat(duration).overridingErrorMessage( + "Should have waited for log to appear during %d ms but waited %d ms", + 100, + duration).isGreaterThanOrEqualTo(100); } @Test void closeLogStreamOnceFound() { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); AtomicBoolean streamClosed = new AtomicBoolean(false); Stream logStream = Stream.of(WAITED_LOG).onClose(() -> streamClosed.set(true)); when(dockerClient.logs(argThat(argument -> true))).thenReturn(logStream); @@ -174,22 +175,20 @@ void closeLogStreamOnceFound() { @Test void closeLogEvenWithExceptionOnRead() { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); AtomicBoolean streamClosed = new AtomicBoolean(false); Stream logStream = Stream.generate(() -> { throw new RuntimeException(); }) .onClose(() -> streamClosed.set(true)); when(dockerClient.logs(argThat(argument -> true))).thenReturn(logStream); - assertThatThrownBy( - () -> dockerExtension.beforeAll(new FakeExtensionContext(WaitForLogTest.class)) - ); + assertThatThrownBy(() -> dockerExtension.beforeAll(new FakeExtensionContext(WaitForLogTest.class))); assertThat(streamClosed.get()).as("Stream should be closed").isTrue(); } @Test void timeoutIfLogDoesNotAppear() { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> { ExtensionContext context = new FakeExtensionContext(TimeoutTest.class); sendLogAndTimeExecution(1, TimeUnit.SECONDS, () -> dockerExtension.beforeAll(context)); @@ -198,18 +197,16 @@ void timeoutIfLogDoesNotAppear() { @Test void throwsExceptionIfLogNotFoundAndLogsEnded() { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> { - ExtensionContext context = - new FakeExtensionContext(WaitForNotPresentLogTest.class); - sendLogAndTimeExecution(100, TimeUnit.MILLISECONDS, - () -> dockerExtension.beforeAll(context)); + ExtensionContext context = new FakeExtensionContext(WaitForNotPresentLogTest.class); + sendLogAndTimeExecution(100, TimeUnit.MILLISECONDS, () -> dockerExtension.beforeAll(context)); }).withMessageContaining("not found"); } @Test void beInterruptible() throws ExecutionException, InterruptedException { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); ExtensionContext context = new FakeExtensionContext(InterruptionTest.class); Thread mainThread = Thread.currentThread(); CountDownLatch logRequest = new CountDownLatch(1); @@ -218,15 +215,13 @@ void beInterruptible() throws ExecutionException, InterruptedException { return unfoundableLog(); }); CompletableFuture voidCompletableFuture = runAsync(() -> { - assertThat(logRequest) - .overridingErrorMessage("should have ask for logs") - .isDownBefore(500, TimeUnit.MILLISECONDS); + assertThat(logRequest).overridingErrorMessage("should have ask for logs") + .isDownBefore(500, TimeUnit.MILLISECONDS); mainThread.interrupt(); }); dockerExtension.beforeAll(context); - assertThat(Thread.interrupted()) - .overridingErrorMessage("Interrupted thread should still interrupted") - .isTrue(); + assertThat(Thread.interrupted()).overridingErrorMessage("Interrupted thread should still interrupted") + .isTrue(); assertExecutionOf(voidCompletableFuture::get).hasNoAssertionFailures(); } @@ -266,17 +261,19 @@ private Future sendLogAfter(int waitingTime, TimeUnit timeUnit, ExecutorServi @Nested class AfterEachTestsShould { - ExtensionContext defaultCreationContainerTest = new FakeExtensionContext(DefaultCreationContainerTest.class); + private final FakeExtensionContext defaultContext = new FakeExtensionContext(DefaultCreationContainerTest + .class); @BeforeEach void callBefore() { - when (dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo(CONTAINER_ID, null)); - dockerExtension.beforeEach(defaultCreationContainerTest); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); + dockerExtension.beforeEach(defaultContext); } @Test void stopContainer() { - dockerExtension.afterEach(defaultCreationContainerTest); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); + dockerExtension.afterEach(defaultContext); verify(dockerClient).stopAndRemoveContainer(CONTAINER_ID); } @@ -296,8 +293,7 @@ class AfterAllTestsShould { @BeforeEach void callBefore() { - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())) - .thenReturn(new ContainerInfo(CONTAINER_ID, null)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(CONTAINER_INFO); dockerExtension.beforeAll(onePortExtensionContext); } @@ -313,12 +309,14 @@ void notStopContainerMarkedAsRenewable() { dockerExtension.afterAll(context); verify(dockerClient, never()).stopAndRemoveContainer(any()); } + @Test void disconnectFromAndRemoveNetworks() { ExtensionContext context = new FakeExtensionContext(TwoNetworksTest.class); List networkIds = Arrays.asList("111", "222"); - when(dockerClient.startContainer(anyString(), anyMap(), any(), any())) - .thenReturn(new ContainerInfo(CONTAINER_ID, networkIds)); + when(dockerClient.startContainer(anyString(), anyMap(), any(), any())).thenReturn(new ContainerInfo( + CONTAINER_ID, + networkIds)); dockerExtension.beforeAll(context); dockerExtension.afterAll(context); verify(dockerClient, times(2)).disconnectFromNetwork(eq(CONTAINER_ID), argThat(networkIds::contains)); diff --git a/src/test/java/com/github/junit5docker/StartMultipleDockerAnnotationsIT.java b/src/test/java/com/github/junit5docker/StartMultipleDockerAnnotationsIT.java index d59ad3c..1a379ce 100644 --- a/src/test/java/com/github/junit5docker/StartMultipleDockerAnnotationsIT.java +++ b/src/test/java/com/github/junit5docker/StartMultipleDockerAnnotationsIT.java @@ -25,13 +25,15 @@ import static org.assertj.core.api.Assertions.assertThat; @Dockers({ - @Docker(image = "faustxvi/simple-two-ports", ports = @Port(exposed = 8081, inner = 8080), networks = {TEST_NETWORK_1, TEST_NETWORK_2}), - @Docker(image = "faustxvi/simple-two-ports", ports = @Port(exposed = 8082, inner = 8080), networks = {TEST_NETWORK_1, TEST_NETWORK_2}) -}) + @Docker(image = "faustxvi/simple-two-ports", ports = @Port(exposed = 8081, inner = 8080), + networks = {TEST_NETWORK_1, TEST_NETWORK_2}), + @Docker(image = "faustxvi/simple-two-ports", ports = @Port(exposed = 8082, inner = 8080), + networks = {TEST_NETWORK_1, TEST_NETWORK_2})}) @DisplayName("Multiple docker containers with multiple networks") class StartMultipleDockerAnnotationsIT { static final String TEST_NETWORK_1 = "testNetwork1"; + static final String TEST_NETWORK_2 = "testNetwork2"; @BeforeAll @@ -83,8 +85,13 @@ private static void assertContainersConnectedToNetwork(String networkName) { } private static List getNetworks(String networkName) { - DockerClient dockerClient = DockerClientBuilder.getInstance(createDefaultConfigBuilder().withApiVersion("1.22")).build(); - return dockerClient.listNetworksCmd().exec().stream().filter(n -> networkName.equals(n.getName())).collect(Collectors.toList()); + DockerClient dockerClient = DockerClientBuilder.getInstance(createDefaultConfigBuilder().withApiVersion("1.22")) + .build(); + return dockerClient.listNetworksCmd() + .exec() + .stream() + .filter(n -> networkName.equals(n.getName())) + .collect(Collectors.toList()); } private static boolean canConnectOnPort(int port) { diff --git a/src/test/java/com/github/junit5docker/fakes/FakeExtensionContext.java b/src/test/java/com/github/junit5docker/fakes/FakeExtensionContext.java index 2893337..acd7fca 100644 --- a/src/test/java/com/github/junit5docker/fakes/FakeExtensionContext.java +++ b/src/test/java/com/github/junit5docker/fakes/FakeExtensionContext.java @@ -11,7 +11,9 @@ import java.util.Set; public class FakeExtensionContext implements ExtensionContext { + private final Class testSampleClass; + private final ExtensionValuesStore valuesStore; public FakeExtensionContext(Class testSampleClass) {