From d6590e893c735098ca032d2e4099181dbbe918be Mon Sep 17 00:00:00 2001 From: Michael Weng Date: Fri, 8 Nov 2024 15:26:45 -0500 Subject: [PATCH] Add integration tests for build commands (#1185) * Add integration tests for build commands - Validate the workflow of user calling the Swift: Run Build/Clean Build/Debug Build commands. - Ensure Swift: Run Build will not get blocked by pre-set breakpoint. - Ensure Swift: Clean Build will result in a cleaned up .build folder. - Ensure Swift: Debug Build will stop on a breakpoint and resume. Issue: #1184 * - Added module enum for workbench commands string constant - Added comments for clarification - Added utilities to listen for dap message, this is useful for test synchronization. Code takes inspiration from https://github.com/swiftlang/vscode-swift/pull/1126 * - Rename from utilies/command.ts to utilies/commands.ts - Minor cosmetic change to utilies/commands.ts * - Fix a bug in updateSettings where promise is not being exlicitly returned, causing restore of setting being not awaitable - Make makeDebugConfigurations to be awaitable - Change launch to also update the key for ASLR disable settings - Make the test properly set up and reset the settings that update the launch config --- src/commands.ts | 12 ++- src/commands/build.ts | 8 +- src/debugger/launch.ts | 17 +++- src/ui/ReloadExtension.ts | 3 +- src/utilities/commands.ts | 19 ++++ .../BackgroundCompilation.test.ts | 3 +- .../DiagnosticsManager.test.ts | 3 +- test/integration-tests/commands/build.test.ts | 97 +++++++++++++++++++ .../editor/CommentCompletion.test.ts | 3 +- .../TestExplorerIntegration.test.ts | 3 +- .../testexplorer/utilities.ts | 6 +- test/unit-tests/ui/ReloadExtension.test.ts | 3 +- test/utilities/debug.ts | 78 +++++++++++++++ 13 files changed, 234 insertions(+), 21 deletions(-) create mode 100644 src/utilities/commands.ts create mode 100644 test/integration-tests/commands/build.test.ts create mode 100644 test/utilities/debug.ts diff --git a/src/commands.ts b/src/commands.ts index bd366549c..28e33560b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -64,6 +64,12 @@ export function registerToolchainCommands( ]; } +export enum Commands { + RUN = "swift.run", + DEBUG = "swift.debug", + CLEAN_BUILD = "swift.cleanBuild", +} + /** * Registers this extension's commands in the given {@link vscode.ExtensionContext context}. */ @@ -74,9 +80,9 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { resolveDependencies(ctx) ), vscode.commands.registerCommand("swift.updateDependencies", () => updateDependencies(ctx)), - vscode.commands.registerCommand("swift.run", () => runBuild(ctx)), - vscode.commands.registerCommand("swift.debug", () => debugBuild(ctx)), - vscode.commands.registerCommand("swift.cleanBuild", () => cleanBuild(ctx)), + vscode.commands.registerCommand(Commands.RUN, () => runBuild(ctx)), + vscode.commands.registerCommand(Commands.DEBUG, () => debugBuild(ctx)), + vscode.commands.registerCommand(Commands.CLEAN_BUILD, () => cleanBuild(ctx)), vscode.commands.registerCommand("swift.runTestsMultipleTimes", item => { if (ctx.currentFolder) { return runTestMultipleTimes(ctx.currentFolder, item, false); diff --git a/src/commands/build.ts b/src/commands/build.ts index 6a36b521c..b6decb5a1 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -23,14 +23,14 @@ import { FolderContext } from "../FolderContext"; * Executes a {@link vscode.Task task} to run swift target. */ export async function runBuild(ctx: WorkspaceContext) { - await debugBuildWithOptions(ctx, { noDebug: true }); + return await debugBuildWithOptions(ctx, { noDebug: true }); } /** * Executes a {@link vscode.Task task} to debug swift target. */ export async function debugBuild(ctx: WorkspaceContext) { - await debugBuildWithOptions(ctx, {}); + return await debugBuildWithOptions(ctx, {}); } /** @@ -41,7 +41,7 @@ export async function cleanBuild(ctx: WorkspaceContext) { if (!current) { return; } - await folderCleanBuild(current); + return await folderCleanBuild(current); } /** @@ -62,7 +62,7 @@ export async function folderCleanBuild(folderContext: FolderContext) { folderContext.workspaceContext.toolchain ); - await executeTaskWithUI(task, "Clean Build", folderContext); + return await executeTaskWithUI(task, "Clean Build", folderContext); } /** diff --git a/src/debugger/launch.ts b/src/debugger/launch.ts index e96c42023..87bf04edf 100644 --- a/src/debugger/launch.ts +++ b/src/debugger/launch.ts @@ -29,9 +29,13 @@ import { CI_DISABLE_ASLR } from "./lldb"; * @param ctx folder context to create launch configurations for * @param yes automatically answer yes to dialogs */ -export async function makeDebugConfigurations(ctx: FolderContext, message?: string, yes = false) { +export async function makeDebugConfigurations( + ctx: FolderContext, + message?: string, + yes = false +): Promise { if (!configuration.folder(ctx.workspaceFolder).autoGenerateLaunchConfigurations) { - return; + return false; } const wsLaunchSection = vscode.workspace.getConfiguration("launch", ctx.folder); const launchConfigs = wsLaunchSection.get("configurations") || []; @@ -41,6 +45,8 @@ export async function makeDebugConfigurations(ctx: FolderContext, message?: stri "cwd", "preLaunchTask", "type", + "disableASLR", + "initCommands", `env.${swiftLibraryPathKey()}`, ]; const configUpdates: { index: number; config: vscode.DebugConfiguration }[] = []; @@ -96,6 +102,7 @@ export async function makeDebugConfigurations(ctx: FolderContext, message?: stri vscode.ConfigurationTarget.WorkspaceFolder ); } + return true; } // Return debug launch configuration for an executable in the given folder @@ -178,15 +185,17 @@ export async function debugLaunchConfig( config: vscode.DebugConfiguration, options: vscode.DebugSessionOptions = {} ) { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { vscode.debug.startDebugging(workspaceFolder, config, options).then( started => { if (started) { const terminateSession = vscode.debug.onDidTerminateDebugSession(async () => { // dispose terminate debug handler terminateSession.dispose(); - resolve(); + resolve(true); }); + } else { + resolve(false); } }, reason => { diff --git a/src/ui/ReloadExtension.ts b/src/ui/ReloadExtension.ts index 2ae9f2c8d..b8d201589 100644 --- a/src/ui/ReloadExtension.ts +++ b/src/ui/ReloadExtension.ts @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import * as vscode from "vscode"; +import { Workbench } from "../utilities/commands"; /** * Prompts the user to reload the extension in cases where we are unable to do @@ -29,7 +30,7 @@ export async function showReloadExtensionNotification( const buttons: ("Reload Extensions" | T)[] = ["Reload Extensions", ...items]; const selected = await vscode.window.showWarningMessage(message, ...buttons); if (selected === "Reload Extensions") { - await vscode.commands.executeCommand("workbench.action.reloadWindow"); + await vscode.commands.executeCommand(Workbench.ACTION_RELOADWINDOW); } return selected; } diff --git a/src/utilities/commands.ts b/src/utilities/commands.ts new file mode 100644 index 000000000..cc994f76c --- /dev/null +++ b/src/utilities/commands.ts @@ -0,0 +1,19 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +export enum Workbench { + ACTION_DEBUG_CONTINUE = "workbench.action.debug.continue", + ACTION_CLOSEALLEDITORS = "workbench.action.closeAllEditors", + ACTION_RELOADWINDOW = "workbench.action.reloadWindow", +} diff --git a/test/integration-tests/BackgroundCompilation.test.ts b/test/integration-tests/BackgroundCompilation.test.ts index 08b14961a..e39ecd3f7 100644 --- a/test/integration-tests/BackgroundCompilation.test.ts +++ b/test/integration-tests/BackgroundCompilation.test.ts @@ -18,6 +18,7 @@ import { WorkspaceContext } from "../../src/WorkspaceContext"; import { globalWorkspaceContextPromise } from "./extension.test"; import { testAssetUri } from "../fixtures"; import { waitForNoRunningTasks } from "../utilities"; +import { Workbench } from "../../src/utilities/commands"; suite("BackgroundCompilation Test Suite", () => { let workspaceContext: WorkspaceContext; @@ -31,7 +32,7 @@ suite("BackgroundCompilation Test Suite", () => { suiteTeardown(async () => { await vscode.workspace.getConfiguration("swift").update("backgroundCompilation", undefined); - await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS); }); test("build all on save @slow", async () => { diff --git a/test/integration-tests/DiagnosticsManager.test.ts b/test/integration-tests/DiagnosticsManager.test.ts index 28acdd340..e84d2d18c 100644 --- a/test/integration-tests/DiagnosticsManager.test.ts +++ b/test/integration-tests/DiagnosticsManager.test.ts @@ -23,6 +23,7 @@ import { DiagnosticsManager } from "../../src/DiagnosticsManager"; import { FolderContext } from "../../src/FolderContext"; import { Version } from "../../src/utilities/version"; import { folderContextPromise, globalWorkspaceContextPromise } from "./extension.test"; +import { Workbench } from "../../src/utilities/commands"; const waitForDiagnostics = (uris: vscode.Uri[], allowEmpty: boolean = true) => new Promise(res => @@ -907,7 +908,7 @@ suite("DiagnosticsManager Test Suite", async function () { }); teardown(async () => { - await vscode.commands.executeCommand("workbench.action.closeAllEditors"); + await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS); }); test("Provides swift diagnostics", async () => { diff --git a/test/integration-tests/commands/build.test.ts b/test/integration-tests/commands/build.test.ts new file mode 100644 index 000000000..9d59656a5 --- /dev/null +++ b/test/integration-tests/commands/build.test.ts @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; +import { expect } from "chai"; +import { folderContextPromise, globalWorkspaceContextPromise } from "../extension.test"; +import { waitForNoRunningTasks } from "../../utilities"; +import { testAssetUri } from "../../fixtures"; +import { FolderContext } from "../../../src/FolderContext"; +import { WorkspaceContext } from "../../../src/WorkspaceContext"; +import { Commands } from "../../../src/commands"; +import { makeDebugConfigurations } from "../../../src/debugger/launch"; +import { Workbench } from "../../../src/utilities/commands"; +import { continueSession, waitForDebugAdapterCommand } from "../../utilities/debug"; +import { SettingsMap, updateSettings } from "../testexplorer/utilities"; + +suite("Build Commands", function () { + let folderContext: FolderContext; + let workspaceContext: WorkspaceContext; + let settingsTeardown: () => Promise; + const uri = testAssetUri("defaultPackage/Sources/PackageExe/main.swift"); + const breakpoints = [ + new vscode.SourceBreakpoint(new vscode.Location(uri, new vscode.Position(2, 0))), + ]; + + suiteSetup(async function () { + workspaceContext = await globalWorkspaceContextPromise; + await waitForNoRunningTasks(); + folderContext = await folderContextPromise("defaultPackage"); + await workspaceContext.focusFolder(folderContext); + await vscode.window.showTextDocument(uri); + settingsTeardown = await updateSettings({ + "swift.autoGenerateLaunchConfigurations": true, + }); + await makeDebugConfigurations(folderContext, undefined, true); + }); + + suiteTeardown(async () => { + await settingsTeardown(); + await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS); + }); + + test("Swift: Run Build", async () => { + // A breakpoint will have not effect on the Run command. + vscode.debug.addBreakpoints(breakpoints); + + const result = await vscode.commands.executeCommand(Commands.RUN); + expect(result).to.be.true; + + vscode.debug.removeBreakpoints(breakpoints); + }); + + test("Swift: Clean Build", async () => { + const buildPath = path.join(folderContext.folder.fsPath, ".build"); + const beforeItemCount = fs.readdirSync(buildPath).length; + + const result = await vscode.commands.executeCommand(Commands.CLEAN_BUILD); + expect(result).to.be.true; + + const afterItemCount = fs.readdirSync(buildPath).length; + // This test will run in order after the Swift: Run Build test, + // where .build folder is going to be filled with built artifacts. + // After executing the clean command the build directory is guranteed to have less entry. + expect(afterItemCount).to.be.lessThan(beforeItemCount); + }); + + test("Swift: Debug Build @slow", async () => { + vscode.debug.addBreakpoints(breakpoints); + // Promise used to indicate we hit the break point. + // NB: "stopped" is the exact command when debuggee has stopped due to break point, + // but "stackTrace" is the deterministic sync point we will use to make sure we can execute continue + const bpPromise = waitForDebugAdapterCommand( + "Debug PackageExe (defaultPackage)", + "stackTrace", + workspaceContext + ); + + const result = vscode.commands.executeCommand(Commands.DEBUG); + expect(result).to.eventually.be.true; + + await bpPromise.then(() => continueSession()); + vscode.debug.removeBreakpoints(breakpoints); + }); +}); diff --git a/test/integration-tests/editor/CommentCompletion.test.ts b/test/integration-tests/editor/CommentCompletion.test.ts index 9f8655a91..0d4f9ecb8 100644 --- a/test/integration-tests/editor/CommentCompletion.test.ts +++ b/test/integration-tests/editor/CommentCompletion.test.ts @@ -15,6 +15,7 @@ import * as assert from "assert"; import * as vscode from "vscode"; import { CommentCompletionProviders } from "../../../src/editor/CommentCompletion"; +import { Workbench } from "../../../src/utilities/commands"; suite("CommentCompletion Test Suite", () => { let document: vscode.TextDocument | undefined; @@ -31,7 +32,7 @@ suite("CommentCompletion Test Suite", () => { if (editor && document) { await vscode.window.showTextDocument(document, editor.viewColumn); - await vscode.commands.executeCommand("workbench.action.closeActiveEditor"); + await vscode.commands.executeCommand(Workbench.ACTION_CLOSEALLEDITORS); } provider.dispose(); diff --git a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts index cdd9533a4..02bd97b60 100644 --- a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts +++ b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts @@ -24,6 +24,7 @@ import { eventPromise, gatherTests, runTest, + SettingsMap, setupTestExplorerTest, waitForTestExplorerReady, } from "./utilities"; @@ -51,7 +52,7 @@ suite("Test Explorer Suite", function () { let testExplorer: TestExplorer; suite("Debugging", function () { - let settingsTeardown: () => void; + let settingsTeardown: () => Promise; async function runXCTest() { const suiteId = "PackageTests.PassingXCTestSuite"; diff --git a/test/integration-tests/testexplorer/utilities.ts b/test/integration-tests/testexplorer/utilities.ts index 4f7a9b52b..ec310067d 100644 --- a/test/integration-tests/testexplorer/utilities.ts +++ b/test/integration-tests/testexplorer/utilities.ts @@ -188,7 +188,7 @@ export type SettingsMap = { [key: string]: unknown }; * "section.name" format, and the value is the new setting value. * @returns A function that, when called, resets the settings back to their original values. */ -export async function updateSettings(settings: SettingsMap): Promise<() => Promise> { +export async function updateSettings(settings: SettingsMap): Promise<() => Promise> { const applySettings = async (settings: SettingsMap) => { const savedOriginalSettings: SettingsMap = {}; Object.keys(settings).forEach(async setting => { @@ -224,9 +224,7 @@ export async function updateSettings(settings: SettingsMap): Promise<() => Promi const savedOriginalSettings = await applySettings(settings); // Clients call the callback to reset updated settings to their original value - return async () => { - await applySettings(savedOriginalSettings); - }; + return async () => await applySettings(savedOriginalSettings); } /** diff --git a/test/unit-tests/ui/ReloadExtension.test.ts b/test/unit-tests/ui/ReloadExtension.test.ts index 2c3b3052f..422bfce23 100644 --- a/test/unit-tests/ui/ReloadExtension.test.ts +++ b/test/unit-tests/ui/ReloadExtension.test.ts @@ -15,6 +15,7 @@ import { expect } from "chai"; import { mockGlobalObject } from "../../MockUtils"; import * as vscode from "vscode"; import { showReloadExtensionNotification } from "../../../src/ui/ReloadExtension"; +import { Workbench } from "../../../src/utilities/commands"; suite("showReloadExtensionNotification()", async function () { const mockedVSCodeWindow = mockGlobalObject(vscode, "window"); @@ -38,7 +39,7 @@ suite("showReloadExtensionNotification()", async function () { await showReloadExtensionNotification("Want to reload?"); expect(mockedVSCodeCommands.executeCommand).to.have.been.calledOnceWithExactly( - "workbench.action.reloadWindow" + Workbench.ACTION_RELOADWINDOW ); }); diff --git a/test/utilities/debug.ts b/test/utilities/debug.ts new file mode 100644 index 000000000..083438ab2 --- /dev/null +++ b/test/utilities/debug.ts @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2024 the VS Code Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of VS Code Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import * as vscode from "vscode"; +import { Workbench } from "../../src/utilities/commands"; +import { DebugAdapter } from "../../src/debugger/debugAdapter"; +import { WorkspaceContext } from "../../src/WorkspaceContext"; + +export async function continueSession(): Promise { + await vscode.commands.executeCommand(Workbench.ACTION_DEBUG_CONTINUE); +} + +/** + * Waits for a specific message from the debug adapter. + * + * @param name The name of the debug session to wait for. + * @param matches A function to match the desired message. + * @param workspaceContext The workspace context containing toolchain information. + * @returns A promise that resolves with the matching message. + */ +export async function waitForDebugAdapterMessage( + name: string, + matches: (message: any) => boolean, + workspaceContext: WorkspaceContext +): Promise { + return await new Promise(res => { + const disposable = vscode.debug.registerDebugAdapterTrackerFactory( + DebugAdapter.getLaunchConfigType(workspaceContext.toolchain.swiftVersion), + { + createDebugAdapterTracker: function ( + session: vscode.DebugSession + ): vscode.ProviderResult { + if (session.name !== name) { + return; + } + return { + onDidSendMessage(message) { + if (matches(message)) { + disposable.dispose(); + res(message); + } + }, + }; + }, + } + ); + }); +} + +/** + * Waits for a specific command to be sent by the debug adapter. + * + * @param name The name of the debug session to wait for. + * @param command The command to wait for. + * @param workspaceContext The workspace context containing toolchain information. + * @returns A promise that resolves with the matching command message. + */ +export async function waitForDebugAdapterCommand( + name: string, + command: string, + workspaceContext: WorkspaceContext +): Promise { + return await waitForDebugAdapterMessage( + name, + (m: any) => m.command === command, + workspaceContext + ); +}