From b7bd98b4ee1315acdd906e4f9ad112f344c249fc Mon Sep 17 00:00:00 2001 From: Sergei Egorov Date: Tue, 19 Dec 2017 13:26:19 +0100 Subject: [PATCH] Add "VncRecordingContainer" (#526) Add "VncRecordingContainer" - Network-based, attachable re-implementation of VncRecordingSidekickContainer --- CHANGELOG.md | 1 + .../containers/GenericContainer.java | 4 +- .../testcontainers/containers/Network.java | 8 + .../containers/VncRecordingContainer.java | 137 ++++++++++++++++++ .../VncRecordingSidekickContainer.java | 3 + .../containers/traits/VncService.java | 1 + .../containers/BrowserWebDriverContainer.java | 83 +++++++---- .../junit/BaseWebDriverContainerTest.java | 8 +- ...ChromeRecordingWebDriverContainerTest.java | 41 +++--- 9 files changed, 233 insertions(+), 53 deletions(-) create mode 100644 core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java diff --git a/CHANGELOG.md b/CHANGELOG.md index dc1a6927a0d..8238ea46596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Changed - Added `getDatabaseName` method to JdbcDatabaseContainer, MySQLContainer, PostgreSQLContainer ([\#473](https://github.com/testcontainers/testcontainers-java/issues/473)) +- Added `VncRecordingContainer` - Network-based, attachable re-implementation of `VncRecordingSidekickContainer` ([\#526](https://github.com/testcontainers/testcontainers-java/pull/526)) ## [1.5.0] - 2017-12-12 ### Fixed diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index d8ebb36ec93..9cd915ffe35 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -84,7 +84,9 @@ public class GenericContainer> private Network network; @NonNull - private List networkAliases = new ArrayList<>(); + private List networkAliases = new ArrayList<>(Arrays.asList( + "tc-" + Base58.randomString(8) + )); @NonNull private Future image; diff --git a/core/src/main/java/org/testcontainers/containers/Network.java b/core/src/main/java/org/testcontainers/containers/Network.java index 1e1fbf31781..dc848b02b8c 100644 --- a/core/src/main/java/org/testcontainers/containers/Network.java +++ b/core/src/main/java/org/testcontainers/containers/Network.java @@ -9,6 +9,7 @@ import org.testcontainers.DockerClientFactory; import org.testcontainers.utility.ResourceReaper; +import java.util.Collections; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicBoolean; @@ -16,6 +17,13 @@ public interface Network extends AutoCloseable, TestRule { + Network SHARED = new NetworkImpl(false, null, Collections.emptySet(), null) { + @Override + public void close() { + // Do not allow users to close SHARED network, only ResourceReaper is allowed to close (destroy) it + } + }; + String getId(); static Network newNetwork() { diff --git a/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java b/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java new file mode 100644 index 00000000000..b8a26bfe65b --- /dev/null +++ b/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java @@ -0,0 +1,137 @@ +package org.testcontainers.containers; + +import com.github.dockerjava.api.model.Frame; +import lombok.Getter; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.ToString; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.rnorth.ducttape.TimeoutException; +import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.containers.output.FrameConsumerResultCallback; +import org.testcontainers.utility.TestcontainersConfiguration; + +import java.io.Closeable; +import java.io.File; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * 'Sidekick container' with the sole purpose of recording the VNC screen output from another container. + * + */ +@Getter +@ToString +public class VncRecordingContainer extends GenericContainer { + + private static final String RECORDING_FILE_NAME = "/screen.flv"; + + public static final String DEFAULT_VNC_PASSWORD = "secret"; + + public static final int DEFAULT_VNC_PORT = 5900; + + private final String targetNetworkAlias; + + private String vncPassword = DEFAULT_VNC_PASSWORD; + + private int vncPort = 5900; + + private int frameRate = 30; + + public VncRecordingContainer(@NonNull GenericContainer targetContainer) { + this( + targetContainer.getNetwork(), + targetContainer.getNetworkAliases().stream() + .findFirst() + .orElseThrow(() -> new IllegalStateException("Target container must have a network alias")) + ); + } + + /** + * Create a sidekick container and attach it to another container. The VNC output of that container will be recorded. + */ + public VncRecordingContainer(@NonNull Network network, @NonNull String targetNetworkAlias) throws IllegalStateException { + super(TestcontainersConfiguration.getInstance().getVncRecordedContainerImage()); + + this.targetNetworkAlias = targetNetworkAlias; + withNetwork(network); + + waitingFor(new AbstractWaitStrategy() { + + @Override + protected void waitUntilReady() { + try { + Unreliables.retryUntilTrue((int) startupTimeout.toMillis(), TimeUnit.MILLISECONDS, () -> { + CountDownLatch latch = new CountDownLatch(1); + + FrameConsumerResultCallback callback = new FrameConsumerResultCallback() { + @Override + public void onNext(Frame frame) { + if (frame != null && new String(frame.getPayload()).contains("Connected")) { + latch.countDown(); + } + } + }; + + try ( + Closeable __ = dockerClient.logContainerCmd(containerId) + .withFollowStream(true) + .withSince(0) + .withStdErr(true) + .exec(callback) + ) { + return latch.await(1, TimeUnit.SECONDS); + } + }); + } catch (TimeoutException e) { + throw new ContainerLaunchException("Timed out waiting for log output", e); + } + } + }); + } + + public VncRecordingContainer withVncPassword(@NonNull String vncPassword) { + this.vncPassword = vncPassword; + return this; + } + + public VncRecordingContainer withVncPort(int vncPort) { + this.vncPort = vncPort; + return this; + } + + public VncRecordingContainer withFrameRate(int frameRate) { + this.frameRate = frameRate; + return this; + } + + @Override + protected void configure() { + withCreateContainerCmdModifier(it -> it.withEntrypoint("/bin/sh")); + setCommand( + "-c", + "echo '" + Base64.encodeBase64String(vncPassword.getBytes()) + "' | base64 -d > /vnc_password && " + + "flvrec.py -o " + RECORDING_FILE_NAME + " -d -r " + frameRate + " -P /vnc_password " + targetNetworkAlias + " " + vncPort + ); + } + + @SneakyThrows + public InputStream streamRecording() { + TarArchiveInputStream archiveInputStream = new TarArchiveInputStream( + dockerClient.copyArchiveFromContainerCmd(containerId, RECORDING_FILE_NAME).exec() + ); + archiveInputStream.getNextEntry(); + return archiveInputStream; + } + + @SneakyThrows + public void saveRecordingToFile(File file) { + try(InputStream inputStream = streamRecording()) { + Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/org/testcontainers/containers/VncRecordingSidekickContainer.java b/core/src/main/java/org/testcontainers/containers/VncRecordingSidekickContainer.java index 04f01c33623..d1dfb5350c0 100644 --- a/core/src/main/java/org/testcontainers/containers/VncRecordingSidekickContainer.java +++ b/core/src/main/java/org/testcontainers/containers/VncRecordingSidekickContainer.java @@ -17,7 +17,10 @@ /** * 'Sidekick container' with the sole purpose of recording the VNC screen output from another container. + * + * @deprecated please use {@link VncRecordingContainer} */ +@Deprecated public class VncRecordingSidekickContainer, T extends VncService & LinkableContainer> extends GenericContainer { private final T vncServiceContainer; private final Path tempDir; diff --git a/core/src/main/java/org/testcontainers/containers/traits/VncService.java b/core/src/main/java/org/testcontainers/containers/traits/VncService.java index 6329805b74d..c0f6458c2c0 100644 --- a/core/src/main/java/org/testcontainers/containers/traits/VncService.java +++ b/core/src/main/java/org/testcontainers/containers/traits/VncService.java @@ -3,6 +3,7 @@ /** * A container which exposes a VNC server. */ +@Deprecated public interface VncService { /** * @return a URL which can be used to connect to the VNC server from the machine running the tests. Exposed for convenience, e.g. to aid manual debugging diff --git a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java index d3f66dd372d..6e2375f5082 100644 --- a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java +++ b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java @@ -23,8 +23,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; -import java.util.ArrayList; -import java.util.Collection; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -54,7 +52,7 @@ public class BrowserWebDriverContainer currentVncRecordings = new ArrayList<>(); + private VncRecordingContainer vncRecordingContainer = null; private static final Logger LOGGER = LoggerFactory.getLogger(BrowserWebDriverContainer.class); @@ -102,6 +100,16 @@ protected void configure() { throw new IllegalStateException(); } + if (recordingMode != VncRecordingMode.SKIP) { + if (getNetwork() == null) { + withNetwork(Network.SHARED); + } + + vncRecordingContainer = new VncRecordingContainer(this) + .withVncPassword(DEFAULT_PASSWORD) + .withVncPort(VNC_PORT); + } + if (!customImageNameIsSet) { super.setDockerImageName(getImageForCapabilities(desiredCapabilities)); } @@ -164,19 +172,15 @@ public int getPort() { @Override protected void containerIsStarted(InspectContainerResponse containerInfo) { - if (recordingMode != VncRecordingMode.SKIP) { - LOGGER.debug("Starting VNC recording"); - - VncRecordingSidekickContainer recordingSidekickContainer = new VncRecordingSidekickContainer<>(this); - - recordingSidekickContainer.start(); - currentVncRecordings.add(recordingSidekickContainer); - } - driver = Unreliables.retryUntilSuccess(30, TimeUnit.SECONDS, Timeouts.getWithTimeout(10, TimeUnit.SECONDS, () -> () -> new RemoteWebDriver(getSeleniumAddress(), desiredCapabilities))); + + if (vncRecordingContainer != null) { + LOGGER.debug("Starting VNC recording"); + vncRecordingContainer.start(); + } } /** @@ -193,39 +197,54 @@ public RemoteWebDriver getWebDriver() { @Override protected void failed(Throwable e, Description description) { - switch (recordingMode) { - case RECORD_FAILING: - case RECORD_ALL: - stopAndRetainRecordingForDescriptionAndSuccessState(description, false); - break; - } - currentVncRecordings.clear(); + stopAndRetainRecordingForDescriptionAndSuccessState(description, false); } @Override protected void succeeded(Description description) { - switch (recordingMode) { - case RECORD_ALL: - stopAndRetainRecordingForDescriptionAndSuccessState(description, true); - break; - } - currentVncRecordings.clear(); + stopAndRetainRecordingForDescriptionAndSuccessState(description, true); } @Override - protected void finished(Description description) { + public void stop() { if (driver != null) { - driver.quit(); + try { + driver.quit(); + } catch (Exception e) { + LOGGER.debug("Failed to quit the driver", e); + } } - this.stop(); + + if (vncRecordingContainer != null) { + try { + vncRecordingContainer.stop(); + } catch (Exception e) { + LOGGER.debug("Failed to stop vncRecordingContainer", e); + } + } + + super.stop(); } private void stopAndRetainRecordingForDescriptionAndSuccessState(Description description, boolean succeeded) { - File recordingFile = recordingFileFactory.recordingFileForTest(vncRecordingDirectory, description, succeeded); - LOGGER.info("Screen recordings for test {} will be stored at: {}", description.getDisplayName(), recordingFile); + final boolean shouldRecord; + switch (recordingMode) { + case RECORD_ALL: + shouldRecord = true; + break; + case RECORD_FAILING: + shouldRecord = !succeeded; + break; + default: + shouldRecord = false; + break; + } + + if (shouldRecord) { + File recordingFile = recordingFileFactory.recordingFileForTest(vncRecordingDirectory, description, succeeded); + LOGGER.info("Screen recordings for test {} will be stored at: {}", description.getDisplayName(), recordingFile); - for (VncRecordingSidekickContainer container : currentVncRecordings) { - container.stopAndRetainRecording(recordingFile); + vncRecordingContainer.saveRecordingToFile(recordingFile); } } diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/BaseWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/BaseWebDriverContainerTest.java index d9c76626a9e..1f8a68f857f 100644 --- a/modules/selenium/src/test/java/org/testcontainers/junit/BaseWebDriverContainerTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/junit/BaseWebDriverContainerTest.java @@ -8,6 +8,7 @@ import java.util.concurrent.TimeUnit; import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals; +import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue; /** * @@ -28,15 +29,18 @@ protected void doSimpleWebdriverTest(BrowserWebDriverContainer rule) { } @NotNull - private RemoteWebDriver setupDriverFromRule(BrowserWebDriverContainer rule) { + private static RemoteWebDriver setupDriverFromRule(BrowserWebDriverContainer rule) { RemoteWebDriver driver = rule.getWebDriver(); driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS); return driver; } - protected void doSimpleExplore(BrowserWebDriverContainer rule) { + protected static void doSimpleExplore(BrowserWebDriverContainer rule) { RemoteWebDriver driver = setupDriverFromRule(rule); driver.get("http://en.wikipedia.org/wiki/Randomness"); + + // Oh! The irony! + assertTrue("Randomness' description has the word 'pattern'", driver.findElementByPartialLinkText("pattern").isDisplayed()); } } diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java index 6eba1ae478d..770e8e8bc30 100644 --- a/modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java @@ -2,6 +2,8 @@ import org.junit.Rule; import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; import org.openqa.selenium.remote.DesiredCapabilities; import org.testcontainers.containers.BrowserWebDriverContainer; import org.testcontainers.containers.DefaultRecordingFileFactory; @@ -10,29 +12,32 @@ import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL; -/** - * - */ +@RunWith(Enclosed.class) public class ChromeRecordingWebDriverContainerTest extends BaseWebDriverContainerTest { - @Rule - public BrowserWebDriverContainer chromeThatRecordsAllTests = new BrowserWebDriverContainer() - .withDesiredCapabilities(DesiredCapabilities.chrome()) - .withRecordingMode(RECORD_ALL, new File("./target/")) - .withRecordingFileFactory(new DefaultRecordingFileFactory()); + public static class ChromeThatRecordsAllTests { - @Rule - public BrowserWebDriverContainer chromeThatRecordsFailingTests = new BrowserWebDriverContainer() - .withDesiredCapabilities(DesiredCapabilities.chrome()); + @Rule + public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() + .withDesiredCapabilities(DesiredCapabilities.chrome()) + .withRecordingMode(RECORD_ALL, new File("./target/")) + .withRecordingFileFactory(new DefaultRecordingFileFactory()); - - @Test - public void recordingTestThatShouldBeRecordedButDeleted() { - doSimpleExplore(chromeThatRecordsFailingTests); + @Test + public void recordingTestThatShouldBeRecordedAndRetained() { + doSimpleExplore(chrome); + } } - @Test - public void recordingTestThatShouldBeRecordedAndRetained() { - doSimpleExplore(chromeThatRecordsAllTests); + public static class ChromeThatRecordsFailingTests { + + @Rule + public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() + .withDesiredCapabilities(DesiredCapabilities.chrome()); + + @Test + public void recordingTestThatShouldBeRecordedButDeleted() { + doSimpleExplore(chrome); + } } }