Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "VncRecordingContainer" #526

Merged
merged 10 commits into from
Dec 19, 2017
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought it's nice to have it configurable :) Some people do crazy stuff during their tests :D


public VncRecordingContainer(@NonNull GenericContainer<?> targetContainer) {
this(
targetContainer.getNetwork(),
targetContainer.getNetworkAliases().stream()
.findFirst()
.orElseThrow(() -> new IllegalStateException("Target container must have a network alias"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could make this less likely to happen by always creating a default (random?) alias for a container whenever you join it to a network?

Does this already happen / would it cause problems if we did it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if container already started, we can't modify its aliases I think

Copy link
Member Author

@bsideup bsideup Dec 18, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually, we CAN alias during connect:
https://docs.docker.com/engine/reference/commandline/network_connect/#description

I'll check if it's possible with docker-java, will make users' life much easier

);
}

/**
* 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() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our standard LogWaitStrategy didn't work here and also requires a Regexp while here contains is more than enough. Also, in debug mode vncrec.py produces a lot of output, makes sense to do as little as possible here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could/should ship a log wait strategy that is based on contains anyway. It seems to me that LogWaitStrategy is used for contains semantics more often than for a full blown regex.

Not saying we have to do that now, but keen for your thoughts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point not needing a Regex, but why didn't LogWaitStrategy work?


@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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__ == unused

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting with Java 9, underscores won't be legal variable names (Java Language Changes for Java SE 9) (okay, maybe double underscore is fine 😜 )

.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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leading to Wiki authors breaking our tests 🤣

}

}
Loading