Skip to content

Commit

Permalink
Add "VncRecordingContainer" (#526)
Browse files Browse the repository at this point in the history
Add "VncRecordingContainer" - Network-based, attachable re-implementation of VncRecordingSidekickContainer
  • Loading branch information
bsideup authored and rnorth committed Dec 19, 2017
1 parent 6d42f41 commit b7bd98b
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 53 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>
private Network network;

@NonNull
private List<String> networkAliases = new ArrayList<>();
private List<String> networkAliases = new ArrayList<>(Arrays.asList(
"tc-" + Base58.randomString(8)
));

@NonNull
private Future<String> image;
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/java/org/testcontainers/containers/Network.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@
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;
import java.util.function.Consumer;

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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<VncRecordingContainer> {

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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<SELF extends VncRecordingSidekickContainer<SELF, T>, T extends VncService & LinkableContainer> extends GenericContainer<SELF> {
private final T vncServiceContainer;
private final Path tempDir;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -54,7 +52,7 @@ public class BrowserWebDriverContainer<SELF extends BrowserWebDriverContainer<SE
private RecordingFileFactory recordingFileFactory;
private File vncRecordingDirectory = new File("/tmp");

private final Collection<VncRecordingSidekickContainer> currentVncRecordings = new ArrayList<>();
private VncRecordingContainer vncRecordingContainer = null;

private static final Logger LOGGER = LoggerFactory.getLogger(BrowserWebDriverContainer.class);

Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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();
}
}

/**
Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.util.concurrent.TimeUnit;

import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;

/**
*
Expand All @@ -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());
}

}
Loading

0 comments on commit b7bd98b

Please sign in to comment.