From b9afd9bb621c43f57c223a88904ab6f08f497f9e Mon Sep 17 00:00:00 2001 From: "Michael (SPG) Weng" Date: Tue, 15 Oct 2024 13:25:33 -0400 Subject: [PATCH] Add integration tests for package dependencies - Verify various contract calls with spm - Exercises use local version workflow Issue: #1052 --- assets/test/Swift-Markdown/Package.swift | 23 ++ .../Sources/PackageLib/PackageLib.swift | 1 + assets/test/dependencies/Package.swift | 2 +- assets/test/dependencies/Sources/main.swift | 4 +- src/commands/build.ts | 5 +- src/commands/dependencies/unedit.ts | 2 +- .../ui/PackageDependencyProvider.test.ts | 257 ++++++++++++------ 7 files changed, 200 insertions(+), 94 deletions(-) create mode 100644 assets/test/Swift-Markdown/Package.swift create mode 100644 assets/test/Swift-Markdown/Sources/PackageLib/PackageLib.swift diff --git a/assets/test/Swift-Markdown/Package.swift b/assets/test/Swift-Markdown/Package.swift new file mode 100644 index 000000000..5d582c6cd --- /dev/null +++ b/assets/test/Swift-Markdown/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:6.0 +// 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 + 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/build.ts b/src/commands/build.ts index 6a36b521c..f7c151db7 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/unedit.ts b/src/commands/dependencies/unedit.ts index 9f989955b..e8641cf28 100644 --- a/src/commands/dependencies/unedit.ts +++ b/src/commands/dependencies/unedit.ts @@ -35,7 +35,7 @@ export async function uneditDependency(identifier: string, ctx: WorkspaceContext }); } -async function uneditFolderDependency( +export async function uneditFolderDependency( folder: FolderContext, identifier: string, ctx: WorkspaceContext, diff --git a/test/integration-tests/ui/PackageDependencyProvider.test.ts b/test/integration-tests/ui/PackageDependencyProvider.test.ts index 57c4ce6f4..41a96583c 100644 --- a/test/integration-tests/ui/PackageDependencyProvider.test.ts +++ b/test/integration-tests/ui/PackageDependencyProvider.test.ts @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import { expect } from "chai"; +import * as vscode from "vscode"; import { PackageDependenciesProvider, PackageNode, @@ -20,100 +21,176 @@ import { import { folderContextPromise, globalWorkspaceContextPromise } from "../extension.test"; import { executeTaskAndWaitForResult, waitForNoRunningTasks } from "../../utilities"; import { getBuildAllTask, SwiftTask } from "../../../src/tasks/SwiftTaskProvider"; -import { testAssetPath } from "../../fixtures"; +import { testAssetPath, testAssetUri } from "../../fixtures"; import { Version } from "../../../src/utilities/version"; +import { FolderContext } from "../../../src/FolderContext"; +import { WorkspaceContext } from "../../../src/WorkspaceContext"; +import { resolveDependencies } from "../../../src/commands/dependencies/resolve"; +import { updateDependencies } from "../../../src/commands/dependencies/update"; +import { useLocalDependency } from "../../../src/commands/dependencies/useLocal"; +import { uneditFolderDependency } from "../../../src/commands/dependencies/unedit"; +import { resetPackage } from "../../../src/commands/resetPackage"; +import * as utilities from "../../../src/commands/utilities"; +import * as sinon from "sinon"; +import { mockGlobalObject } from "../../MockUtils"; suite("PackageDependencyProvider Test Suite", function () { - let treeProvider: PackageDependenciesProvider; - this.timeout(2 * 60 * 1000); // Allow up to 2 minutes to build - - suiteSetup(async function () { - const workspaceContext = await globalWorkspaceContextPromise; - // workspace-state.json was not introduced until swift 5.7 - if (workspaceContext.toolchain.swiftVersion.isLessThan(new Version(5, 7, 0))) { - this.skip(); - } - await waitForNoRunningTasks(); - const folderContext = await folderContextPromise("dependencies"); - await executeTaskAndWaitForResult((await getBuildAllTask(folderContext)) as SwiftTask); - await workspaceContext.focusFolder(folderContext); - treeProvider = new PackageDependenciesProvider(workspaceContext); + // Allow up to 2 minutes to build, + // full workflow's interaction with spm is also longer than the default timeout + this.timeout(2 * 60 * 1000); + + suite("PackageDependencyProvider Tree Node Tests", function () { + let treeProvider: PackageDependenciesProvider; + + suiteSetup(async function () { + const workspaceContext = await globalWorkspaceContextPromise; + // workspace-state.json was not introduced until swift 5.7 + if (workspaceContext.toolchain.swiftVersion.isLessThan(new Version(5, 7, 0))) { + this.skip(); + } + await waitForNoRunningTasks(); + const folderContext = await folderContextPromise("dependencies"); + await executeTaskAndWaitForResult((await getBuildAllTask(folderContext)) as SwiftTask); + await workspaceContext.focusFolder(folderContext); + treeProvider = new PackageDependenciesProvider(workspaceContext); + }); + + suiteTeardown(() => { + treeProvider?.dispose(); + }); + + test("Includes remote dependency", async () => { + const items = await treeProvider.getChildren(); + + const dep = items.find(n => n.name === "swift-markdown") as PackageNode; + expect(dep).to.not.be.undefined; + expect(dep?.location).to.equal("https://github.com/swiftlang/swift-markdown.git"); + expect(dep?.path).to.equal( + `${testAssetPath("dependencies")}/.build/checkouts/swift-markdown` + ); + }); + + test("Includes local dependency", async () => { + const items = await treeProvider.getChildren(); + + const dep = items.find(n => n.name === "defaultpackage") as PackageNode; + expect(dep).to.not.be.undefined; + expect(dep?.location).to.equal(testAssetPath("defaultPackage")); + expect(dep?.path).to.equal(testAssetPath("defaultPackage")); + }); + + test("Lists local dependency file structure", async () => { + const items = await treeProvider.getChildren(); + + const dep = items.find(n => n.name === "defaultpackage") as PackageNode; + expect(dep).to.not.be.undefined; + + const folders = await treeProvider.getChildren(dep); + const folder = folders.find(n => n.name === "Sources"); + expect(folder).to.not.be.undefined; + + expect(folder?.path).to.equal(`${testAssetPath("defaultPackage")}/Sources`); + + const childFolders = await treeProvider.getChildren(folder); + const childFolder = childFolders.find(n => n.name === "PackageExe"); + expect(childFolder).to.not.be.undefined; + + expect(childFolder?.path).to.equal( + `${testAssetPath("defaultPackage")}/Sources/PackageExe` + ); + + const files = await treeProvider.getChildren(childFolder); + const file = files.find(n => n.name === "main.swift"); + expect(file).to.not.be.undefined; + + expect(file?.path).to.equal( + `${testAssetPath("defaultPackage")}/Sources/PackageExe/main.swift` + ); + }); + + test("Lists remote dependency file structure", async () => { + const items = await treeProvider.getChildren(); + + const dep = items.find(n => n.name === "swift-markdown") as PackageNode; + expect(dep).to.not.be.undefined; + + const folders = await treeProvider.getChildren(dep); + const folder = folders.find(n => n.name === "Sources"); + expect(folder).to.not.be.undefined; + + const path = `${testAssetPath("dependencies")}/.build/checkouts/swift-markdown`; + expect(folder?.path).to.equal(`${path}/Sources`); + + const childFolders = await treeProvider.getChildren(folder); + const childFolder = childFolders.find(n => n.name === "CAtomic"); + expect(childFolder).to.not.be.undefined; + + expect(childFolder?.path).to.equal(`${path}/Sources/CAtomic`); + + const files = await treeProvider.getChildren(childFolder); + const file = files.find(n => n.name === "CAtomic.c"); + expect(file).to.not.be.undefined; + + expect(file?.path).to.equal(`${path}/Sources/CAtomic/CAtomic.c`); + }); }); - suiteTeardown(() => { - treeProvider?.dispose(); - }); - - test("Includes remote dependency", async () => { - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "swift-markdown") as PackageNode; - expect(dep).to.not.be.undefined; - expect(dep?.location).to.equal("https://github.com/swiftlang/swift-markdown.git"); - expect(dep?.path).to.equal( - `${testAssetPath("dependencies")}/.build/checkouts/swift-markdown` - ); - }); - - test("Includes local dependency", async () => { - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "defaultpackage") as PackageNode; - expect(dep).to.not.be.undefined; - expect(dep?.location).to.equal(testAssetPath("defaultPackage")); - expect(dep?.path).to.equal(testAssetPath("defaultPackage")); - }); - - test("Lists local dependency file structure", async () => { - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "defaultpackage") as PackageNode; - expect(dep).to.not.be.undefined; - - const folders = await treeProvider.getChildren(dep); - const folder = folders.find(n => n.name === "Sources"); - expect(folder).to.not.be.undefined; - - expect(folder?.path).to.equal(`${testAssetPath("defaultPackage")}/Sources`); - - const childFolders = await treeProvider.getChildren(folder); - const childFolder = childFolders.find(n => n.name === "PackageExe"); - expect(childFolder).to.not.be.undefined; - - expect(childFolder?.path).to.equal(`${testAssetPath("defaultPackage")}/Sources/PackageExe`); - - const files = await treeProvider.getChildren(childFolder); - const file = files.find(n => n.name === "main.swift"); - expect(file).to.not.be.undefined; - - expect(file?.path).to.equal( - `${testAssetPath("defaultPackage")}/Sources/PackageExe/main.swift` - ); - }); - - test("Lists remote dependency file structure", async () => { - const items = await treeProvider.getChildren(); - - const dep = items.find(n => n.name === "swift-markdown") as PackageNode; - expect(dep).to.not.be.undefined; - - const folders = await treeProvider.getChildren(dep); - const folder = folders.find(n => n.name === "Sources"); - expect(folder).to.not.be.undefined; - - const path = `${testAssetPath("dependencies")}/.build/checkouts/swift-markdown`; - expect(folder?.path).to.equal(`${path}/Sources`); - - const childFolders = await treeProvider.getChildren(folder); - const childFolder = childFolders.find(n => n.name === "CAtomic"); - expect(childFolder).to.not.be.undefined; - - expect(childFolder?.path).to.equal(`${path}/Sources/CAtomic`); - - const files = await treeProvider.getChildren(childFolder); - const file = files.find(n => n.name === "CAtomic.c"); - expect(file).to.not.be.undefined; - - expect(file?.path).to.equal(`${path}/Sources/CAtomic/CAtomic.c`); + suite("Full work flow tests", function () { + let folderContext: FolderContext; + let workspaceContext: WorkspaceContext; + const windowMock = mockGlobalObject(vscode, "window"); + + suiteSetup(async function () { + workspaceContext = await globalWorkspaceContextPromise; + await waitForNoRunningTasks(); + folderContext = await folderContextPromise("dependencies"); + await workspaceContext.focusFolder(folderContext); + }); + + test("Use local dependency", async () => { + // Expect to fail without setting up local version + const tasks = (await getBuildAllTask(folderContext)) as SwiftTask; + let { exitCode, output } = await executeTaskAndWaitForResult(tasks); + expect(exitCode).to.not.equal(0); + expect(output).to.include("PackageLib"); + expect(output).to.include("required"); + + // Contract: spm reset, resolve, update should work + const executeTaskSpy = sinon.spy(utilities, "executeTaskWithUI"); + await resolveDependencies(workspaceContext); + await expect(executeTaskSpy.returnValues[0]).to.eventually.be.true; + + await updateDependencies(workspaceContext); + await expect(executeTaskSpy.returnValues[1]).to.eventually.be.true; + + await resetPackage(workspaceContext); + await expect(executeTaskSpy.returnValues[2]).to.eventually.be.true; + await expect(executeTaskSpy.returnValues[3]).to.eventually.be.true; + + // Contract: spm edit with user supplied local version of dependency + windowMock.showOpenDialog.resolves([testAssetUri("Swift-Markdown")]); + const id = "swift-markdown"; + await useLocalDependency(id, workspaceContext); + await expect(executeTaskSpy.returnValues[4]).to.eventually.be.true; + + // This will now pass as we have the required library + ({ exitCode, output } = await executeTaskAndWaitForResult(tasks)); + expect(exitCode).to.equal(0); + expect(output).to.include("defaultpackage"); + expect(output).to.include("not used by any target"); + + // Contract: spm unedit + const updateWorkspaceSpy = sinon.spy(vscode.workspace, "updateWorkspaceFolders"); + // We would love to call uneditDependency for coverage but there's no clean way to get + // a synchronize point for deterministic task completion so just call this function direct + await uneditFolderDependency(workspaceContext.currentFolder!, id, workspaceContext); + expect(updateWorkspaceSpy.calledOnce).to.be.true; + + // Expect to fail again now dependency is missing + ({ exitCode, output } = await executeTaskAndWaitForResult(tasks)); + expect(exitCode).to.not.equal(0); + expect(output).to.include("PackageLib"); + expect(output).to.include("required"); + }); }); });