From e9388efcc05b0363abd9204bec80d8267d28d0d1 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Tue, 6 Jan 2026 14:36:48 -0500 Subject: [PATCH 1/2] Show progress for package description tasks on activation The `swift package show-dependencies` and `swift package describe` commands run on extension activation, and several extension features are gated behind their results. These were run with `execSwift` calls which show no progress in the VS Code UI. The `swift package show-dependencies` command specifically could kick off a package resolution if the dependencies are missing, and for large packages this could take quite some time. As a result the extension could look like it was not activating. Move these two commands into VS Code tasks that report their status in the progress bar, and let the user see the actual commands being run in the terminal. --- CHANGELOG.md | 1 + src/FolderContext.ts | 35 ++-- src/SwiftPackage.ts | 102 +++++---- src/commands/dependencies/describe.ts | 125 +++++++++++ src/commands/dependencies/show.ts | 48 +++++ src/commands/utilities.ts | 6 +- src/tasks/SwiftTaskProvider.ts | 2 + src/utilities/utilities.ts | 15 ++ test/integration-tests/SwiftPackage.test.ts | 195 +++++++++++------- .../testexplorer/LSPTestDiscovery.test.ts | 7 +- .../testexplorer/TestDiscovery.test.ts | 6 +- 11 files changed, 398 insertions(+), 144 deletions(-) create mode 100644 src/commands/dependencies/describe.ts create mode 100644 src/commands/dependencies/show.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5487a8d3f..7397c4915 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Fix the wrong toolchain being shown as selected when using swiftly v1.0.1 ([#2014](https://github.com/swiftlang/vscode-swift/pull/2014)) - Fix extension displaying SwiftPM's project view and automatic build tasks even when `disableSwiftPMIntegration` was true ([#2011](https://github.com/swiftlang/vscode-swift/pull/2011)) - Validate extension settings and warn if they are invalid ([#2016](https://github.com/swiftlang/vscode-swift/pull/2016)) +- Show progress when describing/listing dependencies on package load ([#2028](https://github.com/swiftlang/vscode-swift/pull/2028)) ## 2.14.3 - 2025-12-15 diff --git a/src/FolderContext.ts b/src/FolderContext.ts index 27607a1bd..11825e909 100644 --- a/src/FolderContext.ts +++ b/src/FolderContext.ts @@ -75,6 +75,7 @@ export class FolderContext implements vscode.Disposable { /** dispose of any thing FolderContext holds */ dispose() { this.linuxMain?.dispose(); + this.swiftPackage.dispose(); this.packageWatcher.dispose(); this.testExplorer?.dispose(); this.backgroundCompilation.dispose(); @@ -138,11 +139,7 @@ export class FolderContext implements vscode.Disposable { const { linuxMain, swiftPackage } = await workspaceContext.statusItem.showStatusWhileRunning(statusItemText, async () => { const linuxMain = await LinuxMain.create(folder); - const swiftPackage = await SwiftPackage.create( - folder, - toolchain, - configuration.disableSwiftPMIntegration - ); + const swiftPackage = await SwiftPackage.create(folder); return { linuxMain, swiftPackage }; }); workspaceContext.statusItem.end(statusItemText); @@ -156,16 +153,22 @@ export class FolderContext implements vscode.Disposable { workspaceContext ); - const error = await swiftPackage.error; - if (error) { - void vscode.window.showErrorMessage( - `Failed to load ${folderContext.name}/Package.swift: ${error.message}` - ); - workspaceContext.logger.info( - `Failed to load Package.swift: ${error.message}`, - folderContext.name - ); - } + // List the package's dependencies without blocking folder creation + void swiftPackage + .loadPackageState(folderContext) + .then(async () => await swiftPackage.error) + .catch(error => error) + .then(async error => { + if (error) { + void vscode.window.showErrorMessage( + `Failed to load ${folderContext.name}/Package.swift: ${error.message}` + ); + workspaceContext.logger.info( + `Failed to load Package.swift: ${error.message}`, + folderContext.name + ); + } + }); // Start watching for changes to Package.swift, Package.resolved and .swift-version await folderContext.packageWatcher.install(); @@ -200,7 +203,7 @@ export class FolderContext implements vscode.Disposable { /** reload swift package for this folder */ async reload() { - await this.swiftPackage.reload(this.toolchain, configuration.disableSwiftPMIntegration); + await this.swiftPackage.reload(this); } /** reload Package.resolved for this folder */ diff --git a/src/SwiftPackage.ts b/src/SwiftPackage.ts index dbcfc46bb..3c4c176f2 100644 --- a/src/SwiftPackage.ts +++ b/src/SwiftPackage.ts @@ -15,15 +15,18 @@ import * as fs from "fs/promises"; import * as path from "path"; import * as vscode from "vscode"; +import { FolderContext } from "./FolderContext"; +import { describePackage } from "./commands/dependencies/describe"; +import { showPackageDependencies } from "./commands/dependencies/show"; import { SwiftLogger } from "./logging/SwiftLogger"; import { BuildFlags } from "./toolchain/BuildFlags"; import { SwiftToolchain } from "./toolchain/toolchain"; import { isPathInsidePath } from "./utilities/filesystem"; import { lineBreakRegex } from "./utilities/tasks"; -import { execSwift, getErrorDescription, hashString } from "./utilities/utilities"; +import { execSwift, getErrorDescription, hashString, unwrapPromise } from "./utilities/utilities"; /** Swift Package Manager contents */ -interface PackageContents { +export interface PackageContents { name: string; products: Product[]; dependencies: Dependency[]; @@ -196,9 +199,12 @@ function isError(state: SwiftPackageState): state is Error { /** * Class holding Swift Package Manager Package */ -export class SwiftPackage { +export class SwiftPackage implements vscode.Disposable { public plugins: PackagePlugin[] = []; private _contents: SwiftPackageState | undefined; + private contentsPromise: Promise; + private contentsResolve: (value: SwiftPackageState | PromiseLike) => void; + private tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); /** * SwiftPackage Constructor @@ -208,34 +214,26 @@ export class SwiftPackage { */ private constructor( readonly folder: vscode.Uri, - private contentsPromise: Promise, public resolved: PackageResolved | undefined, // TODO: Make private again public workspaceState: WorkspaceState | undefined - ) {} + ) { + const { promise, resolve } = unwrapPromise(); + this.contentsPromise = promise; + this.contentsResolve = resolve; + } /** * Create a SwiftPackage from a folder * @param folder folder package is in - * @param toolchain Swift toolchain to use - * @param disableSwiftPMIntegration Whether to disable SwiftPM integration * @returns new SwiftPackage */ - public static async create( - folder: vscode.Uri, - toolchain: SwiftToolchain, - disableSwiftPMIntegration: boolean = false - ): Promise { + public static async create(folder: vscode.Uri): Promise { const [resolved, workspaceState] = await Promise.all([ SwiftPackage.loadPackageResolved(folder), SwiftPackage.loadWorkspaceState(folder), ]); - return new SwiftPackage( - folder, - SwiftPackage.loadPackage(folder, toolchain, disableSwiftPMIntegration), - resolved, - workspaceState - ); + return new SwiftPackage(folder, resolved, workspaceState); } /** @@ -259,13 +257,21 @@ export class SwiftPackage { /** * Run `swift package describe` and return results * @param folder folder package is in - * @param toolchain Swift toolchain to use * @param disableSwiftPMIntegration Whether to disable SwiftPM integration * @returns results of `swift package describe` */ - static async loadPackage( - folder: vscode.Uri, - toolchain: SwiftToolchain, + public async loadPackageState( + folderContext: FolderContext, + disableSwiftPMIntegration: boolean = false + ): Promise { + const resolve = this.contentsResolve; + const result = await this.performLoadPackageState(folderContext, disableSwiftPMIntegration); + resolve(result); + return result; + } + + private async performLoadPackageState( + folderContext: FolderContext, disableSwiftPMIntegration: boolean = false ): Promise { // When SwiftPM integration is disabled, return undefined to disable all features @@ -273,31 +279,32 @@ export class SwiftPackage { return undefined; } + // If there is an existing package load, cancel any running taks first before loading a new one. + this.tokenSource.cancel(); + this.tokenSource.dispose(); + this.tokenSource = new vscode.CancellationTokenSource(); + try { // Use swift package describe to describe the package targets, products, and platforms // Use swift package show-dependencies to get the dependencies in a tree format - const [describe, dependencies] = await Promise.all([ - execSwift(["package", "describe", "--type", "json"], toolchain, { - cwd: folder.fsPath, - }), - execSwift(["package", "show-dependencies", "--format", "json"], toolchain, { - cwd: folder.fsPath, - }), - ]); + const describe = await describePackage(folderContext, this.tokenSource.token); + const dependencies = await showPackageDependencies( + folderContext, + this.tokenSource.token + ); const packageState = { - ...(JSON.parse(SwiftPackage.trimStdout(describe.stdout)) as PackageContents), - dependencies: JSON.parse(SwiftPackage.trimStdout(dependencies.stdout)).dependencies, + ...(describe as PackageContents), + dependencies: dependencies, }; return packageState; - } catch (error) { - const execError = error as { stderr: string }; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); // if caught error and it begins with "error: root manifest" then there is no Package.swift if ( - execError.stderr !== undefined && - (execError.stderr.startsWith("error: root manifest") || - execError.stderr.startsWith("error: Could not find Package.swift")) + errorMessage.startsWith("error: root manifest") || + errorMessage.startsWith("error: Could not find Package.swift") ) { return undefined; } else { @@ -378,14 +385,18 @@ export class SwiftPackage { } /** Reload swift package */ - public async reload(toolchain: SwiftToolchain, disableSwiftPMIntegration: boolean = false) { - const loadedContents = await SwiftPackage.loadPackage( - this.folder, - toolchain, + public async reload(folderContext: FolderContext, disableSwiftPMIntegration: boolean = false) { + const { promise, resolve } = unwrapPromise(); + this.contentsPromise = promise; + this.contentsResolve = resolve; + + const loadedContents = await this.performLoadPackageState( + folderContext, disableSwiftPMIntegration ); + this._contents = loadedContents; - this.contentsPromise = Promise.resolve(loadedContents); + resolve(loadedContents); } /** Reload Package.resolved file */ @@ -573,7 +584,7 @@ export class SwiftPackage { ); } - private static trimStdout(stdout: string): string { + static trimStdout(stdout: string): string { // remove lines from `swift package describe` until we find a "{" while (!stdout.startsWith("{")) { const firstNewLine = stdout.indexOf("\n"); @@ -581,6 +592,11 @@ export class SwiftPackage { } return stdout; } + + dispose() { + this.tokenSource.cancel(); + this.tokenSource.dispose(); + } } export enum TargetType { diff --git a/src/commands/dependencies/describe.ts b/src/commands/dependencies/describe.ts new file mode 100644 index 000000000..8cf9f4a51 --- /dev/null +++ b/src/commands/dependencies/describe.ts @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 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 { FolderContext } from "../../FolderContext"; +import { PackageContents, SwiftPackage } from "../../SwiftPackage"; +import { SwiftTaskProvider, createSwiftTask } from "../../tasks/SwiftTaskProvider"; +import { packageName } from "../../utilities/tasks"; +import { executeTaskWithUI, updateAfterError } from "../utilities"; + +/** + * Configuration for executing a Swift package command + */ +export interface SwiftPackageCommandConfig { + /** The Swift command arguments (e.g., ["package", "show-dependencies", "--format", "json"]) */ + args: string[]; + /** The task name for the SwiftTaskProvider */ + taskName: string; + /** The UI message to display during execution */ + uiMessage: string; + /** The command name for error messages */ + commandName: string; +} + +/** + * Execute a Swift package command and return the parsed JSON output + * @param folderContext folder to run the command in + * @param config command configuration + * @returns parsed JSON output from the command + */ +export async function executeSwiftPackageCommand( + folderContext: FolderContext, + config: SwiftPackageCommandConfig, + token?: vscode.CancellationToken +): Promise { + const task = createSwiftTask( + config.args, + config.taskName, + { + cwd: folderContext.folder, + scope: folderContext.workspaceFolder, + packageName: packageName(folderContext), + presentationOptions: { reveal: vscode.TaskRevealKind.Silent }, + dontTriggerTestDiscovery: true, + group: vscode.TaskGroup.Build, + }, + folderContext.toolchain, + undefined, + { readOnlyTerminal: true } + ); + + const outputChunks: string[] = []; + task.execution.onDidWrite((data: string) => { + outputChunks.push(data); + }); + + const success = await executeTaskWithUI( + task, + config.uiMessage, + folderContext, + false, + false, + token + ); + updateAfterError(success, folderContext); + + const output = outputChunks.join(""); + + if (!success) { + throw new Error(output); + } + + if (!output.trim()) { + throw new Error(`No output received from swift ${config.commandName} command`); + } + + try { + const trimmedOutput = SwiftPackage.trimStdout(output); + const parsedOutput = JSON.parse(trimmedOutput); + + // Validate the parsed output is an object + if (!parsedOutput || typeof parsedOutput !== "object") { + throw new Error(`Invalid format received from swift ${config.commandName} command`); + } + + return parsedOutput as T; + } catch (parseError) { + throw new Error( + `Failed to parse ${config.commandName} output: ${parseError instanceof Error ? parseError.message : "Unknown error"}` + ); + } +} + +/** + * Run `swift package describe` inside a folder + * @param folderContext folder to run describe for + */ +export async function describePackage( + folderContext: FolderContext, + token?: vscode.CancellationToken +): Promise { + const result = await executeSwiftPackageCommand( + folderContext, + { + args: ["package", "describe", "--type", "json"], + taskName: SwiftTaskProvider.describePackageName, + uiMessage: "Describing Package", + commandName: "package describe", + }, + token + ); + + return result; +} diff --git a/src/commands/dependencies/show.ts b/src/commands/dependencies/show.ts new file mode 100644 index 000000000..e5396e1dc --- /dev/null +++ b/src/commands/dependencies/show.ts @@ -0,0 +1,48 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the VS Code Swift open source project +// +// Copyright (c) 2021-2025 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 { FolderContext } from "../../FolderContext"; +import { Dependency } from "../../SwiftPackage"; +import { SwiftTaskProvider } from "../../tasks/SwiftTaskProvider"; +import { executeSwiftPackageCommand } from "./describe"; + +/** + * Run `swift package show-dependencies` inside a folder + * @param folderContext folder to run show-dependencies for + */ +export async function showPackageDependencies( + folderContext: FolderContext, + token?: vscode.CancellationToken +): Promise { + const result = await executeSwiftPackageCommand<{ dependencies: Dependency[] }>( + folderContext, + { + args: ["package", "show-dependencies", "--format", "json"], + taskName: SwiftTaskProvider.showDependenciesName, + uiMessage: "Determining Dependencies", + commandName: "package show-dependencies", + }, + token + ); + + // Validate the parsed output has the expected structure + if (!Array.isArray(result.dependencies)) { + throw new Error( + "Invalid dependencies format received from swift package show-dependencies command" + ); + } + + return result.dependencies; +} diff --git a/src/commands/utilities.ts b/src/commands/utilities.ts index 155820acb..4ba587b01 100644 --- a/src/commands/utilities.ts +++ b/src/commands/utilities.ts @@ -32,7 +32,8 @@ export async function executeTaskWithUI( description: string, folderContext: FolderContext, showErrors = false, - checkAlreadyRunning: boolean = false + checkAlreadyRunning: boolean = false, + token?: vscode.CancellationToken ): Promise { try { const exitCode = await folderContext.taskQueue.queueOperation( @@ -40,7 +41,8 @@ export async function executeTaskWithUI( showStatusItem: true, checkAlreadyRunning, log: description, - }) + }), + token ); if (exitCode === 0) { return true; diff --git a/src/tasks/SwiftTaskProvider.ts b/src/tasks/SwiftTaskProvider.ts index c0bc32cb0..e5d070004 100644 --- a/src/tasks/SwiftTaskProvider.ts +++ b/src/tasks/SwiftTaskProvider.ts @@ -366,6 +366,8 @@ export class SwiftTaskProvider implements vscode.TaskProvider { static cleanBuildName = "Clean Build"; static resolvePackageName = "Resolve Package Dependencies"; static updatePackageName = "Update Package Dependencies"; + static showDependenciesName = "List Package Dependencies"; + static describePackageName = "Describe Package"; constructor(private workspaceContext: WorkspaceContext) {} diff --git a/src/utilities/utilities.ts b/src/utilities/utilities.ts index a5314258a..c30f446bb 100644 --- a/src/utilities/utilities.ts +++ b/src/utilities/utilities.ts @@ -315,6 +315,21 @@ export function compactMap( return acc; }, []); } + +/** + * Create a promise that can be resolved outside the promise executor. + * @returns An object containing the promise that can be awaited, and its resolve and reject functions. + */ +export function unwrapPromise() { + let resolve: (value: T | PromiseLike) => void; + let reject: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve: resolve!, reject: reject! }; +} + /** * Get path to swift executable, or executable in swift bin folder * diff --git a/test/integration-tests/SwiftPackage.test.ts b/test/integration-tests/SwiftPackage.test.ts index 6ee6d7b59..b3282f6c1 100644 --- a/test/integration-tests/SwiftPackage.test.ts +++ b/test/integration-tests/SwiftPackage.test.ts @@ -12,101 +12,144 @@ // //===----------------------------------------------------------------------===// import * as assert from "assert"; +import { afterEach } from "mocha"; +import { FolderContext } from "@src/FolderContext"; import { SwiftPackage } from "@src/SwiftPackage"; -import { SwiftToolchain } from "@src/toolchain/toolchain"; -import { Version } from "@src/utilities/version"; +import { WorkspaceContext } from "@src/WorkspaceContext"; -import { testAssetUri } from "../fixtures"; import { tag } from "../tags"; +import { activateExtensionForSuite } from "./utilities/testutilities"; tag("medium").suite("SwiftPackage Test Suite", function () { - let toolchain: SwiftToolchain; + let swiftPackage: SwiftPackage; - setup(async () => { - toolchain = await SwiftToolchain.create("/path/to/extension"); - }); + function getFolderContext(ctx: WorkspaceContext, asset: string): FolderContext { + const folders = ctx.folders.filter(folder => folder.name.endsWith(asset)); + if (folders.length === 0) { + throw new Error(`Test asset folder ${asset} not found`); + } + return folders[0]; + } - test("No package", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("empty-folder"), toolchain); - assert.strictEqual(await spmPackage.foundPackage, false); + afterEach(() => { + if (swiftPackage) { + swiftPackage.dispose(); + } }); - test("Invalid package", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("invalid-package"), toolchain); - assert.strictEqual(await spmPackage.foundPackage, true); - assert.strictEqual(await spmPackage.isValid, false); - }); + suite("empty-folder", () => { + const asset = "empty-folder"; - test("Library package", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("package2"), toolchain); - assert.strictEqual(await spmPackage.isValid, true); - assert.strictEqual((await spmPackage.libraryProducts).length, 1); - assert.strictEqual((await spmPackage.libraryProducts)[0].name, "package2"); - assert.strictEqual((await spmPackage.dependencies).length, 0); - assert.strictEqual((await spmPackage.targets).length, 2); - }); + activateExtensionForSuite({ + async setup(ctx) { + swiftPackage = getFolderContext(ctx, asset).swiftPackage; + }, + testAssets: [asset], + }); - test("Package resolve v2", async function () { - if (!toolchain) { - return; - } - if ( - (process.platform === "win32" && - toolchain.swiftVersion.isLessThan(new Version(6, 0, 0))) || - toolchain.swiftVersion.isLessThan(new Version(5, 6, 0)) - ) { - this.skip(); - } - const spmPackage = await SwiftPackage.create(testAssetUri("package5.6"), toolchain); - assert.strictEqual(await spmPackage.isValid, true); - assert(spmPackage.resolved !== undefined); + test("No package", async () => { + assert.strictEqual(await swiftPackage.foundPackage, false); + }); }); - test("Identity case-insensitivity", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("identity-case"), toolchain); - assert.strictEqual(await spmPackage.isValid, true); - assert.strictEqual((await spmPackage.dependencies).length, 1); - assert(spmPackage.resolved !== undefined); - assert.strictEqual(spmPackage.resolved.pins.length, 1); - assert.strictEqual(spmPackage.resolved.pins[0].identity, "yams"); + suite("invalid-package", () => { + const asset = "invalid-package"; + + activateExtensionForSuite({ + async setup(ctx) { + swiftPackage = getFolderContext(ctx, asset).swiftPackage; + }, + testAssets: [asset], + }); + + test("Invalid Package", async () => { + assert.strictEqual(await swiftPackage.foundPackage, true); + assert.strictEqual(await swiftPackage.isValid, false); + }); }); - test("Identity different from name", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("identity-different"), toolchain); - assert.strictEqual(await spmPackage.isValid, true); - assert.strictEqual((await spmPackage.dependencies).length, 1); - assert(spmPackage.resolved !== undefined); - assert.strictEqual(spmPackage.resolved.pins.length, 1); - assert.strictEqual(spmPackage.resolved.pins[0].identity, "swift-log"); + suite("package2", () => { + const asset = "package2"; + let folderContext: FolderContext; + + activateExtensionForSuite({ + async setup(ctx) { + folderContext = getFolderContext(ctx, asset); + swiftPackage = folderContext.swiftPackage; + }, + testAssets: [asset], + }); + + test("Library Package", async () => { + assert.strictEqual(await swiftPackage.isValid, true); + assert.strictEqual((await swiftPackage.libraryProducts).length, 1); + assert.strictEqual((await swiftPackage.libraryProducts)[0].name, "package2"); + assert.strictEqual((await swiftPackage.dependencies).length, 0); + assert.strictEqual((await swiftPackage.targets).length, 2); + }); + + test("Disabled SwiftPM integration returns undefined package", async () => { + await swiftPackage.reload(folderContext, true); + + assert.strictEqual(await swiftPackage.isValid, false); + assert.strictEqual(await swiftPackage.foundPackage, false); + assert.strictEqual((await swiftPackage.executableProducts).length, 0); + assert.strictEqual((await swiftPackage.libraryProducts).length, 0); + assert.strictEqual((await swiftPackage.dependencies).length, 0); + assert.strictEqual((await swiftPackage.targets).length, 0); + }); }); - test("Disabled SwiftPM integration returns undefined package", async () => { - const spmPackage = await SwiftPackage.create( - testAssetUri("package2"), - toolchain, - true // disableSwiftPMIntegration - ); - assert.strictEqual(await spmPackage.isValid, false); - assert.strictEqual(await spmPackage.foundPackage, false); - assert.strictEqual((await spmPackage.executableProducts).length, 0); - assert.strictEqual((await spmPackage.libraryProducts).length, 0); - assert.strictEqual((await spmPackage.dependencies).length, 0); - assert.strictEqual((await spmPackage.targets).length, 0); + suite("identity-case", () => { + const asset = "identity-case"; + + activateExtensionForSuite({ + async setup(ctx) { + swiftPackage = getFolderContext(ctx, asset).swiftPackage; + }, + testAssets: [asset], + }); + + test("Identity case-insensitivity", async function () { + const isValid = await swiftPackage.isValid; + if (!isValid) { + // Sometimes SPM fails to resolve swift-log with + // Couldn’t get revision ‘1.6.2^{commit}’: + // fatal: Needed a single revision + // This is an issue with SPM itself so we skip in that case. + this.skip(); + } + assert.strictEqual((await swiftPackage.dependencies).length, 1); + assert(swiftPackage.resolved !== undefined); + assert.strictEqual(swiftPackage.resolved.pins.length, 1); + assert.strictEqual(swiftPackage.resolved.pins[0].identity, "yams"); + }); }); - test("Reload with disabled SwiftPM integration returns undefined package", async () => { - const spmPackage = await SwiftPackage.create(testAssetUri("package2"), toolchain, false); - // First verify it loaded normally - assert.strictEqual(await spmPackage.isValid, true); - assert.strictEqual((await spmPackage.libraryProducts).length, 1); - - // Now reload with disabled integration - await spmPackage.reload(toolchain, true); - assert.strictEqual(await spmPackage.isValid, false); - assert.strictEqual(await spmPackage.foundPackage, false); - assert.strictEqual((await spmPackage.libraryProducts).length, 0); - assert.strictEqual((await spmPackage.dependencies).length, 0); - assert.strictEqual((await spmPackage.targets).length, 0); + suite("identity-different", () => { + const asset = "identity-different"; + + activateExtensionForSuite({ + async setup(ctx) { + swiftPackage = getFolderContext(ctx, asset).swiftPackage; + }, + testAssets: [asset], + }); + + test("Identity case-different", async function () { + const isValid = await swiftPackage.isValid; + if (!isValid) { + // Sometimes SPM fails to resolve swift-log with + // Couldn’t get revision ‘1.6.2^{commit}’: + // fatal: Needed a single revision + // This is an issue with SPM itself so we skip in that case. + this.skip(); + } + assert.strictEqual((await swiftPackage.dependencies).length, 1); + assert(swiftPackage.resolved !== undefined); + assert.strictEqual(swiftPackage.resolved.pins.length, 1); + assert.strictEqual(swiftPackage.resolved.pins[0].identity, "swift-log"); + }); }); }); diff --git a/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts b/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts index 15a2c9e20..6b74259ed 100644 --- a/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts +++ b/test/integration-tests/testexplorer/LSPTestDiscovery.test.ts @@ -34,7 +34,6 @@ import { TextDocumentTestsRequest, WorkspaceTestsRequest, } from "@src/sourcekit-lsp/extensions"; -import { SwiftToolchain } from "@src/toolchain/toolchain"; import { instance, mockFn, mockObject } from "../../MockUtils"; @@ -88,7 +87,11 @@ suite("LSPTestDiscovery Suite", () => { beforeEach(async function () { this.timeout(10000000); - pkg = await SwiftPackage.create(file, await SwiftToolchain.create("/path/to/extension")); + pkg = await SwiftPackage.create(file); + + // Provde an undefined target as a mock to avoid loading actual package info. + pkg.getTarget = () => Promise.resolve(undefined); + client = new TestLanguageClient(); discoverer = new LSPTestDiscovery( instance( diff --git a/test/integration-tests/testexplorer/TestDiscovery.test.ts b/test/integration-tests/testexplorer/TestDiscovery.test.ts index 62fe6c318..1a69ea786 100644 --- a/test/integration-tests/testexplorer/TestDiscovery.test.ts +++ b/test/integration-tests/testexplorer/TestDiscovery.test.ts @@ -24,7 +24,6 @@ import { } from "@src/TestExplorer/TestDiscovery"; import { reduceTestItemChildren } from "@src/TestExplorer/TestUtils"; import { TestStyle } from "@src/sourcekit-lsp/extensions"; -import { SwiftToolchain } from "@src/toolchain/toolchain"; suite("TestDiscovery Suite", () => { let testController: vscode.TestController; @@ -223,10 +222,7 @@ suite("TestDiscovery Suite", () => { test("updates tests from classes within a swift package", async () => { const targetFolder = vscode.Uri.file("file:///some/"); - const swiftPackage = await SwiftPackage.create( - targetFolder, - await SwiftToolchain.create("/path/to/extension") - ); + const swiftPackage = await SwiftPackage.create(targetFolder); const testTargetName = "TestTarget"; const target: Target = { c99name: testTargetName, From b61edef4ca09643850eb7138ea4a507adfcf5d36 Mon Sep 17 00:00:00 2001 From: Paul LeMarquand Date: Fri, 9 Jan 2026 13:27:50 -0500 Subject: [PATCH 2/2] Address feedback --- CHANGELOG.md | 5 ++++- src/SwiftPackage.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7397c4915..f12f3e0bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,15 @@ ## {{releaseVersion}} - {{releaseDate}} +### Added + +- Show progress when describing/listing dependencies on package load ([#2028](https://github.com/swiftlang/vscode-swift/pull/2028)) + ### Fixed - Fix the wrong toolchain being shown as selected when using swiftly v1.0.1 ([#2014](https://github.com/swiftlang/vscode-swift/pull/2014)) - Fix extension displaying SwiftPM's project view and automatic build tasks even when `disableSwiftPMIntegration` was true ([#2011](https://github.com/swiftlang/vscode-swift/pull/2011)) - Validate extension settings and warn if they are invalid ([#2016](https://github.com/swiftlang/vscode-swift/pull/2016)) -- Show progress when describing/listing dependencies on package load ([#2028](https://github.com/swiftlang/vscode-swift/pull/2028)) ## 2.14.3 - 2025-12-15 diff --git a/src/SwiftPackage.ts b/src/SwiftPackage.ts index 3c4c176f2..d8de7f67f 100644 --- a/src/SwiftPackage.ts +++ b/src/SwiftPackage.ts @@ -279,7 +279,7 @@ export class SwiftPackage implements vscode.Disposable { return undefined; } - // If there is an existing package load, cancel any running taks first before loading a new one. + // If there is an existing package load, cancel any running tasks first before loading a new one. this.tokenSource.cancel(); this.tokenSource.dispose(); this.tokenSource = new vscode.CancellationTokenSource();