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

[WIP] Screenshots and screen recordings of tests #541

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions detox/local-cli/detox-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ program
'Artifacts destination path (currently will contain only logs). If the destination already exists, it will be removed first')
.option('-p, --platform [ios/android]',
'Run platform specific tests. Runs tests with invert grep on \':platform:\', '
+ 'e.g test with substring \':ios:\' in its name will not run when passing \'--platform android\'')
+ 'e.g test with substring \':ios:\' in its name will not run when passing \'--platform android\'')
.option('--take-screenshots', '')
.option('--record-videos', '')
.parse(process.argv);

const config = require(path.join(process.cwd(), 'package.json')).detox;
Expand Down Expand Up @@ -53,9 +55,11 @@ function runMocha() {
const artifactsLocation = program.artifactsLocation ? `--artifacts-location ${program.artifactsLocation}` : '';
const configFile = runnerConfig ? `--opts ${runnerConfig}` : '';
const platform = program.platform ? `--grep ${getPlatformSpecificString(program.platform)} --invert` : '';
const screenshots = program.takeScreenshots ? `--take-screenshots` : '';
const videos = program.recordVideos ? `--record-videos` : '';

const debugSynchronization = program.debugSynchronization ? `--debug-synchronization ${program.debugSynchronization}` : '';
const command = `node_modules/.bin/mocha ${testFolder} ${configFile} ${configuration} ${loglevel} ${cleanup} ${reuse} ${debugSynchronization} ${platform} ${artifactsLocation}`;
const command = `node_modules/.bin/mocha ${testFolder} ${configFile} ${configuration} ${loglevel} ${cleanup} ${reuse} ${debugSynchronization} ${platform} ${artifactsLocation} ${screenshots} ${videos}`;

console.log(command);
cp.execSync(command, {stdio: 'inherit'});
Expand Down Expand Up @@ -104,4 +108,4 @@ function getPlatformSpecificString(platform) {
}

return platformRevertString;
}
}
1 change: 1 addition & 0 deletions detox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"shell-utils": "^1.0.9",
"tail": "^1.2.3",
"telnet-client": "0.15.3",
"tempfile": "^2.0.0",
"ws": "^1.1.1"
},
"engines": {
Expand Down
9 changes: 9 additions & 0 deletions detox/src/Detox.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ class Detox {
if (this._artifactsPathsProvider !== undefined) {
const testArtifactsPath = this._artifactsPathsProvider.createPathForTest(this._currentTestNumber, ...testNameComponents);
this.device.setArtifactsDestination(testArtifactsPath);
await this.device.prepareArtifacts();
}
}

Expand Down Expand Up @@ -139,6 +140,14 @@ class Detox {
${Object.keys(configurations)}`);
}

if (!('takeScreenshots' in deviceConfig)) {
deviceConfig.takeScreenshots = argparse.getFlag('take-screenshots');
}

if (!('recordVideos' in deviceConfig)) {
deviceConfig.recordVideos = argparse.getFlag('record-videos');
}

return deviceConfig;
}
}
Expand Down
18 changes: 14 additions & 4 deletions detox/src/artifacts/ArtifactsCopier.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
const path = require('path');
const log = require('npmlog');
const sh = require('../utils/sh')
const sh = require('../utils/sh');

class ArtifactsCopier {
constructor(deviceDriver) {
this._deviceDriver = deviceDriver;
this._currentLaunchNumber = 0;
this._currentTestArtifactsDestination = undefined;
this._artifacts = [];
}

prepare(deviceId) {
this._deviceId = deviceId;
}

addArtifact(source, destName) {
this._artifacts.push([source, destName + path.extname(source)]);
}

setArtifactsDestination(artifactsDestination) {
this._currentTestArtifactsDestination = artifactsDestination;
this._currentLaunchNumber = 1;
Expand All @@ -35,8 +41,8 @@ class ArtifactsCopier {
} catch (ex) {
log.warn(`Couldn't copy (cp ${cpArgs})`);
}
}
};

if(this._currentTestArtifactsDestination === undefined) {
return;
}
Expand All @@ -49,8 +55,12 @@ class ArtifactsCopier {
for (const [sourcePath, destinationSuffix] of pathsMapping) {
await copy(sourcePath, destinationSuffix);
}

for (const [source, dest] of this._artifacts) {
await copy(source, dest);
}
}

}

module.exports = ArtifactsCopier;
module.exports = ArtifactsCopier;
24 changes: 24 additions & 0 deletions detox/src/devices/AppleSimUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const _ = require('lodash');
const tempfile = require('tempfile');
const cpp = require('child-process-promise');
const exec = require('../utils/exec');
const retry = require('../utils/retry');
const environment = require('../utils/environment');
Expand Down Expand Up @@ -154,6 +156,28 @@ class AppleSimUtils {
return majorVersion;
}

async takeScreenshot(udid = 'booted') {
const dest = tempfile('.png');
await this._execSimctl({cmd: `io ${udid} screenshot ${dest}`});
return dest;
}

async startVideo(udid = 'booted') {
const dest = tempfile('.mp4');
const promise = cpp.spawn('/usr/bin/xcrun', ['simctl', 'io', udid, 'recordVideo', dest], {
detached: true,
stdio: 'inherit'
});
const cp = promise.childProcess;
cp.dest = dest;
return cp;
}

async stopVideo(udid = 'booted', process) {
process.kill(2);
return process.dest;
}

async _execAppleSimUtils(options, statusLogs, retries, interval) {
const bin = `applesimutils`;
return await exec.execWithRetriesAndLogs(bin, options, statusLogs, retries, interval);
Expand Down
29 changes: 29 additions & 0 deletions detox/src/devices/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ class Device {
this._artifactsCopier.setArtifactsDestination(testArtifactsPath);
}

async prepareArtifacts() {
await this._takeScreenshot('screenshot-before');
await this._startVideo();
}

async finalizeArtifacts() {
await this._stopVideo();
await this._takeScreenshot('screenshot-after');
await this._artifactsCopier.finalizeArtifacts();
}

Expand Down Expand Up @@ -173,6 +180,28 @@ class Device {
await this.deviceDriver.cleanup(this._deviceId, this._bundleId);
}

async _takeScreenshot(name) {
if (this._deviceConfig.takeScreenshots) {
const screenshotFilePath = await this.deviceDriver.takeScreenshot(this._deviceId);
this._artifactsCopier.addArtifact(screenshotFilePath, name);
}
}

async _startVideo() {
if (this._deviceConfig.recordVideos) {
await this.deviceDriver.startVideo(this._deviceId);
}
}

async _stopVideo() {
if (this._deviceConfig.recordVideos) {
const video = await this.deviceDriver.stopVideo(this._deviceId);
if (video) {
this._artifactsCopier.addArtifact(video, 'recording');
}
}
}

_defaultLaunchArgs() {
return {
'detoxServer': this._sessionConfig.server,
Expand Down
9 changes: 9 additions & 0 deletions detox/src/devices/DeviceDriverBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ class DeviceDriverBase {
stderr: undefined
};
}

async takeScreenshot() {
}

async startVideo() {
}

async stopVideo() {
}
}

module.exports = DeviceDriverBase;
61 changes: 61 additions & 0 deletions detox/src/devices/EmulatorDriver.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const _ = require('lodash');
const path = require('path');
const tempfile = require('tempfile');
const Emulator = require('./android/Emulator');
const EmulatorTelnet = require('./android/EmulatorTelnet');
const Environment = require('../utils/environment');
Expand Down Expand Up @@ -81,6 +82,66 @@ class EmulatorDriver extends AndroidDriver {
await telnet.connect(port);
await telnet.kill();
}

async takeScreenshot(deviceId) {
const dst = tempfile('.png');
const src = '/sdcard/screenshot.png';
await this.adb.adbCmd(deviceId, `shell screencap ${src}`);
await this.adb.adbCmd(deviceId, `pull ${src} ${dst}`);
return dst;
}

async startVideo(deviceId) {
const adb = this.adb;

await adb.adbCmd(deviceId, `shell rm -f /sdcard/recording.mp4`);
let {width, height} = await adb.getScreenSize();
let promise = spawnRecording(width *= 2, height *= 2);
promise.catch(handleRecordingTermination);

let recording = false;
let size = 0;
while (!recording) {
size = 0;
recording = true;
// console.log('>>> waiting for recording to start');
try {
size = await adb.getFileSize(deviceId, '/sdcard/recording.mp4');
if (size < 1) {
recording = false;
}
} catch (e) {
recording = false;
}
}

this._video = promise.childProcess;

function spawnRecording(width, height) {
return adb.spawn(deviceId, [
'shell', 'screenrecord', '--size', width + 'x' + height, '--verbose', '/sdcard/recording.mp4'
Copy link
Member

Choose a reason for hiding this comment

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

from https://developer.android.com/studio/command-line/adb.html

"Sets the video size: 1280x720. The default value is the device's native display resolution (if supported), 1280x720 if not. For best results, use a size supported by your device's Advanced Video Coding (AVC) encoder."

Did you encounter any issues with default values ?
Why do you use twice the native resolution for recording ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was left over from my tests, will be removed.

]);
}

function handleRecordingTermination(result) {
const proc = result.childProcess;
if (proc.exitCode !== 0 && proc.signalCode !== 'SIGINT') {
promise = spawnRecording(width >>= 1, height >>= 1);
promise.catch(handleRecordingTermination);
}
}
}

async stopVideo(deviceId) {
if (this._video) {
this._video.kill(2);
await this.adb.adbCmd(deviceId, 'shell sleep 1');
Copy link
Member

Choose a reason for hiding this comment

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

I removed this line and saw the corrupted videos you talked about.
The thing is, this slows down the tests a lot!
I am trying to think of a way to mitigate the penalty:

  1. Let's create each file on the device with a different name.
  2. when test is done, create a copy+cleanup task
  3. add this task to a queue and let this queue run independently of the test lifecycle.

WDYT ?

const dest = tempfile('.mp4');
await this.adb.adbCmd(deviceId, `pull /sdcard/recording.mp4 ${dest}`);
await this.adb.adbCmd(deviceId, `shell rm /sdcard/recording.mp4`);
return dest;
}
}
}

module.exports = EmulatorDriver;
18 changes: 18 additions & 0 deletions detox/src/devices/SimulatorDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class SimulatorDriver extends IosDriver {
constructor(client) {
super(client);
this._applesimutils = new AppleSimUtils();
this._video = null;
this._videoFile = null;
}

async prepare() {
Expand Down Expand Up @@ -95,6 +97,22 @@ class SimulatorDriver extends IosDriver {
getLogsPaths(deviceId) {
return this._applesimutils.getLogsPaths(deviceId);
}

async takeScreenshot(deviceId) {
return await this._applesimutils.takeScreenshot(deviceId);
}

async startVideo(deviceId) {
this._video = await this._applesimutils.startVideo(deviceId);
}

async stopVideo(deviceId) {
if (this._video) {
const video = await this._applesimutils.stopVideo(deviceId, this._video);
this._video = null;
return video;
}
}
}

module.exports = SimulatorDriver;
24 changes: 24 additions & 0 deletions detox/src/devices/android/ADB.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const path = require('path');
const exec = require('../../utils/exec').execWithRetriesAndLogs;
const spawn = require('child-process-promise').spawn;
const _ = require('lodash');
const EmulatorTelnet = require('./EmulatorTelnet');
const Environment = require('../../utils/environment');
Expand Down Expand Up @@ -102,6 +103,29 @@ class ADB {
async sleep(ms = 0) {
return new Promise((resolve, reject) => setTimeout(resolve, ms));
}

spawn(deviceId, params) {
const serial = deviceId ? ['-s', deviceId] : [];
// console.log(`>>> ${this.adbBin} ${serial.join(' ')} ${params.join(' ')}`);
return spawn(this.adbBin, [...serial, ...params], {
detached: true,
stdio: 'inherit'
});
}

async getScreenSize(deviceId) {
const {stdout} = await this.adbCmd(deviceId, `shell wm size`);
const [width, height] = stdout.split(' ').pop().split('x');
return {
width: parseInt(width, 10),
height: parseInt(height, 10)
};
}

async getFileSize(deviceId, path) {
const {stdout} = await this.adbCmd(deviceId, `shell wc -c ${path}`);
return parseInt(stdout, 10);
}
}

module.exports = ADB;
11 changes: 10 additions & 1 deletion detox/src/utils/argparse.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ function getArgValue(key) {
return value;
}

function getFlag(key) {
// console.log('key', key, argv);
if (argv && argv[key]) {
return true;
}
return false;
}

module.exports = {
getArgValue
getArgValue,
getFlag
};
4 changes: 2 additions & 2 deletions detox/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"test": ":",
"packager": "react-native start",
"detox-server": "detox run-server",
"e2e:ios": "detox test --configuration ios.sim.release --debug-synchronization 10000 --platform ios",
"e2e:android": "detox test --configuration android.emu.release --loglevel verbose --platform android",
"e2e:ios": "detox test --artifacts-location ./logs --take-screenshots --record-videos --configuration ios.sim.release --debug-synchronization 10000 --platform ios",
"e2e:android": "detox test --artifacts-location ./logs --take-screenshots --record-videos --configuration android.emu.release --loglevel verbose --platform android",
"build:ios": "detox build --configuration ios.sim.release",
"build:android": "detox build --configuration android.emu.release"
},
Expand Down