Skip to content

Commit b7bd98b

Browse files
bsideuprnorth
authored andcommitted
Add "VncRecordingContainer" (#526)
Add "VncRecordingContainer" - Network-based, attachable re-implementation of VncRecordingSidekickContainer
1 parent 6d42f41 commit b7bd98b

File tree

9 files changed

+233
-53
lines changed

9 files changed

+233
-53
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
77

88
### Changed
99
- Added `getDatabaseName` method to JdbcDatabaseContainer, MySQLContainer, PostgreSQLContainer ([\#473](https://github.com/testcontainers/testcontainers-java/issues/473))
10+
- Added `VncRecordingContainer` - Network-based, attachable re-implementation of `VncRecordingSidekickContainer` ([\#526](https://github.com/testcontainers/testcontainers-java/pull/526))
1011

1112
## [1.5.0] - 2017-12-12
1213
### Fixed

core/src/main/java/org/testcontainers/containers/GenericContainer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ public class GenericContainer<SELF extends GenericContainer<SELF>>
8484
private Network network;
8585

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

8991
@NonNull
9092
private Future<String> image;

core/src/main/java/org/testcontainers/containers/Network.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@
99
import org.testcontainers.DockerClientFactory;
1010
import org.testcontainers.utility.ResourceReaper;
1111

12+
import java.util.Collections;
1213
import java.util.Set;
1314
import java.util.UUID;
1415
import java.util.concurrent.atomic.AtomicBoolean;
1516
import java.util.function.Consumer;
1617

1718
public interface Network extends AutoCloseable, TestRule {
1819

20+
Network SHARED = new NetworkImpl(false, null, Collections.emptySet(), null) {
21+
@Override
22+
public void close() {
23+
// Do not allow users to close SHARED network, only ResourceReaper is allowed to close (destroy) it
24+
}
25+
};
26+
1927
String getId();
2028

2129
static Network newNetwork() {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package org.testcontainers.containers;
2+
3+
import com.github.dockerjava.api.model.Frame;
4+
import lombok.Getter;
5+
import lombok.NonNull;
6+
import lombok.SneakyThrows;
7+
import lombok.ToString;
8+
import org.apache.commons.codec.binary.Base64;
9+
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
10+
import org.rnorth.ducttape.TimeoutException;
11+
import org.rnorth.ducttape.unreliables.Unreliables;
12+
import org.testcontainers.containers.output.FrameConsumerResultCallback;
13+
import org.testcontainers.utility.TestcontainersConfiguration;
14+
15+
import java.io.Closeable;
16+
import java.io.File;
17+
import java.io.InputStream;
18+
import java.nio.file.Files;
19+
import java.nio.file.StandardCopyOption;
20+
import java.util.concurrent.CountDownLatch;
21+
import java.util.concurrent.TimeUnit;
22+
23+
/**
24+
* 'Sidekick container' with the sole purpose of recording the VNC screen output from another container.
25+
*
26+
*/
27+
@Getter
28+
@ToString
29+
public class VncRecordingContainer extends GenericContainer<VncRecordingContainer> {
30+
31+
private static final String RECORDING_FILE_NAME = "/screen.flv";
32+
33+
public static final String DEFAULT_VNC_PASSWORD = "secret";
34+
35+
public static final int DEFAULT_VNC_PORT = 5900;
36+
37+
private final String targetNetworkAlias;
38+
39+
private String vncPassword = DEFAULT_VNC_PASSWORD;
40+
41+
private int vncPort = 5900;
42+
43+
private int frameRate = 30;
44+
45+
public VncRecordingContainer(@NonNull GenericContainer<?> targetContainer) {
46+
this(
47+
targetContainer.getNetwork(),
48+
targetContainer.getNetworkAliases().stream()
49+
.findFirst()
50+
.orElseThrow(() -> new IllegalStateException("Target container must have a network alias"))
51+
);
52+
}
53+
54+
/**
55+
* Create a sidekick container and attach it to another container. The VNC output of that container will be recorded.
56+
*/
57+
public VncRecordingContainer(@NonNull Network network, @NonNull String targetNetworkAlias) throws IllegalStateException {
58+
super(TestcontainersConfiguration.getInstance().getVncRecordedContainerImage());
59+
60+
this.targetNetworkAlias = targetNetworkAlias;
61+
withNetwork(network);
62+
63+
waitingFor(new AbstractWaitStrategy() {
64+
65+
@Override
66+
protected void waitUntilReady() {
67+
try {
68+
Unreliables.retryUntilTrue((int) startupTimeout.toMillis(), TimeUnit.MILLISECONDS, () -> {
69+
CountDownLatch latch = new CountDownLatch(1);
70+
71+
FrameConsumerResultCallback callback = new FrameConsumerResultCallback() {
72+
@Override
73+
public void onNext(Frame frame) {
74+
if (frame != null && new String(frame.getPayload()).contains("Connected")) {
75+
latch.countDown();
76+
}
77+
}
78+
};
79+
80+
try (
81+
Closeable __ = dockerClient.logContainerCmd(containerId)
82+
.withFollowStream(true)
83+
.withSince(0)
84+
.withStdErr(true)
85+
.exec(callback)
86+
) {
87+
return latch.await(1, TimeUnit.SECONDS);
88+
}
89+
});
90+
} catch (TimeoutException e) {
91+
throw new ContainerLaunchException("Timed out waiting for log output", e);
92+
}
93+
}
94+
});
95+
}
96+
97+
public VncRecordingContainer withVncPassword(@NonNull String vncPassword) {
98+
this.vncPassword = vncPassword;
99+
return this;
100+
}
101+
102+
public VncRecordingContainer withVncPort(int vncPort) {
103+
this.vncPort = vncPort;
104+
return this;
105+
}
106+
107+
public VncRecordingContainer withFrameRate(int frameRate) {
108+
this.frameRate = frameRate;
109+
return this;
110+
}
111+
112+
@Override
113+
protected void configure() {
114+
withCreateContainerCmdModifier(it -> it.withEntrypoint("/bin/sh"));
115+
setCommand(
116+
"-c",
117+
"echo '" + Base64.encodeBase64String(vncPassword.getBytes()) + "' | base64 -d > /vnc_password && " +
118+
"flvrec.py -o " + RECORDING_FILE_NAME + " -d -r " + frameRate + " -P /vnc_password " + targetNetworkAlias + " " + vncPort
119+
);
120+
}
121+
122+
@SneakyThrows
123+
public InputStream streamRecording() {
124+
TarArchiveInputStream archiveInputStream = new TarArchiveInputStream(
125+
dockerClient.copyArchiveFromContainerCmd(containerId, RECORDING_FILE_NAME).exec()
126+
);
127+
archiveInputStream.getNextEntry();
128+
return archiveInputStream;
129+
}
130+
131+
@SneakyThrows
132+
public void saveRecordingToFile(File file) {
133+
try(InputStream inputStream = streamRecording()) {
134+
Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
135+
}
136+
}
137+
}

core/src/main/java/org/testcontainers/containers/VncRecordingSidekickContainer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
/**
1919
* 'Sidekick container' with the sole purpose of recording the VNC screen output from another container.
20+
*
21+
* @deprecated please use {@link VncRecordingContainer}
2022
*/
23+
@Deprecated
2124
public class VncRecordingSidekickContainer<SELF extends VncRecordingSidekickContainer<SELF, T>, T extends VncService & LinkableContainer> extends GenericContainer<SELF> {
2225
private final T vncServiceContainer;
2326
private final Path tempDir;

core/src/main/java/org/testcontainers/containers/traits/VncService.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/**
44
* A container which exposes a VNC server.
55
*/
6+
@Deprecated
67
public interface VncService {
78
/**
89
* @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

modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
import java.net.MalformedURLException;
2424
import java.net.URL;
2525
import java.time.Duration;
26-
import java.util.ArrayList;
27-
import java.util.Collection;
2826
import java.util.Set;
2927
import java.util.concurrent.TimeUnit;
3028

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

57-
private final Collection<VncRecordingSidekickContainer> currentVncRecordings = new ArrayList<>();
55+
private VncRecordingContainer vncRecordingContainer = null;
5856

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

@@ -102,6 +100,16 @@ protected void configure() {
102100
throw new IllegalStateException();
103101
}
104102

103+
if (recordingMode != VncRecordingMode.SKIP) {
104+
if (getNetwork() == null) {
105+
withNetwork(Network.SHARED);
106+
}
107+
108+
vncRecordingContainer = new VncRecordingContainer(this)
109+
.withVncPassword(DEFAULT_PASSWORD)
110+
.withVncPort(VNC_PORT);
111+
}
112+
105113
if (!customImageNameIsSet) {
106114
super.setDockerImageName(getImageForCapabilities(desiredCapabilities));
107115
}
@@ -164,19 +172,15 @@ public int getPort() {
164172

165173
@Override
166174
protected void containerIsStarted(InspectContainerResponse containerInfo) {
167-
if (recordingMode != VncRecordingMode.SKIP) {
168-
LOGGER.debug("Starting VNC recording");
169-
170-
VncRecordingSidekickContainer recordingSidekickContainer = new VncRecordingSidekickContainer<>(this);
171-
172-
recordingSidekickContainer.start();
173-
currentVncRecordings.add(recordingSidekickContainer);
174-
}
175-
176175
driver = Unreliables.retryUntilSuccess(30, TimeUnit.SECONDS,
177176
Timeouts.getWithTimeout(10, TimeUnit.SECONDS,
178177
() ->
179178
() -> new RemoteWebDriver(getSeleniumAddress(), desiredCapabilities)));
179+
180+
if (vncRecordingContainer != null) {
181+
LOGGER.debug("Starting VNC recording");
182+
vncRecordingContainer.start();
183+
}
180184
}
181185

182186
/**
@@ -193,39 +197,54 @@ public RemoteWebDriver getWebDriver() {
193197

194198
@Override
195199
protected void failed(Throwable e, Description description) {
196-
switch (recordingMode) {
197-
case RECORD_FAILING:
198-
case RECORD_ALL:
199-
stopAndRetainRecordingForDescriptionAndSuccessState(description, false);
200-
break;
201-
}
202-
currentVncRecordings.clear();
200+
stopAndRetainRecordingForDescriptionAndSuccessState(description, false);
203201
}
204202

205203
@Override
206204
protected void succeeded(Description description) {
207-
switch (recordingMode) {
208-
case RECORD_ALL:
209-
stopAndRetainRecordingForDescriptionAndSuccessState(description, true);
210-
break;
211-
}
212-
currentVncRecordings.clear();
205+
stopAndRetainRecordingForDescriptionAndSuccessState(description, true);
213206
}
214207

215208
@Override
216-
protected void finished(Description description) {
209+
public void stop() {
217210
if (driver != null) {
218-
driver.quit();
211+
try {
212+
driver.quit();
213+
} catch (Exception e) {
214+
LOGGER.debug("Failed to quit the driver", e);
215+
}
219216
}
220-
this.stop();
217+
218+
if (vncRecordingContainer != null) {
219+
try {
220+
vncRecordingContainer.stop();
221+
} catch (Exception e) {
222+
LOGGER.debug("Failed to stop vncRecordingContainer", e);
223+
}
224+
}
225+
226+
super.stop();
221227
}
222228

223229
private void stopAndRetainRecordingForDescriptionAndSuccessState(Description description, boolean succeeded) {
224-
File recordingFile = recordingFileFactory.recordingFileForTest(vncRecordingDirectory, description, succeeded);
225-
LOGGER.info("Screen recordings for test {} will be stored at: {}", description.getDisplayName(), recordingFile);
230+
final boolean shouldRecord;
231+
switch (recordingMode) {
232+
case RECORD_ALL:
233+
shouldRecord = true;
234+
break;
235+
case RECORD_FAILING:
236+
shouldRecord = !succeeded;
237+
break;
238+
default:
239+
shouldRecord = false;
240+
break;
241+
}
242+
243+
if (shouldRecord) {
244+
File recordingFile = recordingFileFactory.recordingFileForTest(vncRecordingDirectory, description, succeeded);
245+
LOGGER.info("Screen recordings for test {} will be stored at: {}", description.getDisplayName(), recordingFile);
226246

227-
for (VncRecordingSidekickContainer container : currentVncRecordings) {
228-
container.stopAndRetainRecording(recordingFile);
247+
vncRecordingContainer.saveRecordingToFile(recordingFile);
229248
}
230249
}
231250

modules/selenium/src/test/java/org/testcontainers/junit/BaseWebDriverContainerTest.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.concurrent.TimeUnit;
99

1010
import static org.rnorth.visibleassertions.VisibleAssertions.assertEquals;
11+
import static org.rnorth.visibleassertions.VisibleAssertions.assertTrue;
1112

1213
/**
1314
*
@@ -28,15 +29,18 @@ protected void doSimpleWebdriverTest(BrowserWebDriverContainer rule) {
2829
}
2930

3031
@NotNull
31-
private RemoteWebDriver setupDriverFromRule(BrowserWebDriverContainer rule) {
32+
private static RemoteWebDriver setupDriverFromRule(BrowserWebDriverContainer rule) {
3233
RemoteWebDriver driver = rule.getWebDriver();
3334
driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
3435
return driver;
3536
}
3637

37-
protected void doSimpleExplore(BrowserWebDriverContainer rule) {
38+
protected static void doSimpleExplore(BrowserWebDriverContainer rule) {
3839
RemoteWebDriver driver = setupDriverFromRule(rule);
3940
driver.get("http://en.wikipedia.org/wiki/Randomness");
41+
42+
// Oh! The irony!
43+
assertTrue("Randomness' description has the word 'pattern'", driver.findElementByPartialLinkText("pattern").isDisplayed());
4044
}
4145

4246
}

0 commit comments

Comments
 (0)