From 8424f8e3a65a6116edc62a61fee344657f483863 Mon Sep 17 00:00:00 2001 From: Priyambada Roul Date: Tue, 17 Jun 2025 22:42:12 +0530 Subject: [PATCH] Refactor list-available command to support json format --- .../SwiftlyDocs.docc/swiftly-cli-reference.md | 7 +- Sources/Swiftly/ListAvailable.swift | 59 +--- Sources/Swiftly/OutputSchema.swift | 122 +++++++- Sources/Swiftly/Use.swift | 6 +- Sources/SwiftlyCore/OutputFormatter.swift | 18 +- Sources/SwiftlyCore/SwiftlyCore.swift | 4 +- Tests/SwiftlyTests/OutputSchemaTests.swift | 184 +++++++++++++ Tests/SwiftlyTests/SwiftlyCoreTests.swift | 2 +- Tests/SwiftlyTests/UseTests.swift | 260 ++++++++++++------ 9 files changed, 511 insertions(+), 151 deletions(-) create mode 100644 Tests/SwiftlyTests/OutputSchemaTests.swift diff --git a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md index f8c0b1f6..3071f8a0 100644 --- a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md +++ b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md @@ -107,7 +107,7 @@ written to this file as commands that can be run after the installation. List toolchains available for install. ``` -swiftly list-available [] [--version] [--help] +swiftly list-available [] [--format=] [--version] [--help] ``` **toolchain-selector:** @@ -135,6 +135,11 @@ The installed snapshots for a given development branch can be listed by specifyi Note that listing available snapshots before the latest release (major and minor number) is unsupported. +**--format=\:** + +*Output format (text, json)* + + **--version:** *Show the version.* diff --git a/Sources/Swiftly/ListAvailable.swift b/Sources/Swiftly/ListAvailable.swift index 40a83efe..dedd18b8 100644 --- a/Sources/Swiftly/ListAvailable.swift +++ b/Sources/Swiftly/ListAvailable.swift @@ -1,4 +1,5 @@ import ArgumentParser +import Foundation import SwiftlyCore struct ListAvailable: SwiftlyCommand { @@ -35,12 +36,11 @@ struct ListAvailable: SwiftlyCommand { )) var toolchainSelector: String? - private enum CodingKeys: String, CodingKey { - case toolchainSelector - } + @Option(name: .long, help: "Output format (text, json)") + var format: SwiftlyCore.OutputFormat = .text mutating func run() async throws { - try await self.run(Swiftly.createDefaultContext()) + try await self.run(Swiftly.createDefaultContext(format: self.format)) } mutating func run(_ ctx: SwiftlyCoreContext) async throws { @@ -76,48 +76,17 @@ struct ListAvailable: SwiftlyCommand { let installedToolchains = Set(config.listInstalledToolchains(selector: selector)) let (inUse, _) = try await selectToolchain(ctx, config: &config) - let printToolchain = { (toolchain: ToolchainVersion) in - var message = "\(toolchain)" - if installedToolchains.contains(toolchain) { - message += " (installed)" - } - if let inUse, toolchain == inUse { - message += " (in use)" - } - if toolchain == config.inUse { - message += " (default)" - } - await ctx.message(message) - } - - if let selector { - let modifier = switch selector { - case let .stable(major, minor, nil): - if let minor { - "Swift \(major).\(minor) release" - } else { - "Swift \(major) release" - } - case .snapshot(.main, nil): - "main development snapshot" - case let .snapshot(.release(major, minor), nil): - "\(major).\(minor) development snapshot" - default: - "matching" - } + let filteredToolchains = selector == nil ? toolchains.filter { $0.isStableRelease() } : toolchains - let message = "Available \(modifier) toolchains" - await ctx.message(message) - await ctx.message(String(repeating: "-", count: message.count)) - for toolchain in toolchains { - await printToolchain(toolchain) - } - } else { - await ctx.message("Available release toolchains") - await ctx.message("----------------------------") - for toolchain in toolchains where toolchain.isStableRelease() { - await printToolchain(toolchain) - } + let availableToolchainInfos = filteredToolchains.compactMap { toolchain -> AvailableToolchainInfo? in + AvailableToolchainInfo( + version: toolchain, + inUse: inUse == toolchain, + isDefault: toolchain == config.inUse, + installed: installedToolchains.contains(toolchain) + ) } + + try await ctx.output(AvailableToolchainsListInfo(toolchains: availableToolchainInfos, selector: selector)) } } diff --git a/Sources/Swiftly/OutputSchema.swift b/Sources/Swiftly/OutputSchema.swift index 2c1959e6..8302865a 100644 --- a/Sources/Swiftly/OutputSchema.swift +++ b/Sources/Swiftly/OutputSchema.swift @@ -33,7 +33,10 @@ struct ToolchainSetInfo: OutputData { let versionFile: String? var description: String { - var message = self.isGlobal ? "The global default toolchain has been set to `\(self.version)`" : "The file `\(self.versionFile ?? ".swift-version")` has been set to `\(self.version)`" + var message = + self.isGlobal + ? "The global default toolchain has been set to `\(self.version)`" + : "The file `\(self.versionFile ?? ".swift-version")` has been set to `\(self.version)`" if let previousVersion = previousVersion { message += " (was \(previousVersion.name))" } @@ -55,3 +58,120 @@ enum ToolchainSource: Codable, CustomStringConvertible { } } } + +struct AvailableToolchainInfo: OutputData { + let version: ToolchainVersion + let inUse: Bool + let isDefault: Bool + let installed: Bool + + var description: String { + var message = "\(version)" + if self.installed { + message += " (installed)" + } + if self.inUse { + message += " (in use)" + } + if self.isDefault { + message += " (default)" + } + return message + } + + private enum CodingKeys: String, CodingKey { + case version + case inUse + case `default` + case installed + } + + private enum ToolchainVersionCodingKeys: String, CodingKey { + case name + case type + case branch + case major + case minor + case patch + case date + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.inUse, forKey: .inUse) + try container.encode(self.isDefault, forKey: .default) + try container.encode(self.installed, forKey: .installed) + + // Encode the version as a object + var versionContainer = container.nestedContainer( + keyedBy: ToolchainVersionCodingKeys.self, forKey: .version + ) + try versionContainer.encode(self.version.name, forKey: .name) + + switch self.version { + case let .stable(release): + try versionContainer.encode("stable", forKey: .type) + try versionContainer.encode(release.major, forKey: .major) + try versionContainer.encode(release.minor, forKey: .minor) + try versionContainer.encode(release.patch, forKey: .patch) + case let .snapshot(snapshot): + try versionContainer.encode("snapshot", forKey: .type) + try versionContainer.encode(snapshot.date, forKey: .date) + try versionContainer.encode(snapshot.branch.name, forKey: .branch) + + if let major = snapshot.branch.major, + let minor = snapshot.branch.minor + { + try versionContainer.encode(major, forKey: .major) + try versionContainer.encode(minor, forKey: .minor) + } + } + } +} + +struct AvailableToolchainsListInfo: OutputData { + let toolchains: [AvailableToolchainInfo] + let selector: ToolchainSelector? + + init(toolchains: [AvailableToolchainInfo], selector: ToolchainSelector? = nil) { + self.toolchains = toolchains + self.selector = selector + } + + private enum CodingKeys: String, CodingKey { + case toolchains + } + + var description: String { + var lines: [String] = [] + + if let selector = selector { + let modifier = + switch selector + { + case let .stable(major, minor, nil): + if let minor { + "Swift \(major).\(minor) release" + } else { + "Swift \(major) release" + } + case .snapshot(.main, nil): + "main development snapshot" + case let .snapshot(.release(major, minor), nil): + "\(major).\(minor) development snapshot" + default: + "matching" + } + + let header = "Available \(modifier) toolchains" + lines.append(header) + lines.append(String(repeating: "-", count: header.count)) + } else { + lines.append("Available release toolchains") + lines.append("----------------------------") + } + + lines.append(contentsOf: self.toolchains.map(\.description)) + return lines.joined(separator: "\n") + } +} diff --git a/Sources/Swiftly/Use.swift b/Sources/Swiftly/Use.swift index 6759b535..2e595c4e 100644 --- a/Sources/Swiftly/Use.swift +++ b/Sources/Swiftly/Use.swift @@ -88,7 +88,7 @@ struct Use: SwiftlyCommand { if self.printLocation { let location = LocationInfo(path: "\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion))") - await ctx.output(location) + try await ctx.output(location) return } @@ -100,7 +100,7 @@ struct Use: SwiftlyCommand { } let toolchainInfo = ToolchainInfo(version: selectedVersion, source: source) - await ctx.output(toolchainInfo) + try await ctx.output(toolchainInfo) return } @@ -150,7 +150,7 @@ struct Use: SwiftlyCommand { configFile = nil } - await ctx.output(ToolchainSetInfo( + try await ctx.output(ToolchainSetInfo( version: toolchain, previousVersion: selectedVersion, isGlobal: isGlobal, diff --git a/Sources/SwiftlyCore/OutputFormatter.swift b/Sources/SwiftlyCore/OutputFormatter.swift index dca0677c..94a103bd 100644 --- a/Sources/SwiftlyCore/OutputFormatter.swift +++ b/Sources/SwiftlyCore/OutputFormatter.swift @@ -11,10 +11,10 @@ public enum OutputFormat: String, Sendable, CaseIterable, ExpressibleByArgument } public protocol OutputFormatter { - func format(_ data: OutputData) -> String + func format(_ data: OutputData) throws -> String } -public protocol OutputData: Codable, CustomStringConvertible { +public protocol OutputData: Encodable, CustomStringConvertible { var description: String { get } } @@ -26,19 +26,21 @@ public struct TextOutputFormatter: OutputFormatter { } } +public enum OutputFormatterError: Error { + case encodingError(String) +} + public struct JSONOutputFormatter: OutputFormatter { public init() {} - public func format(_ data: OutputData) -> String { + public func format(_ data: OutputData) throws -> String { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys] - let jsonData = try? encoder.encode(data) - - guard let jsonData = jsonData, let result = String(data: jsonData, encoding: .utf8) else { - return "{}" + let jsonData = try encoder.encode(data) + guard let result = String(data: jsonData, encoding: .utf8) else { + throw OutputFormatterError.encodingError("Failed to encode JSON data as a string in UTF-8.") } - return result } } diff --git a/Sources/SwiftlyCore/SwiftlyCore.swift b/Sources/SwiftlyCore/SwiftlyCore.swift index 3606a692..a78add9c 100644 --- a/Sources/SwiftlyCore/SwiftlyCore.swift +++ b/Sources/SwiftlyCore/SwiftlyCore.swift @@ -98,13 +98,13 @@ public struct SwiftlyCoreContext: Sendable { } } - public func output(_ data: OutputData) async { + public func output(_ data: OutputData) async throws { let formattedOutput: String switch self.format { case .text: formattedOutput = TextOutputFormatter().format(data) case .json: - formattedOutput = JSONOutputFormatter().format(data) + formattedOutput = try JSONOutputFormatter().format(data) } await self.print(formattedOutput) } diff --git a/Tests/SwiftlyTests/OutputSchemaTests.swift b/Tests/SwiftlyTests/OutputSchemaTests.swift new file mode 100644 index 00000000..096a3b29 --- /dev/null +++ b/Tests/SwiftlyTests/OutputSchemaTests.swift @@ -0,0 +1,184 @@ +import Foundation +@testable import Swiftly +@testable import SwiftlyCore +import Testing + +@Suite struct OutputSchemaTests { + // MARK: - Test Data Setup + + static let testStableToolchain = ToolchainVersion.stable(.init(major: 5, minor: 8, patch: 1)) + static let testMainSnapshot = ToolchainVersion.snapshot(.init(branch: .main, date: "2023-07-15")) + static let testReleaseSnapshot = ToolchainVersion.snapshot(.init(branch: .release(major: 5, minor: 9), date: "2023-07-10")) + + static let testStableVersionInfo = ToolchainVersion.stable(.init(major: 5, minor: 8, patch: 1)) + + static let testMainSnapshotVersionInfo = ToolchainVersion.snapshot(.init(branch: .main, date: "2023-07-15")) + + static let testReleaseSnapshotVersionInfo = ToolchainVersion.snapshot(.init(branch: .release(major: 5, minor: 9), date: "2023-07-10")) + + // MARK: - ToolchainVersion Tests + + @Test func toolchainVersionName() async throws { + #expect(Self.testStableVersionInfo.name == "5.8.1") + #expect(Self.testMainSnapshotVersionInfo.name == "main-snapshot-2023-07-15") + #expect(Self.testReleaseSnapshotVersionInfo.name == "5.9-snapshot-2023-07-10") + } + + // MARK: - AvailableToolchainInfo Tests + + @Test func availableToolchainInfoDescription() async throws { + let basicToolchain = AvailableToolchainInfo( + version: Self.testStableVersionInfo, + inUse: false, + isDefault: false, + installed: false + ) + #expect(basicToolchain.description == "Swift 5.8.1") + + let installedToolchain = AvailableToolchainInfo( + version: Self.testStableVersionInfo, + inUse: false, + isDefault: false, + installed: true + ) + #expect(installedToolchain.description == "Swift 5.8.1 (installed)") + + let inUseToolchain = AvailableToolchainInfo( + version: Self.testStableVersionInfo, + inUse: true, + isDefault: false, + installed: true + ) + #expect(inUseToolchain.description == "Swift 5.8.1 (installed) (in use)") + + let defaultToolchain = AvailableToolchainInfo( + version: Self.testStableVersionInfo, + inUse: true, + isDefault: true, + installed: true + ) + #expect(defaultToolchain.description == "Swift 5.8.1 (installed) (in use) (default)") + + let defaultOnlyToolchain = AvailableToolchainInfo( + version: Self.testStableVersionInfo, + inUse: false, + isDefault: true, + installed: false + ) + #expect(defaultOnlyToolchain.description == "Swift 5.8.1 (default)") + } + + @Test func availableToolchainsListInfoDescriptionNoSelector() async throws { + let toolchains = [ + AvailableToolchainInfo( + version: Self.testStableVersionInfo, + inUse: true, + isDefault: true, + installed: true + ), + AvailableToolchainInfo( + version: Self.testMainSnapshotVersionInfo, + inUse: false, + isDefault: false, + installed: false + ), + ] + + let listInfo = AvailableToolchainsListInfo(toolchains: toolchains) + let description = listInfo.description + + #expect(description.contains("Available release toolchains")) + #expect(description.contains("----------------------------")) + #expect(description.contains("Swift 5.8.1 (installed) (in use) (default)")) + #expect(description.contains("main-snapshot-2023-07-15")) + } + + @Test func availableToolchainsListInfoDescriptionWithStableSelector() async throws { + let toolchains = [ + AvailableToolchainInfo( + version: Self.testStableVersionInfo, + inUse: true, + isDefault: true, + installed: true + ), + ] + + let selector = ToolchainSelector.stable(major: 5, minor: 8, patch: nil) + let listInfo = AvailableToolchainsListInfo(toolchains: toolchains, selector: selector) + let description = listInfo.description + + #expect(description.contains("Available Swift 5.8 release toolchains")) + #expect(description.contains("Swift 5.8.1 (installed) (in use) (default)")) + } + + @Test func availableToolchainsListInfoDescriptionWithMajorOnlySelector() async throws { + let majorOnlySelector = ToolchainSelector.stable(major: 5, minor: nil, patch: nil) + let majorOnlyListInfo = AvailableToolchainsListInfo( + toolchains: [AvailableToolchainInfo( + version: Self.testStableVersionInfo, + inUse: false, + isDefault: false, + installed: true + )], + selector: majorOnlySelector + ) + #expect(majorOnlyListInfo.description.contains("Available Swift 5 release toolchains")) + #expect(majorOnlyListInfo.description.contains("Swift 5.8.1 (installed)")) + } + + @Test func availableToolchainsListInfoDescriptionWithMainSnapshotSelector() async throws { + let mainSnapshotSelector = ToolchainSelector.snapshot(branch: .main, date: nil) + let mainSnapshotListInfo = AvailableToolchainsListInfo( + toolchains: [AvailableToolchainInfo( + version: Self.testMainSnapshotVersionInfo, + inUse: false, + isDefault: false, + installed: false + )], + selector: mainSnapshotSelector + ) + #expect(mainSnapshotListInfo.description.contains("Available main development snapshot toolchains")) + #expect(mainSnapshotListInfo.description.contains("main-snapshot-2023-07-15")) + } + + @Test func availableToolchainsListInfoDescriptionWithReleaseSnapshotSelector() async throws { + let releaseSnapshotSelector = ToolchainSelector.snapshot(branch: .release(major: 5, minor: 9), date: nil) + let releaseSnapshotListInfo = AvailableToolchainsListInfo( + toolchains: [AvailableToolchainInfo( + version: Self.testReleaseSnapshotVersionInfo, + inUse: false, + isDefault: false, + installed: true + )], + selector: releaseSnapshotSelector + ) + #expect(releaseSnapshotListInfo.description.contains("Available 5.9 development snapshot toolchains")) + #expect(releaseSnapshotListInfo.description.contains("5.9-snapshot-2023-07-10 (installed)")) + } + + @Test func availableToolchainsListInfoDescriptionWithSpecificVersionSelector() async throws { + let specificSelector = ToolchainSelector.stable(major: 5, minor: 8, patch: 1) + let specificListInfo = AvailableToolchainsListInfo( + toolchains: [AvailableToolchainInfo( + version: Self.testStableVersionInfo, + inUse: false, + isDefault: false, + installed: false + )], + selector: specificSelector + ) + #expect(specificListInfo.description.contains("Available matching toolchains")) + #expect(specificListInfo.description.contains("Swift 5.8.1")) + } + + @Test func availableToolchainsListInfoEmptyToolchains() async throws { + let listInfo = AvailableToolchainsListInfo(toolchains: []) + let description = listInfo.description + + #expect(description.contains("Available release toolchains")) + #expect(description.contains("----------------------------")) + // Should not contain any toolchain entries + #expect(!description.contains("Swift 5.8.1")) + #expect(!description.contains("snapshot")) + } +} diff --git a/Tests/SwiftlyTests/SwiftlyCoreTests.swift b/Tests/SwiftlyTests/SwiftlyCoreTests.swift index 789d72e0..a23844e7 100644 --- a/Tests/SwiftlyTests/SwiftlyCoreTests.swift +++ b/Tests/SwiftlyTests/SwiftlyCoreTests.swift @@ -7,7 +7,7 @@ import Testing actor TestOutputCapture: OutputHandler { private(set) var outputLines: [String] = [] - func handleOutputLine(_ string: String) { + func handleOutputLine(_ string: String) async { self.outputLines.append(string) } diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index e2b0f23e..4a38c810 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -1,9 +1,10 @@ import Foundation -@testable import Swiftly -@testable import SwiftlyCore import SystemPackage import Testing +@testable import Swiftly +@testable import SwiftlyCore + @Suite struct UseTests { static let homeName = "useTests" @@ -17,16 +18,24 @@ import Testing /// Tests that the `use` command can switch between installed stable release toolchains. @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useStable() async throws { - try await self.useAndValidate(argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable) - try await self.useAndValidate(argument: ToolchainVersion.newStable.name, expectedVersion: .newStable) - try await self.useAndValidate(argument: ToolchainVersion.newStable.name, expectedVersion: .newStable) + try await self.useAndValidate( + argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable + ) + try await self.useAndValidate( + argument: ToolchainVersion.newStable.name, expectedVersion: .newStable + ) + try await self.useAndValidate( + argument: ToolchainVersion.newStable.name, expectedVersion: .newStable + ) } /// Tests that that "latest" can be provided to the `use` command to select the installed stable release /// toolchain with the most recent version. @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useLatestStable() async throws { // Use an older toolchain. - try await self.useAndValidate(argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable) + try await self.useAndValidate( + argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable + ) // Use latest, assert that it switched to the latest installed stable release. try await self.useAndValidate(argument: "latest", expectedVersion: .newStable) @@ -35,16 +44,22 @@ import Testing try await self.useAndValidate(argument: "latest", expectedVersion: .newStable) // Explicitly specify the current latest toolchain, assert no errors and no changes were made. - try await self.useAndValidate(argument: ToolchainVersion.newStable.name, expectedVersion: .newStable) + try await self.useAndValidate( + argument: ToolchainVersion.newStable.name, expectedVersion: .newStable + ) // Switch back to the old toolchain, verify it works. - try await self.useAndValidate(argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable) + try await self.useAndValidate( + argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable + ) } /// Tests that the latest installed patch release toolchain for a given major/minor version pair can be selected by /// omitting the patch version (e.g. `use 5.6`). @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useLatestStablePatch() async throws { - try await self.useAndValidate(argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable) + try await self.useAndValidate( + argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable + ) let oldStableVersion = ToolchainVersion.oldStable.asStableRelease! @@ -62,7 +77,9 @@ import Testing // Switch back to an older patch, try selecting a newer version that isn't installed, and assert // that nothing changed. - try await self.useAndValidate(argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable) + try await self.useAndValidate( + argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable + ) let latestPatch = ToolchainVersion.oldStableNewPatch.asStableRelease!.patch try await self.useAndValidate( argument: "\(oldStableVersion.major).\(oldStableVersion.minor).\(latestPatch + 1)", @@ -73,33 +90,52 @@ import Testing /// Tests that the `use` command can switch between installed main snapshot toolchains. @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useMainSnapshot() async throws { // Switch to a non-snapshot. - try await self.useAndValidate(argument: ToolchainVersion.newStable.name, expectedVersion: .newStable) - try await self.useAndValidate(argument: ToolchainVersion.oldMainSnapshot.name, expectedVersion: .oldMainSnapshot) - try await self.useAndValidate(argument: ToolchainVersion.newMainSnapshot.name, expectedVersion: .newMainSnapshot) + try await self.useAndValidate( + argument: ToolchainVersion.newStable.name, expectedVersion: .newStable + ) + try await self.useAndValidate( + argument: ToolchainVersion.oldMainSnapshot.name, expectedVersion: .oldMainSnapshot + ) + try await self.useAndValidate( + argument: ToolchainVersion.newMainSnapshot.name, expectedVersion: .newMainSnapshot + ) // Verify that using the same snapshot again doesn't throw an error. - try await self.useAndValidate(argument: ToolchainVersion.newMainSnapshot.name, expectedVersion: .newMainSnapshot) - try await self.useAndValidate(argument: ToolchainVersion.oldMainSnapshot.name, expectedVersion: .oldMainSnapshot) + try await self.useAndValidate( + argument: ToolchainVersion.newMainSnapshot.name, expectedVersion: .newMainSnapshot + ) + try await self.useAndValidate( + argument: ToolchainVersion.oldMainSnapshot.name, expectedVersion: .oldMainSnapshot + ) } /// Tests that the latest installed main snapshot toolchain can be selected by omitting the /// date (e.g. `use main-snapshot`). - @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useLatestMainSnapshot() async throws { + @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useLatestMainSnapshot() async throws + { // Switch to a non-snapshot. - try await self.useAndValidate(argument: ToolchainVersion.newStable.name, expectedVersion: .newStable) + try await self.useAndValidate( + argument: ToolchainVersion.newStable.name, expectedVersion: .newStable + ) // Switch to the latest main snapshot. try await self.useAndValidate(argument: "main-snapshot", expectedVersion: .newMainSnapshot) // Switch to it again, assert no errors or changes were made. try await self.useAndValidate(argument: "main-snapshot", expectedVersion: .newMainSnapshot) // Switch to it again, this time by name. Assert no errors or changes were made. - try await self.useAndValidate(argument: ToolchainVersion.newMainSnapshot.name, expectedVersion: .newMainSnapshot) + try await self.useAndValidate( + argument: ToolchainVersion.newMainSnapshot.name, expectedVersion: .newMainSnapshot + ) // Switch to an older snapshot, verify it works. - try await self.useAndValidate(argument: ToolchainVersion.oldMainSnapshot.name, expectedVersion: .oldMainSnapshot) + try await self.useAndValidate( + argument: ToolchainVersion.oldMainSnapshot.name, expectedVersion: .oldMainSnapshot + ) } /// Tests that the `use` command can switch between installed release snapshot toolchains. @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useReleaseSnapshot() async throws { // Switch to a non-snapshot. - try await self.useAndValidate(argument: ToolchainVersion.newStable.name, expectedVersion: .newStable) + try await self.useAndValidate( + argument: ToolchainVersion.newStable.name, expectedVersion: .newStable + ) try await self.useAndValidate( argument: ToolchainVersion.oldReleaseSnapshot.name, expectedVersion: .oldReleaseSnapshot @@ -121,11 +157,17 @@ import Testing /// Tests that the latest installed release snapshot toolchain can be selected by omitting the /// date (e.g. `use 5.7-snapshot`). - @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useLatestReleaseSnapshot() async throws { + @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useLatestReleaseSnapshot() + async throws + { // Switch to a non-snapshot. - try await self.useAndValidate(argument: ToolchainVersion.newStable.name, expectedVersion: .newStable) + try await self.useAndValidate( + argument: ToolchainVersion.newStable.name, expectedVersion: .newStable + ) // Switch to the latest snapshot for the given release. - guard case let .release(major, minor) = ToolchainVersion.newReleaseSnapshot.asSnapshot!.branch else { + guard + case let .release(major, minor) = ToolchainVersion.newReleaseSnapshot.asSnapshot!.branch + else { fatalError("expected release in snapshot release version") } try await self.useAndValidate( @@ -150,7 +192,8 @@ import Testing } /// Tests that the `use` command gracefully exits when executed before any toolchains have been installed. - @Test(.mockedSwiftlyVersion(), .mockHomeToolchains(toolchains: [])) func useNoInstalledToolchains() async throws { + @Test(.mockedSwiftlyVersion(), .mockHomeToolchains(toolchains: [])) + func useNoInstalledToolchains() async throws { try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "latest"]) var config = try await Config.load() @@ -165,13 +208,19 @@ import Testing /// Tests that the `use` command gracefully handles being executed with toolchain names that haven't been installed. @Test(.mockedSwiftlyVersion(), .mockHomeToolchains()) func useNonExistent() async throws { // Switch to a valid toolchain. - try await self.useAndValidate(argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable) + try await self.useAndValidate( + argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable + ) // Try various non-existent toolchains. try await self.useAndValidate(argument: "1.2.3", expectedVersion: .oldStable) - try await self.useAndValidate(argument: "5.7-snapshot-1996-01-01", expectedVersion: .oldStable) + try await self.useAndValidate( + argument: "5.7-snapshot-1996-01-01", expectedVersion: .oldStable + ) try await self.useAndValidate(argument: "6.7-snapshot", expectedVersion: .oldStable) - try await self.useAndValidate(argument: "main-snapshot-1996-01-01", expectedVersion: .oldStable) + try await self.useAndValidate( + argument: "main-snapshot-1996-01-01", expectedVersion: .oldStable + ) } /// Tests that the `use` command works with all the installed toolchains in this test harness. @@ -193,19 +242,28 @@ import Testing .newMainSnapshot, .newReleaseSnapshot, ] - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { - for toolchain in toolchains { - try await SwiftlyTests.runCommand(Use.self, ["use", "-g", toolchain.name]) - - var output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", "-g"]) - - #expect(output.contains(where: { $0.contains(String(describing: toolchain)) })) - - output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", "-g", "--print-location"]) - - #expect(output.contains(where: { $0.contains(Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, toolchain).string) })) + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) + { + for toolchain in toolchains { + try await SwiftlyTests.runCommand(Use.self, ["use", "-g", toolchain.name]) + + var output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", "-g"]) + + #expect(output.contains(where: { $0.contains(String(describing: toolchain)) })) + + output = try await SwiftlyTests.runWithMockedIO( + Use.self, ["use", "-g", "--print-location"] + ) + + #expect( + output.contains(where: { + $0.contains( + Swiftly.currentPlatform.findToolchainLocation( + SwiftlyTests.ctx, toolchain + ).string) + })) + } } - } } /// Tests in-use toolchain selected by the .swift-version file. @@ -215,46 +273,53 @@ import Testing .newMainSnapshot, .newReleaseSnapshot, ] - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { - let versionFile = SwiftlyTests.ctx.currentDirectory / ".swift-version" - - // GIVEN: a directory with a swift version file that selects a particular toolchain - try ToolchainVersion.newStable.name.write(to: versionFile, atomically: true) - // WHEN: checking which toolchain is selected with the use command - var output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use"]) - // THEN: the output shows this toolchain is in use with this working directory - #expect(output.contains(where: { $0.contains(ToolchainVersion.newStable.name) })) - - // GIVEN: a directory with a swift version file that selects a particular toolchain - // WHEN: using another toolchain version - output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", ToolchainVersion.newMainSnapshot.name]) - // THEN: the swift version file is updated to this toolchain version - var versionFileContents = try String(contentsOf: versionFile) - #expect(ToolchainVersion.newMainSnapshot.name == versionFileContents) - // THEN: the use command reports this toolchain to be in use - #expect(output.contains(where: { $0.contains(ToolchainVersion.newMainSnapshot.name) })) - - // GIVEN: a directory with no swift version file at the top of a git repository - try await fs.remove(atPath: versionFile) - let gitDir = SwiftlyTests.ctx.currentDirectory / ".git" - try await fs.mkdir(atPath: gitDir) - // WHEN: using a toolchain version - try await SwiftlyTests.runCommand(Use.self, ["use", ToolchainVersion.newReleaseSnapshot.name]) - // THEN: a swift version file is created - #expect(try await fs.exists(atPath: versionFile)) - // THEN: the version file contains the specified version - versionFileContents = try String(contentsOf: versionFile) - #expect(ToolchainVersion.newReleaseSnapshot.name == versionFileContents) - - // GIVEN: a directory with a swift version file at the top of a git repository - try "1.2.3".write(to: versionFile, atomically: true) - // WHEN: using with a toolchain selector that can select more than one version, but matches one of the installed toolchains - let broadSelector = ToolchainSelector.stable(major: ToolchainVersion.newStable.asStableRelease!.major, minor: nil, patch: nil) - try await SwiftlyTests.runCommand(Use.self, ["use", broadSelector.description]) - // THEN: the swift version file is set to the specific toolchain version that was installed including major, minor, and patch - versionFileContents = try String(contentsOf: versionFile) - #expect(ToolchainVersion.newStable.name == versionFileContents) - } + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) + { + let versionFile = SwiftlyTests.ctx.currentDirectory / ".swift-version" + + // GIVEN: a directory with a swift version file that selects a particular toolchain + try ToolchainVersion.newStable.name.write(to: versionFile, atomically: true) + // WHEN: checking which toolchain is selected with the use command + var output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use"]) + // THEN: the output shows this toolchain is in use with this working directory + #expect(output.contains(where: { $0.contains(ToolchainVersion.newStable.name) })) + + // GIVEN: a directory with a swift version file that selects a particular toolchain + // WHEN: using another toolchain version + output = try await SwiftlyTests.runWithMockedIO( + Use.self, ["use", ToolchainVersion.newMainSnapshot.name] + ) + // THEN: the swift version file is updated to this toolchain version + var versionFileContents = try String(contentsOf: versionFile) + #expect(ToolchainVersion.newMainSnapshot.name == versionFileContents) + // THEN: the use command reports this toolchain to be in use + #expect(output.contains(where: { $0.contains(ToolchainVersion.newMainSnapshot.name) })) + + // GIVEN: a directory with no swift version file at the top of a git repository + try await fs.remove(atPath: versionFile) + let gitDir = SwiftlyTests.ctx.currentDirectory / ".git" + try await fs.mkdir(atPath: gitDir) + // WHEN: using a toolchain version + try await SwiftlyTests.runCommand( + Use.self, ["use", ToolchainVersion.newReleaseSnapshot.name] + ) + // THEN: a swift version file is created + #expect(try await fs.exists(atPath: versionFile)) + // THEN: the version file contains the specified version + versionFileContents = try String(contentsOf: versionFile) + #expect(ToolchainVersion.newReleaseSnapshot.name == versionFileContents) + + // GIVEN: a directory with a swift version file at the top of a git repository + try "1.2.3".write(to: versionFile, atomically: true) + // WHEN: using with a toolchain selector that can select more than one version, but matches one of the installed toolchains + let broadSelector = ToolchainSelector.stable( + major: ToolchainVersion.newStable.asStableRelease!.major, minor: nil, patch: nil + ) + try await SwiftlyTests.runCommand(Use.self, ["use", broadSelector.description]) + // THEN: the swift version file is set to the specific toolchain version that was installed including major, minor, and patch + versionFileContents = try String(contentsOf: versionFile) + #expect(ToolchainVersion.newStable.name == versionFileContents) + } } /// Tests that running a use command without an argument prints the currently in-use toolchain. @@ -264,17 +329,32 @@ import Testing .newMainSnapshot, .newReleaseSnapshot, ] - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { - let decoder = JSONDecoder() - for toolchain in toolchains { - var output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", "-g", "--format", "json", toolchain.name], format: .json) - let result = try decoder.decode(ToolchainSetInfo.self, from: output[0].data(using: .utf8)!) - #expect(result.version == toolchain) - - output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", "-g", "--print-location", "--format", "json"], format: .json) - let result2 = try decoder.decode(LocationInfo.self, from: output[0].data(using: .utf8)!) - #expect(result2.path == Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, toolchain).string) + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) + { + for toolchain in toolchains { + var output = try await SwiftlyTests.runWithMockedIO( + Use.self, ["use", "-g", "--format", "json", toolchain.name], format: .json + ) + // Decode the output to a dictionary, to avoid making everything Decodable. + let result = + try JSONSerialization.jsonObject( + with: output[0].data(using: .utf8)!, options: [] + ) as! [String: Any] + #expect(result["version"] as! String == toolchain.name) + + output = try await SwiftlyTests.runWithMockedIO( + Use.self, ["use", "-g", "--print-location", "--format", "json"], format: .json + ) + let result2 = + try JSONSerialization.jsonObject( + with: output[0].data(using: .utf8)!, options: [] + ) as! [String: Any] + #expect( + result2["path"] as! String + == Swiftly.currentPlatform.findToolchainLocation( + SwiftlyTests.ctx, toolchain + ).string) + } } - } } }