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 {