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 12 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
2 changes: 1 addition & 1 deletion detox/ios/EarlGrey
Submodule EarlGrey updated 143 files
37 changes: 25 additions & 12 deletions detox/local-cli/detox-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
const program = require('commander');
const path = require('path');
const cp = require('child_process');
const _ = require('lodash');

program
.option('-o, --runner-config [config]',
`Test runner config file, defaults to e2e/mocha.opts for mocha and e2e/config.json' for jest`)
Expand All @@ -26,6 +28,8 @@ program
+ 'e.g test with substring \':ios:\' in its name will not run when passing \'--platform android\'')
.parse(process.argv);

clearDeviceRegistryLockFile();

const config = require(path.join(process.cwd(), 'package.json')).detox;

const testFolder = getConfigFor('specs', 'e2e');
Expand Down Expand Up @@ -68,25 +72,27 @@ function runMocha() {
const debugSynchronization = program.debugSynchronization ? `--debug-synchronization ${program.debugSynchronization}` : '';
const command = `node_modules/.bin/mocha ${testFolder} ${configFile} ${configuration} ${loglevel} ${cleanup} ${reuse} ${debugSynchronization} ${platform} ${artifactsLocation}`;

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

function runJest() {
const currentConfiguration = config.configurations && config.configurations[program.configuration];
const maxTestWorkers = _.get(currentConfiguration, 'maxTestWorkers', 1);
Copy link
Member

Choose a reason for hiding this comment

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

Please use the same way to get configuration on all variables, either move all to _.get, use getConfigFor, or "vanilla"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

const configFile = runnerConfig ? `--config=${runnerConfig}` : '';
const platform = program.platform ? `--testNamePattern='^((?!${getPlatformSpecificString(program.platform)}).)*$'` : '';
const command = `node_modules/.bin/jest ${testFolder} ${configFile} --runInBand ${platform}`;
console.log(command);
const command = `node_modules/.bin/jest ${testFolder} ${configFile} --maxWorkers=${maxTestWorkers} ${platform}`;
const env = Object.assign({}, process.env, {
configuration: program.configuration,
loglevel: program.loglevel,
cleanup: program.cleanup,
reuse: program.reuse,
debugSynchronization: program.debugSynchronization,
artifactsLocationartifactsLocation: program.artifactsLocation,
maxTestWorkers,
});
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
})
env
});
}

Expand Down Expand Up @@ -115,4 +121,11 @@ function getPlatformSpecificString(platform) {
}

return platformRevertString;
}
}

function clearDeviceRegistryLockFile() {
const fs = require('fs');
const LOCK_FILE = './device.registry.state.lock';
fs.writeFileSync(LOCK_FILE, '[]');
}

1 change: 1 addition & 0 deletions detox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"ini": "^1.3.4",
"lodash": "^4.14.1",
"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
50 changes: 29 additions & 21 deletions detox/src/devices/AppleSimUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,27 @@ 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);
const response = await this._execAppleSimUtils({ args: `--list "${correctQuery}"` }, 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 +49,27 @@ 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 create(type) {
const result = await this._execSimctl({ cmd: `list runtimes -j` });
const stdout = _.get(result, 'stdout');
const runtimes = JSON.parse(stdout);
const newestRuntime = _.maxBy(runtimes.runtimes, r => Number(r.version));
if (newestRuntime) {
Copy link
Member

Choose a reason for hiding this comment

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

What happens if there no newestRuntime ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch. added handling

console.log('Creating simulator', `create "${type}" "${type}" "${newestRuntime.identifier}"`)
await this._execSimctl({cmd: `create "${type}" "${type}" "${newestRuntime.identifier}"`});
Copy link
Member

Choose a reason for hiding this comment

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

name should not be the type:

const name = `detox_${type}_${count}`
await this._execSimctl({cmd: `create "${name}" "${type}" "${newestRuntime.identifier}"`});

Now, when you change the name, you won't be able to query applesimutils by name anymore. @LeoNatan added query byType and byId. please check the implementation in:
https://github.com/wix/detox/compare/parallelSupport#diff-2ec04c0d120ceb57a189afbc6a59a9e8R27
https://github.com/wix/detox/compare/parallelSupport#diff-2ec04c0d120ceb57a189afbc6a59a9e8R39

return true;
}
await this.waitForDeviceState(udid, 'Shutdown');
await this._bootDeviceByXcodeVersion(udid);
await this.waitForDeviceState(udid, 'Booted');
}

async install(udid, absPath) {
Expand Down Expand Up @@ -200,6 +207,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 +253,4 @@ class LogsInfo {
}
}

module.exports = AppleSimUtils;
module.exports = AppleSimUtils;
145 changes: 113 additions & 32 deletions detox/src/devices/AppleSimUtils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,45 @@ describe('AppleSimUtils', () => {
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledTimes(1);
});

describe('findDevicesUDID', () => {

it('return multiple devices', async () => {
exec.execWithRetriesAndLogs.mockReturnValueOnce(Promise.resolve({
stdout: JSON.stringify([
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 6",
"udid": "the uuid1",
"os": {
"version": "10.3.1",
"availability": "(available)",
"name": "iOS 10.3",
"identifier": "com.apple.CoreSimulator.SimRuntime.iOS-10-3",
"buildversion": "14E8301"
}
},
{
"state": "Shutdown",
"availability": "(available)",
"name": "iPhone 6",
"udid": "the uuid2",
"os": {
"version": "10.3.1",
"availability": "(available)",
"name": "iOS 10.3",
"identifier": "com.apple.CoreSimulator.SimRuntime.iOS-10-3",
"buildversion": "14E8301"
}
}
])
}));
const result = await uut.findDevicesUDID('iPhone 7');
expect(result).toEqual(['the uuid1', 'the uuid2']);
});
});


describe('findDeviceUDID', () => {
it('correct params', async () => {
expect(exec.execWithRetriesAndLogs).not.toHaveBeenCalled();
Expand All @@ -37,7 +76,7 @@ describe('AppleSimUtils', () => {
} catch (e) { }
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledTimes(1);
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledWith('applesimutils', {
args: `--list "iPhone 6" --maxResults=1`
args: `--list "iPhone 6"`
}, expect.anything(), 1, undefined);
});

Expand All @@ -46,7 +85,7 @@ describe('AppleSimUtils', () => {
await uut.findDeviceUDID('iPhone 6 , iOS 10.3');
} catch (e) { }
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledWith('applesimutils', {
args: `--list "iPhone 6, OS=iOS 10.3" --maxResults=1`
args: `--list "iPhone 6, OS=iOS 10.3"`
}, expect.anything(), 1, undefined);
});

Expand Down Expand Up @@ -146,30 +185,6 @@ describe('AppleSimUtils', () => {
});
});

describe('waitForDeviceState', () => {
it('findsDeviceByUdid', async () => {
uut.findDeviceByUDID = jest.fn(() => Promise.resolve({ udid: 'the udid', state: 'the state' }));
retry.mockImplementation((opts, fn) => Promise.resolve(fn()));
const result = await uut.waitForDeviceState(`the udid`, `the state`);
expect(uut.findDeviceByUDID).toHaveBeenCalledTimes(1);
expect(result).toEqual({ udid: 'the udid', state: 'the state' });
});

it('waits for state to be equal', async () => {
uut.findDeviceByUDID = jest.fn(() => Promise.resolve({ udid: 'the udid', state: 'different state' }));
retry.mockImplementation((opts, fn) => Promise.resolve(fn()));
try {
await uut.waitForDeviceState(`the udid`, `the state`);
fail(`should throw`);
} catch (e) {
expect(e).toEqual(new Error(`device is in state 'different state'`));
}
expect(uut.findDeviceByUDID).toHaveBeenCalledTimes(1);
expect(retry).toHaveBeenCalledTimes(1);
expect(retry).toHaveBeenCalledWith({ retries: 10, interval: 1000 }, expect.any(Function));
});
});

describe('getXcodeVersion', () => {
it('returns xcode major version', async () => {
exec.execWithRetriesAndLogs.mockReturnValueOnce(Promise.resolve({ stdout: 'Xcode 123.456\nBuild version 123abc123\n' }));
Expand Down Expand Up @@ -206,23 +221,22 @@ describe('AppleSimUtils', () => {
});

describe('boot', () => {

it('waits for device by udid to be Shutdown, boots magically, then waits for state to be Booted', async () => {
uut.findDeviceByUDID = jest.fn(() => Promise.resolve({ state: 'unknown' }));
uut.waitForDeviceState = jest.fn(() => Promise.resolve(true));
uut.getXcodeVersion = jest.fn(() => Promise.resolve(1));
expect(exec.execWithRetriesAndLogs).not.toHaveBeenCalled();
await uut.boot('some udid');
await uut.boot('some-udid');
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledWith(expect.stringMatching('xcode-select -p'), undefined, expect.anything(), 1);
expect(uut.waitForDeviceState).toHaveBeenCalledTimes(2);
expect(uut.waitForDeviceState.mock.calls[0]).toEqual([`some udid`, `Shutdown`]);
expect(uut.waitForDeviceState.mock.calls[1]).toEqual([`some udid`, `Booted`]);
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledWith(expect.stringMatching('bootstatus some-udid'), undefined, expect.anything(), 1);
});

it('skips if device state was already Booted', async () => {
uut.findDeviceByUDID = jest.fn(() => Promise.resolve({ state: 'Booted' }));
uut.getXcodeVersion = jest.fn(() => Promise.resolve(1));
await uut.boot('udid');
expect(uut.findDeviceByUDID).toHaveBeenCalledTimes(1);
expect(uut.findDeviceByUDID).toHaveBeenCalledWith('udid');
expect(exec.execWithRetriesAndLogs).not.toHaveBeenCalled();
});

Expand All @@ -239,12 +253,79 @@ describe('AppleSimUtils', () => {
uut.getXcodeVersion = jest.fn(() => Promise.resolve(9));
await uut.boot('udid');
expect(uut.getXcodeVersion).toHaveBeenCalledTimes(1);
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledTimes(1);
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledWith(expect.stringMatching('xcrun simctl boot udid'), undefined, expect.anything(), 10);

});
});

describe('create', () => {
it('calls xcrun', async () => {
exec.execWithRetriesAndLogs.mockReturnValueOnce(Promise.resolve({stdout: "{}"}));

const created = await uut.create('name');
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledTimes(1);
expect(exec.execWithRetriesAndLogs).toHaveBeenCalledWith(
`/usr/bin/xcrun simctl list runtimes -j`,
undefined,
expect.anything(),
1);
expect(created).toEqual(undefined);
});

it('creates using the newest runtime version', async () => {
const runtimes = {
"runtimes" : [
{
"buildversion" : "13C75",
"availability" : "(available)",
"name" : "iOS 9.2",
"identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-9-2",
"version" : "9.2"
},
{
"buildversion" : "13E233",
"availability" : "(available)",
"name" : "iOS 9.3",
"identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-9-3",
"version" : "9.3"
},
{
"buildversion" : "15C107",
"availability" : "(available)",
"name" : "iOS 11.2",
"identifier" : "com.apple.CoreSimulator.SimRuntime.iOS-11-2",
"version" : "11.2"
},
{
"buildversion" : "15K104",
"availability" : "(available)",
"name" : "tvOS 11.2",
"identifier" : "com.apple.CoreSimulator.SimRuntime.tvOS-11-2",
"version" : "11.2"
},
{
"buildversion" : "15S100",
"availability" : "(available)",
"name" : "watchOS 4.2",
"identifier" : "com.apple.CoreSimulator.SimRuntime.watchOS-4-2",
"version" : "4.2"
}
]
};
exec.execWithRetriesAndLogs.mockReturnValueOnce(Promise.resolve({stdout: JSON.stringify(runtimes)}));

const created = await uut.create('iPhone 8 Plus');

expect(exec.execWithRetriesAndLogs).toHaveBeenCalledWith(
`/usr/bin/xcrun simctl create "iPhone 8 Plus" "iPhone 8 Plus" "com.apple.CoreSimulator.SimRuntime.iOS-11-2"`,
undefined,
expect.anything(),
1);
expect(created).toEqual(true);
});
});


describe('install', () => {
it('calls xcrun', async () => {
await uut.install('udid', 'somePath');
Expand Down
Loading