diff --git a/src/taco-dependency-installer/package.json b/src/taco-dependency-installer/package.json index 153e33fe..472f8517 100644 --- a/src/taco-dependency-installer/package.json +++ b/src/taco-dependency-installer/package.json @@ -35,7 +35,8 @@ "should": "4.3.0", "taco-tests-utils": "file:../taco-tests-utils", "typescript": "~1.6.2", - "mockery": "~1.4.0" + "mockery": "~1.4.0", + "mock-fs": "^3.4.0" }, "license": "MIT" } diff --git a/src/taco-dependency-installer/test/androidSdkInstaller.ts b/src/taco-dependency-installer/test/androidSdkInstaller.ts index 6a263a91..78574b90 100644 --- a/src/taco-dependency-installer/test/androidSdkInstaller.ts +++ b/src/taco-dependency-installer/test/androidSdkInstaller.ts @@ -11,6 +11,10 @@ /// /// /// +/// +/// +/// +/// "use strict"; @@ -23,6 +27,8 @@ var shouldModule = require("should"); import installerProtocol = require("../elevatedInstallerProtocol"); import ILogger = installerProtocol.ILogger; import mockery = require("mockery"); +import mockFs = require("mock-fs"); +import path = require("path"); import Q = require("q"); import tacoTestsUtils = require("taco-tests-utils"); import _ = require("lodash"); @@ -50,64 +56,95 @@ class FakeLogger implements ILogger { } describe("AndroidSdkInstaller telemetry", () => { + // Parameters for AndroidSdkInstaller + var steps: DependencyInstallerInterfaces.IStepsDeclaration; + var installerInfo: DependencyInstallerInterfaces.IInstallerData = { + installSource: "", + sha1: "", + bytes: 0, + installDestination: "", + steps: steps + }; + var softwareVersion: string = ""; + var installTo: string = "C:\\Program Files (x86)\\Android"; // Default installation directory in windows + + // Mocks used by the tests + var mockPath: typeof path; + var fakeTelemetryHelper: TacoTestsUtils.TelemetryFakes.Helper; + var fakeProcess: nodeFakes.Process; + var androidSdkInstallerClass: any; + var childProcessModule: nodeFakes.ChildProcessModule; + before(() => { // We tell mockery to replace "require()" with our own custom mock objects mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); + + fakeProcess = new nodeFakes.Process() + .fakeDeterministicHrtime(); + + var fakeProcessUtilsModule = { ProcessUtils: fakeProcess.buildProcessUtils() }; + + mockery.registerMock("./processUtils", fakeProcessUtilsModule); // TelemetryHelper loads ./processUtils + var tacoUtils: typeof TacoUtility = require("taco-utils"); + tacoUtils.Telemetry.init("TACO/dependencyInstaller", "1.2.3", false); + + // Register mocks. child_process and taco-utils mocks needs to be registered before + // AndroidSdkInstaller is required for the mocking to work + childProcessModule = new nodeFakes.ChildProcessModule().fakeAllExecCallsEndingWithErrors(); + mockery.registerMock("child_process", childProcessModule); + + // Reload taco-tests-utils but now with the fake processUtils loaded, so the fake telemetry will use the fake process + var tacoTestsUtilsWithMocks: typeof tacoTestsUtils = require("taco-tests-utils"); + + fakeTelemetryHelper = new tacoTestsUtilsWithMocks.TelemetryFakes.Helper(); + var tacoUtilsWithFakes = _.extend({}, tacoUtils, { TelemetryHelper: fakeTelemetryHelper, HasFakes: true }, + fakeProcessUtilsModule); + mockery.registerMock("taco-utils", tacoUtilsWithFakes); // AndroidSdkInstaller loads taco-utils + + // We need to mock path if we want to run windows tests on a mac, so it'll use ; as path delimiter + mockPath = _.extend({}, path); + mockery.registerMock("path", mockPath); // installerUtils uses path.delimiter, and it breaks the Windows tests on mac if not + + // We require the AndroidSdkInstaller file, which will use all the mocked dependencies + androidSdkInstallerClass = require("../installers/androidSdkInstaller"); }); after(() => { // Clean up and revert everything back to normal mockery.deregisterAll(); mockery.disable(); + mockFs.restore(); }); + beforeEach(() => { + fakeTelemetryHelper.clear(); // So we'll only get the new events in each scenario + steps = { download: false, install: false, updateVariables: false, postInstall: false }; // We reset all the steps to false + fakeProcess.clearEnv(); // Reset environment variables, given that we modify some of them in the tests + }); + + function telemetryGeneratedShouldBe(expectedTelemetry: TacoUtility.ICommandTelemetryProperties[], + expectedMessagePattern: RegExp, done: MochaDone): Q.Promise { + var androidSdkInstaller = new androidSdkInstallerClass(installerInfo, softwareVersion, installTo, + new FakeLogger(), steps); + + return androidSdkInstaller.run() + .then(() => Q.reject(new Error("Should have gotten a rejection in this test")), (error: Error) => { + return fakeTelemetryHelper.getAllSentEvents().then((allSentEvents: TelemetryEvent[]) => { + // We check the message first, because some coding defects can make the tests end in unexpected states + error.message.should.match(expectedMessagePattern); + + // Then we validate the telemetry + allSentEvents.should.eql(expectedTelemetry); + }); + }).done(done, done); + } + describe("updateVariablesDarwin", () => { it("generates telemetry if there is an error on the update command", (done: MochaDone) => { - var fakeProcessUtilsModule = { - ProcessUtils: new nodeFakes.Process().fakeMacOS() - .fakeDeterministicHrtime().buildProcessUtils() - }; - mockery.registerMock("./processUtils", fakeProcessUtilsModule); // TelemetryHelper loads ./processUtils - var tacoUtils: typeof TacoUtility = require("taco-utils"); - tacoUtils.Telemetry.init("TACO/dependencyInstaller", "1.2.3", false); + fakeProcess.fakeMacOS(); + steps.updateVariables = true; // We only test this step on this test - // Register mocks. child_process and taco-utils mocks needs to be register before - // AndroidSdkInstaller is required for the mock to work - mockery.registerMock("child_process", new nodeFakes.ChildProcessModule().fakeAllExecCallsEndingWithErrors()); - - // Reload taco-tests-utils but now with the fake processUtils loaded, so the fake telemetry will use the fake process - var tacoTestsUtilsWithMocks: typeof tacoTestsUtils = require("taco-tests-utils"); - - var fakeTelemetryHelper: TacoTestsUtils.TelemetryFakes.Helper = new tacoTestsUtilsWithMocks.TelemetryFakes.Helper(); - var tacoUtilsWithFakes = _.extend({}, tacoUtils, { TelemetryHelper: fakeTelemetryHelper, HasFakes: true }, - fakeProcessUtilsModule); - mockery.registerMock("taco-utils", tacoUtilsWithFakes); // AndroidSdkInstaller loads taco-utils - - // We require the AndroidSdkInstaller file, which will use all the mocked dependencies - var androidSdkInstallerClass = require("../installers/androidSdkInstaller"); - - var steps: DependencyInstallerInterfaces.IStepsDeclaration = { - download: false, - install: false, - updateVariables: true, // We only test this step on this test - postInstall: false - }; - - var installerInfo: DependencyInstallerInterfaces.IInstallerData = { - installSource: "", - sha1: "", - bytes: 0, - installDestination: "", - steps: steps - }; - - var softwareVersion: string = ""; - var installTo: string = ""; - - var androidSdkInstaller = new androidSdkInstallerClass(installerInfo, softwareVersion, installTo, - new FakeLogger(), steps); - - var expectedTelemetry = [ + var expectedTelemetry: TacoUtility.ICommandTelemetryProperties[] = [ { "initialStep.time": { isPii: false, value: "2000" }, step: { isPii: false, value: "initialStep" } @@ -121,12 +158,193 @@ describe("AndroidSdkInstaller telemetry", () => { } ]; - return androidSdkInstaller.run() - .then(() => Q.reject(new Error("Should have gotten a rejection in this test")), () => { - return fakeTelemetryHelper.getAllSentEvents().then((allSentEvents: TelemetryEvent[]) => { - allSentEvents.should.eql(expectedTelemetry); - }); - }).done(done, done); + return telemetryGeneratedShouldBe(expectedTelemetry, /Error while executing/, done); + }); + }); + + describe("installation", () => { + it("generates telemetry error if there is no install location", (done: MochaDone) => { + fakeProcess.fakeMacOS(); + steps.install = true; // We only test this step on this test + installTo = ""; // We don't have an install location + + var expectedTelemetry: TacoUtility.ICommandTelemetryProperties[] = [ + { + "initialStep.time": { isPii: false, value: "2000" }, + step: { isPii: false, value: "initialStep" } + }, + { + "error.description": { isPii: false, value: "NeedInstallDestination on installDefault" }, + "install.time": { isPii: false, value: "2000" }, + lastStepExecuted: { isPii: false, value: "install" }, + step: { isPii: false, value: "install" }, + time: { isPii: false, value: "3000" } + }]; + + return telemetryGeneratedShouldBe(expectedTelemetry, /NeedInstallDestination/, done); + }); + }); + + describe("post-installation in mac", () => { + it("generates telemetry error if we can't give executable permissions to the android executable", (done: MochaDone) => { + fakeProcess.fakeMacOS(); + + steps.postInstall = true; // We only test this step on this test + steps.updateVariables = true; // We need this step because post-install uses this.androidHomeValue populated in this step + + // child_process.exec will succeed while setting the path, and fail while giving permissions + childProcessModule.fakeUsingCommandToDetermineResult((command: string) => /export PATH=/.test(command), + (command: string) => /chmod a\+x/.test(command)); + + var filePath = path.join(fakeProcess.env.HOME, ".bash_profile"); + var files: mockFs.Config = {}; + files[filePath] = ""; + mockFs(files); + + var expectedTelemetry: TacoUtility.ICommandTelemetryProperties[] = [ + { + "initialStep.time": { isPii: false, value: "2000" }, + step: { isPii: false, value: "initialStep" } + }, + { + step: { isPii: false, value: "updateVariables" }, + "updateVariables.time": { isPii: false, value: "1000" } + }, + { + "error.description": { + isPii: false, + value: "ErrorOnChildProcess on addExecutePermission" + }, + lastStepExecuted: { isPii: false, value: "postInstall" }, + "postInstall.time": { isPii: false, value: "2000" }, + step: { isPii: false, value: "postInstall" }, + time: { isPii: false, value: "5000" } + } + ]; + + return telemetryGeneratedShouldBe(expectedTelemetry, /Error while executing/g, done); + }); + }); + + describe("post-installation in windows", () => { + beforeEach(() => { + fakeProcess.fakeWindows(); + mockPath.delimiter = ";"; // Installer utils uses this, and the path logic breaks in Mac if we don't mock it + + steps.postInstall = true; // We only test this step on this test + steps.updateVariables = true; // We need this step because post-install uses this.androidHomeValue populated in this step + + // Set ANDROID_HOME so updateVariables won't try to re-set it + installTo = "C:\\Program Files (x86)\\Android"; // We don't have an install location + var androidHomePath = fakeProcess.env.ANDROID_HOME = mockPath.join(installTo, "android-sdk-windows"); + + // Set android paths in the PATH so we won't have to add them + var platformToolsPath = mockPath.join(androidHomePath, "platform-tools"); + var androidToolsPath = mockPath.join(androidHomePath, "tools"); + fakeProcess.env.PATH = platformToolsPath + mockPath.delimiter + androidToolsPath; + }); + + it("installAndroidPackages generates telemetry error if the command used generates errors in stderr", (done: MochaDone) => { + var spawnErrorMessage = "Couldn't find the Android update command executable"; + childProcessModule.mockSpawn.setDefault( + childProcessModule.mockSpawn.simple(/*exitCode*/ 0, /*stdout*/ "", /*stderr*/ spawnErrorMessage)); + + var expectedTelemetry: TacoUtility.ICommandTelemetryProperties[] = [ + { + "initialStep.time": { isPii: false, value: "2000" }, + step: { isPii: false, value: "initialStep" } + }, + { + step: { isPii: false, value: "updateVariables" }, + "updateVariables.time": { isPii: false, value: "1000" } + }, + { + "error.code": { isPii: false, value: "0" }, + "error.description": { + isPii: false, + value: "ErrorOnExitOfChildProcess on postInstallDefault" + }, + "error.message": { + isPii: true, + value: "Couldn't find the Android update command executable" + }, + lastStepExecuted: { isPii: false, value: "postInstall" }, + "postInstall.time": { isPii: false, value: "2000" }, + step: { isPii: false, value: "postInstall" }, + time: { isPii: false, value: "5000" } + } + ]; + + return telemetryGeneratedShouldBe(expectedTelemetry, new RegExp(spawnErrorMessage), done); + }); + + it("installAndroidPackages generates telemetry error if the command used is invalid", (done: MochaDone) => { + var spawnErrorMessage = "The command is not recognized by the system"; + childProcessModule.mockSpawn.setDefault(function (callback: Function): void { + /* Warning: The "this" in the next line is the one passed by mockSpawn library. For that + to work this context needs to be a JavaScript lambda function. Do not convert this to + an arrow function, or this will break because of the change in semantics. */ + this.emit("error", new Error(spawnErrorMessage)); // invokes childProcess.on('error') + setTimeout(() => callback(8), 0); // Then the child process ends with an arbitrary error code of 8 + }); + + var expectedTelemetry: TacoUtility.ICommandTelemetryProperties[] = [ + { + "initialStep.time": { isPii: false, value: "2000" }, + step: { isPii: false, value: "initialStep" } + }, + { + step: { isPii: false, value: "updateVariables" }, + "updateVariables.time": { isPii: false, value: "1000" } + }, + { + "error.description": { + isPii: false, + value: "ErrorOnChildProcess on postInstallDefault" + }, + lastStepExecuted: { isPii: false, value: "postInstall" }, + "postInstall.time": { isPii: false, value: "2000" }, + step: { isPii: false, value: "postInstall" }, + time: { isPii: false, value: "5000" } + } + ]; + + return telemetryGeneratedShouldBe(expectedTelemetry, new RegExp(spawnErrorMessage), done); + }); + + it("killAdb generates telemetry error if the command used is invalid", (done: MochaDone) => { + // First call to install the android packages will succeed + childProcessModule.mockSpawn.sequence.add(childProcessModule.mockSpawn.simple(/*exitCode*/ 0, /*stdout*/ "", /*stderr*/ "")); + + // Second call to kill adb will fail + var spawnErrorMessage = "The kill adb command is not recognized by the system"; + childProcessModule.mockSpawn.sequence.add(function (callback: Function): void { + /* Warning: The "this" in the next line is the one passed by mockSpawn library. For that + to work this context needs to be a JavaScript lambda function. Do not convert this to + an arrow function, or this will break because of the change in semantics. */ + this.emit("error", new Error(spawnErrorMessage)); // invokes childProcess.on('error') + setTimeout(() => callback(8), 0); // Then the child process ends with an arbitrary error code of 8 + }); + + var expectedTelemetry: TacoUtility.ICommandTelemetryProperties[] = [ + { + "initialStep.time": { isPii: false, value: "2000" }, + step: { isPii: false, value: "initialStep" } + }, + { + step: { isPii: false, value: "updateVariables" }, + "updateVariables.time": { isPii: false, value: "1000" } + }, + { + "error.description": { isPii: false, value: "ErrorOnKillingAdb in killAdb" }, + lastStepExecuted: { isPii: false, value: "postInstall" }, + "postInstall.time": { isPii: false, value: "2000" }, + step: { isPii: false, value: "postInstall" }, + time: { isPii: false, value: "5000" } + } + ]; + + return telemetryGeneratedShouldBe(expectedTelemetry, new RegExp(spawnErrorMessage), done); }); }); }); diff --git a/src/taco-dependency-installer/utils/installerUtils.ts b/src/taco-dependency-installer/utils/installerUtils.ts index 9c834ed6..287b8aa6 100644 --- a/src/taco-dependency-installer/utils/installerUtils.ts +++ b/src/taco-dependency-installer/utils/installerUtils.ts @@ -41,7 +41,7 @@ module InstallerUtils { } class InstallerUtils { - private static pathName: string = os.platform() === "win32" ? "Path" : "PATH"; + private static PATH_NAME: string = "PATH"; // *nix uses uppercase and it's case sensitive. Windows is case insensitive /** * Verifies if the specified file is valid by comparing its sha1 signature and its size in bytes with the provided expectedSha1 and expectedBytes. * @@ -119,8 +119,8 @@ class InstallerUtils { public static promptUser(message: string): Q.Promise { var deferred: Q.Deferred = Q.defer(); var rl: readline.ReadLine = readline.createInterface({ - input: process.stdin, - output: process.stdout + input: tacoUtils.ProcessUtils.getProcess().stdin, + output: tacoUtils.ProcessUtils.getProcess().stdout }); rl.question(message, function (answer: string): void { @@ -143,9 +143,9 @@ class InstallerUtils { * @return {Q.Promise} A promise resolved with a boolean indicating whether the specified environment variable must be set */ public static mustSetSystemVariable(name: string, value: string, logger: ILogger): Q.Promise { - if (!process.env[name]) { + if (!tacoUtils.ProcessUtils.getProcess().env[name]) { return Q.resolve(true); - } else if (path.resolve(utils.expandEnvironmentVariables(process.env[name])) === path.resolve(utils.expandEnvironmentVariables(value))) { + } else if (path.resolve(utils.expandEnvironmentVariables(tacoUtils.ProcessUtils.getProcess().env[name])) === path.resolve(utils.expandEnvironmentVariables(value))) { // If this environment variable is already defined, but it is already set to what we need, we don't need to set it again return Q.resolve(false); } @@ -197,7 +197,7 @@ class InstallerUtils { * * @return {boolean} A boolean set to true if the Path system variable already contains the specified value in one of its segments */ - public static pathContains(valueToCheck: string, pathValue: string = process.env[InstallerUtils.pathName]): boolean { + public static pathContains(valueToCheck: string, pathValue: string = tacoUtils.ProcessUtils.getProcess().env[InstallerUtils.PATH_NAME]): boolean { if (!pathValue) { return false; } diff --git a/src/taco-dependency-installer/utils/win32/installerUtilsWin32.ts b/src/taco-dependency-installer/utils/win32/installerUtilsWin32.ts index 371b5a2c..2067bbc1 100644 --- a/src/taco-dependency-installer/utils/win32/installerUtilsWin32.ts +++ b/src/taco-dependency-installer/utils/win32/installerUtilsWin32.ts @@ -28,7 +28,7 @@ class InstallerUtilsWin32 { * * @param {string} name The name of the environment variable to set * @param {string} value The desired value for the specified environment variable - * @param {InstallerProtocol.ILogger} logger The logger for the current process + * @param {InstallerProtocol.ILogger} logger The logger for the process * * @return {Q.Promise} A promise resolved with an empty object if the operation succeeds, or rejected with the appropriate error if not */ @@ -52,8 +52,8 @@ class InstallerUtilsWin32 { * @return {Q.Promise} A promise resolved with an empty object if the operation succeeds, or rejected with the appropriate error if not */ public static addToPathIfNeededWin32(addToPath: string[]): Q.Promise { - var pathName: string = "Path"; - var pathValue: string = process.env[pathName]; + var pathName: string = "PATH"; // Windows is case-insensitive. We use uppercase to be more compatible with *nix systems + var pathValue: string = tacoUtils.ProcessUtils.getProcess().env[pathName]; addToPath.forEach(function (value: string): void { if (!installerUtils.pathContains(value)) { @@ -61,7 +61,7 @@ class InstallerUtilsWin32 { } }); - if (pathValue === process.env[pathName]) { + if (pathValue === tacoUtils.ProcessUtils.getProcess().env[pathName]) { return Q.resolve({}); } @@ -78,13 +78,13 @@ class InstallerUtilsWin32 { * @return {Q.Promise} A promise resolved with an empty object if the operation succeeds, or rejected with the appropriate error if not */ public static setEnvironmentVariableWin32(name: string, value: string): Q.Promise { - if (process.platform !== "win32") { + if (tacoUtils.ProcessUtils.getProcess().platform !== "win32") { // No-op for platforms other than win32 return Q.resolve({}); } // Set variable for this running process - process.env[name] = value; + tacoUtils.ProcessUtils.getProcess().env[name] = value; // Set variable for the system var scriptPath: string = path.resolve(__dirname, "setSystemVariable.ps1"); diff --git a/src/taco-tests-utils/nodeFakes.ts b/src/taco-tests-utils/nodeFakes.ts index b99e906e..dc31250d 100644 --- a/src/taco-tests-utils/nodeFakes.ts +++ b/src/taco-tests-utils/nodeFakes.ts @@ -17,9 +17,18 @@ import _ = require("lodash"); +/* tslint:disable:no-var-requires */ +/* We don't have a .d.ts file for mock-spawn */ +var mockSpawnModule = require("mock-spawn"); +/* tslint:enable:no-var-requires */ + export module NodeFakes { export interface IEnvironmentVariables { + // We should add here the environment variables that we use in TACO. Remember to also add them in the .d.ts file HOME?: string; + ANDROID_HOME?: string; + PATH?: string; + TACO_UNIT_TEST?: string; } export type IChildProcess = NodeJSChildProcess.ChildProcess; @@ -30,6 +39,8 @@ export module NodeFakes { export type ExecSecondArgument = IExecOptions | Callback; + export type CommandTester = (command: string) => boolean; + export type ExecFileOptions = { cwd?: string; stdio?: any; customFds?: any; env?: any; encoding?: string; timeout?: number; maxBuffer?: string; killSignal?: string; @@ -68,7 +79,7 @@ export module NodeFakes { public env: IEnvironmentVariables; constructor() { - this.env = _.extend({}, process.env); + this.clearEnv(); } public asProcess(): NodeJS.Process { @@ -109,7 +120,12 @@ export module NodeFakes { public fakeWindows(): Process { var username = "my_username"; this.asProcess().platform = "win32"; - // this.asProcess().env.HOME = "C:\\Users\\" + username; + this.asProcess().env.HOME = "C:\\Users\\" + username; + return this; + } + + public clearEnv(): Process { + this.env = { TACO_UNIT_TEST: "true" }; return this; } } @@ -175,27 +191,42 @@ export module NodeFakes { } export class ChildProcessModule /* implements typeof NodeJSChildProcess*/ { - /** Methods to configure the fake process **/ + /** mock spawn variable. Docs at https://www.npmjs.com/package/mock-spawn **/ + public mockSpawn: any = mockSpawnModule(); + + // We simulate that all calls to exect are succesfull + public fakeAllExecCallsSucceed(): ChildProcessModule { + this.exec = (command: string, optionsOrCallback: ExecSecondArgument, callback: Callback = null): IChildProcess => { + return this.callCallback(command, optionsOrCallback, callback, true); + }; + return this; + } // We simulate that all calls to exect end with an error public fakeAllExecCallsEndingWithErrors(): ChildProcessModule { + this.exec = (command: string, optionsOrCallback: ExecSecondArgument, callback: Callback = null): IChildProcess => { + return this.callCallback(command, optionsOrCallback, callback, false); + }; + return this; + } + + public fakeUsingCommandToDetermineResult(successFilter: CommandTester, failureFilter: CommandTester): ChildProcessModule { this.exec = (command: string, optionsOrCallback: ExecSecondArgument, callback?: Callback): IChildProcess => { - var realCallback = (callback || optionsOrCallback); + var isSuccess: boolean = successFilter(command); + var isFailure: boolean = failureFilter(command); - // We call the callback in an async way - setTimeout(() => { - realCallback(new Error("Error while executing " + command), /*stdout*/ new Buffer(""), /*stderr*/ new Buffer("")); - }, 0); - - return new ChildProcess(); + if (isSuccess !== isFailure) { + return this.callCallback(command, optionsOrCallback, callback, isSuccess); + } else { + throw new Error("A command should be exactly a success, or a failure. It can't be both nor either.\n" + + "Command: " + command + " isSuccess: " + isSuccess + " isFailure: " + isFailure); + } }; return this; } public spawn(command: string, args?: string[], options?: SpawnOptions): IChildProcess { - /* TODO: We should consider integrating this method with this library https://www.npmjs.com/package/mock-spawn - if we need to mock spawn */ - throw this.notImplementedError(); + return this.mockSpawn(command, args, options); } public exec(command: string, options: IExecOptions, callback: Callback): IChildProcess; @@ -215,6 +246,17 @@ export module NodeFakes { throw this.notImplementedError(); } + private callCallback(command: string, optionsOrCallback: ExecSecondArgument, + callback: Callback, wasSuccessful: boolean): ChildProcess { + var realCallback = (callback || optionsOrCallback); + // We call the callback in an async way + var error = wasSuccessful ? null : new Error("Error while executing " + command); + setTimeout(() => { + realCallback(error, /*stdout*/ new Buffer(""), /*stderr*/ new Buffer("")); + }, 0); + return new ChildProcess(); + } + private notImplementedError(): Error { return Error("This method hasn't been implemented for this test"); } diff --git a/src/taco-tests-utils/package.json b/src/taco-tests-utils/package.json index 257dfb13..19039622 100644 --- a/src/taco-tests-utils/package.json +++ b/src/taco-tests-utils/package.json @@ -18,7 +18,8 @@ "main": "tacoTestsUtils.js", "dependencies": { "lodash": "^3.10.1", - "q": "~1.4.1" + "q": "~1.4.1", + "mock-spawn": "~0.2.6" }, "devDependencies": { "typescript": "~1.6.2" diff --git a/src/taco-tests-utils/telemetryFakes.ts b/src/taco-tests-utils/telemetryFakes.ts index 35a71e22..cc52b1a0 100644 --- a/src/taco-tests-utils/telemetryFakes.ts +++ b/src/taco-tests-utils/telemetryFakes.ts @@ -66,5 +66,9 @@ export module TelemetryFakes { () => generator.time(null, () => codeGeneratingTelemetry(generator)), () => generator.sendAndNotify()); // After } + + public clear(): void { + this.telemetryGenerators = []; + } } } diff --git a/src/taco-utils/resourceManager.ts b/src/taco-utils/resourceManager.ts index f9f582bd..d7768af9 100644 --- a/src/taco-utils/resourceManager.ts +++ b/src/taco-utils/resourceManager.ts @@ -13,8 +13,9 @@ import fs = require ("fs"); import path = require ("path"); import argsHelper = require ("./argsHelper"); +import processUtils = require("./processUtils"); +import resourceSet = require("./resourceSet"); import tacoGlobalConfig = require ("./tacoGlobalConfig"); -import resourceSet = require ("./resourceSet"); import ArgsHelper = argsHelper.ArgsHelper; import TacoGlobalConfig = tacoGlobalConfig.TacoGlobalConfig; @@ -95,7 +96,7 @@ module TacoUtility { var args: string[] = ArgsHelper.getOptionalArgsArrayFromFunctionCall(arguments, 1); var result: string = this.getStringForLocale(this.bestLanguageMatch(this.getCurrentLocale()), id, args); - if (result && process.env["TACO_UNIT_TEST"]) { + if (result && processUtils.ProcessUtils.getProcess().env["TACO_UNIT_TEST"]) { // Mock out resources for consistency in unit tests, but only if they exist return id; } else { diff --git a/src/typings/mock-fs.d.ts b/src/typings/mock-fs.d.ts new file mode 100644 index 00000000..7b167bc3 --- /dev/null +++ b/src/typings/mock-fs.d.ts @@ -0,0 +1,51 @@ +// Type definitions for mock-fs 2.5.0 +// Project: https://github.com/tschaub/mock-fs +// Definitions by: Wim Looman +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +/// + +declare module "mock-fs" { + import fs = require("fs"); + + function mock(config?: mock.Config): void; + + module mock { + function file(config: FileConfig): File; + function directory(config: DirectoryConfig): Directory; + function symlink(config: SymlinkConfig): Symlink; + + function restore(): void; + + function fs(config?: Config): typeof fs; + + interface Config { + [path: string]: string | Buffer | File | Directory | Symlink | Config; + } + + interface CommonConfig { + mode?: number; + uid?: number; + git?: number; + atime?: Date; + ctime?: Date; + mtime?: Date; + } + + interface FileConfig extends CommonConfig { + content: string | Buffer; + } + interface DirectoryConfig extends CommonConfig { + items: Config; + } + interface SymlinkConfig extends CommonConfig { + path: string; + } + + class File { private _file: any; } + class Directory { private _directory: any; } + class Symlink { private _symlink: any; } + } + + export = mock; +} diff --git a/src/typings/nodeFakes.d.ts b/src/typings/nodeFakes.d.ts index 40d5ed20..24b065f7 100644 --- a/src/typings/nodeFakes.d.ts +++ b/src/typings/nodeFakes.d.ts @@ -11,11 +11,16 @@ declare module TacoTestsUtils { export module NodeFakes { interface IEnvironmentVariables { + // We should add here the environment variables that we use in TACO. Remember to also add them in the .ts file HOME?: string; + ANDROID_HOME?: string; + PATH?: string; + TACO_UNIT_TEST?: string; } type IChildProcess = NodeJSChildProcess.ChildProcess; type IExecOptions = NodeJSChildProcess.IExecOptions; type Callback = (error: Error, stdout: Buffer, stderr: Buffer) => void; + type CommandTester = (command: string) => boolean; type ExecSecondArgument = IExecOptions | Callback; type ExecFileOptions = { cwd?: string; @@ -83,14 +88,16 @@ declare module TacoTestsUtils { getProcess(): NodeJS.Process; } class Process { - env: IEnvironmentVariables; - constructor(); - asProcess(): NodeJS.Process; - buildProcessUtils(): IProcessUtils; + public env: IEnvironmentVariables; + public constructor(); + public asProcess(): NodeJS.Process; + public buildProcessUtils(): IProcessUtils; + /** Methods to configure the fake process **/ - fakeDeterministicHrtime(): Process; - fakeMacOS(): Process; - fakeWindows(): Process; + public fakeDeterministicHrtime(): Process; + public fakeMacOS(): Process; + public fakeWindows(): Process; + public clearEnv(): Process; } abstract class EventEmitter implements NodeJS.EventEmitter { protected abstract notImplementedError(): Error; @@ -115,7 +122,17 @@ declare module TacoTestsUtils { } class ChildProcessModule { /** Methods to configure the fake process **/ + // This method will make all child_process.exec call succeed + fakeAllExecCallsSucceed(): ChildProcessModule; + // This method will make all child_process.exec call fail fakeAllExecCallsEndingWithErrors(): ChildProcessModule; + // This method will make some child_process.exec call fail, and some succeed based on the lambda tests passed + fakeUsingCommandToDetermineResult(successFilter: CommandTester, failureFilter: CommandTester): ChildProcessModule; + + /** mock spawn variable. Docs at https://www.npmjs.com/package/mock-spawn **/ + public mockSpawn: any; // This is an instance of mockSpawn(); + + /** child_process methods **/ spawn(command: string, args?: string[], options?: SpawnOptions): IChildProcess; exec(command: string, options: IExecOptions, callback: Callback): IChildProcess; exec(command: string, callback: Callback): IChildProcess; diff --git a/src/typings/tacoTestsUtils.ts b/src/typings/tacoTestsUtils.d.ts similarity index 100% rename from src/typings/tacoTestsUtils.ts rename to src/typings/tacoTestsUtils.d.ts diff --git a/src/typings/telemetryFakes.d.ts b/src/typings/telemetryFakes.d.ts index 0ecf5697..1b0d7fa6 100644 --- a/src/typings/telemetryFakes.d.ts +++ b/src/typings/telemetryFakes.d.ts @@ -22,6 +22,7 @@ declare module TacoTestsUtils { generate(componentName: string, codeGeneratingTelemetry: { (telemetry: TacoUtility.TelemetryGenerator): T; }): T; // Warning: We have no way of rejecting this promise, so we might get a test timeout if the events are never sent getAllSentEvents(): Q.Promise; + clear(): void; } } }