diff --git a/.vsts-ci/templates/ci-general.yml b/.vsts-ci/templates/ci-general.yml index 3bd642c534..1419e4d3fa 100644 --- a/.vsts-ci/templates/ci-general.yml +++ b/.vsts-ci/templates/ci-general.yml @@ -46,6 +46,7 @@ steps: inputs: targetType: inline script: | + Get-Module -ListAvailable Pester Install-Module InvokeBuild -Scope CurrentUser -Force Invoke-Build Write-Host "##vso[task.setvariable variable=vsixPath]$(Resolve-Path powershell-*.vsix)" diff --git a/src/features/Examples.ts b/src/features/Examples.ts index 94007b5d05..96861ac9f5 100644 --- a/src/features/Examples.ts +++ b/src/features/Examples.ts @@ -2,19 +2,20 @@ // Licensed under the MIT License. import path = require("path"); +import utils = require("../utils") import vscode = require("vscode"); export class ExamplesFeature implements vscode.Disposable { private command: vscode.Disposable; - private examplesPath: string; + private examplesPath: vscode.Uri; constructor() { - this.examplesPath = path.resolve(__dirname, "../../examples"); + this.examplesPath = vscode.Uri.file(path.resolve(__dirname, "../examples")); this.command = vscode.commands.registerCommand("PowerShell.OpenExamplesFolder", () => { - vscode.commands.executeCommand( - "vscode.openFolder", - vscode.Uri.file(this.examplesPath), - true); + vscode.commands.executeCommand("vscode.openFolder", this.examplesPath, true); + // Return existence of the path for testing. The `vscode.openFolder` + // command should do this, but doesn't (yet). + return utils.fileExists(this.examplesPath); }); } diff --git a/src/features/PesterTests.ts b/src/features/PesterTests.ts index 35d753f3b1..bd11d319eb 100644 --- a/src/features/PesterTests.ts +++ b/src/features/PesterTests.ts @@ -18,25 +18,25 @@ export class PesterTestsFeature implements vscode.Disposable { private invokePesterStubScriptPath: string; constructor(private sessionManager: SessionManager) { - this.invokePesterStubScriptPath = path.resolve(__dirname, "../../modules/PowerShellEditorServices/InvokePesterStub.ps1"); + this.invokePesterStubScriptPath = path.resolve(__dirname, "../modules/PowerShellEditorServices/InvokePesterStub.ps1"); // File context-menu command - Run Pester Tests this.command = vscode.commands.registerCommand( "PowerShell.RunPesterTestsFromFile", (fileUri) => { - this.launchAllTestsInActiveEditor(LaunchType.Run, fileUri); + return this.launchAllTestsInActiveEditor(LaunchType.Run, fileUri); }); // File context-menu command - Debug Pester Tests this.command = vscode.commands.registerCommand( "PowerShell.DebugPesterTestsFromFile", (fileUri) => { - this.launchAllTestsInActiveEditor(LaunchType.Debug, fileUri); + return this.launchAllTestsInActiveEditor(LaunchType.Debug, fileUri); }); // This command is provided for usage by PowerShellEditorServices (PSES) only this.command = vscode.commands.registerCommand( "PowerShell.RunPesterTests", (uriString, runInDebugger, describeBlockName?, describeBlockLineNumber?, outputPath?) => { - this.launchTests(uriString, runInDebugger, describeBlockName, describeBlockLineNumber, outputPath); + return this.launchTests(uriString, runInDebugger, describeBlockName, describeBlockLineNumber, outputPath); }); } @@ -44,10 +44,13 @@ export class PesterTestsFeature implements vscode.Disposable { this.command.dispose(); } - private launchAllTestsInActiveEditor(launchType: LaunchType, fileUri: vscode.Uri) { + private async launchAllTestsInActiveEditor( + launchType: LaunchType, + fileUri: vscode.Uri): Promise { + const uriString = (fileUri || vscode.window.activeTextEditor.document.uri).toString(); const launchConfig = this.createLaunchConfig(uriString, launchType); - this.launch(launchConfig); + return this.launch(launchConfig); } private async launchTests( @@ -55,11 +58,11 @@ export class PesterTestsFeature implements vscode.Disposable { runInDebugger: boolean, describeBlockName?: string, describeBlockLineNumber?: number, - outputPath?: string) { + outputPath?: string): Promise { const launchType = runInDebugger ? LaunchType.Debug : LaunchType.Run; const launchConfig = this.createLaunchConfig(uriString, launchType, describeBlockName, describeBlockLineNumber, outputPath); - this.launch(launchConfig); + return this.launch(launchConfig); } private createLaunchConfig( @@ -126,9 +129,9 @@ export class PesterTestsFeature implements vscode.Disposable { return launchConfig; } - private launch(launchConfig) { + private async launch(launchConfig): Promise { // Create or show the interactive console - // TODO #367: Check if "newSession" mode is configured + // TODO: #367 Check if "newSession" mode is configured vscode.commands.executeCommand("PowerShell.ShowSessionConsole", true); // Write out temporary debug session file @@ -137,6 +140,10 @@ export class PesterTestsFeature implements vscode.Disposable { this.sessionManager.getSessionDetails()); // TODO: Update to handle multiple root workspaces. - vscode.debug.startDebugging(vscode.workspace.workspaceFolders[0], launchConfig); + // + // Ensure the necessary script exists (for testing). The debugger will + // start regardless, but we also pass its success along. + return utils.fileExists(this.invokePesterStubScriptPath) + && vscode.debug.startDebugging(vscode.workspace.workspaceFolders[0], launchConfig); } } diff --git a/src/settings.ts b/src/settings.ts index 1403512e95..32f5aa0d6e 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -157,6 +157,8 @@ export function load(): ISettings { const defaultDeveloperSettings: IDeveloperSettings = { featureFlags: [], + // From `/out/main.js` we go to the directory before and + // then into the other repo. bundledModulesPath: "../../PowerShellEditorServices/module", editorServicesLogLevel: "Normal", editorServicesWaitForDebugger: false, @@ -234,7 +236,7 @@ export function load(): ISettings { promptToUpdatePackageManagement: configuration.get("promptToUpdatePackageManagement", true), bundledModulesPath: - "../modules", + "../modules", // Because the extension is always at `/out/main.js` useX86Host: configuration.get("useX86Host", false), enableProfileLoading: diff --git a/src/utils.ts b/src/utils.ts index c5d3a880f9..e4c89b2bbe 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -6,12 +6,14 @@ import fs = require("fs"); import os = require("os"); import path = require("path"); +import vscode = require("vscode"); export const PowerShellLanguageId = "powershell"; -export function ensurePathExists(targetPath: string) { +export function ensurePathExists(targetPath: string): void { // Ensure that the path exists try { + // TODO: Use vscode.workspace.fs fs.mkdirSync(targetPath); } catch (e) { // If the exception isn't to indicate that the folder exists already, rethrow it. @@ -21,6 +23,23 @@ export function ensurePathExists(targetPath: string) { } } +// Check that the file exists in an asynchronous manner that relies solely on the VS Code API, not Node's fs library. +export async function fileExists(targetPath: string | vscode.Uri): Promise { + try { + await vscode.workspace.fs.stat( + targetPath instanceof vscode.Uri + ? targetPath + : vscode.Uri.file(targetPath)); + return true; + } catch (e) { + if (e instanceof vscode.FileSystemError.FileNotFound) { + return false; + } + throw e; + } + +} + export function getPipePath(pipeName: string) { if (os.platform() === "win32") { return "\\\\.\\pipe\\" + pipeName; @@ -45,7 +64,7 @@ export interface IEditorServicesSessionDetails { export type IReadSessionFileCallback = (details: IEditorServicesSessionDetails) => void; -const sessionsFolder = path.resolve(__dirname, "..", "..", "sessions/"); +const sessionsFolder = path.resolve(__dirname, "../sessions"); const sessionFilePathPrefix = path.resolve(sessionsFolder, "PSES-VSCode-" + process.env.VSCODE_PID); // Create the sessions path if it doesn't exist already diff --git a/test/core/paths.test.ts b/test/core/paths.test.ts new file mode 100644 index 0000000000..c74216a189 --- /dev/null +++ b/test/core/paths.test.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import * as assert from "assert"; +import * as fs from "fs"; +import * as path from "path"; +import * as vscode from "vscode"; +import { suiteSetup } from "mocha"; +import utils = require("../utils"); + +suite("Path assumptions", () => { + suiteSetup(utils.ensureExtensionIsActivated); + + // TODO: This is skipped because it intereferes with other tests. Either + // need to find a way to close the opened folder via a Code API, or find + // another way to test this. + test.skip("The examples folder can be opened (and exists)", async () => { + assert(await vscode.commands.executeCommand("PowerShell.OpenExamplesFolder")); + }); + + test("The session folder is created in the right place", async () => { + assert(fs.existsSync(path.resolve(utils.rootPath, "sessions"))); + }); + + test("The logs folder is created in the right place", async () => { + assert(fs.existsSync(path.resolve(utils.rootPath, "logs"))); + }); +}); diff --git a/test/features/CustomViews.test.ts b/test/features/CustomViews.test.ts index 57b44075a1..dc17ee136e 100644 --- a/test/features/CustomViews.test.ts +++ b/test/features/CustomViews.test.ts @@ -70,7 +70,7 @@ hello content: "console.log('asdf');", }, { - fileName: "../../testCustomViews.js", + fileName: "../testCustomViews.js", content: "console.log('asdf');", }, ], @@ -78,7 +78,7 @@ hello expectedHtmlString: ` hello - + `, }, @@ -129,7 +129,7 @@ hello styleSheetPaths: cssPaths, }; try { - assert.equal(htmlContentView.getContent(), testCase.expectedHtmlString); + assert.strictEqual(htmlContentView.getContent(), testCase.expectedHtmlString); } finally { jsPaths.forEach((jsPath) => fs.unlinkSync(vscode.Uri.parse(jsPath).fsPath)); cssPaths.forEach((cssPath) => fs.unlinkSync(vscode.Uri.parse(cssPath).fsPath)); diff --git a/test/features/ExternalApi.test.ts b/test/features/ExternalApi.test.ts index 262f1b207f..880773c46f 100644 --- a/test/features/ExternalApi.test.ts +++ b/test/features/ExternalApi.test.ts @@ -2,27 +2,19 @@ // Licensed under the MIT License. import * as assert from "assert"; -import * as vscode from "vscode"; -import { before, beforeEach, afterEach } from "mocha"; +import { suiteSetup, setup, teardown } from "mocha"; +import utils = require("../utils"); import { IExternalPowerShellDetails, IPowerShellExtensionClient } from "../../src/features/ExternalApi"; -// tslint:disable-next-line: no-var-requires -const PackageJSON: any = require("../../../package.json"); -const testExtensionId = `${PackageJSON.publisher}.${PackageJSON.name}`; - suite("ExternalApi feature - Registration API", () => { let powerShellExtensionClient: IPowerShellExtensionClient; - before(async () => { - const powershellExtension = vscode.extensions.getExtension(testExtensionId); - if (!powershellExtension.isActive) { - powerShellExtensionClient = await powershellExtension.activate(); - return; - } + suiteSetup(async () => { + const powershellExtension = await utils.ensureExtensionIsActivated(); powerShellExtensionClient = powershellExtension!.exports as IPowerShellExtensionClient; }); test("It can register and unregister an extension", () => { - const sessionId: string = powerShellExtensionClient.registerExternalExtension(testExtensionId); + const sessionId: string = powerShellExtensionClient.registerExternalExtension(utils.extensionId); assert.notStrictEqual(sessionId , ""); assert.notStrictEqual(sessionId , null); assert.strictEqual( @@ -31,7 +23,7 @@ suite("ExternalApi feature - Registration API", () => { }); test("It can register and unregister an extension with a version", () => { - const sessionId: string = powerShellExtensionClient.registerExternalExtension(testExtensionId, "v2"); + const sessionId: string = powerShellExtensionClient.registerExternalExtension(utils.extensionId, "v2"); assert.notStrictEqual(sessionId , ""); assert.notStrictEqual(sessionId , null); assert.strictEqual( @@ -48,12 +40,12 @@ suite("ExternalApi feature - Registration API", () => { }); test("It can't register the same extension twice", async () => { - const sessionId: string = powerShellExtensionClient.registerExternalExtension(testExtensionId); + const sessionId: string = powerShellExtensionClient.registerExternalExtension(utils.extensionId); try { assert.throws( - () => powerShellExtensionClient.registerExternalExtension(testExtensionId), + () => powerShellExtensionClient.registerExternalExtension(utils.extensionId), { - message: `The extension '${testExtensionId}' is already registered.` + message: `The extension '${utils.extensionId}' is already registered.` }); } finally { powerShellExtensionClient.unregisterExternalExtension(sessionId); @@ -73,20 +65,16 @@ suite("ExternalApi feature - Other APIs", () => { let sessionId: string; let powerShellExtensionClient: IPowerShellExtensionClient; - before(async () => { - const powershellExtension = vscode.extensions.getExtension(testExtensionId); - if (!powershellExtension.isActive) { - powerShellExtensionClient = await powershellExtension.activate(); - return; - } + suiteSetup(async () => { + const powershellExtension = await utils.ensureExtensionIsActivated(); powerShellExtensionClient = powershellExtension!.exports as IPowerShellExtensionClient; }); - beforeEach(() => { - sessionId = powerShellExtensionClient.registerExternalExtension(testExtensionId); + setup(() => { + sessionId = powerShellExtensionClient.registerExternalExtension(utils.extensionId); }); - afterEach(() => { + teardown(() => { powerShellExtensionClient.unregisterExternalExtension(sessionId); }); @@ -105,6 +93,6 @@ suite("ExternalApi feature - Other APIs", () => { assert.notStrictEqual(versionDetails.version, ""); assert.notStrictEqual(versionDetails.version, null); - // Start up can take some time... so set the time out to 30s + // Start up can take some time...so set the timeout to 30 seconds. }).timeout(30000); }); diff --git a/test/features/ISECompatibility.test.ts b/test/features/ISECompatibility.test.ts index c8843e93ba..7fac60591d 100644 --- a/test/features/ISECompatibility.test.ts +++ b/test/features/ISECompatibility.test.ts @@ -3,37 +3,60 @@ import * as assert from "assert"; import * as vscode from "vscode"; +import { suiteSetup, setup, suiteTeardown, teardown } from "mocha"; import { ISECompatibilityFeature } from "../../src/features/ISECompatibility"; +import utils = require("../utils"); suite("ISECompatibility feature", () => { + let currentTheme: string; + + suiteSetup(async () => { + // Save user's current theme. + currentTheme = await vscode.workspace.getConfiguration("workbench").get("colorTheme"); + await utils.ensureExtensionIsActivated(); + }); + + setup(async () => { await vscode.commands.executeCommand("PowerShell.EnableISEMode"); }); + + teardown(async () => { await vscode.commands.executeCommand("PowerShell.DisableISEMode"); }); + + suiteTeardown(async () => { + // Reset user's current theme. + await vscode.workspace.getConfiguration("workbench").update("colorTheme", currentTheme, true); + assert.strictEqual(vscode.workspace.getConfiguration("workbench").get("colorTheme"), currentTheme); + }); + test("It sets ISE Settings", async () => { - await vscode.commands.executeCommand("PowerShell.EnableISEMode"); for (const iseSetting of ISECompatibilityFeature.settings) { const currently = vscode.workspace.getConfiguration(iseSetting.path).get(iseSetting.name); - assert.equal(currently, iseSetting.value); + assert.strictEqual(currently, iseSetting.value); } }); + test("It unsets ISE Settings", async () => { // Change state to something that DisableISEMode will change await vscode.workspace.getConfiguration("workbench").update("colorTheme", "PowerShell ISE", true); - assert.equal(vscode.workspace.getConfiguration("workbench").get("colorTheme"), "PowerShell ISE"); + assert.strictEqual(vscode.workspace.getConfiguration("workbench").get("colorTheme"), "PowerShell ISE"); await vscode.commands.executeCommand("PowerShell.DisableISEMode"); for (const iseSetting of ISECompatibilityFeature.settings) { const currently = vscode.workspace.getConfiguration(iseSetting.path).get(iseSetting.name); - assert.notEqual(currently, iseSetting.value); + assert.notStrictEqual(currently, iseSetting.value); } }).timeout(10000); - test("It leaves Theme after being changed after enabling ISE Mode", async () => { - await vscode.commands.executeCommand("PowerShell.EnableISEMode"); - assert.equal(vscode.workspace.getConfiguration("workbench").get("colorTheme"), "PowerShell ISE"); - await vscode.workspace.getConfiguration("workbench").update("colorTheme", "Dark+", true); + test("It doesn't change theme when disabled if theme was manually changed after being enabled", async () => { + assert.strictEqual(vscode.workspace.getConfiguration("workbench").get("colorTheme"), "PowerShell ISE"); + + // "Manually" change theme after enabling ISE mode. Use a built-in theme but not the default. + await vscode.workspace.getConfiguration("workbench").update("colorTheme", "Monokai", true); + assert.strictEqual(vscode.workspace.getConfiguration("workbench").get("colorTheme"), "Monokai"); + await vscode.commands.executeCommand("PowerShell.DisableISEMode"); for (const iseSetting of ISECompatibilityFeature.settings) { const currently = vscode.workspace.getConfiguration(iseSetting.path).get(iseSetting.name); - assert.notEqual(currently, iseSetting.value); + assert.notStrictEqual(currently, iseSetting.value); } - assert.equal(vscode.workspace.getConfiguration("workbench").get("colorTheme"), "Dark+"); + assert.strictEqual(vscode.workspace.getConfiguration("workbench").get("colorTheme"), "Monokai"); }).timeout(10000); }); diff --git a/test/features/RunCode.test.ts b/test/features/RunCode.test.ts index 90745f3090..226c02138c 100644 --- a/test/features/RunCode.test.ts +++ b/test/features/RunCode.test.ts @@ -2,8 +2,13 @@ // Licensed under the MIT License. import * as assert from "assert"; +import * as fs from "fs"; +import * as path from "path"; import rewire = require("rewire"); import vscode = require("vscode"); +import { suiteSetup } from "mocha"; +import utils = require("../utils"); +import { sleep } from "../../src/utils"; // Setup function that is not exported. const customViews = rewire("../../src/features/RunCode"); @@ -15,6 +20,8 @@ enum LaunchType { } suite("RunCode tests", () => { + suiteSetup(utils.ensureExtensionIsActivated); + test("Can create the launch config", () => { const commandToRun: string = "Invoke-Build"; const args: string[] = ["Clean"]; @@ -33,6 +40,23 @@ suite("RunCode tests", () => { const actual: object = createLaunchConfig(LaunchType.Debug, commandToRun, args); - assert.deepEqual(actual, expected); + assert.deepStrictEqual(actual, expected); }); + + test("Can run Pester tests from file", async () => { + const pesterTests = path.resolve(__dirname, "../../../examples/Tests/SampleModule.Tests.ps1"); + assert(fs.existsSync(pesterTests)); + + // Open the PowerShell file with Pester tests and then wait a while for + // the extension to finish connecting to the server. + await vscode.commands.executeCommand("vscode.open", vscode.Uri.file(pesterTests)); + await sleep(15000); + + // Now run the Pester tests, check the debugger started, wait a bit for + // it to run, and then kill it for safety's sake. + assert(await vscode.commands.executeCommand("PowerShell.RunPesterTestsFromFile")); + assert(vscode.debug.activeDebugSession !== undefined); + await sleep(5000); + await vscode.debug.stopDebugging(); + }).timeout(30000); }); diff --git a/test/runTests.ts b/test/runTests.ts index 6bff341563..b449ae7cc4 100644 --- a/test/runTests.ts +++ b/test/runTests.ts @@ -29,7 +29,7 @@ async function main() { }); } catch (err) { // tslint:disable-next-line:no-console - console.error("Failed to run tests"); + console.error(`Failed to run tests: ${err}`); process.exit(1); } } diff --git a/test/utils.ts b/test/utils.ts new file mode 100644 index 0000000000..dd3fd01624 --- /dev/null +++ b/test/utils.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +"use strict"; + +import * as path from "path"; +import * as vscode from "vscode"; + +// This lets us test the rest of our path assumptions against the baseline of +// this test file existing at `/out/test/utils.js`. +export const rootPath = path.resolve(__dirname, "../../") +// tslint:disable-next-line: no-var-requires +const packageJSON: any = require(path.resolve(rootPath, "package.json")); +export const extensionId = `${packageJSON.publisher}.${packageJSON.name}`; + +export async function ensureExtensionIsActivated(): Promise> { + const extension = vscode.extensions.getExtension(extensionId); + if (!extension.isActive) { await extension.activate(); } + return extension; +}