import { spawn , execSync } from "child_process" ; import { sleep , waitUntil } from "./utilities" ; import * as kill from "tree-kill" ; import * as cp from "child_process" ; import { SmokeTestLogger } from "./smokeTestLogger" ; import { ExpoClientData } from "./androidEmulatorManager" ; const XDL = require ( "xdl" ) ; interface IiOSSimulator { system : string ; name : string ; id : string ; state : DeviceState ; } enum DeviceState { Booted = "Booted" , Shutdown = "Shutdown" , Unknown = "Unknown" } interface RunResult { Successful : boolean ; FailedState ? : DeviceState ; } export default class IosSimulatorManager { private static readonly SIMULATOR_START_TIMEOUT = 300000 ; private static readonly SIMULATOR_TERMINATE_TIMEOUT = 30000 ; private static readonly APP_INSTALL_AND_BUILD_TIMEOUT = 600000 ; private static readonly APP_INIT_TIMEOUT = 40000 ; private simulator : IiOSSimulator ; constructor ( name = process . env . IOS_SIMULATOR , iosVersion = process . env . IOS_VERSION ) { if ( ! name ) { throw new Error ( "Passed iOS simulator name and process.env.IOS_SIMULATOR is not defined!" ) ; } if ( process . platform === "darwin" ) { if ( iosVersion ) { iosVersion = `iOS ${ iosVersion }` ; } this . updateSimulatorState ( name , iosVersion ) ; } } public getSimulator ( ) { return this . simulator ; } public static getIOSBuildPath ( iosProjectRoot , projectWorkspaceConfigName , configuration , scheme , sdkType ) { const buildSettings = cp . execFileSync ( "xcodebuild" , [ "-workspace" , projectWorkspaceConfigName , "-scheme" , scheme , "-sdk" , sdkType , "-configuration" , configuration , "-showBuildSettings" , ] , { encoding : "utf8" , cwd : iosProjectRoot , } ) ; const targetBuildDir = this . getTargetBuildDir ( < string > buildSettings ) ; if ( ! targetBuildDir ) { throw new Error ( "Failed to get the target build directory." ) ; } return targetBuildDir ; } private updateSimulatorState ( name , iosVersion ? ) { const simulators = IosSimulatorManager . collectSimulators ( ) ; const simulator = this . findSimulator ( simulators , name , iosVersion ) ; if ( ! simulator ) { throw new Error ( `Could not find simulator with name: '${ name }'${ iosVersion ? ` and iOS version: '${ iosVersion }'` : "" } in system. Exiting...` ) ; } this . simulator = simulator ; } public async runIosSimulator ( ) { await this . shutdownSimulator ( ) ; await this . eraseSimulator ( ) ; SmokeTestLogger . info ( `*** Executing iOS simulator with 'xcrun simctl boot "${ this . simulator . name }"' command...` ) ; await this . bootSimulator ( ) ; await sleep ( 15 * 1000 ) ; } public async bootSimulator ( ) { const cmd = "boot" ; const result = await IosSimulatorManager . runSimCtlCommand ( [ cmd , this . simulator . name ] ) ; if ( ! result . Successful ) { if ( result . FailedState === DeviceState . Booted ) { } else { throw IosSimulatorManager . getRunError ( cmd , result . FailedState ) ; } } this . updateSimulatorState ( this . simulator . name , this . simulator . system ) ; } public async shutdownSimulator ( ) { await IosSimulatorManager . shutdownSimulator ( this . simulator . name ) ; this . updateSimulatorState ( this . simulator . name , this . simulator . system ) ; } public async eraseSimulator ( ) { const cmd = "erase" ; const result = await IosSimulatorManager . runSimCtlCommand ( [ cmd , this . simulator . name ] ) ; if ( ! result . Successful ) { throw IosSimulatorManager . getRunError ( cmd , result . FailedState ) ; } this . updateSimulatorState ( this . simulator . name , this . simulator . system ) ; } public async waitUntilIosSimulatorStarting ( ) { const condition = ( ) => { this . updateSimulatorState ( this . simulator . name , this . simulator . system ) ; if ( this . simulator . state === DeviceState . Booted ) { return true ; } else return false ; } ; const result = await waitUntil ( condition , IosSimulatorManager . SIMULATOR_START_TIMEOUT ) ; if ( result ) { SmokeTestLogger . success ( `*** iOS simulator ${ this . simulator . name } has been started.` ) ; } else { SmokeTestLogger . error ( `*** Could not start iOS simulator ${ this . simulator . name } after ${ IosSimulatorManager . SIMULATOR_START_TIMEOUT }.` ) ; } return result ; } public async waitUntilIosSimulatorTerminating ( ) { const condition = ( ) => { this . updateSimulatorState ( this . simulator . name , this . simulator . system ) ; if ( this . simulator . state === DeviceState . Shutdown ) { return true ; } else return false ; } ; const result = await waitUntil ( condition , IosSimulatorManager . SIMULATOR_TERMINATE_TIMEOUT ) ; if ( result ) { SmokeTestLogger . success ( `*** iOS simulator ${ this . simulator . name } has been terminated.` ) ; } else { SmokeTestLogger . error ( `*** Could not terminate iOS simulator ${ this . simulator . name } after ${ IosSimulatorManager . SIMULATOR_TERMINATE_TIMEOUT }.` ) ; } return result ; } public async waitUntilIosAppIsInstalled ( appBundleId ) { let launched = false ; const predicate = `eventMessage contains "Launch successful for '${ appBundleId }'"` ; const args = [ "simctl" , "spawn" , this . simulator . name , "log" , "stream" , "--predicate" , predicate , ] ; const proc = spawn ( "xcrun" , args , { stdio : "pipe" } ) ; proc . stdout . on ( "data" , ( data ) => { data = data . toString ( ) ; SmokeTestLogger . info ( data ) ; if ( data . startsWith ( "Filtering the log data" ) ) { return ; } const regexp = new RegExp ( `Launch successful for '${ appBundleId }'` ) ; if ( regexp . test ( data ) ) { launched = true ; } } ) ; proc . stderr . on ( "error" , ( data ) => { SmokeTestLogger . error ( data . toString ( ) ) ; } ) ; proc . on ( "error" , err => { SmokeTestLogger . error ( err ) ; kill ( proc . pid ) ; } ) ; let awaitRetries = IosSimulatorManager . APP_INSTALL_AND_BUILD_TIMEOUT / 1000 ; let retry = 1 ; await new Promise < void > ( ( resolve , reject ) => { const check = setInterval ( async ( ) => { if ( retry % 5 === 0 ) { SmokeTestLogger . info ( `*** Check if app with bundleId ${ appBundleId } is installed, ${ retry } attempt` ) ; } if ( launched ) { clearInterval ( check ) ; const initTimeout = IosSimulatorManager . APP_INIT_TIMEOUT || 10000 ; SmokeTestLogger . success ( `*** Installed ${ appBundleId } app found, await ${ initTimeout }ms for initializing...` ) ; await sleep ( initTimeout ) ; resolve ( ) ; } else { retry ++ ; if ( retry >= awaitRetries ) { clearInterval ( check ) ; kill ( proc . pid , ( ) => { reject ( `${ appBundleId } not found after ${ IosSimulatorManager . APP_INSTALL_AND_BUILD_TIMEOUT }ms` ) ; } ) ; } } } , 1000 ) ; } ) ; } public async getExpoAndroidClientForSDK ( expoSdkMajorVersion ) { const sdkVersion = ( await XDL . Versions . sdkVersionsAsync ( ) ) [ `${ expoSdkMajorVersion }.0.0` ] ; return { url : sdkVersion . iosClientUrl , version : sdkVersion . iosClientVersion , } ; } public async installExpoAppOnIos ( ) { this . updateSimulatorState ( this . simulator . name , this . simulator . system ) ; if ( this . simulator . state === DeviceState . Booted ) { const expoClientData = await this . getExpoAndroidClientForSDK ( process . env . EXPO_SDK_MAJOR_VERSION || "" ) ; SmokeTestLogger . projectPatchingLog ( `*** Installing Expo app v${ expoClientData . version } on iOS simulator using Expo XDL function` ) ; await XDL . Simulator . installExpoOnSimulatorAsync ( { simulator : { name : this . simulator . name || "" , udid : this . simulator . id || "" , } , url : expoClientData . url , version : expoClientData . version , } ) ; } else { throw new Error ( "*** Could not install Expo app on iOS simulator because it is not booted" ) ; } } private findSimulator ( simulators , name , system ? ) { const foundSimulator = simulators . find ( value => value . name === name && ( ! system || value . system === system ) ) ; if ( ! foundSimulator ) { return null ; } return foundSimulator ; } private static getBootedDevices ( ) { const simulators = this . collectSimulators ( ) ; const bootedSimulators = simulators . filter ( sim => sim . state === DeviceState . Booted ) ; return bootedSimulators ; } private static collectSimulators ( ) { const simulators = [ ] ; const res = execSync ( "xcrun simctl list --json devices available" ) . toString ( ) ; const simulatorsJson = JSON . parse ( res ) ; Object . keys ( simulatorsJson . devices ) . forEach ( rawSystem => { const temp = rawSystem . split ( "." ) . slice ( - 1 ) [ 0 ] . split ( "-" ) ; const system = `${ temp [ 0 ] } ${ temp . slice ( 1 ) . join ( "." ) }` ; simulatorsJson . devices [ rawSystem ] . forEach ( ( device ) => { simulators . push ( { name : device . name , id : device . udid , system , state : device . state , } ) ; } ) ; } ) ; return simulators ; } private static async runSimCtlCommand ( args ) { return new Promise < RunResult > ( ( resolve , reject ) => { let stderr = "" ; const command = "xcrun" ; const commandArgs = [ "simctl" ] . concat ( args ) ; const cp = spawn ( command , commandArgs ) ; cp . on ( "close" , ( ) => { const lines = stderr . split ( "\n" ) . filter ( value => value ) ; if ( lines . length === 0 ) { resolve ( { Successful : true , } ) ; return ; } const lastLine = lines [ lines . length - 1 ] ; if ( lastLine . startsWith ( `Unable to ${ args [ 0 ] }` ) ) { const match = lastLine . match ( /in current state: (.+)/ ) ; if ( ! match || match . length !== 2 ) { reject ( new Error ( `Error parsing ${ [ command ] . concat ( commandArgs ) . join ( " " ) } output` ) ) ; } const state = DeviceState [ match ! [ 1 ] ] ; if ( ! state ) { SmokeTestLogger . warn ( `Unknown state: ${ match ! [ 1 ] }` ) ; resolve ( { Successful : false , FailedState : DeviceState . Unknown , } ) ; } else { resolve ( { Successful : false , FailedState : state , } ) ; } resolve ( { Successful : true , } ) ; } else { reject ( new Error ( `Error occurred while running ${ [ command ] . concat ( commandArgs ) . join ( " " ) }` ) ) ; } } ) ; cp . stderr . on ( "data" , ( chunk ) => { stderr += chunk ; process . stderr . write ( chunk ) ; } ) ; cp . stdout . on ( "data" , ( chunk ) => { process . stdout . write ( chunk ) ; } ) ; } ) ; } private static getRunError ( command , failedState ? ) { return new Error ( `Couldn't run ${ command } simulator` + failedState ? `, because it in ${ failedState } state` : "" ) ; } public static async shutdownAllSimulators ( ) { const promises = [ ] ; IosSimulatorManager . getBootedDevices ( ) . forEach ( device => { promises . push ( IosSimulatorManager . shutdownSimulator ( device . name ) ) ; } ) ; await Promise . all ( promises ) ; return this . waitUntilAllIosSimulatorsTerminating ( ) ; } private static async shutdownSimulator ( simulatorName ) { const cmd = "shutdown" ; const result = await IosSimulatorManager . runSimCtlCommand ( [ cmd , simulatorName ] ) ; if ( ! result . Successful ) { if ( result . FailedState === DeviceState . Shutdown ) { } else { throw IosSimulatorManager . getRunError ( cmd , result . FailedState ) ; } } SmokeTestLogger . success ( `*** iOS simulators with name "${ simulatorName }" has been terminated.` ) ; } public static async waitUntilAllIosSimulatorsTerminating ( ) { const condition = ( ) => { if ( IosSimulatorManager . getBootedDevices ( ) . length === 0 ) { return true ; } else return false ; } ; const result = await waitUntil ( condition , IosSimulatorManager . SIMULATOR_TERMINATE_TIMEOUT ) ; if ( result ) { SmokeTestLogger . success ( `*** All iOS simulators has been terminated.` ) ; } else { SmokeTestLogger . error ( `*** Could not terminate all iOS simulators after ${ IosSimulatorManager . SIMULATOR_TERMINATE_TIMEOUT }.` ) ; } return result ; } param { string } buildSettings returns { string | null } private static getTargetBuildDir ( buildSettings ) { const targetBuildMatch = /TARGET_BUILD_DIR = (.+)$/m . exec ( buildSettings ) ; return targetBuildMatch && targetBuildMatch [ 1 ] ? targetBuildMatch [ 1 ] . trim ( ) : null ; } }