-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Changes from 5 commits
7f599e8
0020cba
0f09dbe
fd409b9
176eafe
067f2d2
d283f0e
bd5eb9b
1add62a
f0c6336
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
package org.testcontainers.containers; | ||
|
||
import com.github.dockerjava.api.model.ContainerNetwork; | ||
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.Base58; | ||
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; | ||
import java.util.function.Supplier; | ||
|
||
/** | ||
* '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 Supplier<String> targetContainerIdSupplier; | ||
|
||
private String vncPassword = DEFAULT_VNC_PASSWORD; | ||
|
||
private int vncPort = 5900; | ||
|
||
private int frameRate = 30; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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::getContainerId); | ||
} | ||
|
||
public VncRecordingContainer(@NonNull Supplier<String> targetContainerIdSupplier) { | ||
super(TestcontainersConfiguration.getInstance().getVncRecordedContainerImage()); | ||
this.targetContainerIdSupplier = targetContainerIdSupplier; | ||
|
||
waitingFor(new AbstractWaitStrategy() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Not saying we have to do that now, but keen for your thoughts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see your point not needing a Regex, but why didn't |
||
|
||
@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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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")); | ||
|
||
if (getNetwork() == null) { | ||
withNetwork(Network.SHARED); | ||
} | ||
|
||
String alias = "vnchost-" + Base58.randomString(8); | ||
dockerClient.connectToNetworkCmd() | ||
.withContainerId(targetContainerIdSupplier.get()) | ||
.withNetworkId(getNetwork().getId()) | ||
.withContainerNetwork(new ContainerNetwork().withAliases(alias)) | ||
.exec(); | ||
|
||
setCommand( | ||
"-c", | ||
"echo '" + Base64.encodeBase64String(vncPassword.getBytes()) + "' | base64 -d > /vnc_password && " + | ||
"flvrec.py -o " + RECORDING_FILE_NAME + " -d -r " + frameRate + " -P /vnc_password " + alias + " " + vncPort | ||
); | ||
} | ||
|
||
@Override | ||
public void stop() { | ||
try { | ||
dockerClient.disconnectFromNetworkCmd() | ||
.withContainerId(targetContainerIdSupplier.get()) | ||
.withNetworkId(getNetwork().getId()) | ||
.withForce(true) | ||
.exec(); | ||
} catch (Exception e) { | ||
logger().warn("Failed to disconnect container with id '{}' from network '{}'", targetContainerIdSupplier.get(), getNetwork().getId(), e); | ||
} | ||
|
||
super.stop(); | ||
} | ||
|
||
@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 |
---|---|---|
|
@@ -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<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); | ||
|
||
|
@@ -102,6 +100,12 @@ protected void configure() { | |
throw new IllegalStateException(); | ||
} | ||
|
||
if (recordingMode != VncRecordingMode.SKIP) { | ||
vncRecordingContainer = new VncRecordingContainer(this) | ||
.withVncPassword(DEFAULT_PASSWORD) | ||
.withVncPort(VNC_PORT); | ||
} | ||
|
||
if (!customImageNameIsSet) { | ||
super.setDockerImageName(getImageForCapabilities(desiredCapabilities)); | ||
} | ||
|
@@ -164,19 +168,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, | ||
Timeouts.getWithTimeout(1, TimeUnit.SECONDS, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not directly related to this PR, but sometimes it fails to init, and this code waits for 10s while 1s is enough There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
() -> | ||
() -> new RemoteWebDriver(getSeleniumAddress(), desiredCapabilities))); | ||
|
||
if (vncRecordingContainer != null) { | ||
LOGGER.debug("Starting VNC recording"); | ||
vncRecordingContainer.start(); | ||
} | ||
} | ||
|
||
/** | ||
|
@@ -193,39 +193,53 @@ 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); | ||
} | ||
} | ||
|
||
if (vncRecordingContainer != null) { | ||
try { | ||
vncRecordingContainer.stop(); | ||
} catch (Exception e) { | ||
LOGGER.debug("Failed to stop vncRecordingContainer", e); | ||
} | ||
} | ||
this.stop(); | ||
|
||
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; | ||
} | ||
|
||
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); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leading to Wiki authors breaking our tests 🤣 |
||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you meant allow instead of avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
snap! :D