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

refactor: simplify createDevice logic #89

Merged
merged 9 commits into from
Nov 20, 2019
Merged
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
89 changes: 48 additions & 41 deletions lib/simctl.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { exec, SubProcess } from 'teen_process';
import { retryInterval, waitForCondition } from 'asyncbox';
import { logger, fs, tempDir, util } from 'appium-support';
import { logger, fs, tempDir } from 'appium-support';
import _ from 'lodash';
import { getClangVersion, getVersion } from 'appium-xcode';
import semver from 'semver';


const log = logger.getLogger('simctl');

// command line tools and xcode version can be different
const CMDLINE_TOOLS_CLANG_FORMAT_CHANGED_VERSION = '1001.0.46';
const XCODE_FORMAT_CHANGED_VERSION = '10.2';

const SIM_RUNTIME_NAME = 'com.apple.CoreSimulator.SimRuntime.';
const SIM_RUNTIME_NAME_SUFFIX_IOS = 'iOS';
const DEFAULT_CREATE_SIMULATOR_TIMEOUT = 10000;
Expand Down Expand Up @@ -345,85 +341,96 @@ async function shutdown (udid) {
*
* @param {string} name - The device name to be created.
* @param {string} deviceTypeId - Device type, for example 'iPhone 6'.
* @param {string} runtimeId - Platform version, for example '10.3'.
* @param {string} platformVersion - Platform version, for example '10.3'.
* @param {?SimCreationOpts} opts - Simulator options for creating devices.
* @return {string} The UDID of the newly created device.
* @throws {Error} If the corresponding simctl subcommand command
* returns non-zero return code.
*/
async function createDevice (name, deviceTypeId, runtimeId, opts = {}) {
async function createDevice (name, deviceTypeId, platformVersion, opts = {}) {
const {
platform = SIM_RUNTIME_NAME_SUFFIX_IOS,
timeout = DEFAULT_CREATE_SIMULATOR_TIMEOUT
} = opts;

let runtimeIds = [];

// Try getting runtimeId using JSON flag
let runtimeIdFromJson;
try {
runtimeIdFromJson = await getRuntimeForPlatformVersionViaJson(runtimeId, platform);
runtimeId = runtimeIdFromJson;
runtimeIds.push(await getRuntimeForPlatformVersionViaJson(platformVersion, platform));
} catch (ign) { }

if (!runtimeIdFromJson) {
if (_.isEmpty(runtimeIds)) {
// at first make sure that the runtime id is the right one
// in some versions of Xcode it will be a patch version
let runtimeId;
try {
runtimeId = await getRuntimeForPlatformVersion(runtimeId, platform);
runtimeId = await getRuntimeForPlatformVersion(platformVersion, platform);
} catch (err) {
log.warn(`Unable to find runtime for iOS '${runtimeId}'. Continuing`);
log.warn(`Unable to find runtime for iOS '${platformVersion}'. Continuing`);
runtimeId = platformVersion;
}

const clangVersion = await getClangVersion();
// 1st comparison: clangVersion
// Command line tools version is 10.2+, but xcode 10.1 can happen
let isNewerIdFormatRequired = clangVersion && util.compareVersions(clangVersion, '>=',
CMDLINE_TOOLS_CLANG_FORMAT_CHANGED_VERSION);

if (!isNewerIdFormatRequired) {
// 2nd comparison: getVersion
// The opposite can also happen,
// but the combination of 10.2 command line tools and lower Xcode version happens more frequently
const xcodeVersion = await getVersion(false);
isNewerIdFormatRequired = xcodeVersion && util.compareVersions(xcodeVersion, '>=',
XCODE_FORMAT_CHANGED_VERSION);
// get the possible runtimes, which will be iterated over
// compute the `major.minor` version first since Xcode usually has a runtime
// that does not include the patch version, even if it exists
const runtimeIdSemver = semver.coerce(runtimeId);
if (_.isNil(runtimeIdSemver)) {
throw new Error(`Unable to parse runtime id '${runtimeId}'`);
}

if (isNewerIdFormatRequired) {
runtimeId = `${SIM_RUNTIME_NAME}${platform}-${runtimeId.replace(/\./g, '-')}`;
// start with major-minor version
let potentialRuntimeIds = [`${runtimeIdSemver.major}.${runtimeIdSemver.minor}`];
if (runtimeId.split('.').length === 3) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if (runtimeIdSemver.patch) ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure that there is no situation where a 0 patch version is not valid, and it would be excluded here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm fine with the current implementation as well

// add patch version if it exists
potentialRuntimeIds.push(runtimeId);
}

// add modified versions, since modern Xcodes use this, then the bare
// versions, to accomodate older Xcodes
runtimeIds.push(
...(potentialRuntimeIds.map((id) => `${SIM_RUNTIME_NAME}${platform}-${id.replace(/\./g, '-')}`)),
...potentialRuntimeIds
);
}

log.debug(`Creating simulator with name '${name}', device type id '${deviceTypeId}' and runtime id '${runtimeId}'`);
// go through the runtime ids and try to create a simulator with each
let udid;
try {
const out = await simExec('create', 0, [name, deviceTypeId, runtimeId]);
udid = out.stdout.trim();
} catch (err) {
let reason = err.message;
if (err.stderr) {
reason = err.stderr.trim();
for (const runtimeId of runtimeIds) {
log.debug(`Creating simulator with name '${name}', device type id '${deviceTypeId}' and runtime id '${runtimeId}'`);
try {
const out = await simExec('create', 0, [name, deviceTypeId, runtimeId]);
udid = out.stdout.trim();
break;
} catch (ign) {
// the error gets logged in `simExec`
}
}

if (!udid) {
log.errorAndThrow(`Could not create simulator with name '${name}', device ` +
`type id '${deviceTypeId}' and runtime id '${runtimeId}'. Reason: '${reason}'`);
`type id '${deviceTypeId}', with runtime ids ` +
`${runtimeIds.map((id) => `'${id}'`).join(', ')}`);
}

// make sure that it gets out of the "Creating" state
let retries = parseInt(timeout / 1000, 10);
const retries = parseInt(timeout / 1000, 10);
await retryInterval(retries, 1000, async () => {
let devices = await getDevices();
const devices = _.values(await getDevices());
for (const deviceArr of _.values(devices)) {
for (const device of deviceArr) {
if (device.udid === udid) {
if (device.state === 'Creating') {
// need to retry
throw new Error('Device still being created');
throw new Error(`Device with udid '${udid}' still being created`);
} else {
// stop looking, we're done
return;
}
}
}
}
throw new Error(`Device with udid '${udid}' not yet created`);
});

return udid;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"appium-xcode": "^3.8.0",
"asyncbox": "^2.3.1",
"lodash": "^4.2.1",
"semver": "^6.3.0",
"source-map-support": "^0.5.5",
"teen_process": "^1.5.1"
},
Expand Down
2 changes: 1 addition & 1 deletion test/simctl-e2e-specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('simctl', function () {
err = e;
}
should.exist(err);
err.message.should.include('Invalid device type: bar');
err.message.should.include(`Unable to parse runtime id 'baz'`);
});

describe('on running Simulator', function () {
Expand Down
59 changes: 34 additions & 25 deletions test/simctl-specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import sinon from 'sinon';
import * as TeenProcess from 'teen_process';
import _ from 'lodash';
import { getDevices, createDevice } from '../lib/simctl';
import * as xcode from 'appium-xcode';


const devicePayloads = [
[
Expand Down Expand Up @@ -119,23 +119,18 @@ describe('simctl', function () {

describe('#createDevice', function () {
const devicesPayload = devicePayloads[0][0];
const getClangVersionStub = sinon.stub(xcode, 'getClangVersion');
const getXcodeVersionStub = sinon.stub(xcode, 'getVersion');
afterEach(function () {
execStub.resetHistory();
getClangVersionStub.resetHistory();
});
after(function () {
execStub.reset();
getClangVersionStub.reset();
});

it('should create iOS simulator by default', async function () {
it('should create iOS simulator', async function () {
execStub.onCall(0).returns({stdout: 'not json'})
.onCall(1).returns({stdout: 'com.apple.CoreSimulator.SimRuntime.iOS-12-1-1', stderr: ''})
.onCall(1).returns({stdout: '12.1.1', stderr: ''})
.onCall(2).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''})
.onCall(3).returns(devicesPayload);
getClangVersionStub.returns('1001.0.46.3');

const devices = await createDevice(
'name',
Expand All @@ -144,12 +139,12 @@ describe('simctl', function () {
{ timeout: 20000 }
);
execStub.getCall(2).args[1].should.eql([
'simctl', 'create', 'name', 'iPhone 6 Plus', 'com.apple.CoreSimulator.SimRuntime.iOS-12-1-1'
'simctl', 'create', 'name', 'iPhone 6 Plus', 'com.apple.CoreSimulator.SimRuntime.iOS-12-1'
]);
devices.should.eql('EE76EA77-E975-4198-9859-69DFF74252D2');
});

it('should create iOS simulator by default and use xcrun simctl "json" parsing', async function () {
it('should create iOS simulator and use xcrun simctl "json" parsing', async function () {
const runtimesJson = `{
"runtimes" : [
{
Expand Down Expand Up @@ -178,7 +173,6 @@ describe('simctl', function () {
execStub.onCall(0).returns({stdout: runtimesJson})
.onCall(1).returns({stdout: 'FA628127-1D5C-45C3-9918-A47BF7E2AE14', stderr: ''})
.onCall(2).returns(devicesPayload);
getClangVersionStub.returns('1.1.1.1');

const devices = await createDevice(
'name',
Expand All @@ -197,7 +191,6 @@ describe('simctl', function () {
.onCall(1).returns({stdout: 'com.apple.CoreSimulator.SimRuntime.tvOS-12-1', stderr: ''})
.onCall(2).returns({stdout: 'FA628127-1D5C-45C3-9918-A47BF7E2AE14', stderr: ''})
.onCall(3).returns(devicesPayload);
getClangVersionStub.returns('1001.0.46.4');

const devices = await createDevice(
'name',
Expand All @@ -211,45 +204,61 @@ describe('simctl', function () {
devices.should.eql('FA628127-1D5C-45C3-9918-A47BF7E2AE14');
});

it('should create iOS simulator by default with lower command line tool but newer xcode version', async function () {
it('should create iOS simulator with old runtime format', async function () {
execStub.onCall(0).returns({stdout: 'invalid json'})
.onCall(1).returns({stdout: '12.1', stderr: ''})
.onCall(2).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''})
.onCall(3).returns(devicesPayload);
getClangVersionStub.returns('1000.11.45.5');
getXcodeVersionStub.returns('10.1');
.onCall(2).throws('Invalid runtime: com.apple.CoreSimulator.SimRuntime.iOS-12-1')
.onCall(3).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''})
.onCall(4).returns(devicesPayload);

const devices = await createDevice(
'name',
'iPhone 6 Plus',
'12.1',
{ timeout: 20000 }
);
execStub.getCall(2).args[1].should.eql([
execStub.getCall(3).args[1].should.eql([
'simctl', 'create', 'name', 'iPhone 6 Plus', '12.1'
]);
devices.should.eql('EE76EA77-E975-4198-9859-69DFF74252D2');
});

it('should create iOS simulator by default with old format', async function () {
it('should create iOS simulator with old runtime format and three-part platform version', async function () {
execStub.onCall(0).returns({stdout: 'invalid json'})
.onCall(1).returns({stdout: '12.1', stderr: ''})
.onCall(2).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''})
.onCall(3).returns(devicesPayload);
getClangVersionStub.returns('1000.11.45.5');
getXcodeVersionStub.returns('10.1');
.onCall(1).returns({stdout: '12.1.1', stderr: ''})
.onCall(2).throws('Invalid runtime: com.apple.CoreSimulator.SimRuntime.iOS-12-1')
.onCall(3).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''})
.onCall(4).returns(devicesPayload);

const devices = await createDevice(
'name',
'iPhone 6 Plus',
'12.1',
{ timeout: 20000 }
);
execStub.getCall(2).args[1].should.eql([
execStub.getCall(3).args[1].should.eql([
'simctl', 'create', 'name', 'iPhone 6 Plus', '12.1'
]);
devices.should.eql('EE76EA77-E975-4198-9859-69DFF74252D2');
});

it('should create iOS simulator with three-part platform version and three-part runtime', async function () {
execStub.onCall(0).returns({stdout: 'invalid json'})
.onCall(1).returns({stdout: '12.1.1', stderr: ''})
.onCall(2).throws('Invalid runtime: com.apple.CoreSimulator.SimRuntime.iOS-12-1')
.onCall(3).returns({stdout: 'EE76EA77-E975-4198-9859-69DFF74252D2', stderr: ''})
.onCall(4).returns(devicesPayload);

const devices = await createDevice(
'name',
'iPhone 6 Plus',
'12.1.1',
{ timeout: 20000 }
);
execStub.getCall(3).args[1].should.eql([
'simctl', 'create', 'name', 'iPhone 6 Plus', 'com.apple.CoreSimulator.SimRuntime.iOS-12-1-1'
]);
devices.should.eql('EE76EA77-E975-4198-9859-69DFF74252D2');
});
});
});