diff --git a/lib/simctl.js b/lib/simctl.js index 078a882..90077e4 100644 --- a/lib/simctl.js +++ b/lib/simctl.js @@ -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; @@ -345,78 +341,88 @@ 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) { + // 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; @@ -424,6 +430,7 @@ async function createDevice (name, deviceTypeId, runtimeId, opts = {}) { } } } + throw new Error(`Device with udid '${udid}' not yet created`); }); return udid; diff --git a/package.json b/package.json index f1652dd..b89a227 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/test/simctl-e2e-specs.js b/test/simctl-e2e-specs.js index e8bd8b8..e3cbfc8 100644 --- a/test/simctl-e2e-specs.js +++ b/test/simctl-e2e-specs.js @@ -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 () { diff --git a/test/simctl-specs.js b/test/simctl-specs.js index 47a1827..6f50e8b 100644 --- a/test/simctl-specs.js +++ b/test/simctl-specs.js @@ -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 = [ [ @@ -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', @@ -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" : [ { @@ -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', @@ -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', @@ -211,13 +204,12 @@ 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', @@ -225,19 +217,18 @@ describe('simctl', function () { '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', @@ -245,11 +236,29 @@ describe('simctl', function () { '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'); + }); }); });