From 1eb122f7952af527c7bab091ae62b45ce00c96a2 Mon Sep 17 00:00:00 2001 From: rosen-vladimirov Date: Thu, 6 Jul 2017 12:42:30 +0300 Subject: [PATCH] Debug on emulator by default when multiple devices/emulators attached When there are multiple devices/emulators, calling `tns debug android` will fail that multiple devices are attached. This break VS Code as previous code of CLI has been using emulators by default. Fix this by using the most recent version (highest API Level) of running emulators, i.e. in case you have three Android emulators with version 5.1, 6.0 and 7.0, the one with 7.0 will be used for debugging. In case the terminal is interactive, CLI will prompt the user for selecting a device on which to start debug operation. In case the terminal is not interactive and there's no emulator running, the device with highest API level will be used. --- Gruntfile.js | 1 - lib/commands/debug.ts | 89 ++++- lib/common | 2 +- lib/constants.ts | 5 + lib/definitions/debug.d.ts | 2 +- lib/services/debug-data-service.ts | 2 +- lib/services/debug-service-base.ts | 9 +- .../livesync/debug-livesync-service.ts | 2 +- test/debug.ts | 346 ++++++++++++++++-- test/stubs.ts | 2 +- 10 files changed, 398 insertions(+), 62 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index a3b13b2453..819193adb7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -133,7 +133,6 @@ module.exports = function (grunt) { grunt.loadNpmTasks("grunt-contrib-watch"); grunt.loadNpmTasks("grunt-shell"); grunt.loadNpmTasks("grunt-ts"); - grunt.loadNpmTasks("grunt-tslint"); grunt.registerTask("set_package_version", function (version) { var buildVersion = version !== undefined ? version : buildNumber; diff --git a/lib/commands/debug.ts b/lib/commands/debug.ts index cddbfb8ada..7c090d2eb3 100644 --- a/lib/commands/debug.ts +++ b/lib/commands/debug.ts @@ -1,4 +1,8 @@ -export abstract class DebugPlatformCommand implements ICommand { +import { CONNECTED_STATUS } from "../common/constants"; +import { isInteractive } from "../common/helpers"; +import { DebugCommandErrors } from "../constants"; + +export abstract class DebugPlatformCommand implements ICommand { public allowedParameters: ICommandParameter[] = []; public platform: string; @@ -12,7 +16,8 @@ protected $logger: ILogger, protected $errors: IErrors, private $debugLiveSyncService: IDebugLiveSyncService, - private $config: IConfiguration) { + private $config: IConfiguration, + private $prompter: IPrompter) { this.$projectData.initializeProjectData(); } @@ -29,11 +34,9 @@ this.$config.debugLivesync = true; - await this.$devicesService.detectCurrentlyAttachedDevices(); + const selectedDeviceForDebug = await this.getDeviceForDebug(); - // Now let's take data for each device: - const devices = this.$devicesService.getDeviceInstances(); - const deviceDescriptors: ILiveSyncDeviceInfo[] = devices.filter(d => !this.platform || d.deviceInfo.platform === this.platform) + const deviceDescriptors: ILiveSyncDeviceInfo[] = [selectedDeviceForDebug] .map(d => { const info: ILiveSyncDeviceInfo = { identifier: d.deviceInfo.identifier, @@ -70,6 +73,62 @@ await this.$debugLiveSyncService.liveSync(deviceDescriptors, liveSyncInfo); } + public async getDeviceForDebug(): Promise { + if (this.$options.forDevice && this.$options.emulator) { + this.$errors.fail(DebugCommandErrors.UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR); + } + + await this.$devicesService.detectCurrentlyAttachedDevices(); + + if (this.$options.device) { + const device = await this.$devicesService.getDevice(this.$options.device); + return device; + } + + // Now let's take data for each device: + const availableDevicesAndEmulators = this.$devicesService.getDeviceInstances() + .filter(d => d.deviceInfo.status === CONNECTED_STATUS && (!this.platform || d.deviceInfo.platform.toLowerCase() === this.platform.toLowerCase())); + + const selectedDevices = availableDevicesAndEmulators.filter(d => this.$options.emulator ? d.isEmulator : (this.$options.forDevice ? !d.isEmulator : true)); + + if (selectedDevices.length > 1) { + if (isInteractive()) { + const choices = selectedDevices.map(e => `${e.deviceInfo.identifier} - ${e.deviceInfo.displayName}`); + + const selectedDeviceString = await this.$prompter.promptForChoice("Select device for debugging", choices); + + const selectedDevice = _.find(selectedDevices, d => `${d.deviceInfo.identifier} - ${d.deviceInfo.displayName}` === selectedDeviceString); + return selectedDevice; + } else { + const sortedInstances = _.sortBy(selectedDevices, e => e.deviceInfo.version); + const emulators = sortedInstances.filter(e => e.isEmulator); + const devices = sortedInstances.filter(d => !d.isEmulator); + let selectedInstance: Mobile.IDevice; + + if (this.$options.emulator || this.$options.forDevice) { + // When --emulator or --forDevice is passed, the instances are already filtered + // So we are sure we have exactly the type we need and we can safely return the last one (highest OS version). + selectedInstance = _.last(sortedInstances); + } else { + if (emulators.length) { + selectedInstance = _.last(emulators); + } else { + selectedInstance = _.last(devices); + } + } + + this.$logger.warn(`Multiple devices/emulators found. Starting debugger on ${selectedInstance.deviceInfo.identifier}. ` + + "If you want to debug on specific device/emulator, you can specify it with --device option."); + + return selectedInstance; + } + } else if (selectedDevices.length === 1) { + return _.head(selectedDevices); + } + + this.$errors.failWithoutHelp(DebugCommandErrors.NO_DEVICES_EMULATORS_FOUND_FOR_OPTIONS); + } + public async canExecute(args: string[]): Promise { if (!this.$platformService.isPlatformSupportedForOS(this.platform, this.$projectData)) { this.$errors.fail(`Applications for platform ${this.platform} can not be built on this OS`); @@ -85,14 +144,6 @@ emulator: this.$options.emulator, skipDeviceDetectionInterval: true }); - // Start emulator if --emulator is selected or no devices found. - if (this.$options.emulator || this.$devicesService.deviceCount === 0) { - return true; - } - - if (this.$devicesService.deviceCount > 1) { - this.$errors.failWithoutHelp("Multiple devices found! To debug on specific device please select device with --device option."); - } return true; } @@ -111,9 +162,10 @@ export class DebugIOSCommand extends DebugPlatformCommand { $projectData: IProjectData, $platformsData: IPlatformsData, $iosDeviceOperations: IIOSDeviceOperations, - $debugLiveSyncService: IDebugLiveSyncService) { + $debugLiveSyncService: IDebugLiveSyncService, + $prompter: IPrompter) { super($iOSDebugService, $devicesService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger, - $errors, $debugLiveSyncService, $config); + $errors, $debugLiveSyncService, $config, $prompter); // Do not dispose ios-device-lib, so the process will remain alive and the debug application (NativeScript Inspector or Chrome DevTools) will be able to connect to the socket. // In case we dispose ios-device-lib, the socket will be closed and the code will fail when the debug application tries to read/send data to device socket. // That's why the `$ tns debug ios --justlaunch` command will not release the terminal. @@ -146,9 +198,10 @@ export class DebugAndroidCommand extends DebugPlatformCommand { $options: IOptions, $projectData: IProjectData, $platformsData: IPlatformsData, - $debugLiveSyncService: IDebugLiveSyncService) { + $debugLiveSyncService: IDebugLiveSyncService, + $prompter: IPrompter) { super($androidDebugService, $devicesService, $debugDataService, $platformService, $projectData, $options, $platformsData, $logger, - $errors, $debugLiveSyncService, $config); + $errors, $debugLiveSyncService, $config, $prompter); } public async canExecute(args: string[]): Promise { diff --git a/lib/common b/lib/common index 0fb7b5f9c3..c6898cf6d5 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit 0fb7b5f9c3df92a84345735b1416c9013e222efb +Subproject commit c6898cf6d5d309ae2169fad3a487e8672969a15d diff --git a/lib/constants.ts b/lib/constants.ts index 31c19aead4..d58e856ae4 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -87,3 +87,8 @@ export const CONNECTION_ERROR_EVENT_NAME = "connectionError"; export const VERSION_STRING = "version"; export const INSPECTOR_CACHE_DIRNAME = "ios-inspector"; export const POST_INSTALL_COMMAND_NAME = "post-install-cli"; + +export class DebugCommandErrors { + public static UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR = "The options --for-device and --emulator cannot be used simultaneously. Please use only one of them."; + public static NO_DEVICES_EMULATORS_FOUND_FOR_OPTIONS = "Unable to find device or emulator for specified options."; +} diff --git a/lib/definitions/debug.d.ts b/lib/definitions/debug.d.ts index 04008db7ae..5e9cfde535 100644 --- a/lib/definitions/debug.d.ts +++ b/lib/definitions/debug.d.ts @@ -83,7 +83,7 @@ interface IDebugDataService { * @param {IOptions} options The options based on which debugData will be created * @returns {IDebugData} Data describing the required information for starting debug process. */ - createDebugData(projectData: IProjectData, options: IOptions): IDebugData; + createDebugData(projectData: IProjectData, options: IDeviceIdentifier): IDebugData; } /** diff --git a/lib/services/debug-data-service.ts b/lib/services/debug-data-service.ts index 038e2b6091..3584f2165f 100644 --- a/lib/services/debug-data-service.ts +++ b/lib/services/debug-data-service.ts @@ -1,5 +1,5 @@ export class DebugDataService implements IDebugDataService { - public createDebugData(projectData: IProjectData, options: IOptions): IDebugData { + public createDebugData(projectData: IProjectData, options: IDeviceIdentifier): IDebugData { return { applicationIdentifier: projectData.projectId, projectDir: projectData.projectDir, diff --git a/lib/services/debug-service-base.ts b/lib/services/debug-service-base.ts index 8dec2fa5fe..d50327c442 100644 --- a/lib/services/debug-service-base.ts +++ b/lib/services/debug-service-base.ts @@ -14,8 +14,13 @@ export abstract class DebugServiceBase extends EventEmitter implements IPlatform protected getCanExecuteAction(deviceIdentifier: string): (device: Mobile.IDevice) => boolean { return (device: Mobile.IDevice): boolean => { if (deviceIdentifier) { - return device.deviceInfo.identifier === deviceIdentifier - || device.deviceInfo.identifier === this.$devicesService.getDeviceByDeviceOption().deviceInfo.identifier; + let isSearchedDevice = device.deviceInfo.identifier === deviceIdentifier; + if (!isSearchedDevice) { + const deviceByDeviceOption = this.$devicesService.getDeviceByDeviceOption(); + isSearchedDevice = deviceByDeviceOption && device.deviceInfo.identifier === deviceByDeviceOption.deviceInfo.identifier; + } + + return isSearchedDevice; } else { return true; } diff --git a/lib/services/livesync/debug-livesync-service.ts b/lib/services/livesync/debug-livesync-service.ts index 3d8354b2c8..568ba3a3ea 100644 --- a/lib/services/livesync/debug-livesync-service.ts +++ b/lib/services/livesync/debug-livesync-service.ts @@ -46,7 +46,7 @@ export class DebugLiveSyncService extends LiveSyncService implements IDebugLiveS teamId: this.$options.teamId }; - let debugData = this.$debugDataService.createDebugData(this.$projectData, this.$options); + let debugData = this.$debugDataService.createDebugData(this.$projectData, { device: liveSyncResultInfo.deviceAppData.device.deviceInfo.identifier }); const debugService = this.$debugService.getDebugService(liveSyncResultInfo.deviceAppData.device); await this.$platformService.trackProjectType(this.$projectData); diff --git a/test/debug.ts b/test/debug.ts index 4b1aae6a89..d204cb3576 100644 --- a/test/debug.ts +++ b/test/debug.ts @@ -9,6 +9,10 @@ import { FileSystem } from "../lib/common/file-system"; import { AndroidProjectService } from "../lib/services/android-project-service"; import { AndroidDebugBridge } from "../lib/common/mobile/android/android-debug-bridge"; import { AndroidDebugBridgeResultHandler } from "../lib/common/mobile/android/android-debug-bridge-result-handler"; +import { DebugCommandErrors } from "../lib/constants"; +import { CONNECTED_STATUS, UNREACHABLE_STATUS } from "../lib/common/constants"; +const helpers = require("../lib/common/helpers"); +const originalIsInteracive = helpers.isInteractive; function createTestInjector(): IInjector { let testInjector: IInjector = new yok.Yok(); @@ -19,7 +23,6 @@ function createTestInjector(): IInjector { testInjector.register("logger", stubs.LoggerStub); testInjector.register("options", Options); testInjector.register("devicePlatformsConstants", DevicePlatformsConstants); - testInjector.register('devicesService', {}); testInjector.register('childProcess', stubs.ChildProcessStub); testInjector.register('androidDebugService', stubs.DebugServiceStub); testInjector.register('fs', FileSystem); @@ -63,48 +66,319 @@ function createTestInjector(): IInjector { } }); + testInjector.register("prompter", {}); + testInjector.registerCommand("debug|android", DebugAndroidCommand); + return testInjector; } -describe("Debugger tests", () => { - let testInjector: IInjector; +describe("debug command tests", () => { + describe("getDeviceForDebug", () => { + it("throws error when both --for-device and --emulator are passed", async () => { + const testInjector = createTestInjector(); + const options = testInjector.resolve("options"); + options.forDevice = options.emulator = true; + const debugCommand = testInjector.resolveCommand("debug|android"); + await assert.isRejected(debugCommand.getDeviceForDebug(), DebugCommandErrors.UNABLE_TO_USE_FOR_DEVICE_AND_EMULATOR); + }); - beforeEach(() => { - testInjector = createTestInjector(); - }); + it("returns selected device, when --device is passed", async () => { + const testInjector = createTestInjector(); + const devicesService = testInjector.resolve("devicesService"); + const deviceInstance = {}; + const specifiedDeviceOption = "device1"; + devicesService.getDevice = async (deviceOption: string): Promise => { + if (deviceOption === specifiedDeviceOption) { + return deviceInstance; + } + }; - it("Ensures that debugLivesync flag is true when executing debug --watch command", async () => { - let debugCommand: ICommand = testInjector.resolve("debug|android"); - let options: IOptions = testInjector.resolve("options"); - options.watch = true; - await debugCommand.execute(["android", "--watch"]); - let config: IConfiguration = testInjector.resolve("config"); - assert.isTrue(config.debugLivesync); - }); + const options = testInjector.resolve("options"); + options.device = specifiedDeviceOption; + const debugCommand = testInjector.resolveCommand("debug|android"); + const selectedDeviceInstance = await debugCommand.getDeviceForDebug(); + assert.deepEqual(selectedDeviceInstance, deviceInstance); + }); + + const assertErrorIsThrown = async (getDeviceInstancesResult: Mobile.IDevice[], passedOptions?: { forDevice: boolean, emulator: boolean }) => { + const testInjector = createTestInjector(); + if (passedOptions) { + const options = testInjector.resolve("options"); + options.forDevice = passedOptions.forDevice; + options.emulator = passedOptions.emulator; + } + + const devicesService = testInjector.resolve("devicesService"); + devicesService.getDeviceInstances = (): Mobile.IDevice[] => getDeviceInstancesResult; + + const debugCommand = testInjector.resolveCommand("debug|android"); + await assert.isRejected(debugCommand.getDeviceForDebug(), DebugCommandErrors.NO_DEVICES_EMULATORS_FOUND_FOR_OPTIONS); + }; + + it("throws error when there are no devices/emulators available", () => { + return assertErrorIsThrown([]); + }); + + it("throws error when there are no devices/emulators available for selected platform", () => { + return assertErrorIsThrown([ + { + deviceInfo: { + platform: "ios", + status: CONNECTED_STATUS + } + } + ]); + }); + + it("throws error when there are only not-trusted devices/emulators available for selected platform", () => { + return assertErrorIsThrown([ + { + deviceInfo: { + platform: "android", + status: UNREACHABLE_STATUS + } + } + ]); + }); + + it("throws error when there are only devices and --emulator is passed", () => { + return assertErrorIsThrown([ + { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS + }, + isEmulator: false + } + ], { + forDevice: false, + emulator: true + }); + }); + + it("throws error when there are only emulators and --forDevice is passed", () => { + return assertErrorIsThrown([ + { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS + }, + isEmulator: true + } + ], { + forDevice: true, + emulator: false + }); + }); + + it("returns the only available device/emulator when it matches passed -- options", async () => { + const testInjector = createTestInjector(); + const deviceInstance = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS + }, + isEmulator: true + }; + + const devicesService = testInjector.resolve("devicesService"); + devicesService.getDeviceInstances = (): Mobile.IDevice[] => [deviceInstance]; + + const debugCommand = testInjector.resolveCommand("debug|android"); + const actualDeviceInstance = await debugCommand.getDeviceForDebug(); + assert.deepEqual(actualDeviceInstance, deviceInstance); + }); + + describe("when multiple devices are detected", () => { + beforeEach(() => { + helpers.isInteractive = originalIsInteracive; + }); + + after(() => { + helpers.isInteractive = originalIsInteracive; + }); + + describe("when terminal is interactive", () => { + + it("prompts the user with information about available devices for specified platform only and returns the selected device instance", async () => { + helpers.isInteractive = () => true; + const testInjector = createTestInjector(); + const deviceInstance1 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance1", + displayName: "displayName1" + }, + isEmulator: true + }; + + const deviceInstance2 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance2", + displayName: "displayName2" + }, + isEmulator: true + }; + + const iOSDeviceInstance = { + deviceInfo: { + platform: "ios", + status: CONNECTED_STATUS, + identifier: "iosDevice", + displayName: "iPhone" + }, + isEmulator: true + }; + + const devicesService = testInjector.resolve("devicesService"); + devicesService.getDeviceInstances = (): Mobile.IDevice[] => [deviceInstance1, deviceInstance2, iOSDeviceInstance]; - it("Ensures that beforePrepareAllPlugins will not call gradle when livesyncing", async () => { - let config: IConfiguration = testInjector.resolve("config"); - config.debugLivesync = true; - let childProcess: stubs.ChildProcessStub = testInjector.resolve("childProcess"); - let androidProjectService: IPlatformProjectService = testInjector.resolve("androidProjectService"); - let projectData: IProjectData = testInjector.resolve("projectData"); - let spawnFromEventCount = childProcess.spawnFromEventCount; - await androidProjectService.beforePrepareAllPlugins(projectData); - assert.isTrue(spawnFromEventCount === 0); - assert.isTrue(spawnFromEventCount === childProcess.spawnFromEventCount); + let choicesPassedToPrompter: string[]; + const prompter = testInjector.resolve("prompter"); + prompter.promptForChoice = async (promptMessage: string, choices: any[]): Promise => { + choicesPassedToPrompter = choices; + return choices[1]; + }; + + const debugCommand = testInjector.resolveCommand("debug|android"); + const actualDeviceInstance = await debugCommand.getDeviceForDebug(); + const expectedChoicesPassedToPrompter = [deviceInstance1, deviceInstance2].map(d => `${d.deviceInfo.identifier} - ${d.deviceInfo.displayName}`); + assert.deepEqual(choicesPassedToPrompter, expectedChoicesPassedToPrompter); + + assert.deepEqual(actualDeviceInstance, deviceInstance2); + }); + }); + + describe("when terminal is not interactive", () => { + beforeEach(() => { + helpers.isInteractive = () => false; + }); + + const assertCorrectInstanceIsUsed = async (opts: { forDevice: boolean, emulator: boolean, isEmulatorTest: boolean, excludeLastDevice?: boolean }) => { + const testInjector = createTestInjector(); + const deviceInstance1 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance1", + displayName: "displayName1", + version: "5.1" + }, + isEmulator: opts.isEmulatorTest + }; + + const deviceInstance2 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance2", + displayName: "displayName2", + version: "6.0" + }, + isEmulator: opts.isEmulatorTest + }; + + const deviceInstance3 = { + deviceInfo: { + platform: "android", + status: CONNECTED_STATUS, + identifier: "deviceInstance3", + displayName: "displayName3", + version: "7.1" + }, + isEmulator: !opts.isEmulatorTest + }; + + const options = testInjector.resolve("options"); + options.forDevice = opts.forDevice; + options.emulator = opts.emulator; + + const devicesService = testInjector.resolve("devicesService"); + const deviceInstances = [deviceInstance1, deviceInstance2]; + if (!opts.excludeLastDevice) { + deviceInstances.push(deviceInstance3); + } + + devicesService.getDeviceInstances = (): Mobile.IDevice[] => deviceInstances; + + const debugCommand = testInjector.resolveCommand("debug|android"); + const actualDeviceInstance = await debugCommand.getDeviceForDebug(); + + assert.deepEqual(actualDeviceInstance, deviceInstance2); + }; + + it("returns the emulator with highest API level when --emulator is passed", () => { + return assertCorrectInstanceIsUsed({ forDevice: false, emulator: true, isEmulatorTest: true }); + }); + + it("returns the device with highest API level when --forDevice is passed", () => { + return assertCorrectInstanceIsUsed({ forDevice: true, emulator: false, isEmulatorTest: false }); + }); + + it("returns the emulator with highest API level when neither --emulator and --forDevice are passed", () => { + return assertCorrectInstanceIsUsed({ forDevice: false, emulator: false, isEmulatorTest: true }); + }); + + it("returns the device with highest API level when neither --emulator and --forDevice are passed and emulators are not available", async () => { + return assertCorrectInstanceIsUsed({ forDevice: false, emulator: false, isEmulatorTest: false, excludeLastDevice: true }); + }); + }); + }); }); - it("Ensures that beforePrepareAllPlugins will call gradle with clean option when *NOT* livesyncing", async () => { - let config: IConfiguration = testInjector.resolve("config"); - config.debugLivesync = false; - let childProcess: stubs.ChildProcessStub = testInjector.resolve("childProcess"); - let androidProjectService: IPlatformProjectService = testInjector.resolve("androidProjectService"); - let projectData: IProjectData = testInjector.resolve("projectData"); - let spawnFromEventCount = childProcess.spawnFromEventCount; - await androidProjectService.beforePrepareAllPlugins(projectData); - assert.isTrue(childProcess.lastCommand.indexOf("gradle") !== -1); - assert.isTrue(childProcess.lastCommandArgs[0] === "clean"); - assert.isTrue(spawnFromEventCount === 0); - assert.isTrue(spawnFromEventCount + 1 === childProcess.spawnFromEventCount); + describe("Debugger tests", () => { + let testInjector: IInjector; + + beforeEach(() => { + testInjector = createTestInjector(); + }); + + it("Ensures that debugLivesync flag is true when executing debug --watch command", async () => { + const debugCommand = testInjector.resolveCommand("debug|android"); + const options: IOptions = testInjector.resolve("options"); + options.watch = true; + const devicesService = testInjector.resolve("devicesService"); + devicesService.getDeviceInstances = (): Mobile.IDevice[] => { + return [{ deviceInfo: { status: "Connected", platform: "android" } }]; + }; + + const debugLiveSyncService = testInjector.resolve("debugLiveSyncService"); + debugLiveSyncService.liveSync = async (deviceDescriptors: ILiveSyncDeviceInfo[], liveSyncData: ILiveSyncInfo): Promise => { + return null; + }; + + await debugCommand.execute(["android", "--watch"]); + const config: IConfiguration = testInjector.resolve("config"); + assert.isTrue(config.debugLivesync); + }); + + it("Ensures that beforePrepareAllPlugins will not call gradle when livesyncing", async () => { + let config: IConfiguration = testInjector.resolve("config"); + config.debugLivesync = true; + let childProcess: stubs.ChildProcessStub = testInjector.resolve("childProcess"); + let androidProjectService: IPlatformProjectService = testInjector.resolve("androidProjectService"); + let projectData: IProjectData = testInjector.resolve("projectData"); + let spawnFromEventCount = childProcess.spawnFromEventCount; + await androidProjectService.beforePrepareAllPlugins(projectData); + assert.isTrue(spawnFromEventCount === 0); + assert.isTrue(spawnFromEventCount === childProcess.spawnFromEventCount); + }); + + it("Ensures that beforePrepareAllPlugins will call gradle with clean option when *NOT* livesyncing", async () => { + let config: IConfiguration = testInjector.resolve("config"); + config.debugLivesync = false; + let childProcess: stubs.ChildProcessStub = testInjector.resolve("childProcess"); + let androidProjectService: IPlatformProjectService = testInjector.resolve("androidProjectService"); + let projectData: IProjectData = testInjector.resolve("projectData"); + let spawnFromEventCount = childProcess.spawnFromEventCount; + await androidProjectService.beforePrepareAllPlugins(projectData); + assert.isTrue(childProcess.lastCommand.indexOf("gradle") !== -1); + assert.isTrue(childProcess.lastCommandArgs[0] === "clean"); + assert.isTrue(spawnFromEventCount === 0); + assert.isTrue(spawnFromEventCount + 1 === childProcess.spawnFromEventCount); + }); }); }); diff --git a/test/stubs.ts b/test/stubs.ts index 7c12e0e866..8fa4e100a7 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -182,7 +182,7 @@ export class ErrorsStub implements IErrors { fail(opts: { formatStr?: string; errorCode?: number; suppressCommandHelp?: boolean }, ...args: any[]): void; fail(...args: any[]) { - throw args; + throw new Error(require("util").format.apply(null, args || [])); } failWithoutHelp(message: string, ...args: any[]): void {