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

Support parallel test execution #609

Merged
merged 81 commits into from
May 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
ae9c471
switch to e2e tests to jest
Mar 6, 2018
57ef873
downgrade test project to RN51
Mar 6, 2018
c54109e
wait for device based on bootstatus
Feb 20, 2018
f5cd1b9
introduce findDevicesUDID and use in findDeviceUDID
Feb 20, 2018
3c26649
remove redundant check
Mar 6, 2018
bdf4eb8
support multiple workers. for now set to 1
Feb 20, 2018
fb96349
device registry
Mar 6, 2018
bf31acb
device registry
Mar 6, 2018
5c5d923
fix device registry race condition
Mar 6, 2018
f18c34c
move maxTestWorkers param to detox config
Mar 6, 2018
5004898
clean up
Mar 4, 2018
74849ae
-forceExit
Mar 6, 2018
4251edc
error when no runtime is available
Mar 7, 2018
3bc6518
use dot notation
Mar 7, 2018
f839c3d
move lock file to ~/Library/Detox
Mar 7, 2018
fa268cd
adjust lock file retry options
Mar 7, 2018
d5e7dab
use functions instead of consts when declaring functions
Mar 7, 2018
06b917e
use const for detox library root path
Mar 7, 2018
d3dcdfb
Reset EarlGrey submodule
rotemmiz Mar 7, 2018
caa6fc8
dummy commit
Mar 7, 2018
a26e200
verbose postinstall
rotemmiz Mar 7, 2018
08abe3f
Merge branch 'master' into test-parallelization
rotemmiz Mar 7, 2018
70cb9d5
simplified root path to fix Android build
Mar 8, 2018
4c72215
Revert "simplified root path to fix Android build"
Mar 8, 2018
406740e
simplified root path to fix Android build
Mar 8, 2018
04b6243
dummy commit
Mar 10, 2018
dc7f70a
Merge branch 'master' into test-parallelization
Mar 10, 2018
ddbdd06
run e2e w/ parallelization in ci
Mar 10, 2018
7d12cd9
Merge branch 'master' into test-parallelization
Mar 10, 2018
712e6de
dummy commit
Mar 10, 2018
9b0c50a
temp - added troubleshooting logs
Mar 11, 2018
bc79553
Revert "temp - added troubleshooting logs"
Mar 11, 2018
a080a4b
Merge branch 'master' into test-parallelization
Mar 12, 2018
7de97ac
merge
Mar 12, 2018
00c40d3
make sure single works
Mar 12, 2018
3630f5a
fix clear lock file path
Mar 13, 2018
646168c
improve closed socket error message
Mar 15, 2018
73c27e1
Merge branch 'master' into test-parallelization
rotemmiz May 1, 2018
f40373f
WIP
rotemmiz May 2, 2018
5dd5d27
WIP
rotemmiz May 7, 2018
56f208d
WIP
rotemmiz May 8, 2018
9a388b2
Merge branch 'master' into test-parallelization
rotemmiz May 8, 2018
c48d49d
WIP
rotemmiz May 8, 2018
320113c
trigger build
rotemmiz May 8, 2018
ffc4e20
Trigger build
yershalom May 8, 2018
688a31b
Trigger build
yershalom May 8, 2018
1f21ca4
waitFor timeout increased
rotemmiz May 8, 2018
0f9007f
maxWorkers=2
rotemmiz May 8, 2018
52d0f86
Add line for trigger build
yershalom May 8, 2018
5b11bc9
Revert "Add line for trigger build"
yershalom May 8, 2018
d088827
last trigger build
yershalom May 8, 2018
8eea0a7
Revert "last trigger build"
yershalom May 8, 2018
cdd18b0
Update ci.ios.sh
yershalom May 8, 2018
920faba
Add line to ci ios
yershalom May 8, 2018
6931a0e
Fix platfrom for jest
yershalom May 9, 2018
7547c99
Fix jest invert platform
yershalom May 9, 2018
0f64ffa
Trigger build
yershalom May 9, 2018
6267fe2
use detox-test to cleanup lockfile
rotemmiz May 12, 2018
a4484dd
fix waitFor screen, should not be flaky anymore
rotemmiz May 12, 2018
57a7b38
added unit test for a new edge case
rotemmiz May 12, 2018
efb39a6
better visiblity on messages passing on a closed ws
rotemmiz May 12, 2018
a3c9e1e
WAT
rotemmiz May 12, 2018
c9c4089
only call currentStatus if ws is open
rotemmiz May 12, 2018
9b51f72
increase verbosity for debug purposes
rotemmiz May 12, 2018
3a082bc
per platfrom app data path
rotemmiz May 13, 2018
d76a43a
ensure file exists, create including path if needed
rotemmiz May 13, 2018
3f14a38
always query applesimutils byOS
rotemmiz May 14, 2018
f1d8b48
Triggering PR build
yershalom May 14, 2018
c0393b0
print stderrs of after all retries failed
rotemmiz May 14, 2018
86b1b8a
print stderrs of after all retries failed
rotemmiz May 14, 2018
1084c20
Revert "Triggering PR build"
rotemmiz May 14, 2018
87c357f
Trigger PR after fixed jenkins
yershalom May 14, 2018
e6743da
Revert "Trigger PR after fixed jenkins"
yershalom May 14, 2018
0c73826
Last try trigger PR
yershalom May 14, 2018
00cd14b
Revert "Last try trigger PR"
yershalom May 14, 2018
dd79249
Revert "Revert "Last try trigger PR""
rotemmiz May 15, 2018
d91bd9c
Add empty line to package.json for triggering build
yershalom May 15, 2018
f7923af
Merge branch 'master' into test-parallelization
rotemmiz May 21, 2018
c6c90f4
update xcode
rotemmiz May 22, 2018
5830c96
Merge branch 'master' into test-parallelization
rotemmiz May 28, 2018
c6357ae
MOAR retries
rotemmiz May 28, 2018
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
6 changes: 3 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ matrix:
include:
- language: objective-c
os: osx
osx_image: xcode9.2
osx_image: xcode9.3
env:
- REACT_NATIVE_VERSION=0.53.3
install:
Expand All @@ -14,7 +14,7 @@ matrix:
- ./scripts/ci.ios.sh
- language: objective-c
os: osx
osx_image: xcode9.2
osx_image: xcode9.3
env:
- REACT_NATIVE_VERSION=0.51.1
install:
Expand Down Expand Up @@ -42,7 +42,7 @@ matrix:
# Example Projects
- language: objective-c
os: osx
osx_image: xcode9
osx_image: xcode9.3
env:
- REACT_NATIVE_VERSION=0.51.1
install:
Expand Down
47 changes: 30 additions & 17 deletions detox/local-cli/detox-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
const program = require('commander');
const path = require('path');
const cp = require('child_process');

const _ = require('lodash');
const CustomError = require('../src/errors/CustomError');
const environment = require('../src/utils/environment');
const config = require(path.join(process.cwd(), 'package.json')).detox;

class DetoxConfigError extends CustomError {}
Expand Down Expand Up @@ -38,6 +40,10 @@ program
'[Android Only] Launch Emulator in headless mode. Useful when running on CI.')
.parse(process.argv);


clearDeviceRegistryLockFile();


if (program.configuration) {
if (!config.configurations[program.configuration]) {
throw new DetoxConfigError(`Cannot determine configuration '${program.configuration}'.
Expand All @@ -53,9 +59,6 @@ const runner = getConfigFor(['testRunner'], 'mocha');
const runnerConfig = getConfigFor(['runnerConfig'], getDefaultRunnerConfig());
const platform = (config.configurations[program.configuration].type).split('.')[0];

run();


if (typeof program.debugSynchronization === "boolean") {
program.debugSynchronization = 3000;
}
Expand Down Expand Up @@ -102,28 +105,32 @@ function runMocha() {
const binPath = path.join('node_modules', '.bin', 'mocha');
const command = `${binPath} ${testFolder} ${configFile} ${configuration} ${loglevel} ${cleanup} ${reuse} ${debugSynchronization} ${platformString} ${artifactsLocation} ${headless}`;

console.log(command);
cp.execSync(command, {stdio: 'inherit'});
}

function runJest() {
const currentConfiguration = config.configurations && config.configurations[program.configuration];
const maxWorkers = currentConfiguration.maxWorkers || 1;
const configFile = runnerConfig ? `--config=${runnerConfig}` : '';
const platform = program.platform ? `--testNamePattern='^((?!${getPlatformSpecificString(program.platform)}).)*$'` : '';
const binPath = path.join('node_modules', '.bin', 'jest');

const platformString = platform ? `--testNamePattern='^((?!${getPlatformSpecificString(platform)}).)*$'` : '';
const command = `${binPath} ${testFolder} ${configFile} --runInBand ${platformString}`;
const binPath = path.join('node_modules', '.bin', 'jest');
const command = `${binPath} ${testFolder} ${configFile} --maxWorkers=${maxWorkers} ${platformString}`;
const env = Object.assign({}, process.env, {
configuration: program.configuration,
loglevel: program.loglevel,
cleanup: program.cleanup,
reuse: program.reuse,
debugSynchronization: program.debugSynchronization,
artifactsLocation: program.artifactsLocation,
headless: program.headless
});

console.log(command);

cp.execSync(command, {
stdio: 'inherit',
env: Object.assign({}, process.env, {
configuration: program.configuration,
loglevel: program.loglevel,
cleanup: program.cleanup,
reuse: program.reuse,
debugSynchronization: program.debugSynchronization,
artifactsLocation: program.artifactsLocation,
headless: program.headless
})
env
});
}

Expand Down Expand Up @@ -154,10 +161,16 @@ function getPlatformSpecificString(platform) {
return platformRevertString;
}


function clearDeviceRegistryLockFile() {
const fs = require('fs');
fs.writeFileSync(environment.getDeviceLockFilePath(), '[]');
}

function getDefaultConfiguration() {
if (_.size(config.configurations) === 1) {
return _.keys(config.configurations)[0];
}
}


run();
2 changes: 2 additions & 0 deletions detox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"lodash": "^4.14.1",
"minimist": "^1.2.0",
"npmlog": "^4.0.2",
"proper-lockfile": "^3.0.2",
"shell-utils": "^1.0.9",
"tail": "^1.2.3",
"telnet-client": "0.15.3",
Expand Down Expand Up @@ -74,6 +75,7 @@
"debug.js",
"src/ios/earlgreyapi",
"src/android/espressoapi",
"appdatapath.js",
".test.js",
".mock.js"
],
Expand Down
2 changes: 1 addition & 1 deletion detox/src/client/AsyncWebSocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class AsyncWebSocket {

async send(message, messageId) {
if (!this.ws) {
throw new Error(`Can't send a message on a closed websocket, init the by calling 'open()'`);
throw new Error(`Can't send a message on a closed websocket, init the by calling 'open()'. Message: ${JSON.stringify(message)}`);
}

return new Promise(async(resolve, reject) => {
Expand Down
10 changes: 7 additions & 3 deletions detox/src/client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ class Client {
async cleanup() {
clearTimeout(this.slowInvocationStatusHandler);
if (this.isConnected && !this.pandingAppCrash) {
await this.sendAction(new actions.Cleanup(this.successfulTestRun));
if(this.ws.isOpen()) {
await this.sendAction(new actions.Cleanup(this.successfulTestRun));
}
this.isConnected = false;
}

Expand Down Expand Up @@ -102,8 +104,10 @@ class Client {

slowInvocationStatus() {
return setTimeout(async () => {
const status = await this.currentStatus();
this.slowInvocationStatusHandler = this.slowInvocationStatus();
if (this.ws.isOpen()) {
const status = await this.currentStatus();
this.slowInvocationStatusHandler = this.slowInvocationStatus();
}
}, this.slowInvocationTimeout);
}
}
Expand Down
47 changes: 35 additions & 12 deletions detox/src/client/Client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ describe('Client', () => {
beforeEach(() => {
jest.mock('npmlog');
WebSocket = jest.mock('./AsyncWebSocket');
Client = require('./Client');

jest.mock('../utils/argparse');
argparse = require('../utils/argparse');

Client = require('./Client');
});

it(`reloadReactNative() - should receive ready from device and resolve`, async () => {
Expand Down Expand Up @@ -75,6 +76,17 @@ describe('Client', () => {
expect(client.ws.send).not.toHaveBeenCalled();
});

it(`cleanup() - if "connected" but ws is closed should do nothing`, async () => {
await connect();
client.ws.send.mockReturnValueOnce(response("ready", {}, 1));
await client.waitUntilReady();

client.ws.isOpen.mockReturnValue(false);
await client.cleanup();

expect(client.ws.send).toHaveBeenCalledTimes(2);
});

it(`execute() - "invokeResult" on an invocation object should resolve`, async () => {
await connect();
client.ws.send.mockReturnValueOnce(response("invokeResult", {result: "(GREYElementInteraction)"}, 1));
Expand All @@ -85,26 +97,29 @@ describe('Client', () => {
expect(client.ws.send).toHaveBeenCalledTimes(2);
});

async function executeWithSlowInvocation(invocationTime) {
it(`execute() - fast invocation should not trigger "slowInvocationStatus"`, async () => {
argparse.getArgValue.mockReturnValue(2); // set debug-slow-invocations

await connect();

client.ws.send.mockReturnValueOnce(timeout(invocationTime).then(()=> response("invokeResult", {result:"(GREYElementInteraction)"}, 1)))
.mockReturnValueOnce(response("currentStatusResult", {"state":"busy","resources":[{"name":"App State","info":{"prettyPrint":"Waiting for network requests to finish.","elements":["__NSCFLocalDataTask:0x7fc95d72b6c0"],"appState":"Waiting for network requests to finish."}},{"name":"Dispatch Queue","info":{"queue":"OS_dispatch_queue_main: com.apple.main-thread[0x10805ea80] = { xrefcnt = 0x80000000, refcnt = 0x80000000, target = com.apple.root.default-qos.overcommit[0x10805f1c0], width = 0x1, state = 0x000fffe000000403, in-flight = 0, thread = 0x403 }","prettyPrint":"com.apple.main-thread"}}]}, 2));

const call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityLabel:', 'test');
await client.execute(call);
}

it(`execute() - fast invocation should not trigger "slowInvocationStatus"`, async () => {
await executeWithSlowInvocation(1);
expect(client.ws.send).toHaveBeenLastCalledWith({"params": {"args": ["test"], "method": "matcherForAccessibilityLabel:", "target": {"type": "Class", "value": "GREYMatchers"}}, "type": "invoke"}, undefined);
expect(client.ws.send).toHaveBeenCalledTimes(2);
});

it(`execute() - slow invocation should trigger "slowInvocationStatus:`, async () => {
argparse.getArgValue.mockReturnValue(2); // set debug-slow-invocations
await connect();
await executeWithSlowInvocation(4);
expect(client.ws.send).toHaveBeenLastCalledWith({"params": {}, "type": "currentStatus"}, undefined);
expect(client.ws.send).toHaveBeenCalledTimes(3);
});

it(`execute() - slow invocation should do nothing if ws was closed`, async () => {
argparse.getArgValue.mockReturnValue(2); // set debug-slow-invocations
await connect();
client.ws.isOpen.mockReturnValue(false);
await executeWithSlowInvocation(4);

expect(client.ws.send).toHaveBeenCalledTimes(2);
});

it(`execute() - "invokeResult" on an invocation function should resolve`, async () => {
Expand Down Expand Up @@ -185,6 +200,14 @@ describe('Client', () => {
}));
}

async function executeWithSlowInvocation(invocationTime) {
client.ws.send.mockReturnValueOnce(timeout(invocationTime).then(()=> response("invokeResult", {result:"(GREYElementInteraction)"}, 1)))
.mockReturnValueOnce(response("currentStatusResult", {"state":"busy","resources":[{"name":"App State","info":{"prettyPrint":"Waiting for network requests to finish.","elements":["__NSCFLocalDataTask:0x7fc95d72b6c0"],"appState":"Waiting for network requests to finish."}},{"name":"Dispatch Queue","info":{"queue":"OS_dispatch_queue_main: com.apple.main-thread[0x10805ea80] = { xrefcnt = 0x80000000, refcnt = 0x80000000, target = com.apple.root.default-qos.overcommit[0x10805f1c0], width = 0x1, state = 0x000fffe000000403, in-flight = 0, thread = 0x403 }","prettyPrint":"com.apple.main-thread"}}]}, 2));

const call = invoke.call(invoke.IOS.Class('GREYMatchers'), 'matcherForAccessibilityLabel:', 'test');
await client.execute(call);
}

async function timeout(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Expand Down
82 changes: 51 additions & 31 deletions detox/src/devices/AppleSimUtils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const process = require('process');
const _ = require('lodash');
const exec = require('../utils/exec');
const retry = require('../utils/retry');
Expand All @@ -20,22 +21,39 @@ class AppleSimUtils {
}

async findDeviceUDID(query) {
const udids = await this.findDevicesUDID(query);
return udids[0];
}

async findDevicesUDID(query) {
const statusLogs = {
trying: `Searching for device matching ${query}...`
};
let correctQuery = this._correctQueryWithOS(query);
const response = await this._execAppleSimUtils({ args: `--list "${correctQuery}" --maxResults=1` }, statusLogs, 1);

let type;
let os;
if (_.includes(query, ',')) {
const parts = _.split(query, ',');
type = parts[0].trim();
os = parts[1].trim();
} else {
type = query;
const deviceInfo = await this.deviceTypeAndNewestRuntimeFor(query);
os = deviceInfo.newestRuntime.version;
}

const response = await this._execAppleSimUtils({ args: `--list --byType "${type}" --byOS "${os}"`}, statusLogs, 1);
const parsed = this._parseResponseFromAppleSimUtils(response);
const udid = _.get(parsed, [0, 'udid']);
if (!udid) {
const udids = _.map(parsed, 'udid');
if (!udids || !udids.length || !udids[0]) {
throw new Error(`Can't find a simulator to match with "${query}", run 'xcrun simctl list' to list your supported devices.
It is advised to only state a device type, and not to state iOS version, e.g. "iPhone 7"`);
}
return udid;
return udids;
}

async findDeviceByUDID(udid) {
const response = await this._execAppleSimUtils({ args: `--list` }, undefined, 1);
const response = await this._execAppleSimUtils({args: `--list --byId "${udid}"`}, undefined, 1);
const parsed = this._parseResponseFromAppleSimUtils(response);
const device = _.find(parsed, (device) => _.isEqual(device.udid, udid));
if (!device) {
Expand All @@ -44,25 +62,35 @@ class AppleSimUtils {
return device;
}

async waitForDeviceState(udid, state) {
let device;
await retry({ retries: 10, interval: 1000 }, async () => {
device = await this.findDeviceByUDID(udid);
if (!_.isEqual(device.state, state)) {
throw new Error(`device is in state '${device.state}'`);
}
});
return device;
async boot(udid) {
if (!await this.isBooted(udid)) {
await this._bootDeviceByXcodeVersion(udid);
}
}

async boot(udid) {
async isBooted(udid) {
const device = await this.findDeviceByUDID(udid);
if (_.isEqual(device.state, 'Booted') || _.isEqual(device.state, 'Booting')) {
return false;
return (_.isEqual(device.state, 'Booted') || _.isEqual(device.state, 'Booting'));
}

async deviceTypeAndNewestRuntimeFor(name) {
const result = await this._execSimctl({ cmd: `list -j` });
const stdout = _.get(result, 'stdout');
const output = JSON.parse(stdout);
const deviceType = _.filter(output.devicetypes, { 'name': name})[0];
const newestRuntime = _.maxBy(output.runtimes, r => Number(r.version));
return { deviceType, newestRuntime };
}
async create(name) {
const deviceInfo = await this.deviceTypeAndNewestRuntimeFor(name);

if (deviceInfo.newestRuntime) {
const result = await this._execSimctl({cmd: `create "${name}-Detox" "${deviceInfo.deviceType.identifier}" "${deviceInfo.newestRuntime.identifier}"`});
const udid = _.get(result, 'stdout').trim();
return udid;
} else {
throw new Error(`Unable to create device. No runtime found for ${name}`);
}
await this.waitForDeviceState(udid, 'Shutdown');
await this._bootDeviceByXcodeVersion(udid);
await this.waitForDeviceState(udid, 'Booted');
}

async install(udid, absPath) {
Expand Down Expand Up @@ -163,15 +191,6 @@ class AppleSimUtils {
return await exec.execWithRetriesAndLogs(`/usr/bin/xcrun simctl ${cmd}`, undefined, statusLogs, retries);
}

_correctQueryWithOS(query) {
let correctQuery = query;
if (_.includes(query, ',')) {
const parts = _.split(query, ',');
correctQuery = `${parts[0].trim()}, OS=${parts[1].trim()}`;
}
return correctQuery;
}

_parseResponseFromAppleSimUtils(response) {
let out = _.get(response, 'stdout');
if (_.isEmpty(out)) {
Expand Down Expand Up @@ -200,6 +219,7 @@ class AppleSimUtils {
} else {
await this._bootDeviceMagically(udid);
}
await this._execSimctl({ cmd: `bootstatus ${udid}`, retries: 1 });
}

async _bootDeviceMagically(udid) {
Expand Down Expand Up @@ -245,4 +265,4 @@ class LogsInfo {
}
}

module.exports = AppleSimUtils;
module.exports = AppleSimUtils;
Loading