diff --git a/assets/test/Swift-Markdown/Package.swift b/assets/test/Swift-Markdown/Package.swift new file mode 100644 index 000000000..1661647f4 --- /dev/null +++ b/assets/test/Swift-Markdown/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version:5.6 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + // FIXME: Can be changed back to Swift-Markdown when + // https://github.com/swiftlang/swift-package-manager/issues/7931 + // is released in the toolchain + // NB: The name here needs to match the name of the dependencies under assets/test/dependencies/Package.swift + name: "swift-markdown", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "PackageLib", + targets: ["PackageLib"]), + ], + targets: [ + .target( + name: "PackageLib", + dependencies: [] + ), + ] +) diff --git a/assets/test/Swift-Markdown/Sources/PackageLib/PackageLib.swift b/assets/test/Swift-Markdown/Sources/PackageLib/PackageLib.swift new file mode 100644 index 000000000..ad0a342ba --- /dev/null +++ b/assets/test/Swift-Markdown/Sources/PackageLib/PackageLib.swift @@ -0,0 +1 @@ +public let a = "B" diff --git a/assets/test/dependencies/Package.swift b/assets/test/dependencies/Package.swift index 92af0f44e..037aa98a6 100644 --- a/assets/test/dependencies/Package.swift +++ b/assets/test/dependencies/Package.swift @@ -12,7 +12,7 @@ let package = Package( targets: [ .executableTarget( name: "dependencies", - dependencies: [], + dependencies: [.product(name: "PackageLib", package: "Swift-Markdown")], path: "Sources"), ] ) diff --git a/assets/test/dependencies/Sources/main.swift b/assets/test/dependencies/Sources/main.swift index afe1ffafa..a53a27672 100644 --- a/assets/test/dependencies/Sources/main.swift +++ b/assets/test/dependencies/Sources/main.swift @@ -1,2 +1,4 @@ +import PackageLib -print("dependencies") \ No newline at end of file +print("Test Asset:(dependencies)") +print(a) \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index 28e33560b..63be024dd 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -68,6 +68,12 @@ export enum Commands { RUN = "swift.run", DEBUG = "swift.debug", CLEAN_BUILD = "swift.cleanBuild", + RESOLVE_DEPENDENCIES = "swift.resolveDependencies", + UPDATE_DEPENDENCIES = "swift.updateDependencies", + RUN_TESTS_MULTIPLE_TIMES = "swift.runTestsMultipleTimes", + RESET_PACKAGE = "swift.resetPackage", + USE_LOCAL_DEPENDENCY = "swift.useLocalDependency", + UNEDIT_DEPENDENCY = "swift.uneditDependency", } /** @@ -76,14 +82,16 @@ export enum Commands { export function register(ctx: WorkspaceContext): vscode.Disposable[] { return [ vscode.commands.registerCommand("swift.newFile", uri => newSwiftFile(uri)), - vscode.commands.registerCommand("swift.resolveDependencies", () => + vscode.commands.registerCommand(Commands.RESOLVE_DEPENDENCIES, () => resolveDependencies(ctx) ), - vscode.commands.registerCommand("swift.updateDependencies", () => updateDependencies(ctx)), + vscode.commands.registerCommand(Commands.UPDATE_DEPENDENCIES, () => + updateDependencies(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 => { + vscode.commands.registerCommand(Commands.RUN_TESTS_MULTIPLE_TIMES, item => { if (ctx.currentFolder) { return runTestMultipleTimes(ctx.currentFolder, item, false); } @@ -95,7 +103,7 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { }), // Note: This is only available on macOS (gated in `package.json`) because its the only OS that has the iOS SDK available. vscode.commands.registerCommand("swift.switchPlatform", () => switchPlatform()), - vscode.commands.registerCommand("swift.resetPackage", () => resetPackage(ctx)), + vscode.commands.registerCommand(Commands.RESET_PACKAGE, () => resetPackage(ctx)), vscode.commands.registerCommand("swift.runScript", () => runSwiftScript(ctx)), vscode.commands.registerCommand("swift.openPackage", () => { if (ctx.currentFolder) { @@ -112,7 +120,7 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { vscode.commands.registerCommand("swift.insertFunctionComment", () => insertFunctionComment(ctx) ), - vscode.commands.registerCommand("swift.useLocalDependency", item => { + vscode.commands.registerCommand(Commands.USE_LOCAL_DEPENDENCY, item => { if (item instanceof PackageNode) { return useLocalDependency(item.name, ctx); } @@ -122,7 +130,7 @@ export function register(ctx: WorkspaceContext): vscode.Disposable[] { return editDependency(item.name, ctx); } }), - vscode.commands.registerCommand("swift.uneditDependency", item => { + vscode.commands.registerCommand(Commands.UNEDIT_DEPENDENCY, item => { if (item instanceof PackageNode) { return uneditDependency(item.name, ctx); } diff --git a/src/commands/build.ts b/src/commands/build.ts index b6decb5a1..afd2bfe67 100644 --- a/src/commands/build.ts +++ b/src/commands/build.ts @@ -68,7 +68,10 @@ export async function folderCleanBuild(folderContext: FolderContext) { /** * Executes a {@link vscode.Task task} to debug swift target. */ -async function debugBuildWithOptions(ctx: WorkspaceContext, options: vscode.DebugSessionOptions) { +export async function debugBuildWithOptions( + ctx: WorkspaceContext, + options: vscode.DebugSessionOptions +) { const current = ctx.currentFolder; if (!current) { return; diff --git a/src/commands/dependencies/resolve.ts b/src/commands/dependencies/resolve.ts index bc7664315..542c376b9 100644 --- a/src/commands/dependencies/resolve.ts +++ b/src/commands/dependencies/resolve.ts @@ -24,9 +24,10 @@ import { executeTaskWithUI, updateAfterError } from "../utilities"; export async function resolveDependencies(ctx: WorkspaceContext) { const current = ctx.currentFolder; if (!current) { - return; + ctx.outputChannel.log("currentFolder is not set."); + return false; } - await resolveFolderDependencies(current); + return await resolveFolderDependencies(current); } /** @@ -57,4 +58,5 @@ export async function resolveFolderDependencies( checkAlreadyRunning ); updateAfterError(success, folderContext); + return success; } diff --git a/src/commands/dependencies/unedit.ts b/src/commands/dependencies/unedit.ts index 9f989955b..521d71afd 100644 --- a/src/commands/dependencies/unedit.ts +++ b/src/commands/dependencies/unedit.ts @@ -26,12 +26,13 @@ import { FolderContext } from "../../FolderContext"; export async function uneditDependency(identifier: string, ctx: WorkspaceContext) { const currentFolder = ctx.currentFolder; if (!currentFolder) { - return; + ctx.outputChannel.log("currentFolder is not set."); + return false; } ctx.outputChannel.log(`unedit dependency ${identifier}`, currentFolder.name); const status = `Reverting edited dependency ${identifier} (${currentFolder.name})`; - ctx.statusItem.showStatusWhileRunning(status, async () => { - await uneditFolderDependency(currentFolder, identifier, ctx); + return await ctx.statusItem.showStatusWhileRunning(status, async () => { + return await uneditFolderDependency(currentFolder, identifier, ctx); }); } @@ -67,6 +68,7 @@ async function uneditFolderDependency( vscode.workspace.updateWorkspaceFolders(folderIndex, 1); } } + return true; } catch (error) { const execError = error as { stderr: string }; // if error contains "has uncommited changes" then ask if user wants to force the unedit @@ -79,12 +81,13 @@ async function uneditFolderDependency( if (result === "No") { ctx.outputChannel.log(execError.stderr, folder.name); - return; + return false; } await uneditFolderDependency(folder, identifier, ctx, ["--force"]); } else { ctx.outputChannel.log(execError.stderr, folder.name); vscode.window.showErrorMessage(`${execError.stderr}`); } + return false; } } diff --git a/src/commands/dependencies/update.ts b/src/commands/dependencies/update.ts index 50da30b44..b32423c7a 100644 --- a/src/commands/dependencies/update.ts +++ b/src/commands/dependencies/update.ts @@ -24,9 +24,10 @@ import { executeTaskWithUI, updateAfterError } from "./../utilities"; export async function updateDependencies(ctx: WorkspaceContext) { const current = ctx.currentFolder; if (!current) { - return; + ctx.outputChannel.log("currentFolder is not set."); + return false; } - await updateFolderDependencies(current); + return await updateFolderDependencies(current); } /** @@ -46,7 +47,7 @@ export async function updateFolderDependencies(folderContext: FolderContext) { folderContext.workspaceContext.toolchain ); - await executeTaskWithUI(task, "Updating Dependencies", folderContext).then(result => { - updateAfterError(result, folderContext); - }); + const result = await executeTaskWithUI(task, "Updating Dependencies", folderContext); + updateAfterError(result, folderContext); + return result; } diff --git a/src/commands/dependencies/useLocal.ts b/src/commands/dependencies/useLocal.ts index 0762f793e..34c673f68 100644 --- a/src/commands/dependencies/useLocal.ts +++ b/src/commands/dependencies/useLocal.ts @@ -24,10 +24,14 @@ import { executeTaskWithUI } from "../utilities"; * @param identifier Identifier for dependency * @param ctx workspace context */ -export async function useLocalDependency(identifier: string, ctx: WorkspaceContext) { +export async function useLocalDependency( + identifier: string, + ctx: WorkspaceContext +): Promise { const currentFolder = ctx.currentFolder; if (!currentFolder) { - return; + ctx.outputChannel.log("currentFolder is not set."); + return false; } const folders = await vscode.window.showOpenDialog({ canSelectFiles: false, @@ -39,7 +43,7 @@ export async function useLocalDependency(identifier: string, ctx: WorkspaceConte }); if (!folders) { - return; + return false; } const folder = folders[0]; const task = createSwiftTask( @@ -62,4 +66,5 @@ export async function useLocalDependency(identifier: string, ctx: WorkspaceConte if (success) { ctx.fireEvent(currentFolder, FolderOperation.resolvedUpdated); } + return success; } diff --git a/src/commands/resetPackage.ts b/src/commands/resetPackage.ts index 3406d4cde..6d621dee2 100644 --- a/src/commands/resetPackage.ts +++ b/src/commands/resetPackage.ts @@ -26,7 +26,7 @@ export async function resetPackage(ctx: WorkspaceContext) { if (!current) { return; } - await folderResetPackage(current); + return await folderResetPackage(current); } /** @@ -47,22 +47,32 @@ export async function folderResetPackage(folderContext: FolderContext) { folderContext.workspaceContext.toolchain ); - await executeTaskWithUI(task, "Reset Package", folderContext).then(async success => { - if (!success) { - return; - } - const resolveTask = createSwiftTask( - ["package", "resolve"], - SwiftTaskProvider.resolvePackageName, - { - cwd: folderContext.folder, - scope: folderContext.workspaceFolder, - prefix: folderContext.name, - presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, - }, - folderContext.workspaceContext.toolchain - ); + return await executeTaskWithUI(task, "Reset Package", folderContext).then( + async success => { + if (!success) { + return false; + } + const resolveTask = createSwiftTask( + ["package", "resolve"], + SwiftTaskProvider.resolvePackageName, + { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + prefix: folderContext.name, + presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, + }, + folderContext.workspaceContext.toolchain + ); - await executeTaskWithUI(resolveTask, "Resolving Dependencies", folderContext); - }); + const result = await executeTaskWithUI( + resolveTask, + "Resolving Dependencies", + folderContext + ); + return result; + }, + reason => { + return reason; + } + ); } diff --git a/test/integration-tests/commands/dependency.test.ts b/test/integration-tests/commands/dependency.test.ts new file mode 100644 index 000000000..019b22aba --- /dev/null +++ b/test/integration-tests/commands/dependency.test.ts @@ -0,0 +1,142 @@ +//===----------------------------------------------------------------------===// +// +// 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 { expect } from "chai"; +import * as vscode from "vscode"; +import { + PackageDependenciesProvider, + PackageNode, +} from "../../../src/ui/PackageDependencyProvider"; +import { folderContextPromise, globalWorkspaceContextPromise } from "../extension.test"; +import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilities"; +import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; +import { testAssetUri } from "../../fixtures"; +import { FolderContext } from "../../../src/FolderContext"; +import { WorkspaceContext } from "../../../src/WorkspaceContext"; +import * as sinon from "sinon"; +import { Commands } from "../../../src/commands"; + +suite("Dependency Commmands Test Suite", function () { + // full workflow's interaction with spm is longer than the default timeout + // 15 seconds for each test should be more than enough + this.timeout(15 * 1000); + + suite("spm Resolve Update Contract Tests", function () { + let folderContext: FolderContext; + let workspaceContext: WorkspaceContext; + + suiteSetup(async function () { + workspaceContext = await globalWorkspaceContextPromise; + await waitForNoRunningTasks(); + folderContext = await folderContextPromise("dependencies"); + await workspaceContext.focusFolder(folderContext); + }); + + test("Contract: spm resolve", async () => { + const result = await vscode.commands.executeCommand(Commands.RESOLVE_DEPENDENCIES); + expect(result).to.be.true; + }); + }); + + suite("Full Work Flow Test Suite", function () { + let folderContext: FolderContext; + let workspaceContext: WorkspaceContext; + let tasks: SwiftTask; + let treeProvider: PackageDependenciesProvider; + let item: PackageNode; + + suiteSetup(async function () { + workspaceContext = await globalWorkspaceContextPromise; + await waitForNoRunningTasks(); + folderContext = await folderContextPromise("dependencies"); + await workspaceContext.focusFolder(folderContext); + treeProvider = new PackageDependenciesProvider(workspaceContext); + + const items = await treeProvider.getChildren(); + item = items.find(n => n.name === "swift-markdown") as PackageNode; + }); + + suiteTeardown(() => { + treeProvider?.dispose(); + }); + + setup(async function () { + // Check before each test case start: + // Expect to fail without setting up local version + tasks = (await getBuildAllTask(folderContext)) as SwiftTask; + const { exitCode, output } = await executeTaskAndWaitForResult(tasks); + expect(exitCode).to.not.equal(0); + expect(output).to.include("PackageLib"); + expect(output).to.include("required"); + }); + + teardown(async function () { + // Expect to fail again now dependency is missing + const { exitCode, output } = await executeTaskAndWaitForResult(tasks); + expect(exitCode).to.not.equal(0); + expect(output).to.include("PackageLib"); + expect(output).to.include("required"); + }); + + const useLocalDependencyTest = async () => { + // Contract: spm edit with user supplied local version of dependency + const windowMock = sinon.stub(vscode.window, "showOpenDialog"); + windowMock.resolves([testAssetUri("Swift-Markdown")]); + const result = await vscode.commands.executeCommand( + Commands.USE_LOCAL_DEPENDENCY, + item + ); + expect(result).to.be.true; + windowMock.restore(); + + // This will now pass as we have the required library + const { exitCode, output } = await executeTaskAndWaitForResult(tasks); + expect(exitCode).to.equal(0); + expect(output).to.include("defaultpackage"); + expect(output).to.include("not used by any target"); + }; + + test("Use local dependency - Reset", async () => { + await useLocalDependencyTest(); + + // Contract: spm reset + const result = await vscode.commands.executeCommand(Commands.RESET_PACKAGE); + expect(result).to.be.true; + }); + + test("Use local dependency - Add to workspace - Unedit", async () => { + await useLocalDependencyTest(); + + // Contract: spm unedit + const result = await vscode.commands.executeCommand(Commands.UNEDIT_DEPENDENCY, item); + expect(result).to.be.true; + }); + + test("Contract: spm update", async function () { + // This test is flaky, test in CI setting when the below change get merged in and find + // out exactly where the command fails. + // https://github.com/swiftlang/vscode-swift/pull/1194 + this.skip(); + await useLocalDependencyTest(); + + // Contract: spm update + let result = await vscode.commands.executeCommand(Commands.UPDATE_DEPENDENCIES); + expect(result).to.be.true; + + // Clean up + result = await vscode.commands.executeCommand(Commands.UNEDIT_DEPENDENCY, item); + expect(result).to.be.true; + }); + }); +}); diff --git a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts index 02bd97b60..81183e538 100644 --- a/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts +++ b/test/integration-tests/testexplorer/TestExplorerIntegration.test.ts @@ -42,6 +42,7 @@ import { reduceTestItemChildren, } from "../../../src/TestExplorer/TestUtils"; import { runnableTag } from "../../../src/TestExplorer/TestDiscovery"; +import { Commands } from "../../../src/commands"; suite("Test Explorer Suite", function () { const MAX_TEST_RUN_TIME_MINUTES = 5; @@ -304,7 +305,7 @@ suite("Test Explorer Suite", function () { // Stub the showInputBox method to return the input text windowMock.showInputBox.resolves(`${numIterations}`); - vscode.commands.executeCommand("swift.runTestsMultipleTimes", testItems[0]); + vscode.commands.executeCommand(Commands.RUN_TESTS_MULTIPLE_TIMES, testItems[0]); const testRun = await eventPromise(testExplorer.onCreateTestRun); @@ -419,7 +420,7 @@ suite("Test Explorer Suite", function () { // Stub the showInputBox method to return the input text windowMock.showInputBox.resolves(`${numIterations}`); - vscode.commands.executeCommand("swift.runTestsMultipleTimes", testItems[0]); + vscode.commands.executeCommand(Commands.RUN_TESTS_MULTIPLE_TIMES, testItems[0]); const testRun = await eventPromise(testExplorer.onCreateTestRun);