diff --git a/src/commands/installSwiftlyToolchain.ts b/src/commands/installSwiftlyToolchain.ts index 929d8aab9..23fc15e95 100644 --- a/src/commands/installSwiftlyToolchain.ts +++ b/src/commands/installSwiftlyToolchain.ts @@ -16,16 +16,16 @@ import { QuickPickItem } from "vscode"; import { WorkspaceContext } from "../WorkspaceContext"; import { - AvailableToolchain, Swiftly, SwiftlyProgressData, + SwiftlyToolchain, isSnapshotVersion, isStableVersion, } from "../toolchain/swiftly"; import { showReloadExtensionNotification } from "../ui/ReloadExtension"; interface SwiftlyToolchainItem extends QuickPickItem { - toolchain: AvailableToolchain; + toolchain: SwiftlyToolchain; } async function downloadAndInstallToolchain(selected: SwiftlyToolchainItem, ctx: WorkspaceContext) { @@ -225,7 +225,7 @@ export async function installSwiftlySnapshotToolchain(ctx: WorkspaceContext): Pr /** * Sorts toolchains by version with most recent first */ -function sortToolchainsByVersion(toolchains: AvailableToolchain[]): AvailableToolchain[] { +function sortToolchainsByVersion(toolchains: SwiftlyToolchain[]): SwiftlyToolchain[] { return toolchains.sort((a, b) => { // First sort by type (stable before snapshot) if (a.version.type !== b.version.type) { diff --git a/src/toolchain/swiftly.ts b/src/toolchain/swiftly.ts index b041125ee..d0e30bc63 100644 --- a/src/toolchain/swiftly.ts +++ b/src/toolchain/swiftly.ts @@ -31,7 +31,7 @@ const ListResult = z.object({ z.object({ inUse: z.boolean(), isDefault: z.boolean(), - version: z.discriminatedUnion("type", [ + version: z.union([ z.object({ major: z.union([z.number(), z.undefined()]), minor: z.union([z.number(), z.undefined()]), @@ -47,6 +47,11 @@ const ListResult = z.object({ name: z.string(), type: z.literal("snapshot"), }), + z.object({ + name: z.string(), + type: z.literal("system"), + }), + z.object(), ]), }) ), @@ -77,12 +82,20 @@ const SnapshotVersion = z.object({ export type SnapshotVersion = z.infer; +export interface SwiftlyToolchain { + inUse: boolean; + installed: boolean; + isDefault: boolean; + version: StableVersion | SnapshotVersion; +} + const AvailableToolchain = z.object({ inUse: z.boolean(), installed: z.boolean(), isDefault: z.boolean(), - version: z.discriminatedUnion("type", [StableVersion, SnapshotVersion]), + version: z.union([StableVersion, SnapshotVersion, z.object()]), }); +type AvailableToolchain = z.infer; export function isStableVersion( version: StableVersion | SnapshotVersion @@ -99,7 +112,6 @@ export function isSnapshotVersion( const ListAvailableResult = z.object({ toolchains: z.array(AvailableToolchain), }); -export type AvailableToolchain = z.infer; export interface SwiftlyProgressData { step?: { @@ -180,7 +192,9 @@ export class Swiftly { try { const { stdout } = await execFile("swiftly", ["list", "--format=json"]); const response = ListResult.parse(JSON.parse(stdout)); - return response.toolchains.map(t => t.version.name); + return response.toolchains + .filter(t => ["stable", "snapshot", "system"].includes(t.version?.type)) + .map(t => t.version.name); } catch (error) { logger?.error(`Failed to retrieve Swiftly installations: ${error}`); return []; @@ -293,7 +307,7 @@ export class Swiftly { public static async listAvailable( logger?: SwiftLogger, branch?: string - ): Promise { + ): Promise { if (!this.isSupported()) { return []; } @@ -315,7 +329,10 @@ export class Swiftly { args.push(branch); } const { stdout: availableStdout } = await execFile("swiftly", args); - return ListAvailableResult.parse(JSON.parse(availableStdout)).toolchains; + const result = ListAvailableResult.parse(JSON.parse(availableStdout)); + return result.toolchains.filter((t): t is SwiftlyToolchain => + ["stable", "snapshot"].includes(t.version.type) + ); } catch (error) { logger?.error(`Failed to retrieve available Swiftly toolchains: ${error}`); return []; diff --git a/test/unit-tests/toolchain/swiftly.test.ts b/test/unit-tests/toolchain/swiftly.test.ts index 942742114..d192cccf7 100644 --- a/test/unit-tests/toolchain/swiftly.test.ts +++ b/test/unit-tests/toolchain/swiftly.test.ts @@ -73,6 +73,14 @@ suite("Swiftly Unit Tests", () => { // Mock list-available command with JSON output const jsonOutput = { toolchains: [ + { + inUse: false, + isDefault: false, + version: { + name: "xcode", + type: "system", + }, + }, { inUse: true, isDefault: true, @@ -118,6 +126,100 @@ suite("Swiftly Unit Tests", () => { const result = await Swiftly.listAvailableToolchains(); expect(result).to.deep.equal([ + "xcode", + "swift-5.9.0-RELEASE", + "swift-5.8.0-RELEASE", + "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a", + ]); + + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", ["--version"]); + expect(mockUtilities.execFile).to.have.been.calledWith("swiftly", [ + "list", + "--format=json", + ]); + }); + + test("should be able to parse future additions to the output and ignore unexpected types", async () => { + // Mock version check to return 1.1.0 + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + + // Mock list-available command with JSON output + const jsonOutput = { + toolchains: [ + { + inUse: false, + isDefault: false, + version: { + name: "xcode", + type: "system", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: false, + isDefault: false, + version: { + // Try adding an unexpected version type. + type: "something_else", + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: true, + isDefault: true, + version: { + major: 5, + minor: 9, + patch: 0, + name: "swift-5.9.0-RELEASE", + type: "stable", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: false, + isDefault: false, + version: { + major: 5, + minor: 8, + patch: 0, + name: "swift-5.8.0-RELEASE", + type: "stable", + newProp: 1, // Try adding a new property. + }, + newProp: "", // Try adding a new property. + }, + { + inUse: false, + isDefault: false, + version: { + major: 5, + minor: 10, + branch: "development", + date: "2023-10-15", + name: "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a", + type: "snapshot", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + ], + }; + + mockUtilities.execFile.withArgs("swiftly", ["list", "--format=json"]).resolves({ + stdout: JSON.stringify(jsonOutput), + stderr: "", + }); + + const result = await Swiftly.listAvailableToolchains(); + + expect(result).to.deep.equal([ + "xcode", "swift-5.9.0-RELEASE", "swift-5.8.0-RELEASE", "swift-DEVELOPMENT-SNAPSHOT-2023-10-15-a", @@ -342,6 +444,95 @@ suite("Swiftly Unit Tests", () => { ]); }); + test("should be able to parse future additions to the output and ignore unexpected types", async () => { + mockedPlatform.setValue("darwin"); + + mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({ + stdout: "1.1.0\n", + stderr: "", + }); + + const availableResponse = { + toolchains: [ + { + inUse: false, + installed: false, + isDefault: false, + version: { + // Try adding an unexpected version type. + type: "something_else", + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "stable", + major: 6, + minor: 0, + patch: 0, + name: "6.0.0", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "snapshot", + major: 6, + minor: 1, + branch: "main", + date: "2025-01-15", + name: "main-snapshot-2025-01-15", + newProp: 1, // Try adding a new property. + }, + newProp: 1, // Try adding a new property. + }, + ], + }; + + mockUtilities.execFile + .withArgs("swiftly", ["list-available", "--format=json"]) + .resolves({ + stdout: JSON.stringify(availableResponse), + stderr: "", + }); + + const result = await Swiftly.listAvailable(); + expect(result).to.deep.equal([ + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "stable", + major: 6, + minor: 0, + patch: 0, + name: "6.0.0", + }, + }, + { + inUse: false, + installed: false, + isDefault: false, + version: { + type: "snapshot", + major: 6, + minor: 1, + branch: "main", + date: "2025-01-15", + name: "main-snapshot-2025-01-15", + }, + }, + ]); + }); + test("should handle errors when fetching available toolchains", async () => { mockedPlatform.setValue("darwin"); mockUtilities.execFile.withArgs("swiftly", ["--version"]).resolves({