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

test sharding/parallelization #598

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
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);
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 && udids.length ? udids[0] : undefined;
}

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) {
console.log('Creating simulator', `create "${type}" "${type}" "${newestRuntime.identifier}"`)
await this._execSimctl({cmd: `create "${type}" "${type}" "${newestRuntime.identifier}"`});
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