diff --git a/packages/platform-ios/src/commands/runIOS/__tests__/findMatchingSimulator.test.ts b/packages/platform-ios/src/commands/runIOS/__tests__/findMatchingSimulator.test.ts index cc5118970..e6a064c0f 100644 --- a/packages/platform-ios/src/commands/runIOS/__tests__/findMatchingSimulator.test.ts +++ b/packages/platform-ios/src/commands/runIOS/__tests__/findMatchingSimulator.test.ts @@ -17,7 +17,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(available)', @@ -51,7 +51,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPhone 6', + {simulator: 'iPhone 6'}, ), ).toEqual({ udid: 'BA0D93BD-07E6-4182-9B0A-F60A2474139C', @@ -66,7 +66,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 12.1': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-12-1': [ { state: 'Shutdown', isAvailable: true, @@ -103,7 +103,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPhone 6', + {simulator: 'iPhone 6'}, ), ).toEqual({ udid: 'BA0D93BD-07E6-4182-9B0A-F60A2474139C', @@ -118,7 +118,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -152,7 +152,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPhone 6', + {simulator: 'iPhone 6'}, ), ).toEqual(null); }); @@ -162,7 +162,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -211,7 +211,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -243,7 +243,7 @@ describe('findMatchingSimulator', () => { udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508', }, ], - 'iOS 10.0': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-10-0': [ { state: 'Shutdown', availability: '(available)', @@ -286,7 +286,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -335,7 +335,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -369,7 +369,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPhone 6', + {simulator: 'iPhone 6'}, ), ).toEqual({ udid: 'BA0D93BD-07E6-4182-9B0A-F60A2474139C', @@ -384,7 +384,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -416,7 +416,7 @@ describe('findMatchingSimulator', () => { udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508', }, ], - 'iOS 10.0': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-10-0': [ { state: 'Shutdown', availability: '(available)', @@ -459,7 +459,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -491,7 +491,7 @@ describe('findMatchingSimulator', () => { udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508', }, ], - 'iOS 10.0': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-10-0': [ { state: 'Shutdown', availability: '(available)', @@ -519,7 +519,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPhone 6s', + {simulator: 'iPhone 6s'}, ), ).toEqual({ udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508', @@ -534,7 +534,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -566,7 +566,7 @@ describe('findMatchingSimulator', () => { udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508', }, ], - 'iOS 10.0': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-10-0': [ { state: 'Shutdown', availability: '(available)', @@ -594,7 +594,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPhone 6s (10.0)', + {simulator: 'iPhone 6s (10.0)'}, ), ).toEqual({ udid: 'CBBB8FB8-77AB-49A9-8297-4CCFE3189C22', @@ -609,7 +609,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 9.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-9-2': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -641,7 +641,7 @@ describe('findMatchingSimulator', () => { udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508', }, ], - 'iOS 10.0': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-10-0': [ { state: 'Shutdown', availability: '(available)', @@ -663,7 +663,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPhone 6s (10.0)', + {simulator: 'iPhone 6s (10.0)'}, ), ).toEqual(null); }); @@ -673,7 +673,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 12.0': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-12-0': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -711,7 +711,7 @@ describe('findMatchingSimulator', () => { udid: 'B2141C1E-86B7-4A10-82A7-4956799526DF', }, ], - 'iOS 12.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-12-2': [ { state: 'Shutdown', availability: '(available)', @@ -733,7 +733,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPad Pro (9.7-inch)', + {simulator: 'iPad Pro (9.7-inch)'}, ), ).toEqual({ udid: 'B2141C1E-86B7-4A10-82A7-4956799526DF', @@ -748,7 +748,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 12.0': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-12-0': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -780,7 +780,7 @@ describe('findMatchingSimulator', () => { udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508', }, ], - 'iOS 12.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-12-2': [ { state: 'Shutdown', availability: '(available)', @@ -808,7 +808,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPad Pro (9.7-inch) (12.2)', + {simulator: 'iPad Pro (9.7-inch) (12.2)'}, ), ).toEqual({ udid: 'B2141C1E-86B7-4A10-82A7-4956799526DF', @@ -823,7 +823,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'iOS 12.0': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-12-0': [ { state: 'Shutdown', availability: '(unavailable, runtime profile not found)', @@ -855,7 +855,7 @@ describe('findMatchingSimulator', () => { udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508', }, ], - 'iOS 12.2': [ + 'com.apple.CoreSimulator.SimRuntime.iOS-12-2': [ { state: 'Shutdown', availability: '(available)', @@ -883,7 +883,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'iPad Pro (9.7-inch) (12.0)', + {simulator: 'iPad Pro (9.7-inch) (12.0)'}, ), ).toEqual(null); }); @@ -893,7 +893,7 @@ describe('findMatchingSimulator', () => { findMatchingSimulator( { devices: { - 'tvOS 11.2': [ + 'com.apple.CoreSimulator.SimRuntime.tvOS-11-2': [ { state: 'Booted', availability: '(available)', @@ -915,7 +915,7 @@ describe('findMatchingSimulator', () => { ], }, }, - 'Apple TV', + {simulator: 'Apple TV'}, ), ).toEqual({ udid: '816C30EA-38EA-41AC-BFDA-96FB632D522E', @@ -924,4 +924,56 @@ describe('findMatchingSimulator', () => { version: 'tvOS 11.2', }); }); + + it('should return a simulator by UDID', () => { + expect( + findMatchingSimulator( + { + devices: { + 'com.apple.CoreSimulator.SimRuntime.iOS-12-1': [ + { + state: 'Shutdown', + isAvailable: true, + name: 'iPhone 6s', + udid: 'D0F29BE7-CC3C-4976-888D-C739B4F50508', + }, + { + state: 'Shutdown', + isAvailable: true, + name: 'iPhone 6', + udid: 'BA0D93BD-07E6-4182-9B0A-F60A2474139C', + }, + { + state: 'Shutdown', + isAvailable: true, + name: 'iPhone XS Max', + udid: 'B9B5E161-416B-43C4-A78F-729CB96CC8C6', + availabilityError: '', + }, + { + state: 'Shutdown', + isAvailable: true, + name: 'iPad Air', + udid: '1CCBBF8B-5773-4EA6-BD6F-C308C87A1ADB', + availabilityError: '', + }, + { + state: 'Shutdown', + isAvailable: true, + name: 'iPad (5th generation)', + udid: '9564ABEE-9EC2-4B4A-B443-D3710929A45A', + availabilityError: '', + }, + ], + }, + }, + {udid: 'BA0D93BD-07E6-4182-9B0A-F60A2474139C'}, + ), + ).toEqual({ + udid: 'BA0D93BD-07E6-4182-9B0A-F60A2474139C', + name: 'iPhone 6', + booted: false, + version: 'iOS 12.1', + }); + }); }); diff --git a/packages/platform-ios/src/commands/runIOS/__tests__/parseIOSDevicesList.test.ts b/packages/platform-ios/src/commands/runIOS/__tests__/parseIOSDevicesList.test.ts index 8131252db..7da7e06c4 100644 --- a/packages/platform-ios/src/commands/runIOS/__tests__/parseIOSDevicesList.test.ts +++ b/packages/platform-ios/src/commands/runIOS/__tests__/parseIOSDevicesList.test.ts @@ -16,7 +16,8 @@ describe('parseIOSDevicesList', () => { [ 'Known Devices:', 'Maxs MacBook Pro [11111111-1111-1111-1111-111111111111]', - "Max's iPhone (9.2) [11111111111111111111aaaaaaaaaaaaaaaaaaaa]", + "Max's iPhone (9.2) [00008030-000D19512210802E]", + 'other-iphone (9.2) [72a186ccfd93472a186ccfd934]', 'iPad 2 (9.3) [07538CE4-675B-4EDA-90F2-3DD3CD93309D] (Simulator)', 'iPad Air (9.3) [0745F6D1-6DC5-4427-B9A6-6FBA327ED65A] (Simulator)', 'iPhone 6s (9.3) [3DBE4ECF-9A86-469E-921B-EE0F9C9AB8F4] (Simulator)', @@ -32,11 +33,37 @@ describe('parseIOSDevicesList', () => { { name: 'Maxs MacBook Pro', udid: '11111111-1111-1111-1111-111111111111', + type: 'catalyst', }, { name: "Max's iPhone", - udid: '11111111111111111111aaaaaaaaaaaaaaaaaaaa', + udid: '00008030-000D19512210802E', version: '9.2', + type: 'device', + }, + { + name: 'other-iphone', + type: 'device', + udid: '72a186ccfd93472a186ccfd934', + version: '9.2', + }, + { + name: 'iPad 2', + udid: '07538CE4-675B-4EDA-90F2-3DD3CD93309D', + version: '9.3', + type: 'simulator', + }, + { + name: 'iPad Air', + udid: '0745F6D1-6DC5-4427-B9A6-6FBA327ED65A', + version: '9.3', + type: 'simulator', + }, + { + name: 'iPhone 6s', + udid: '3DBE4ECF-9A86-469E-921B-EE0F9C9AB8F4', + version: '9.3', + type: 'simulator', }, ]); }); diff --git a/packages/platform-ios/src/commands/runIOS/findMatchingSimulator.ts b/packages/platform-ios/src/commands/runIOS/findMatchingSimulator.ts index af9d9de11..7b4e03632 100644 --- a/packages/platform-ios/src/commands/runIOS/findMatchingSimulator.ts +++ b/packages/platform-ios/src/commands/runIOS/findMatchingSimulator.ts @@ -22,24 +22,25 @@ import {Device} from '../../types'; */ function findMatchingSimulator( simulators: {devices: {[index: string]: Array}}, - simulatorString: string, + findOptions?: null | {simulator?: string; udid?: string}, ) { if (!simulators.devices) { return null; } const devices = simulators.devices; let simulatorVersion; - let simulatorName; + let simulatorName = null; - const parsedSimulatorName = simulatorString - ? simulatorString.match(/(.*)? (?:\((\d+\.\d+)?\))$/) - : []; - - if (parsedSimulatorName && parsedSimulatorName[2] !== undefined) { - simulatorVersion = parsedSimulatorName[2]; - simulatorName = parsedSimulatorName[1]; - } else { - simulatorName = simulatorString; + if (findOptions && findOptions.simulator) { + const parsedSimulatorName = findOptions.simulator.match( + /(.*)? (?:\((\d+\.\d+)?\))$/, + ); + if (parsedSimulatorName && parsedSimulatorName[2] !== undefined) { + simulatorVersion = parsedSimulatorName[2]; + simulatorName = parsedSimulatorName[1]; + } else { + simulatorName = findOptions.simulator; + } } let match; @@ -74,30 +75,27 @@ function findMatchingSimulator( continue; } const booted = simulator.state === 'Booted'; - if (booted && simulatorName === null) { - return { - udid: simulator.udid, - name: simulator.name, - booted, - version, - }; - } - if (simulator.name === simulatorName && !match) { - match = { - udid: simulator.udid, - name: simulator.name, - booted, - version, - }; - } - // Keeps track of the first available simulator for use if we can't find one above. - if (simulatorName === null && !match) { - match = { - udid: simulator.udid, - name: simulator.name, - booted, - version, - }; + const simulatorDescriptor = { + udid: simulator.udid, + name: simulator.name, + booted, + version, + }; + if (findOptions && findOptions.udid) { + if (simulator.udid === findOptions.udid) { + return simulatorDescriptor; + } + } else { + if (booted && simulatorName === null) { + return simulatorDescriptor; + } + if (simulator.name === simulatorName && !match) { + match = simulatorDescriptor; + } + // Keeps track of the first available simulator for use if we can't find one above. + if (simulatorName === null && !match) { + match = simulatorDescriptor; + } } } } diff --git a/packages/platform-ios/src/commands/runIOS/index.ts b/packages/platform-ios/src/commands/runIOS/index.ts index 10e05adce..3ce749f95 100644 --- a/packages/platform-ios/src/commands/runIOS/index.ts +++ b/packages/platform-ios/src/commands/runIOS/index.ts @@ -28,7 +28,7 @@ import { import {Device} from '../../types'; type FlagsT = { - simulator: string; + simulator?: string; configuration: string; scheme?: string; projectPath: string; @@ -71,43 +71,43 @@ function runIOS(_: Array, ctx: Config, args: FlagsT) { } "${chalk.bold(xcodeProject.name)}"`, ); - const {device, udid} = args; - - if (!device && !udid) { + // No need to load all available devices + if (!args.device && !args.udid) { return runOnSimulator(xcodeProject, scheme, args); } + if (args.device && args.udid) { + return logger.error( + 'The `device` and `udid` options are mutually exclusive.', + ); + } + const devices = parseIOSDevicesList( child_process.execFileSync('xcrun', ['instruments', '-s'], { encoding: 'utf8', }), ); - // first device is always the host Mac - if (devices.length <= 1) { - return logger.error('No iOS devices connected.'); - } - - const selectedDevice = matchingDevice(devices, device, udid); - - if (selectedDevice) { - return runOnDevice(selectedDevice, scheme, xcodeProject, args); - } - - if (device) { - return logger.error( - `Could not find a device named: "${chalk.bold( - String(device), - )}". ${printFoundDevices(devices)}`, - ); - } - - if (udid) { - return logger.error( - `Could not find a device with udid: "${chalk.bold( - udid, - )}". ${printFoundDevices(devices)}`, - ); + if (args.udid) { + const device = devices.find(d => d.udid === args.udid); + if (!device) { + return logger.error( + `Could not find a device with udid: "${chalk.bold( + args.udid, + )}". ${printFoundDevices(devices)}`, + ); + } + if (device.type === 'simulator') { + return runOnSimulator(xcodeProject, scheme, args); + } else { + return runOnDevice(device, scheme, xcodeProject, args); + } + } else { + const physicalDevices = devices.filter(d => d.type !== 'simulator'); + const device = matchingDevice(physicalDevices, args.device); + if (device) { + return runOnDevice(device, scheme, xcodeProject, args); + } } } @@ -137,14 +137,19 @@ async function runOnSimulator( * - iPhone X * - iPhone 8 */ - const fallbackSimulators = ['iPhone X', 'iPhone 8']; const selectedSimulator = fallbackSimulators.reduce((simulator, fallback) => { - return simulator || findMatchingSimulator(simulators, fallback); - }, findMatchingSimulator(simulators, args.simulator)); + return ( + simulator || findMatchingSimulator(simulators, {simulator: fallback}) + ); + }, findMatchingSimulator(simulators, args)); if (!selectedSimulator) { - throw new CLIError(`Could not find "${args.simulator}" simulator`); + throw new CLIError( + `No simulator available with ${ + args.simulator ? `name "${args.simulator}"` : `udid "${args.udid}"` + }`, + ); } /** @@ -444,34 +449,39 @@ function xcprettyAvailable() { function matchingDevice( devices: Array, deviceName: string | true | undefined, - udid: string | undefined, ) { - if (udid) { - return matchingDeviceByUdid(devices, udid); - } - if (deviceName === true && devices.length === 1) { - logger.info( - `Using first available device named "${chalk.bold( - devices[0].name, - )}" due to lack of name supplied.`, - ); - return devices[0]; + if (deviceName === true) { + const firstIOSDevice = devices.find(d => d.type === 'device')!; + if (firstIOSDevice) { + logger.info( + `Using first available device named "${chalk.bold( + firstIOSDevice.name, + )}" due to lack of name supplied.`, + ); + return firstIOSDevice; + } else { + logger.error('No iOS devices connected.'); + return undefined; + } } - return devices.find( + const deviceByName = devices.find( device => device.name === deviceName || formattedDeviceName(device) === deviceName, ); -} - -function matchingDeviceByUdid( - devices: Array, - udid: string | undefined, -) { - return devices.find(device => device.udid === udid); + if (!deviceByName) { + logger.error( + `Could not find a device named: "${chalk.bold( + String(deviceName), + )}". ${printFoundDevices(devices)}`, + ); + } + return deviceByName; } function formattedDeviceName(simulator: Device) { - return `${simulator.name} (${simulator.version})`; + return simulator.version + ? `${simulator.name} (${simulator.version})` + : simulator.name; } function printFoundDevices(devices: Array) { diff --git a/packages/platform-ios/src/commands/runIOS/parseIOSDevicesList.ts b/packages/platform-ios/src/commands/runIOS/parseIOSDevicesList.ts index b3ab45936..fad14cd37 100644 --- a/packages/platform-ios/src/commands/runIOS/parseIOSDevicesList.ts +++ b/packages/platform-ios/src/commands/runIOS/parseIOSDevicesList.ts @@ -8,36 +8,36 @@ import {Device} from '../../types'; /** - * Parses the output of `xcrun simctl list devices` command + * Parses the output of the `xcrun instruments -s` command and returns metadata + * about available iOS simulators and physical devices, as well as host Mac for + * Catalyst purposes. + * * Expected text looks roughly like this: + * + * ``` * Known Devices: - * this-mac-device [ID] - * Some Apple Simulator (Version) [ID] + * this-mac-device [UDID] + * A Physical Device (OS Version) [UDID] + * A Simulator Device (OS Version) [UDID] (Simulator) + * ``` */ function parseIOSDevicesList(text: string): Array { const devices: Array = []; - text.split('\n').forEach((line, index) => { - const device = line.match(/(.*?) \((.*?)\) \[(.*?)\]/); - const noSimulator = line.match(/(.*?) \((.*?)\) \[(.*?)\] \((.*?)\)/); - - if (index === 1) { - const myMac = line.match(/(.*?) \[(.*?)\]/); - if (myMac) { - const name = myMac[1]; - const udid = myMac[2]; - devices.push({ - udid, - name, - }); + text.split('\n').forEach(line => { + const device = line.match( + /(.*?) (\(([0-9\.]+)\) )?\[([0-9A-F-]+)\]( \(Simulator\))?/i, + ); + if (device) { + const [, name, , version, udid, isSimulator] = device; + const metadata: Device = {name, udid}; + if (version) { + metadata.version = version; + metadata.type = isSimulator ? 'simulator' : 'device'; + } else { + metadata.type = 'catalyst'; } - } - - if (device != null && noSimulator == null) { - const name = device[1]; - const version = device[2]; - const udid = device[3]; - devices.push({udid, name, version}); + devices.push(metadata); } }); diff --git a/packages/platform-ios/src/types.ts b/packages/platform-ios/src/types.ts index 21ddbf763..65f28af20 100644 --- a/packages/platform-ios/src/types.ts +++ b/packages/platform-ios/src/types.ts @@ -6,4 +6,6 @@ export interface Device { udid: string; version?: string; availabilityError?: string; + type?: 'simulator' | 'device' | 'catalyst'; + booted?: boolean; }