From 4eddc22b119a59aa157ee357a9d17f0a7b774fc9 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 3 Apr 2025 16:19:33 -0400 Subject: [PATCH 01/12] DRAFT: Refactor global mutable state into a context migrate to swift testing --- Sources/LinuxPlatform/Linux.swift | 60 ++- Sources/MacOSPlatform/MacOS.swift | 53 ++- Sources/Swiftly/Config.swift | 16 +- Sources/Swiftly/Init.swift | 74 ++-- Sources/Swiftly/Install.swift | 68 ++-- Sources/Swiftly/List.swift | 26 +- Sources/Swiftly/ListAvailable.swift | 20 +- Sources/Swiftly/Proxy.swift | 18 +- Sources/Swiftly/Run.swift | 62 ++-- Sources/Swiftly/SelfUpdate.swift | 28 +- Sources/Swiftly/Swiftly.swift | 24 +- Sources/Swiftly/Uninstall.swift | 42 ++- Sources/Swiftly/Update.swift | 45 ++- Sources/Swiftly/Use.swift | 42 ++- Sources/SwiftlyCore/HTTPClient.swift | 59 +-- Sources/SwiftlyCore/Platform.swift | 68 ++-- Sources/SwiftlyCore/SwiftlyCore.swift | 77 ++-- Sources/SwiftlyCore/Utils.swift | 6 +- Tests/SwiftlyTests/E2ETests.swift | 65 ---- Tests/SwiftlyTests/HTTPClientTests.swift | 63 +--- Tests/SwiftlyTests/InitTests.swift | 156 ++++---- Tests/SwiftlyTests/InstallTests.swift | 213 +++++------ Tests/SwiftlyTests/ListTests.swift | 97 +++-- Tests/SwiftlyTests/PlatformTests.swift | 54 +-- Tests/SwiftlyTests/RunTests.swift | 98 +++-- Tests/SwiftlyTests/SelfUpdateTests.swift | 16 +- Tests/SwiftlyTests/SwiftlyTests.swift | 351 ++++++++---------- .../SwiftlyTests/ToolchainSelectorTests.swift | 18 +- Tests/SwiftlyTests/UninstallTests.swift | 200 +++++----- Tests/SwiftlyTests/UpdateTests.swift | 213 +++++------ Tests/SwiftlyTests/UseTests.swift | 224 ++++++----- 31 files changed, 1185 insertions(+), 1371 deletions(-) delete mode 100644 Tests/SwiftlyTests/E2ETests.swift diff --git a/Sources/LinuxPlatform/Linux.swift b/Sources/LinuxPlatform/Linux.swift index 83864638..f435617b 100644 --- a/Sources/LinuxPlatform/Linux.swift +++ b/Sources/LinuxPlatform/Linux.swift @@ -29,25 +29,21 @@ public struct Linux: Platform { } } - public var swiftlyBinDir: URL { - SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) } + public func swiftlyBinDir(_ ctx: SwiftlyCoreContext) -> URL { + ctx.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) } ?? ProcessInfo.processInfo.environment["SWIFTLY_BIN_DIR"].map { URL(fileURLWithPath: $0) } ?? FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent(".local/share/swiftly/bin", isDirectory: true) } - public var swiftlyToolchainsDir: URL { - self.swiftlyHomeDir.appendingPathComponent("toolchains", isDirectory: true) + public func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> URL { + self.swiftlyHomeDir(ctx).appendingPathComponent("toolchains", isDirectory: true) } public var toolchainFileExtension: String { "tar.gz" } - public func isSystemDependencyPresent(_: SystemDependency) -> Bool { - true - } - private static let skipVerificationMessage: String = "To skip signature verification, specify the --no-verify flag." public func verifySwiftlySystemPrerequisites() throws { @@ -330,17 +326,17 @@ public struct Linux: Platform { } } - public func install(from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws { + public func install(_ ctx: SwiftlyCoreContext, from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws { guard tmpFile.fileExists() else { throw SwiftlyError(message: "\(tmpFile) doesn't exist") } - if !self.swiftlyToolchainsDir.fileExists() { - try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir, withIntermediateDirectories: false) + if !self.swiftlyToolchainsDir(ctx).fileExists() { + try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir(ctx), withIntermediateDirectories: false) } - SwiftlyCore.print("Extracting toolchain...") - let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent(version.name) + SwiftlyCore.print(ctx, "Extracting toolchain...") + let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent(version.name) if toolchainDir.fileExists() { try FileManager.default.removeItem(at: toolchainDir) @@ -354,7 +350,7 @@ public struct Linux: Platform { let destination = toolchainDir.appendingPathComponent(String(relativePath)) if verbose { - SwiftlyCore.print("\(destination.path)") + SwiftlyCore.print(ctx, "\(destination.path)") } // prepend /path/to/swiftlyHomeDir/toolchains/ to each file name @@ -362,7 +358,7 @@ public struct Linux: Platform { } } - public func extractSwiftlyAndInstall(from archive: URL) throws { + public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) throws { guard archive.fileExists() else { throw SwiftlyError(message: "\(archive) doesn't exist") } @@ -370,7 +366,7 @@ public struct Linux: Platform { let tmpDir = self.getTempFilePath() try FileManager.default.createDirectory(atPath: tmpDir.path, withIntermediateDirectories: true) - SwiftlyCore.print("Extracting new swiftly...") + SwiftlyCore.print(ctx, "Extracting new swiftly...") try extractArchive(atPath: archive) { name in // Extract to the temporary directory tmpDir.appendingPathComponent(String(name)) @@ -379,8 +375,8 @@ public struct Linux: Platform { try self.runProgram(tmpDir.appendingPathComponent("swiftly").path, "init") } - public func uninstall(_ toolchain: ToolchainVersion, verbose _: Bool) throws { - let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent(toolchain.name) + public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose _: Bool) throws { + let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent(toolchain.name) try FileManager.default.removeItem(at: toolchainDir) } @@ -394,9 +390,9 @@ public struct Linux: Platform { FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") } - public func verifySignature(httpClient: SwiftlyHTTPClient, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws { + public func verifySignature(_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws { if verbose { - SwiftlyCore.print("Downloading toolchain signature...") + SwiftlyCore.print(ctx, "Downloading toolchain signature...") } let sigFile = self.getTempFilePath() @@ -405,12 +401,12 @@ public struct Linux: Platform { try? FileManager.default.removeItem(at: sigFile) } - try await httpClient.downloadFile( + try await ctx.httpClient.downloadFile( url: archiveDownloadURL.appendingPathExtension("sig"), to: sigFile ) - SwiftlyCore.print("Verifying toolchain signature...") + SwiftlyCore.print(ctx, "Verifying toolchain signature...") do { try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: !verbose) } catch { @@ -418,7 +414,7 @@ public struct Linux: Platform { } } - private func manualSelectPlatform(_ platformPretty: String?) async -> PlatformDefinition { + private func manualSelectPlatform(_ ctx: SwiftlyCoreContext, _ platformPretty: String?) async -> PlatformDefinition { if let platformPretty { print("\(platformPretty) is not an officially supported platform, but the toolchains for another platform may still work on it.") } else { @@ -434,7 +430,7 @@ public struct Linux: Platform { \(selections) """) - let choice = SwiftlyCore.readLine(prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ") ?? "0" + let choice = SwiftlyCore.readLine(ctx, prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ") ?? "0" guard let choiceNum = Int(choice) else { fatalError("Installation canceled") @@ -447,7 +443,7 @@ public struct Linux: Platform { return self.linuxPlatforms[choiceNum - 1] } - public func detectPlatform(disableConfirmation: Bool, platform: String?) async throws -> PlatformDefinition { + public func detectPlatform(_ ctx: SwiftlyCoreContext, disableConfirmation: Bool, platform: String?) async throws -> PlatformDefinition { // We've been given a hint to use if let platform { guard let pd = linuxPlatforms.first(where: { $0.nameFull == platform }) else { @@ -475,7 +471,7 @@ public struct Linux: Platform { } else { print(message) } - return await self.manualSelectPlatform(platformPretty) + return await self.manualSelectPlatform(ctx, platformPretty) } let releaseInfo = try String(contentsOfFile: releaseFile, encoding: .utf8) @@ -502,7 +498,7 @@ public struct Linux: Platform { } else { print(message) } - return await self.manualSelectPlatform(platformPretty) + return await self.manualSelectPlatform(ctx, platformPretty) } if (id + (idlike ?? "")).contains("amzn") { @@ -513,7 +509,7 @@ public struct Linux: Platform { } else { print(message) } - return await self.manualSelectPlatform(platformPretty) + return await self.manualSelectPlatform(ctx, platformPretty) } return .amazonlinux2 @@ -525,7 +521,7 @@ public struct Linux: Platform { } else { print(message) } - return await self.manualSelectPlatform(platformPretty) + return await self.manualSelectPlatform(ctx, platformPretty) } return .rhel9 @@ -539,7 +535,7 @@ public struct Linux: Platform { } else { print(message) } - return await self.manualSelectPlatform(platformPretty) + return await self.manualSelectPlatform(ctx, platformPretty) } public func getShell() async throws -> String { @@ -559,8 +555,8 @@ public struct Linux: Platform { return "/bin/bash" } - public func findToolchainLocation(_ toolchain: ToolchainVersion) -> URL { - self.swiftlyToolchainsDir.appendingPathComponent("\(toolchain.name)") + public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL { + self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.name)") } public static let currentPlatform: any Platform = Linux() diff --git a/Sources/MacOSPlatform/MacOS.swift b/Sources/MacOSPlatform/MacOS.swift index 6fd6c351..12a28b2a 100644 --- a/Sources/MacOSPlatform/MacOS.swift +++ b/Sources/MacOSPlatform/MacOS.swift @@ -18,15 +18,15 @@ public struct MacOS: Platform { .appendingPathComponent(".swiftly", isDirectory: true) } - public var swiftlyBinDir: URL { - SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) } + public func swiftlyBinDir(_ ctx: SwiftlyCoreContext) -> URL { + ctx.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) } ?? ProcessInfo.processInfo.environment["SWIFTLY_BIN_DIR"].map { URL(fileURLWithPath: $0) } ?? FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent(".swiftly/bin", isDirectory: true) } - public var swiftlyToolchainsDir: URL { - SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("Toolchains", isDirectory: true) } + public func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> URL { + ctx.mockedHomeDir.map { $0.appendingPathComponent("Toolchains", isDirectory: true) } // The toolchains are always installed here by the installer. We bypass the installer in the case of test mocks ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Developer/Toolchains", isDirectory: true) } @@ -35,11 +35,6 @@ public struct MacOS: Platform { "pkg" } - public func isSystemDependencyPresent(_: SystemDependency) -> Bool { - // All system dependencies on macOS should be present - true - } - public func verifySwiftlySystemPrerequisites() throws { // All system prerequisites are there for swiftly on macOS } @@ -49,24 +44,24 @@ public struct MacOS: Platform { nil } - public func install(from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws { + public func install(_ ctx: SwiftlyCoreContext, from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws { guard tmpFile.fileExists() else { throw SwiftlyError(message: "\(tmpFile) doesn't exist") } - if !self.swiftlyToolchainsDir.fileExists() { - try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir, withIntermediateDirectories: false) + if !self.swiftlyToolchainsDir(ctx).fileExists() { + try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir(ctx), withIntermediateDirectories: false) } - if SwiftlyCore.mockedHomeDir == nil { - SwiftlyCore.print("Installing package in user home directory...") + if ctx.mockedHomeDir == nil { + SwiftlyCore.print(ctx, "Installing package in user home directory...") try runProgram("installer", "-verbose", "-pkg", tmpFile.path, "-target", "CurrentUserHomeDirectory", quiet: !verbose) } else { // In the case of a mock for testing purposes we won't use the installer, perferring a manual process because // the installer will not install to an arbitrary path, only a volume or user home directory. - SwiftlyCore.print("Expanding pkg...") + SwiftlyCore.print(ctx, "Expanding pkg...") let tmpDir = self.getTempFilePath() - let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(version.identifier).xctoolchain", isDirectory: true) + let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(version.identifier).xctoolchain", isDirectory: true) if !toolchainDir.fileExists() { try FileManager.default.createDirectory(at: toolchainDir, withIntermediateDirectories: false) } @@ -78,26 +73,26 @@ public struct MacOS: Platform { payload = tmpDir.appendingPathComponent("\(version.identifier)-osx-package.pkg/Payload") } - SwiftlyCore.print("Untarring pkg Payload...") + SwiftlyCore.print(ctx, "Untarring pkg Payload...") try runProgram("tar", "-C", toolchainDir.path, "-xvf", payload.path, quiet: !verbose) } } - public func extractSwiftlyAndInstall(from archive: URL) throws { + public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) throws { guard archive.fileExists() else { throw SwiftlyError(message: "\(archive) doesn't exist") } let homeDir: URL - if SwiftlyCore.mockedHomeDir == nil { + if ctx.mockedHomeDir == nil { homeDir = FileManager.default.homeDirectoryForCurrentUser - SwiftlyCore.print("Extracting the swiftly package...") + SwiftlyCore.print(ctx, "Extracting the swiftly package...") try runProgram("installer", "-pkg", archive.path, "-target", "CurrentUserHomeDirectory") try? runProgram("pkgutil", "--volume", homeDir.path, "--forget", "org.swift.swiftly") } else { - homeDir = SwiftlyCore.mockedHomeDir ?? FileManager.default.homeDirectoryForCurrentUser + homeDir = ctx.mockedHomeDir ?? FileManager.default.homeDirectoryForCurrentUser let installDir = homeDir.appendingPathComponent(".swiftly") try FileManager.default.createDirectory(atPath: installDir.path, withIntermediateDirectories: true) @@ -114,17 +109,17 @@ public struct MacOS: Platform { throw SwiftlyError(message: "Payload file could not be found at \(tmpDir).") } - SwiftlyCore.print("Extracting the swiftly package into \(installDir.path)...") + SwiftlyCore.print(ctx, "Extracting the swiftly package into \(installDir.path)...") try runProgram("tar", "-C", installDir.path, "-xvf", payload.path, quiet: false) } try self.runProgram(homeDir.appendingPathComponent(".swiftly/bin/swiftly").path, "init") } - public func uninstall(_ toolchain: ToolchainVersion, verbose: Bool) throws { - SwiftlyCore.print("Uninstalling package in user home directory...") + public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose: Bool) throws { + SwiftlyCore.print(ctx, "Uninstalling package in user home directory...") - let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(toolchain.identifier).xctoolchain", isDirectory: true) + let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.identifier).xctoolchain", isDirectory: true) let decoder = PropertyListDecoder() let infoPlist = toolchainDir.appendingPathComponent("Info.plist") @@ -150,12 +145,12 @@ public struct MacOS: Platform { FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID()).pkg") } - public func verifySignature(httpClient _: SwiftlyHTTPClient, archiveDownloadURL _: URL, archive _: URL, verbose _: Bool) async throws { + public func verifySignature(_: SwiftlyCoreContext, archiveDownloadURL _: URL, archive _: URL, verbose _: Bool) async throws { // No signature verification is required on macOS since the pkg files have their own signing // mechanism and the swift.org downloadables are trusted by stock macOS installations. } - public func detectPlatform(disableConfirmation _: Bool, platform _: String?) async -> PlatformDefinition { + public func detectPlatform(_: SwiftlyCoreContext, disableConfirmation _: Bool, platform _: String?) async -> PlatformDefinition { // No special detection required on macOS platform .macOS } @@ -175,8 +170,8 @@ public struct MacOS: Platform { return "/bin/zsh" } - public func findToolchainLocation(_ toolchain: ToolchainVersion) -> URL { - self.swiftlyToolchainsDir.appendingPathComponent("\(toolchain.identifier).xctoolchain") + public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL { + self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.identifier).xctoolchain") } public static let currentPlatform: any Platform = MacOS() diff --git a/Sources/Swiftly/Config.swift b/Sources/Swiftly/Config.swift index 304f1594..0be2aa05 100644 --- a/Sources/Swiftly/Config.swift +++ b/Sources/Swiftly/Config.swift @@ -24,9 +24,9 @@ public struct Config: Codable, Equatable { } /// Read the config file from disk. - public static func load() throws -> Config { + public static func load(_ ctx: SwiftlyCoreContext) throws -> Config { do { - let data = try Data(contentsOf: Swiftly.currentPlatform.swiftlyConfigFile) + let data = try Data(contentsOf: Swiftly.currentPlatform.swiftlyConfigFile(ctx)) var config = try JSONDecoder().decode(Config.self, from: data) if config.version == nil { // Assume that the version of swiftly is 0.3.0 because that is the last @@ -36,7 +36,7 @@ public struct Config: Codable, Equatable { return config } catch { let msg = """ - Could not load swiftly's configuration file at \(Swiftly.currentPlatform.swiftlyConfigFile.path). + Could not load swiftly's configuration file at \(Swiftly.currentPlatform.swiftlyConfigFile(ctx).path). To begin using swiftly you can install it: '\(CommandLine.arguments[0]) init'. """ @@ -45,9 +45,9 @@ public struct Config: Codable, Equatable { } /// Write the contents of this `Config` struct to disk. - public func save() throws { + public func save(_ ctx: SwiftlyCoreContext) throws { let outData = try Self.makeEncoder().encode(self) - try outData.write(to: Swiftly.currentPlatform.swiftlyConfigFile, options: .atomic) + try outData.write(to: Swiftly.currentPlatform.swiftlyConfigFile(ctx), options: .atomic) } public func listInstalledToolchains(selector: ToolchainSelector?) -> [ToolchainVersion] { @@ -70,11 +70,11 @@ public struct Config: Codable, Equatable { /// Load the config, pass it to the provided closure, and then /// save the modified config to disk. - public static func update(f: (inout Config) throws -> Void) throws { - var config = try Config.load() + public static func update(_ ctx: SwiftlyCoreContext, f: (inout Config) throws -> Void) throws { + var config = try Config.load(ctx) try f(&config) // only save the updates if the prior closure invocation succeeded - try config.save() + try config.save(ctx) } } diff --git a/Sources/Swiftly/Init.swift b/Sources/Swiftly/Init.swift index d9c0efca..739055fb 100644 --- a/Sources/Swiftly/Init.swift +++ b/Sources/Swiftly/Init.swift @@ -30,14 +30,18 @@ struct Init: SwiftlyCommand { public mutating func validate() throws {} mutating func run() async throws { - try await Self.execute(assumeYes: self.root.assumeYes, noModifyProfile: self.noModifyProfile, overwrite: self.overwrite, platform: self.platform, verbose: self.root.verbose, skipInstall: self.skipInstall, quietShellFollowup: self.quietShellFollowup) + try await self.run(Swiftly.createDefaultContext()) + } + + mutating func run(_ ctx: SwiftlyCoreContext = Swiftly.createDefaultContext()) async throws { + try await Self.execute(ctx, assumeYes: self.root.assumeYes, noModifyProfile: self.noModifyProfile, overwrite: self.overwrite, platform: self.platform, verbose: self.root.verbose, skipInstall: self.skipInstall, quietShellFollowup: self.quietShellFollowup) } /// Initialize the installation of swiftly. - static func execute(assumeYes: Bool, noModifyProfile: Bool, overwrite: Bool, platform: String?, verbose: Bool, skipInstall: Bool, quietShellFollowup: Bool) async throws { + static func execute(_ ctx: SwiftlyCoreContext, assumeYes: Bool, noModifyProfile: Bool, overwrite: Bool, platform: String?, verbose: Bool, skipInstall: Bool, quietShellFollowup: Bool) async throws { try Swiftly.currentPlatform.verifySwiftlySystemPrerequisites() - var config = try? Config.load() + var config = try? Config.load(ctx) if var config, !overwrite && ( @@ -49,12 +53,12 @@ struct Init: SwiftlyCommand { // This is a simple upgrade from the 0.4.0 pre-releases, or 1.x // Move our executable over to the correct place - try Swiftly.currentPlatform.installSwiftlyBin() + try Swiftly.currentPlatform.installSwiftlyBin(ctx) // Update and save the version config.version = SwiftlyCore.version - try config.save() + try config.save(ctx) return } @@ -75,9 +79,9 @@ struct Init: SwiftlyCommand { Swiftly installs files into the following locations: - \(Swiftly.currentPlatform.swiftlyHomeDir.path) - Directory for configuration files - \(Swiftly.currentPlatform.swiftlyBinDir.path) - Links to the binaries of the active toolchain - \(Swiftly.currentPlatform.swiftlyToolchainsDir.path) - Directory hosting installed toolchains + \(Swiftly.currentPlatform.swiftlyHomeDir(ctx).path) - Directory for configuration files + \(Swiftly.currentPlatform.swiftlyBinDir(ctx).path) - Links to the binaries of the active toolchain + \(Swiftly.currentPlatform.swiftlyToolchainsDir(ctx).path) - Directory hosting installed toolchains These locations can be changed by setting the environment variables SWIFTLY_HOME_DIR and SWIFTLY_BIN_DIR before running 'swiftly init' again. @@ -109,30 +113,34 @@ struct Init: SwiftlyCommand { """ } - SwiftlyCore.print(msg) + SwiftlyCore.print(ctx, msg) - guard SwiftlyCore.promptForConfirmation(defaultBehavior: true) else { + guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: true) else { throw SwiftlyError(message: "swiftly installation has been cancelled") } } - let shell = if let s = ProcessInfo.processInfo.environment["SHELL"] { - s + let shell = if let mockedShell = ctx.mockedShell { + mockedShell } else { - try await Swiftly.currentPlatform.getShell() + if let s = ProcessInfo.processInfo.environment["SHELL"] { + s + } else { + try await Swiftly.currentPlatform.getShell() + } } let envFile: URL let sourceLine: String if shell.hasSuffix("fish") { - envFile = Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("env.fish", isDirectory: false) + envFile = Swiftly.currentPlatform.swiftlyHomeDir(ctx).appendingPathComponent("env.fish", isDirectory: false) sourceLine = """ # Added by swiftly source "\(envFile.path)" """ } else { - envFile = Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("env.sh", isDirectory: false) + envFile = Swiftly.currentPlatform.swiftlyHomeDir(ctx).appendingPathComponent("env.sh", isDirectory: false) sourceLine = """ # Added by swiftly @@ -141,12 +149,12 @@ struct Init: SwiftlyCommand { } if overwrite { - try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyToolchainsDir) - try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyHomeDir) + try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyToolchainsDir(ctx)) + try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyHomeDir(ctx)) } // Go ahead and create the directories as needed - for requiredDir in Swiftly.requiredDirectories { + for requiredDir in Swiftly.requiredDirectories(ctx) { if !requiredDir.fileExists() { do { try FileManager.default.createDirectory(at: requiredDir, withIntermediateDirectories: true) @@ -158,26 +166,26 @@ struct Init: SwiftlyCommand { // Force the configuration to be present. Generate it if it doesn't already exist or overwrite is set if overwrite || config == nil { - let pd = try await Swiftly.currentPlatform.detectPlatform(disableConfirmation: assumeYes, platform: platform) + let pd = try await Swiftly.currentPlatform.detectPlatform(ctx, disableConfirmation: assumeYes, platform: platform) var c = Config(inUse: nil, installedToolchains: [], platform: pd) // Stamp the current version of swiftly on this config c.version = SwiftlyCore.version - try c.save() + try c.save(ctx) config = c } guard var config else { throw SwiftlyError(message: "Configuration could not be set") } // Move our executable over to the correct place - try Swiftly.currentPlatform.installSwiftlyBin() + try Swiftly.currentPlatform.installSwiftlyBin(ctx) if overwrite || !FileManager.default.fileExists(atPath: envFile.path) { - SwiftlyCore.print("Creating shell environment file for the user...") + SwiftlyCore.print(ctx, "Creating shell environment file for the user...") var env = "" if shell.hasSuffix("fish") { env = """ - set -x SWIFTLY_HOME_DIR "\(Swiftly.currentPlatform.swiftlyHomeDir.path)" - set -x SWIFTLY_BIN_DIR "\(Swiftly.currentPlatform.swiftlyBinDir.path)" + set -x SWIFTLY_HOME_DIR "\(Swiftly.currentPlatform.swiftlyHomeDir(ctx).path)" + set -x SWIFTLY_BIN_DIR "\(Swiftly.currentPlatform.swiftlyBinDir(ctx).path)" if not contains "$SWIFTLY_BIN_DIR" $PATH set -x PATH "$SWIFTLY_BIN_DIR" $PATH end @@ -185,8 +193,8 @@ struct Init: SwiftlyCommand { """ } else { env = """ - export SWIFTLY_HOME_DIR="\(Swiftly.currentPlatform.swiftlyHomeDir.path)" - export SWIFTLY_BIN_DIR="\(Swiftly.currentPlatform.swiftlyBinDir.path)" + export SWIFTLY_HOME_DIR="\(Swiftly.currentPlatform.swiftlyHomeDir(ctx).path)" + export SWIFTLY_BIN_DIR="\(Swiftly.currentPlatform.swiftlyBinDir(ctx).path)" if [[ ":$PATH:" != *":$SWIFTLY_BIN_DIR:"* ]]; then export PATH="$SWIFTLY_BIN_DIR:$PATH" fi @@ -198,9 +206,9 @@ struct Init: SwiftlyCommand { } if !noModifyProfile { - SwiftlyCore.print("Updating profile...") + SwiftlyCore.print(ctx, "Updating profile...") - let userHome = FileManager.default.homeDirectoryForCurrentUser + let userHome = ctx.mockedHomeDir ?? FileManager.default.homeDirectoryForCurrentUser let profileHome: URL if shell.hasSuffix("zsh") { @@ -246,15 +254,15 @@ struct Init: SwiftlyCommand { var pathChanged = false if !skipInstall { - let latestVersion = try await Install.resolve(config: config, selector: ToolchainSelector.latest) - (postInstall, pathChanged) = try await Install.execute(version: latestVersion, &config, useInstalledToolchain: true, verifySignature: true, verbose: verbose, assumeYes: assumeYes) + let latestVersion = try await Install.resolve(ctx, config: config, selector: ToolchainSelector.latest) + (postInstall, pathChanged) = try await Install.execute(ctx, version: latestVersion, &config, useInstalledToolchain: true, verifySignature: true, verbose: verbose, assumeYes: assumeYes) } if addEnvToProfile { try Data(sourceLine.utf8).append(to: profileHome) if !quietShellFollowup { - SwiftlyCore.print(""" + SwiftlyCore.print(ctx, """ To begin using installed swiftly from your current shell, first run the following command: \(sourceLine) @@ -264,7 +272,7 @@ struct Init: SwiftlyCommand { // Fish doesn't have path caching, so this might only be needed for bash/zsh if pathChanged && !quietShellFollowup && !shell.hasSuffix("fish") { - SwiftlyCore.print(""" + SwiftlyCore.print(ctx, """ Your shell caches items on your path for better performance. Swiftly has added items to your path that may not get picked up right away. You can update your shell's environment by running @@ -277,7 +285,7 @@ struct Init: SwiftlyCommand { } if let postInstall { - SwiftlyCore.print(""" + SwiftlyCore.print(ctx, """ There are some dependencies that should be installed before using this toolchain. You can run the following script as the system administrator (e.g. root) to prepare your system: diff --git a/Sources/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index 3bbb9299..649cf975 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -69,16 +69,20 @@ struct Install: SwiftlyCommand { } mutating func run() async throws { - try validateSwiftly() + try await self.run(Swiftly.createDefaultContext()) + } + + mutating func run(_ ctx: SwiftlyCoreContext) async throws { + try validateSwiftly(ctx) - var config = try Config.load() + var config = try Config.load(ctx) var selector: ToolchainSelector if let version = self.version { selector = try ToolchainSelector(parsing: version) } else { - if case let (_, result) = try await selectToolchain(config: &config), + if case let (_, result) = try await selectToolchain(ctx, config: &config), case let .swiftVersionFile(_, sel, error) = result { if let sel = sel { @@ -93,8 +97,9 @@ struct Install: SwiftlyCommand { } } - let toolchainVersion = try await Self.resolve(config: config, selector: selector) + let toolchainVersion = try await Self.resolve(ctx, config: config, selector: selector) let (postInstallScript, pathChanged) = try await Self.execute( + ctx, version: toolchainVersion, &config, useInstalledToolchain: self.use, @@ -111,7 +116,7 @@ struct Install: SwiftlyCommand { // Fish doesn't cache its path, so this instruction is not necessary. if pathChanged && !shell.hasSuffix("fish") { - SwiftlyCore.print(""" + SwiftlyCore.print(ctx, """ NOTE: Swiftly has updated some elements in your path and your shell may not yet be aware of the changes. You can update your shell's environment by running @@ -139,6 +144,7 @@ struct Install: SwiftlyCommand { } public static func execute( + _ ctx: SwiftlyCoreContext, version: ToolchainVersion, _ config: inout Config, useInstalledToolchain: Bool, @@ -147,16 +153,16 @@ struct Install: SwiftlyCommand { assumeYes: Bool ) async throws -> (postInstall: String?, pathChanged: Bool) { guard !config.installedToolchains.contains(version) else { - SwiftlyCore.print("\(version) is already installed.") + SwiftlyCore.print(ctx, "\(version) is already installed.") return (nil, false) } // Ensure the system is set up correctly before downloading it. Problems that prevent installation // will throw, while problems that prevent use of the toolchain will be written out as a post install // script for the user to run afterwards. - let postInstallScript = try await Swiftly.currentPlatform.verifySystemPrerequisitesForInstall(httpClient: SwiftlyCore.httpClient, platformName: config.platform.name, version: version, requireSignatureValidation: verifySignature) + let postInstallScript = try await Swiftly.currentPlatform.verifySystemPrerequisitesForInstall(httpClient: ctx.httpClient, platformName: config.platform.name, version: version, requireSignatureValidation: verifySignature) - SwiftlyCore.print("Installing \(version)") + SwiftlyCore.print(ctx, "Installing \(version)") let tmpFile = Swiftly.currentPlatform.getTempFilePath() FileManager.default.createFile(atPath: tmpFile.path, contents: nil) @@ -209,7 +215,7 @@ struct Install: SwiftlyCommand { var lastUpdate = Date() do { - try await SwiftlyCore.httpClient.downloadFile( + try await ctx.httpClient.downloadFile( url: url, to: tmpFile, reportProgress: { progress in @@ -241,23 +247,23 @@ struct Install: SwiftlyCommand { if verifySignature { try await Swiftly.currentPlatform.verifySignature( - httpClient: SwiftlyCore.httpClient, + ctx, archiveDownloadURL: url, archive: tmpFile, verbose: verbose ) } - try Swiftly.currentPlatform.install(from: tmpFile, version: version, verbose: verbose) + try Swiftly.currentPlatform.install(ctx, from: tmpFile, version: version, verbose: verbose) var pathChanged = false // Create proxies if we have a location where we can point them - if let proxyTo = try? Swiftly.currentPlatform.findSwiftlyBin() { + if let proxyTo = try? Swiftly.currentPlatform.findSwiftlyBin(ctx) { // Ensure swiftly doesn't overwrite any existing executables without getting confirmation first. - let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir + let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(ctx) let swiftlyBinDirContents = (try? FileManager.default.contentsOfDirectory(atPath: swiftlyBinDir.path)) ?? [String]() - let toolchainBinDir = Swiftly.currentPlatform.findToolchainBinDir(version) + let toolchainBinDir = Swiftly.currentPlatform.findToolchainBinDir(ctx, version) let toolchainBinDirContents = try FileManager.default.contentsOfDirectory(atPath: toolchainBinDir.path) let existingProxies = swiftlyBinDirContents.filter { bin in @@ -269,25 +275,25 @@ struct Install: SwiftlyCommand { let overwrite = Set(toolchainBinDirContents).subtracting(existingProxies).intersection(swiftlyBinDirContents) if !overwrite.isEmpty && !assumeYes { - SwiftlyCore.print("The following existing executables will be overwritten:") + SwiftlyCore.print(ctx, "The following existing executables will be overwritten:") for executable in overwrite { - SwiftlyCore.print(" \(swiftlyBinDir.appendingPathComponent(executable).path)") + SwiftlyCore.print(ctx, " \(swiftlyBinDir.appendingPathComponent(executable).path)") } - guard SwiftlyCore.promptForConfirmation(defaultBehavior: false) else { + guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: false) else { throw SwiftlyError(message: "Toolchain installation has been cancelled") } } if verbose { - SwiftlyCore.print("Setting up toolchain proxies...") + SwiftlyCore.print(ctx, "Setting up toolchain proxies...") } let proxiesToCreate = Set(toolchainBinDirContents).subtracting(swiftlyBinDirContents).union(overwrite) for p in proxiesToCreate { - let proxy = Swiftly.currentPlatform.swiftlyBinDir.appendingPathComponent(p) + let proxy = Swiftly.currentPlatform.swiftlyBinDir(ctx).appendingPathComponent(p) if proxy.fileExists() { try FileManager.default.removeItem(at: proxy) @@ -304,33 +310,33 @@ struct Install: SwiftlyCommand { config.installedToolchains.insert(version) - try config.save() + try config.save(ctx) // If this is the first installed toolchain, mark it as in-use regardless of whether the // --use argument was provided. if useInstalledToolchain { - try await Use.execute(version, globalDefault: false, &config) + try await Use.execute(ctx, version, globalDefault: false, &config) } // We always update the global default toolchain if there is none set. This could // be the only toolchain that is installed, which makes it the only choice. if config.inUse == nil { config.inUse = version - try config.save() - SwiftlyCore.print("The global default toolchain has been set to `\(version)`") + try config.save(ctx) + SwiftlyCore.print(ctx, "The global default toolchain has been set to `\(version)`") } - SwiftlyCore.print("\(version) installed successfully!") + SwiftlyCore.print(ctx, "\(version) installed successfully!") return (postInstallScript, pathChanged) } /// Utilize the swift.org API along with the provided selector to select a toolchain for install. - public static func resolve(config: Config, selector: ToolchainSelector) async throws -> ToolchainVersion { + public static func resolve(_ ctx: SwiftlyCoreContext, config: Config, selector: ToolchainSelector) async throws -> ToolchainVersion { switch selector { case .latest: - SwiftlyCore.print("Fetching the latest stable Swift release...") + SwiftlyCore.print(ctx, "Fetching the latest stable Swift release...") - guard let release = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform, limit: 1).first else { + guard let release = try await ctx.httpClient.getReleaseToolchains(platform: config.platform, limit: 1).first else { throw SwiftlyError(message: "couldn't get latest releases") } return .stable(release) @@ -346,10 +352,10 @@ struct Install: SwiftlyCommand { return .stable(ToolchainVersion.StableRelease(major: major, minor: minor, patch: patch)) } - SwiftlyCore.print("Fetching the latest stable Swift \(major).\(minor) release...") + SwiftlyCore.print(ctx, "Fetching the latest stable Swift \(major).\(minor) release...") // If a patch was not provided, perform a lookup to get the latest patch release // of the provided major/minor version pair. - let toolchain = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform, limit: 1) { release in + let toolchain = try await ctx.httpClient.getReleaseToolchains(platform: config.platform, limit: 1) { release in release.major == major && release.minor == minor }.first @@ -364,13 +370,13 @@ struct Install: SwiftlyCommand { return ToolchainVersion(snapshotBranch: branch, date: date) } - SwiftlyCore.print("Fetching the latest \(branch) branch snapshot...") + SwiftlyCore.print(ctx, "Fetching the latest \(branch) branch snapshot...") // If a date was not provided, perform a lookup to find the most recent snapshot // for the given branch. let snapshots: [ToolchainVersion.Snapshot] do { - snapshots = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: config.platform, branch: branch, limit: 1) { snapshot in + snapshots = try await ctx.httpClient.getSnapshotToolchains(platform: config.platform, branch: branch, limit: 1) { snapshot in snapshot.branch == branch } } catch let branchNotFoundErr as SwiftlyHTTPClient.SnapshotBranchNotFoundError { diff --git a/Sources/Swiftly/List.swift b/Sources/Swiftly/List.swift index 4ebd5d1d..a9ed84ba 100644 --- a/Sources/Swiftly/List.swift +++ b/Sources/Swiftly/List.swift @@ -34,15 +34,19 @@ struct List: SwiftlyCommand { var toolchainSelector: String? mutating func run() async throws { - try validateSwiftly() + try await self.run(Swiftly.createDefaultContext()) + } + + mutating func run(_ ctx: SwiftlyCoreContext) async throws { + try validateSwiftly(ctx) let selector = try self.toolchainSelector.map { input in try ToolchainSelector(parsing: input) } - var config = try Config.load() + var config = try Config.load(ctx) let toolchains = config.listInstalledToolchains(selector: selector).sorted { $0 > $1 } - let (inUse, _) = try await selectToolchain(config: &config) + let (inUse, _) = try await selectToolchain(ctx, config: &config) let printToolchain = { (toolchain: ToolchainVersion) in var message = "\(toolchain)" @@ -52,7 +56,7 @@ struct List: SwiftlyCommand { if toolchain == config.inUse { message += " (default)" } - SwiftlyCore.print(message) + SwiftlyCore.print(ctx, message) } if let selector { @@ -72,14 +76,14 @@ struct List: SwiftlyCommand { } let message = "Installed \(modifier) toolchains" - SwiftlyCore.print(message) - SwiftlyCore.print(String(repeating: "-", count: message.count)) + SwiftlyCore.print(ctx, message) + SwiftlyCore.print(ctx, String(repeating: "-", count: message.count)) for toolchain in toolchains { printToolchain(toolchain) } } else { - SwiftlyCore.print("Installed release toolchains") - SwiftlyCore.print("----------------------------") + SwiftlyCore.print(ctx, "Installed release toolchains") + SwiftlyCore.print(ctx, "----------------------------") for toolchain in toolchains { guard toolchain.isStableRelease() else { continue @@ -87,9 +91,9 @@ struct List: SwiftlyCommand { printToolchain(toolchain) } - SwiftlyCore.print("") - SwiftlyCore.print("Installed snapshot toolchains") - SwiftlyCore.print("-----------------------------") + SwiftlyCore.print(ctx, "") + SwiftlyCore.print(ctx, "Installed snapshot toolchains") + SwiftlyCore.print(ctx, "-----------------------------") for toolchain in toolchains where toolchain.isSnapshot() { printToolchain(toolchain) } diff --git a/Sources/Swiftly/ListAvailable.swift b/Sources/Swiftly/ListAvailable.swift index b310de32..fd6eb8a8 100644 --- a/Sources/Swiftly/ListAvailable.swift +++ b/Sources/Swiftly/ListAvailable.swift @@ -40,28 +40,32 @@ struct ListAvailable: SwiftlyCommand { } mutating func run() async throws { - try validateSwiftly() + try await self.run(Swiftly.createDefaultContext()) + } + + mutating func run(_ ctx: SwiftlyCoreContext) async throws { + try validateSwiftly(ctx) let selector = try self.toolchainSelector.map { input in try ToolchainSelector(parsing: input) } - let config = try Config.load() + let config = try Config.load(ctx) let tc: [ToolchainVersion] switch selector { case let .snapshot(branch, _): do { - tc = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: config.platform, branch: branch).map { ToolchainVersion.snapshot($0) } + tc = try await ctx.httpClient.getSnapshotToolchains(platform: config.platform, branch: branch).map { ToolchainVersion.snapshot($0) } } catch let branchNotFoundError as SwiftlyHTTPClient.SnapshotBranchNotFoundError { throw SwiftlyError(message: "The snapshot branch \(branchNotFoundError.branch) was not found on swift.org. Note that snapshot toolchains are only available for the current `main` release and the previous x.y (major.minor) release.") } catch { throw error } case .stable, .latest: - tc = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform).map { ToolchainVersion.stable($0) } + tc = try await ctx.httpClient.getReleaseToolchains(platform: config.platform).map { ToolchainVersion.stable($0) } default: - tc = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform).map { ToolchainVersion.stable($0) } + tc = try await ctx.httpClient.getReleaseToolchains(platform: config.platform).map { ToolchainVersion.stable($0) } } let toolchains = tc.filter { selector?.matches(toolchain: $0) ?? true } @@ -76,7 +80,7 @@ struct ListAvailable: SwiftlyCommand { } else if installedToolchains.contains(toolchain) { message += " (installed)" } - SwiftlyCore.print(message) + SwiftlyCore.print(ctx, message) } if let selector { @@ -96,8 +100,8 @@ struct ListAvailable: SwiftlyCommand { } let message = "Available \(modifier) toolchains" - SwiftlyCore.print(message) - SwiftlyCore.print(String(repeating: "-", count: message.count)) + SwiftlyCore.print(ctx, message) + SwiftlyCore.print(ctx, String(repeating: "-", count: message.count)) for toolchain in toolchains { printToolchain(toolchain) } diff --git a/Sources/Swiftly/Proxy.swift b/Sources/Swiftly/Proxy.swift index d0640ee4..b960cc4d 100644 --- a/Sources/Swiftly/Proxy.swift +++ b/Sources/Swiftly/Proxy.swift @@ -4,6 +4,8 @@ import SwiftlyCore @main public enum Proxy { static func main() async throws { + let ctx = SwiftlyCoreContext(httpClient: SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl())) + do { let zero = CommandLine.arguments[0] guard let binName = zero.components(separatedBy: "/").last else { @@ -11,9 +13,11 @@ public enum Proxy { } guard binName != "swiftly" else { + let ctx = SwiftlyCoreContext(httpClient: SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl())) + // Treat this as a swiftly invocation, but first check that we are installed, bootstrapping // the installation process if we aren't. - let configResult = Result { try Config.load() } + let configResult = Result { try Config.load(ctx) } switch configResult { case .success: @@ -25,7 +29,7 @@ public enum Proxy { if CommandLine.arguments.count == 1 { // User ran swiftly with no extra arguments in an uninstalled environment, so we jump directly into // an simple init. - try await Init.execute(assumeYes: false, noModifyProfile: false, overwrite: false, platform: nil, verbose: false, skipInstall: false, quietShellFollowup: false) + try await Init.execute(ctx, assumeYes: false, noModifyProfile: false, overwrite: false, platform: nil, verbose: false, skipInstall: false, quietShellFollowup: false) return } else if CommandLine.arguments.count >= 2 && CommandLine.arguments[1] == "init" { // Let the user run the init command with their arguments, if any. @@ -43,9 +47,9 @@ public enum Proxy { } } - var config = try Config.load() + var config = try Config.load(ctx) - let (toolchain, result) = try await selectToolchain(config: &config) + let (toolchain, result) = try await selectToolchain(ctx, config: &config) // Abort on any errors relating to swift version files if case let .swiftVersionFile(_, _, error) = result, let error = error { @@ -62,14 +66,14 @@ public enum Proxy { } let env = ["SWIFTLY_PROXY_IN_PROGRESS": "1"] - try await Swiftly.currentPlatform.proxy(toolchain, binName, Array(CommandLine.arguments[1...]), env) + try await Swiftly.currentPlatform.proxy(ctx, toolchain, binName, Array(CommandLine.arguments[1...]), env) } catch let terminated as RunProgramError { exit(terminated.exitCode) } catch let error as SwiftlyError { - SwiftlyCore.print(error.message) + SwiftlyCore.print(ctx, error.message) exit(1) } catch { - SwiftlyCore.print("\(error)") + SwiftlyCore.print(ctx, "\(error)") exit(1) } } diff --git a/Sources/Swiftly/Run.swift b/Sources/Swiftly/Run.swift index 2276e9c8..056617d9 100644 --- a/Sources/Swiftly/Run.swift +++ b/Sources/Swiftly/Run.swift @@ -54,16 +54,20 @@ struct Run: SwiftlyCommand { var command: [String] mutating func run() async throws { - try validateSwiftly() + try await self.run(Swiftly.createDefaultContext()) + } + + mutating func run(_ ctx: SwiftlyCoreContext) async throws { + try validateSwiftly(ctx) // Handle the specific case where help is requested of the run subcommand if command == ["--help"] { throw CleanExit.helpRequest(self) } - var config = try Config.load() + var config = try Config.load(ctx) - let (command, selector) = try extractProxyArguments(command: self.command) + let (command, selector) = try Self.extractProxyArguments(command: self.command) let toolchain: ToolchainVersion? @@ -75,7 +79,7 @@ struct Run: SwiftlyCommand { toolchain = matchedToolchain } else { - let (version, result) = try await selectToolchain(config: &config) + let (version, result) = try await selectToolchain(ctx, config: &config) // Abort on any errors relating to swift version files if case let .swiftVersionFile(_, _, error) = result, let error { @@ -90,8 +94,8 @@ struct Run: SwiftlyCommand { } do { - if let outputHandler = SwiftlyCore.outputHandler { - if let output = try await Swiftly.currentPlatform.proxyOutput(toolchain, command[0], [String](command[1...])) { + if let outputHandler = ctx.outputHandler { + if let output = try await Swiftly.currentPlatform.proxyOutput(ctx, toolchain, command[0], [String](command[1...])) { for line in output.split(separator: "\n") { outputHandler.handleOutputLine(String(line)) } @@ -99,42 +103,42 @@ struct Run: SwiftlyCommand { return } - try await Swiftly.currentPlatform.proxy(toolchain, command[0], [String](command[1...])) + try await Swiftly.currentPlatform.proxy(ctx, toolchain, command[0], [String](command[1...])) } catch let terminated as RunProgramError { Foundation.exit(terminated.exitCode) } catch { throw error } } -} -public func extractProxyArguments(command: [String]) throws -> (command: [String], selector: ToolchainSelector?) { - var args: (command: [String], selector: ToolchainSelector?) = (command: [], nil) + public static func extractProxyArguments(command: [String]) throws -> (command: [String], selector: ToolchainSelector?) { + var args: (command: [String], selector: ToolchainSelector?) = (command: [], nil) - var disableEscaping = false + var disableEscaping = false - for c in command { - if !disableEscaping && c == "++" { - disableEscaping = true - continue - } + for c in command { + if !disableEscaping && c == "++" { + disableEscaping = true + continue + } - if !disableEscaping && c.hasPrefix("++") { - args.command.append("+\(String(c.dropFirst(2)))") - continue - } + if !disableEscaping && c.hasPrefix("++") { + args.command.append("+\(String(c.dropFirst(2)))") + continue + } + + if !disableEscaping && c.hasPrefix("+") { + args.selector = try ToolchainSelector(parsing: String(c.dropFirst())) + continue + } - if !disableEscaping && c.hasPrefix("+") { - args.selector = try ToolchainSelector(parsing: String(c.dropFirst())) - continue + args.command.append(c) } - args.command.append(c) - } + guard args.command.count > 0 else { + throw SwiftlyError(message: "Provide at least one command to run.") + } - guard args.command.count > 0 else { - throw SwiftlyError(message: "Provide at least one command to run.") + return args } - - return args } diff --git a/Sources/Swiftly/SelfUpdate.swift b/Sources/Swiftly/SelfUpdate.swift index d3b3d931..982be9d6 100644 --- a/Sources/Swiftly/SelfUpdate.swift +++ b/Sources/Swiftly/SelfUpdate.swift @@ -17,23 +17,27 @@ struct SelfUpdate: SwiftlyCommand { } mutating func run() async throws { - try validateSwiftly() + try await self.run(Swiftly.createDefaultContext()) + } + + mutating func run(_ ctx: SwiftlyCoreContext) async throws { + try validateSwiftly(ctx) - let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir.appendingPathComponent("swiftly") + let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir(ctx).appendingPathComponent("swiftly") guard FileManager.default.fileExists(atPath: swiftlyBin.path) else { throw SwiftlyError(message: "Self update doesn't work when swiftly has been installed externally. Please keep it updated from the source where you installed it in the first place.") } - let _ = try await Self.execute(verbose: self.root.verbose) + let _ = try await Self.execute(ctx, verbose: self.root.verbose) } - public static func execute(verbose: Bool) async throws -> SwiftlyVersion { - SwiftlyCore.print("Checking for swiftly updates...") + public static func execute(_ ctx: SwiftlyCoreContext, verbose: Bool) async throws -> SwiftlyVersion { + SwiftlyCore.print(ctx, "Checking for swiftly updates...") - let swiftlyRelease = try await SwiftlyCore.httpClient.getCurrentSwiftlyRelease() + let swiftlyRelease = try await ctx.httpClient.getCurrentSwiftlyRelease() guard try swiftlyRelease.swiftlyVersion > SwiftlyCore.version else { - SwiftlyCore.print("Already up to date.") + SwiftlyCore.print(ctx, "Already up to date.") return SwiftlyCore.version } @@ -62,7 +66,7 @@ struct SelfUpdate: SwiftlyCommand { let version = try swiftlyRelease.swiftlyVersion - SwiftlyCore.print("A new version is available: \(version)") + SwiftlyCore.print(ctx, "A new version is available: \(version)") let tmpFile = Swiftly.currentPlatform.getTempFilePath() FileManager.default.createFile(atPath: tmpFile.path, contents: nil) @@ -75,7 +79,7 @@ struct SelfUpdate: SwiftlyCommand { header: "Downloading swiftly \(version)" ) do { - try await SwiftlyCore.httpClient.downloadFile( + try await ctx.httpClient.downloadFile( url: downloadURL, to: tmpFile, reportProgress: { progress in @@ -95,10 +99,10 @@ struct SelfUpdate: SwiftlyCommand { } animation.complete(success: true) - try await Swiftly.currentPlatform.verifySignature(httpClient: SwiftlyCore.httpClient, archiveDownloadURL: downloadURL, archive: tmpFile, verbose: verbose) - try Swiftly.currentPlatform.extractSwiftlyAndInstall(from: tmpFile) + try await Swiftly.currentPlatform.verifySignature(ctx, archiveDownloadURL: downloadURL, archive: tmpFile, verbose: verbose) + try Swiftly.currentPlatform.extractSwiftlyAndInstall(ctx, from: tmpFile) - SwiftlyCore.print("Successfully updated swiftly to \(version) (was \(SwiftlyCore.version))") + SwiftlyCore.print(ctx, "Successfully updated swiftly to \(version) (was \(SwiftlyCore.version))") return version } } diff --git a/Sources/Swiftly/Swiftly.swift b/Sources/Swiftly/Swiftly.swift index df096f69..92b34192 100644 --- a/Sources/Swiftly/Swiftly.swift +++ b/Sources/Swiftly/Swiftly.swift @@ -36,18 +36,24 @@ public struct Swiftly: SwiftlyCommand { ] ) + public static func createDefaultContext() -> SwiftlyCoreContext { + SwiftlyCoreContext(httpClient: SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl())) + } + /// The list of directories that swiftly needs to exist in order to execute. /// If they do not exist when a swiftly command is invoked, they will be created. - public static var requiredDirectories: [URL] { + public static func requiredDirectories(_ ctx: SwiftlyCoreContext) -> [URL] { [ - Swiftly.currentPlatform.swiftlyHomeDir, - Swiftly.currentPlatform.swiftlyBinDir, - Swiftly.currentPlatform.swiftlyToolchainsDir, + Swiftly.currentPlatform.swiftlyHomeDir(ctx), + Swiftly.currentPlatform.swiftlyBinDir(ctx), + Swiftly.currentPlatform.swiftlyToolchainsDir(ctx), ] } public init() {} + public mutating func run(_: SwiftlyCoreContext) async throws {} + #if os(Linux) static let currentPlatform = Linux.currentPlatform #elseif os(macOS) @@ -55,7 +61,9 @@ public struct Swiftly: SwiftlyCommand { #endif } -public protocol SwiftlyCommand: AsyncParsableCommand {} +public protocol SwiftlyCommand: AsyncParsableCommand { + mutating func run(_ ctx: SwiftlyCoreContext) async throws +} extension Data { func append(to file: URL) throws { @@ -72,8 +80,8 @@ extension Data { } extension SwiftlyCommand { - public mutating func validateSwiftly() throws { - for requiredDir in Swiftly.requiredDirectories { + public mutating func validateSwiftly(_ ctx: SwiftlyCoreContext) throws { + for requiredDir in Swiftly.requiredDirectories(ctx) { guard requiredDir.fileExists() else { do { try FileManager.default.createDirectory(at: requiredDir, withIntermediateDirectories: true) @@ -85,6 +93,6 @@ extension SwiftlyCommand { } // Verify that the configuration exists and can be loaded - _ = try Config.load() + _ = try Config.load(ctx) } } diff --git a/Sources/Swiftly/Uninstall.swift b/Sources/Swiftly/Uninstall.swift index e0b4179b..0a3e3d4c 100644 --- a/Sources/Swiftly/Uninstall.swift +++ b/Sources/Swiftly/Uninstall.swift @@ -44,8 +44,12 @@ struct Uninstall: SwiftlyCommand { @OptionGroup var root: GlobalOptions mutating func run() async throws { - try validateSwiftly() - let startingConfig = try Config.load() + try await self.run(Swiftly.createDefaultContext()) + } + + mutating func run(_ ctx: SwiftlyCoreContext) async throws { + try validateSwiftly(ctx) + let startingConfig = try Config.load(ctx) let toolchains: [ToolchainVersion] if self.toolchain == "all" { @@ -65,27 +69,27 @@ struct Uninstall: SwiftlyCommand { } guard !toolchains.isEmpty else { - SwiftlyCore.print("No toolchains matched \"\(self.toolchain)\"") + SwiftlyCore.print(ctx, "No toolchains matched \"\(self.toolchain)\"") return } if !self.root.assumeYes { - SwiftlyCore.print("The following toolchains will be uninstalled:") + SwiftlyCore.print(ctx, "The following toolchains will be uninstalled:") for toolchain in toolchains { - SwiftlyCore.print(" \(toolchain)") + SwiftlyCore.print(ctx, " \(toolchain)") } - guard SwiftlyCore.promptForConfirmation(defaultBehavior: true) else { - SwiftlyCore.print("Aborting uninstall") + guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: true) else { + SwiftlyCore.print(ctx, "Aborting uninstall") return } } - SwiftlyCore.print() + SwiftlyCore.print(ctx) for toolchain in toolchains { - var config = try Config.load() + var config = try Config.load(ctx) // If the in-use toolchain was one of the uninstalled toolchains, use a new toolchain. if toolchain == config.inUse { @@ -105,31 +109,31 @@ struct Uninstall: SwiftlyCommand { ?? config.listInstalledToolchains(selector: .latest).filter({ !toolchains.contains($0) }).max() ?? config.installedToolchains.filter({ !toolchains.contains($0) }).max() { - try await Use.execute(toUse, globalDefault: true, &config) + try await Use.execute(ctx, toUse, globalDefault: true, &config) } else { // If there are no more toolchains installed, just unuse the currently active toolchain. config.inUse = nil - try config.save() + try config.save(ctx) } } - try await Self.execute(toolchain, &config, verbose: self.root.verbose) + try await Self.execute(ctx, toolchain, &config, verbose: self.root.verbose) } - SwiftlyCore.print() - SwiftlyCore.print("\(toolchains.count) toolchain(s) successfully uninstalled") + SwiftlyCore.print(ctx) + SwiftlyCore.print(ctx, "\(toolchains.count) toolchain(s) successfully uninstalled") } - static func execute(_ toolchain: ToolchainVersion, _ config: inout Config, verbose: Bool) async throws { - SwiftlyCore.print("Uninstalling \(toolchain)...", terminator: "") + static func execute(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ config: inout Config, verbose: Bool) async throws { + SwiftlyCore.print(ctx, "Uninstalling \(toolchain)...", terminator: "") config.installedToolchains.remove(toolchain) // This is here to prevent the inUse from referencing a toolchain that is not installed if config.inUse == toolchain { config.inUse = nil } - try config.save() + try config.save(ctx) - try Swiftly.currentPlatform.uninstall(toolchain, verbose: verbose) - SwiftlyCore.print("done") + try Swiftly.currentPlatform.uninstall(ctx, toolchain, verbose: verbose) + SwiftlyCore.print(ctx, "done") } } diff --git a/Sources/Swiftly/Update.swift b/Sources/Swiftly/Update.swift index 2382bf43..095c23b9 100644 --- a/Sources/Swiftly/Update.swift +++ b/Sources/Swiftly/Update.swift @@ -77,38 +77,43 @@ struct Update: SwiftlyCommand { case toolchain, root, verify, postInstallFile } - public mutating func run() async throws { - try validateSwiftly() - var config = try Config.load() + mutating func run() async throws { + try await self.run(Swiftly.createDefaultContext()) + } + + public mutating func run(_ ctx: SwiftlyCoreContext) async throws { + try validateSwiftly(ctx) + var config = try Config.load(ctx) - guard let parameters = try await self.resolveUpdateParameters(&config) else { + guard let parameters = try await self.resolveUpdateParameters(ctx, &config) else { if let toolchain = self.toolchain { - SwiftlyCore.print("No installed toolchain matched \"\(toolchain)\"") + SwiftlyCore.print(ctx, "No installed toolchain matched \"\(toolchain)\"") } else { - SwiftlyCore.print("No toolchains are currently installed") + SwiftlyCore.print(ctx, "No toolchains are currently installed") } return } - guard let newToolchain = try await self.lookupNewToolchain(config, parameters) else { - SwiftlyCore.print("\(parameters.oldToolchain) is already up to date") + guard let newToolchain = try await self.lookupNewToolchain(ctx, config, parameters) else { + SwiftlyCore.print(ctx, "\(parameters.oldToolchain) is already up to date") return } guard !config.installedToolchains.contains(newToolchain) else { - SwiftlyCore.print("The newest version of \(parameters.oldToolchain) (\(newToolchain)) is already installed") + SwiftlyCore.print(ctx, "The newest version of \(parameters.oldToolchain) (\(newToolchain)) is already installed") return } if !self.root.assumeYes { - SwiftlyCore.print("Update \(parameters.oldToolchain) -> \(newToolchain)?") - guard SwiftlyCore.promptForConfirmation(defaultBehavior: true) else { - SwiftlyCore.print("Aborting") + SwiftlyCore.print(ctx, "Update \(parameters.oldToolchain) -> \(newToolchain)?") + guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: true) else { + SwiftlyCore.print(ctx, "Aborting") return } } let (postInstallScript, pathChanged) = try await Install.execute( + ctx, version: newToolchain, &config, useInstalledToolchain: config.inUse == parameters.oldToolchain, @@ -117,8 +122,8 @@ struct Update: SwiftlyCommand { assumeYes: self.root.assumeYes ) - try await Uninstall.execute(parameters.oldToolchain, &config, verbose: self.root.verbose) - SwiftlyCore.print("Successfully updated \(parameters.oldToolchain) ⟶ \(newToolchain)") + try await Uninstall.execute(ctx, parameters.oldToolchain, &config, verbose: self.root.verbose) + SwiftlyCore.print(ctx, "Successfully updated \(parameters.oldToolchain) ⟶ \(newToolchain)") if let postInstallScript { guard let postInstallFile = self.postInstallFile else { @@ -136,7 +141,7 @@ struct Update: SwiftlyCommand { } if pathChanged { - SwiftlyCore.print(""" + SwiftlyCore.print(ctx, """ NOTE: Swiftly has updated some elements in your path and your shell may not yet be aware of the changes. You can update your shell's environment by running @@ -154,7 +159,7 @@ struct Update: SwiftlyCommand { /// If the selector does not match an installed toolchain, this returns nil. /// If no selector is provided, the currently in-use toolchain will be used as the basis for the returned /// parameters. - private func resolveUpdateParameters(_ config: inout Config) async throws -> UpdateParameters? { + private func resolveUpdateParameters(_ ctx: SwiftlyCoreContext, _ config: inout Config) async throws -> UpdateParameters? { let selector = try self.toolchain.map { try ToolchainSelector(parsing: $0) } let oldToolchain: ToolchainVersion? @@ -165,7 +170,7 @@ struct Update: SwiftlyCommand { // 5.5.1 and 5.5.2 are installed (5.5.2 will be updated). oldToolchain = toolchains.max() } else { - (oldToolchain, _) = try await selectToolchain(config: &config) + (oldToolchain, _) = try await selectToolchain(ctx, config: &config) } guard let oldToolchain else { @@ -195,10 +200,10 @@ struct Update: SwiftlyCommand { /// Tries to find a toolchain version that meets the provided parameters, if one exists. /// This does not download the toolchain, but it does query the swift.org API to find the suitable toolchain. - private func lookupNewToolchain(_ config: Config, _ bounds: UpdateParameters) async throws -> ToolchainVersion? { + private func lookupNewToolchain(_ ctx: SwiftlyCoreContext, _ config: Config, _ bounds: UpdateParameters) async throws -> ToolchainVersion? { switch bounds { case let .stable(old, range): - return try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform, limit: 1) { release in + return try await ctx.httpClient.getReleaseToolchains(platform: config.platform, limit: 1) { release in switch range { case .latest: return release > old @@ -211,7 +216,7 @@ struct Update: SwiftlyCommand { case let .snapshot(old): let newerSnapshotToolchains: [ToolchainVersion.Snapshot] do { - newerSnapshotToolchains = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: config.platform, branch: old.branch, limit: 1) { snapshot in + newerSnapshotToolchains = try await ctx.httpClient.getSnapshotToolchains(platform: config.platform, branch: old.branch, limit: 1) { snapshot in snapshot.branch == old.branch && snapshot.date > old.date } } catch let branchNotFoundErr as SwiftlyHTTPClient.SnapshotBranchNotFoundError { diff --git a/Sources/Swiftly/Use.swift b/Sources/Swiftly/Use.swift index 6b72e1f9..764df27c 100644 --- a/Sources/Swiftly/Use.swift +++ b/Sources/Swiftly/Use.swift @@ -55,12 +55,16 @@ struct Use: SwiftlyCommand { var toolchain: String? mutating func run() async throws { - try validateSwiftly() - var config = try Config.load() + try await self.run(Swiftly.createDefaultContext()) + } + + mutating func run(_ ctx: SwiftlyCoreContext) async throws { + try validateSwiftly(ctx) + var config = try Config.load(ctx) // This is the bare use command where we print the selected toolchain version (or the path to it) guard let toolchain = self.toolchain else { - let (selectedVersion, result) = try await selectToolchain(config: &config, globalDefault: self.globalDefault) + let (selectedVersion, result) = try await selectToolchain(ctx, config: &config, globalDefault: self.globalDefault) // Abort on any errors with the swift version files if case let .swiftVersionFile(_, _, error) = result, let error { @@ -74,7 +78,7 @@ struct Use: SwiftlyCommand { if self.printLocation { // Print the toolchain location and exit - SwiftlyCore.print("\(Swiftly.currentPlatform.findToolchainLocation(selectedVersion).path)") + SwiftlyCore.print(ctx, "\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion).path)") return } @@ -87,7 +91,7 @@ struct Use: SwiftlyCommand { message += " (default)" } - SwiftlyCore.print(message) + SwiftlyCore.print(ctx, message) return } @@ -99,16 +103,16 @@ struct Use: SwiftlyCommand { let selector = try ToolchainSelector(parsing: toolchain) guard let toolchain = config.listInstalledToolchains(selector: selector).max() else { - SwiftlyCore.print("No installed toolchains match \"\(toolchain)\"") + SwiftlyCore.print(ctx, "No installed toolchains match \"\(toolchain)\"") return } - try await Self.execute(toolchain, globalDefault: self.globalDefault, assumeYes: self.root.assumeYes, &config) + try await Self.execute(ctx, toolchain, globalDefault: self.globalDefault, assumeYes: self.root.assumeYes, &config) } /// Use a toolchain. This method can modify and save the input config and also create/modify a `.swift-version` file. - static func execute(_ toolchain: ToolchainVersion, globalDefault: Bool, assumeYes: Bool = true, _ config: inout Config) async throws { - let (selectedVersion, result) = try await selectToolchain(config: &config, globalDefault: globalDefault) + static func execute(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, globalDefault: Bool, assumeYes: Bool = true, _ config: inout Config) async throws { + let (selectedVersion, result) = try await selectToolchain(ctx, config: &config, globalDefault: globalDefault) var message: String @@ -117,12 +121,12 @@ struct Use: SwiftlyCommand { try toolchain.name.write(to: versionFile, atomically: true, encoding: .utf8) message = "The file `\(versionFile.path)` has been set to `\(toolchain)`" - } else if let newVersionFile = findNewVersionFile(), !globalDefault { + } else if let newVersionFile = findNewVersionFile(ctx), !globalDefault { if !assumeYes { - SwiftlyCore.print("A new file `\(newVersionFile)` will be created to set the new in-use toolchain for this project. Alternatively, you can set your default globally with the `--global-default` flag. Proceed with creating this file?") + SwiftlyCore.print(ctx, "A new file `\(newVersionFile)` will be created to set the new in-use toolchain for this project. Alternatively, you can set your default globally with the `--global-default` flag. Proceed with creating this file?") - guard SwiftlyCore.promptForConfirmation(defaultBehavior: true) else { - SwiftlyCore.print("Aborting setting in-use toolchain") + guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: true) else { + SwiftlyCore.print(ctx, "Aborting setting in-use toolchain") return } } @@ -132,7 +136,7 @@ struct Use: SwiftlyCommand { message = "The file `\(newVersionFile.path)` has been set to `\(toolchain)`" } else { config.inUse = toolchain - try config.save() + try config.save(ctx) message = "The global default toolchain has been set to `\(toolchain)`" } @@ -140,11 +144,11 @@ struct Use: SwiftlyCommand { message += " (was \(selectedVersion.name))" } - SwiftlyCore.print(message) + SwiftlyCore.print(ctx, message) } - static func findNewVersionFile() -> URL? { - var cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + static func findNewVersionFile(_ ctx: SwiftlyCoreContext) -> URL? { + var cwd = ctx.currentDirectory while cwd.path != "" && cwd.path != "/" { guard FileManager.default.fileExists(atPath: cwd.path) else { @@ -189,9 +193,9 @@ public enum ToolchainSelectionResult { /// If such a case happens then the toolchain version in the tuple will be nil, but the /// result will be .swiftVersionFile and a detailed error about the problem. This error /// can be thrown by the client, or ignored. -public func selectToolchain(config: inout Config, globalDefault: Bool = false) async throws -> (ToolchainVersion?, ToolchainSelectionResult) { +public func selectToolchain(_ ctx: SwiftlyCoreContext, config: inout Config, globalDefault: Bool = false) async throws -> (ToolchainVersion?, ToolchainSelectionResult) { if !globalDefault { - var cwd = URL(fileURLWithPath: FileManager.default.currentDirectoryPath) + var cwd = ctx.currentDirectory while cwd.path != "" && cwd.path != "/" { guard FileManager.default.fileExists(atPath: cwd.path) else { diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 09d97b17..77228705 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -78,8 +78,8 @@ struct SwiftlyUserAgentMiddleware: ClientMiddleware { } } -/// An `HTTPRequestExecutor` backed by the shared `HTTPClient`. -class HTTPRequestExecutorImpl: HTTPRequestExecutor { +/// An `HTTPRequestExecutor` backed by a shared `HTTPClient`. This makes actual network requests. +public class HTTPRequestExecutorImpl: HTTPRequestExecutor { let httpClient: HTTPClient public init() { @@ -216,8 +216,8 @@ extension Components.Schemas.SourceBranch { } extension Components.Schemas.Architecture { - static var x8664: Components.Schemas.Architecture = .init(Components.Schemas.KnownArchitecture.x8664) - static var aarch64: Components.Schemas.Architecture = .init(Components.Schemas.KnownArchitecture.aarch64) + static let x8664: Components.Schemas.Architecture = .init(Components.Schemas.KnownArchitecture.x8664) + static let aarch64: Components.Schemas.Architecture = .init(Components.Schemas.KnownArchitecture.aarch64) } extension Components.Schemas.Platform { @@ -295,55 +295,24 @@ extension Components.Schemas.DevToolchainForArch { /// HTTPClient wrapper used for interfacing with various REST APIs and downloading things. public struct SwiftlyHTTPClient { + public let httpRequestExecutor: HTTPRequestExecutor + + public init(httpRequestExecutor: HTTPRequestExecutor) { + self.httpRequestExecutor = httpRequestExecutor + } + private struct Response { let status: HTTPResponseStatus let buffer: ByteBuffer } - public init() {} - - private func get(url: String, headers: [String: String], maxBytes: Int) async throws -> Response { - var request = makeRequest(url: url) - - for (k, v) in headers { - request.headers.add(name: k, value: v) - } - - let response = try await SwiftlyCore.httpRequestExecutor.execute(request, timeout: .seconds(30)) - - return Response(status: response.status, buffer: try await response.body.collect(upTo: maxBytes)) - } - public struct JSONNotFoundError: LocalizedError { public var url: String } - /// Decode the provided type `T` from the JSON body of the response from a GET request - /// to the given URL. - public func getFromJSON( - url: String, - type: T.Type, - headers: [String: String] = [:] - ) async throws -> T { - // Maximum expected size for a JSON payload for an API is 1MB - let response = try await self.get(url: url, headers: headers, maxBytes: 1024 * 1024) - - switch response.status { - case .ok: - break - case .notFound: - throw SwiftlyHTTPClient.JSONNotFoundError(url: url) - default: - let json = String(buffer: response.buffer) - throw SwiftlyError(message: "Received \(response.status) when reaching \(url) for JSON: \(json)") - } - - return try JSONDecoder().decode(type.self, from: response.buffer) - } - /// Return the current Swiftly release using the swift.org API. public func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { - try await SwiftlyCore.httpRequestExecutor.getCurrentSwiftlyRelease() + try await self.httpRequestExecutor.getCurrentSwiftlyRelease() } /// Return an array of released Swift versions that match the given filter, up to the provided @@ -356,7 +325,7 @@ public struct SwiftlyHTTPClient { ) async throws -> [ToolchainVersion.StableRelease] { let arch = a ?? cpuArch - let releases = try await SwiftlyCore.httpRequestExecutor.getReleaseToolchains() + let releases = try await self.httpRequestExecutor.getReleaseToolchains() var swiftOrgFiltered: [ToolchainVersion.StableRelease] = try releases.compactMap { swiftOrgRelease in if platform.name != PlatformDefinition.macOS.name { @@ -433,7 +402,7 @@ public struct SwiftlyHTTPClient { .init("\(major).\(minor)") } - let devToolchains = try await SwiftlyCore.httpRequestExecutor.getSnapshotToolchains(branch: sourceBranch, platform: platformId) + let devToolchains = try await self.httpRequestExecutor.getSnapshotToolchains(branch: sourceBranch, platform: platformId) let arch = a ?? cpuArch.value2 @@ -488,7 +457,7 @@ public struct SwiftlyHTTPClient { } let request = makeRequest(url: url.absoluteString) - let response = try await SwiftlyCore.httpRequestExecutor.execute(request, timeout: .seconds(30)) + let response = try await self.httpRequestExecutor.execute(request, timeout: .seconds(30)) switch response.status { case .ok: diff --git a/Sources/SwiftlyCore/Platform.swift b/Sources/SwiftlyCore/Platform.swift index 8491ff5f..652d65a4 100644 --- a/Sources/SwiftlyCore/Platform.swift +++ b/Sources/SwiftlyCore/Platform.swift @@ -39,7 +39,7 @@ public struct RunProgramError: Swift.Error { } public protocol Platform { - /// The platform-specific defaut location on disk for swiftly's home + /// The platform-specific default location on disk for swiftly's home /// directory. var defaultSwiftlyHomeDirectory: URL { get } @@ -49,30 +49,26 @@ public protocol Platform { /// If a mocked home directory is set, this will be the "bin" subdirectory of the home directory. /// If not, this will be the SWIFTLY_BIN_DIR environment variable if set. If that's also unset, /// this will default to the platform's default location. - var swiftlyBinDir: URL { get } + func swiftlyBinDir(_ ctx: SwiftlyCoreContext) -> URL /// The "toolchains" subdirectory that contains the Swift toolchains managed by swiftly. - var swiftlyToolchainsDir: URL { get } + func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> URL /// The file extension of the downloaded toolchain for this platform. /// e.g. for Linux systems this is "tar.gz" and on macOS it's "pkg". var toolchainFileExtension: String { get } - /// Checks whether a given system dependency has been installed yet or not. - /// This will only really used on Linux. - func isSystemDependencyPresent(_ dependency: SystemDependency) -> Bool - /// Installs a toolchain from a file on disk pointed to by the given URL. /// After this completes, a user can “use” the toolchain. - func install(from: URL, version: ToolchainVersion, verbose: Bool) throws + func install(_ ctx: SwiftlyCoreContext, from: URL, version: ToolchainVersion, verbose: Bool) throws /// Extract swiftly from the provided downloaded archive and install /// ourselves from that. - func extractSwiftlyAndInstall(from archive: URL) throws + func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) throws /// Uninstalls a toolchain associated with the given version. /// If this version is in use, the next latest version will be used afterwards. - func uninstall(_ version: ToolchainVersion, verbose: Bool) throws + func uninstall(_ ctx: SwiftlyCoreContext, _ version: ToolchainVersion, verbose: Bool) throws /// Get the name of the swiftly release binary. func getExecutableName() -> String @@ -98,19 +94,19 @@ public protocol Platform { /// Downloads the signature file associated with the archive and verifies it matches the downloaded archive. /// Throws an error if the signature does not match. - func verifySignature(httpClient: SwiftlyHTTPClient, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws + func verifySignature(_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws /// Detect the platform definition for this platform. - func detectPlatform(disableConfirmation: Bool, platform: String?) async throws -> PlatformDefinition + func detectPlatform(_ ctx: SwiftlyCoreContext, disableConfirmation: Bool, platform: String?) async throws -> PlatformDefinition /// Get the user's current login shell func getShell() async throws -> String /// Find the location where the toolchain should be installed. - func findToolchainLocation(_ toolchain: ToolchainVersion) -> URL + func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL /// Find the location of the toolchain binaries. - func findToolchainBinDir(_ toolchain: ToolchainVersion) -> URL + func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL } extension Platform { @@ -127,20 +123,20 @@ extension Platform { /// -- config.json /// ``` /// - public var swiftlyHomeDir: URL { - SwiftlyCore.mockedHomeDir + public func swiftlyHomeDir(_ ctx: SwiftlyCoreContext) -> URL { + ctx.mockedHomeDir ?? ProcessInfo.processInfo.environment["SWIFTLY_HOME_DIR"].map { URL(fileURLWithPath: $0) } ?? self.defaultSwiftlyHomeDirectory } /// The URL of the configuration file in swiftly's home directory. - public var swiftlyConfigFile: URL { - self.swiftlyHomeDir.appendingPathComponent("config.json") + public func swiftlyConfigFile(_ ctx: SwiftlyCoreContext) -> URL { + self.swiftlyHomeDir(ctx).appendingPathComponent("config.json") } #if os(macOS) || os(Linux) - func proxyEnv(_ toolchain: ToolchainVersion) throws -> [String: String] { - let tcPath = self.findToolchainLocation(toolchain).appendingPathComponent("usr/bin") + func proxyEnv(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) throws -> [String: String] { + let tcPath = self.findToolchainLocation(ctx, toolchain).appendingPathComponent("usr/bin") guard tcPath.fileExists() else { throw SwiftlyError(message: "Toolchain \(toolchain) could not be located. You can try `swiftly uninstall \(toolchain)` to uninstall it and then `swiftly install \(toolchain)` to install it again.") } @@ -161,8 +157,8 @@ extension Platform { /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with /// the exit code and program information. /// - public func proxy(_ toolchain: ToolchainVersion, _ command: String, _ arguments: [String], _ env: [String: String] = [:]) async throws { - var newEnv = try self.proxyEnv(toolchain) + public func proxy(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String], _ env: [String: String] = [:]) async throws { + var newEnv = try self.proxyEnv(ctx, toolchain) for (key, value) in env { newEnv[key] = value } @@ -174,8 +170,8 @@ extension Platform { /// In the case where the command exit with a non-zero exit code a RunProgramError is thrown with /// the exit code and program information. /// - public func proxyOutput(_ toolchain: ToolchainVersion, _ command: String, _ arguments: [String]) async throws -> String? { - try await self.runProgramOutput(command, arguments, env: self.proxyEnv(toolchain)) + public func proxyOutput(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ command: String, _ arguments: [String]) async throws -> String? { + try await self.runProgramOutput(command, arguments, env: self.proxyEnv(ctx, toolchain)) } /// Run a program. @@ -278,7 +274,7 @@ extension Platform { } // Install ourselves in the final location - public func installSwiftlyBin() throws { + public func installSwiftlyBin(_ ctx: SwiftlyCoreContext) throws { // First, let's find out where we are. let cmd = CommandLine.arguments[0] let cmdAbsolute = if cmd.hasPrefix("/") { @@ -305,17 +301,17 @@ extension Platform { return } - // Proceed only if we're not running in the context of a test. - guard !cmdAbsolute.hasSuffix("xctest") else { + // Proceed only if we're not running in the context of a mocked home directory. + guard ctx.mockedHomeDir == nil else { return } // We're already running from where we would be installing ourselves. - guard case let swiftlyHomeBin = self.swiftlyBinDir.appendingPathComponent("swiftly", isDirectory: false).path, cmdAbsolute != swiftlyHomeBin else { + guard case let swiftlyHomeBin = self.swiftlyBinDir(ctx).appendingPathComponent("swiftly", isDirectory: false).path, cmdAbsolute != swiftlyHomeBin else { return } - SwiftlyCore.print("Installing swiftly in \(swiftlyHomeBin)...") + SwiftlyCore.print(ctx, "Installing swiftly in \(swiftlyHomeBin)...") if FileManager.default.fileExists(atPath: swiftlyHomeBin) { try FileManager.default.removeItem(atPath: swiftlyHomeBin) @@ -325,13 +321,13 @@ extension Platform { try FileManager.default.moveItem(atPath: cmdAbsolute, toPath: swiftlyHomeBin) } catch { try FileManager.default.copyItem(atPath: cmdAbsolute, toPath: swiftlyHomeBin) - SwiftlyCore.print("Swiftly has been copied into the installation directory. You can remove '\(cmdAbsolute)'. It is no longer needed.") + SwiftlyCore.print(ctx, "Swiftly has been copied into the installation directory. You can remove '\(cmdAbsolute)'. It is no longer needed.") } } // Find the location where swiftly should be executed. - public func findSwiftlyBin() throws -> String? { - let swiftlyHomeBin = self.swiftlyBinDir.appendingPathComponent("swiftly", isDirectory: false).path + public func findSwiftlyBin(_ ctx: SwiftlyCoreContext) throws -> String? { + let swiftlyHomeBin = self.swiftlyBinDir(ctx).appendingPathComponent("swiftly", isDirectory: false).path // First, let's find out where we are. let cmd = CommandLine.arguments[0] @@ -365,13 +361,9 @@ extension Platform { return FileManager.default.fileExists(atPath: swiftlyHomeBin) ? swiftlyHomeBin : nil } - public func findToolchainBinDir(_ toolchain: ToolchainVersion) -> URL { - self.findToolchainLocation(toolchain).appendingPathComponent("usr/bin") + public func findToolchainBinDir(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL { + self.findToolchainLocation(ctx, toolchain).appendingPathComponent("usr/bin") } #endif } - -public struct SystemDependency {} - -public struct Snapshot: Decodable {} diff --git a/Sources/SwiftlyCore/SwiftlyCore.swift b/Sources/SwiftlyCore/SwiftlyCore.swift index 76a104b8..7900a83c 100644 --- a/Sources/SwiftlyCore/SwiftlyCore.swift +++ b/Sources/SwiftlyCore/SwiftlyCore.swift @@ -2,32 +2,65 @@ import Foundation public let version = SwiftlyVersion(major: 1, minor: 1, patch: 0, suffix: "dev") -/// A separate home directory to use for testing purposes. This overrides swiftly's default -/// home directory location logic. -public var mockedHomeDir: URL? - -/// This is the default http client that swiftly uses for its network -/// requests. -public var httpClient = SwiftlyHTTPClient() - -/// An HTTP request executor that allows different transport level configuration -/// such as allowing a proxy to be configured, or for the purpose of mocking -/// for tests. -public var httpRequestExecutor: HTTPRequestExecutor = HTTPRequestExecutorImpl() - /// Protocol defining a handler for information swiftly intends to print to stdout. /// This is currently only used to intercept print statements for testing. public protocol OutputHandler { func handleOutputLine(_ string: String) } -/// The output handler to use, if any. -public var outputHandler: (any OutputHandler)? +/// Protocol defining a provider for information swiftly intends to read from stdin. +public protocol InputProvider { + func readLine() -> String? +} + +/// This struct provides a actor-specific and mockable context for swiftly. +public struct SwiftlyCoreContext { + /// A separate home directory to use for testing purposes. This overrides swiftly's default + /// home directory location logic. + public var mockedHomeDir: URL? + + /// A separate current working directory to use for testing purposes. This overrides + /// swiftly's default current working directory logic. + public var currentDirectory: URL + + /// A chosen shell for the current user as a typical path to the shell's binary + /// location (e.g. /bin/sh). This overrides swiftly's default shell detection mechanisms + /// for testing purposes. + public var mockedShell: String? + + /// This is the default http client that swiftly uses for its network + /// requests. + public var httpClient: SwiftlyHTTPClient + + /// The output handler to use, if any. + public var outputHandler: (any OutputHandler)? + + /// The input probider to use, if any + public var inputProvider: (any InputProvider)? + + public init(httpClient: SwiftlyHTTPClient) { + self.httpClient = httpClient + self.currentDirectory = URL.currentDirectory() + } + + public init( + mockedHomeDir: URL?, + httpClient: SwiftlyHTTPClient, + outputHandler: (any OutputHandler)?, + inputProvider: (any InputProvider)? + ) { + self.mockedHomeDir = mockedHomeDir + self.currentDirectory = mockedHomeDir ?? URL.currentDirectory() + self.httpClient = httpClient + self.outputHandler = outputHandler + self.inputProvider = inputProvider + } +} /// Pass the provided string to the set output handler if any. /// If no output handler has been set, just print to stdout. -public func print(_ string: String = "", terminator: String? = nil) { - guard let handler = SwiftlyCore.outputHandler else { +public func print(_ ctx: SwiftlyCoreContext, _ string: String = "", terminator: String? = nil) { + guard let handler = ctx.outputHandler else { if let terminator { Swift.print(string, terminator: terminator) } else { @@ -38,15 +71,9 @@ public func print(_ string: String = "", terminator: String? = nil) { handler.handleOutputLine(string + (terminator ?? "")) } -public protocol InputProvider { - func readLine() -> String? -} - -public var inputProvider: (any InputProvider)? - -public func readLine(prompt: String) -> String? { +public func readLine(_ ctx: SwiftlyCoreContext, prompt: String) -> String? { print(prompt, terminator: ": \n") - guard let provider = SwiftlyCore.inputProvider else { + guard let provider = ctx.inputProvider else { return Swift.readLine(strippingNewline: true) } return provider.readLine() diff --git a/Sources/SwiftlyCore/Utils.swift b/Sources/SwiftlyCore/Utils.swift index 163f351c..93f815fe 100644 --- a/Sources/SwiftlyCore/Utils.swift +++ b/Sources/SwiftlyCore/Utils.swift @@ -16,7 +16,7 @@ extension URL { } } -public func promptForConfirmation(defaultBehavior: Bool) -> Bool { +public func promptForConfirmation(_ ctx: SwiftlyCoreContext, defaultBehavior: Bool) -> Bool { let options: String if defaultBehavior { options = "(Y/n)" @@ -25,10 +25,10 @@ public func promptForConfirmation(defaultBehavior: Bool) -> Bool { } while true { - let answer = (SwiftlyCore.readLine(prompt: "Proceed? \(options)") ?? (defaultBehavior ? "y" : "n")).lowercased() + let answer = (SwiftlyCore.readLine(ctx, prompt: "Proceed? \(options)") ?? (defaultBehavior ? "y" : "n")).lowercased() guard ["y", "n", ""].contains(answer) else { - SwiftlyCore.print("Please input either \"y\" or \"n\", or press ENTER to use the default.") + SwiftlyCore.print(ctx, "Please input either \"y\" or \"n\", or press ENTER to use the default.") continue } diff --git a/Tests/SwiftlyTests/E2ETests.swift b/Tests/SwiftlyTests/E2ETests.swift deleted file mode 100644 index 36f86704..00000000 --- a/Tests/SwiftlyTests/E2ETests.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Foundation -@testable import Swiftly -@testable import SwiftlyCore -import XCTest - -final class E2ETests: SwiftlyTests { - /// Tests that `swiftly init` and `swiftly install latest` successfully installs the latest stable release. - /// - /// This will modify the user's system, but will undo those changes afterwards. - func testInstallLatest() async throws { - try await self.rollbackLocalChanges { - // Clear out the config.json to proceed with the init - try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile) - - let shell = if let s = ProcessInfo.processInfo.environment["SHELL"] { - s - } else { - try await Swiftly.currentPlatform.getShell() - } - - var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--no-modify-profile"]) - try await initCmd.run() - - var config = try Config.load() - - // Config now exists and is the correct version - XCTAssertEqual(SwiftlyCore.version, config.version) - - // Check the environment script, if the shell is supported - let envScript: URL? = if shell.hasSuffix("bash") || shell.hasSuffix("zsh") { - Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("env.sh") - } else if shell.hasSuffix("fish") { - Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("env.fish") - } else { - nil - } - - if let envScript { - XCTAssertTrue(envScript.fileExists()) - } - - var cmd = try self.parseCommand(Install.self, ["install", "latest", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() - - config = try Config.load() - - guard !config.installedToolchains.isEmpty else { - XCTFail("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") - return - } - - let installedToolchain = config.installedToolchains.first! - - guard case let .stable(release) = installedToolchain else { - XCTFail("expected swiftly install latest to install release toolchain but got \(installedToolchain)") - return - } - - // As of writing this, 5.8.0 is the latest stable release. Assert it is at least that new. - XCTAssertTrue(release >= ToolchainVersion.StableRelease(major: 5, minor: 8, patch: 0)) - - try await validateInstalledToolchains([installedToolchain], description: "install latest") - } - } -} diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index 275e9ab2..a440bdc5 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -1,56 +1,17 @@ @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class HTTPClientTests: SwiftlyTests { - func testGet() async throws { - // GIVEN: we have a swiftly http client - // WHEN: we make get request for a particular type of JSON - var releases: [Components.Schemas.Release] = try await SwiftlyCore.httpClient.getFromJSON( - url: "https://www.swift.org/api/v1/install/releases.json", - type: [Components.Schemas.Release].self, - headers: [:] - ) - // THEN: we get a decoded JSON response - XCTAssertTrue(releases.count > 0) - - // GIVEN: we have a swiftly http client - // WHEN: we make a request to an invalid URL path - var exceptionThrown = false - do { - releases = try await SwiftlyCore.httpClient.getFromJSON( - url: "https://www.swift.org/api/v1/install/releases-invalid.json", - type: [Components.Schemas.Release].self, - headers: [:] - ) - } catch { - exceptionThrown = true - } - // THEN: we receive an exception - XCTAssertTrue(exceptionThrown) - - // GIVEN: we have a swiftly http client - // WHEN: we make a request to an invalid host path - exceptionThrown = false - do { - releases = try await SwiftlyCore.httpClient.getFromJSON( - url: "https://invalid.swift.org/api/v1/install/releases.json", - type: [Components.Schemas.Release].self, - headers: [:] - ) - } catch { - exceptionThrown = true - } - // THEN: we receive an exception - XCTAssertTrue(exceptionThrown) +@Suite struct HTTPClientTests { + @Test func getSwiftlyReleaseMetadataFromSwiftOrg() async throws { + let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()) + let currentRelease = try await httpClient.getCurrentSwiftlyRelease() + #expect(throws: Never.self) { try currentRelease.swiftlyVersion } } - func testGetSwiftlyReleaseMetadataFromSwiftOrg() async throws { - let currentRelease = try await SwiftlyCore.httpClient.getCurrentSwiftlyRelease() - XCTAssertNoThrow(try currentRelease.swiftlyVersion) - } + @Test func getToolchainMetdataFromSwiftOrg() async throws { + let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()) - func testGetToolchainMetdataFromSwiftOrg() async throws { let supportedPlatforms: [PlatformDefinition] = [ .macOS, .ubuntu2404, @@ -78,18 +39,18 @@ final class HTTPClientTests: SwiftlyTests { for platform in supportedPlatforms { // GIVEN: we have a swiftly http client with swift.org metadata capability // WHEN: we ask for the first five releases of a supported platform in a supported arch - let releases = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: platform, arch: arch, limit: 5) + let releases = try await httpClient.getReleaseToolchains(platform: platform, arch: arch, limit: 5) // THEN: we get at least 1 release - XCTAssertTrue(1 <= releases.count) + #expect(1 <= releases.count) if newPlatforms.contains(platform) { continue } // Newer distros don't have main snapshots yet for branch in branches { // GIVEN: we have a swiftly http client with swift.org metadata capability // WHEN: we ask for the first five snapshots on a branch for a supported platform and arch - let snapshots = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: platform, arch: arch.value2!, branch: branch, limit: 5) + let snapshots = try await httpClient.getSnapshotToolchains(platform: platform, arch: arch.value2!, branch: branch, limit: 5) // THEN: we get at least 3 releases - XCTAssertTrue(3 <= snapshots.count) + #expect(3 <= snapshots.count) } } } diff --git a/Tests/SwiftlyTests/InitTests.swift b/Tests/SwiftlyTests/InitTests.swift index 99ec696b..dfd85fb6 100644 --- a/Tests/SwiftlyTests/InitTests.swift +++ b/Tests/SwiftlyTests/InitTests.swift @@ -1,128 +1,126 @@ import Foundation @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class InitTests: SwiftlyTests { - func testInitFresh() async throws { - try await self.rollbackLocalChanges { +@Suite struct InitTests { + @Test func initFresh() async throws { + try await SwiftlyTests.withTestHome { // GIVEN: a fresh user account without Swiftly installed - try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile) - let shell = if let s = ProcessInfo.processInfo.environment["SHELL"] { - s - } else { - try await Swiftly.currentPlatform.getShell() - } - let envScript: URL? - if shell.hasSuffix("bash") || shell.hasSuffix("zsh") { - envScript = Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("env.sh") - } else if shell.hasSuffix("fish") { - envScript = Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("env.fish") - } else { - envScript = nil - } + try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) + + // AND: the user is using the bash shell + let shell = "/bin/bash" + var ctx = SwiftlyTests.ctx + ctx.mockedShell = shell + + try await SwiftlyTests.$ctx.withValue(ctx) { + let envScript: URL? + if shell.hasSuffix("bash") || shell.hasSuffix("zsh") { + envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("env.sh") + } else if shell.hasSuffix("fish") { + envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("env.fish") + } else { + envScript = nil + } - if let envScript { - XCTAssertFalse(envScript.fileExists()) - } + if let envScript { + #expect(!envScript.fileExists()) + } - // WHEN: swiftly is invoked to init the user account and finish swiftly installation - var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) - try await initCmd.run() - - // THEN: it creates a valid configuration at the correct version - let config = try Config.load() - XCTAssertEqual(SwiftlyCore.version, config.version) - - // AND: it creates an environment script suited for the type of shell - if let envScript { - XCTAssertTrue(envScript.fileExists()) - if let scriptContents = try? String(contentsOf: envScript) { - XCTAssertTrue(scriptContents.contains("SWIFTLY_HOME_DIR")) - XCTAssertTrue(scriptContents.contains("SWIFTLY_BIN_DIR")) - XCTAssertTrue(scriptContents.contains(Swiftly.currentPlatform.swiftlyHomeDir.path)) - XCTAssertTrue(scriptContents.contains(Swiftly.currentPlatform.swiftlyBinDir.path)) + // WHEN: swiftly is invoked to init the user account and finish swiftly installation + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) + + // THEN: it creates a valid configuration at the correct version + let config = try Config.load(SwiftlyTests.ctx) + #expect(SwiftlyCore.version == config.version) + + // AND: it creates an environment script suited for the type of shell + if let envScript { + #expect(envScript.fileExists()) + if let scriptContents = try? String(contentsOf: envScript) { + #expect(scriptContents.contains("SWIFTLY_HOME_DIR")) + #expect(scriptContents.contains("SWIFTLY_BIN_DIR")) + #expect(scriptContents.contains(Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).path)) + #expect(scriptContents.contains(Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx).path)) + } } - } - // AND: it sources the script from the user profile - if let envScript { - var foundSourceLine = false - for p in [".profile", ".zprofile", ".bash_profile", ".bash_login", ".config/fish/conf.d/swiftly.fish"] { - let profile = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(p) - if profile.fileExists() { - if let profileContents = try? String(contentsOf: profile), profileContents.contains(envScript.path) { - foundSourceLine = true - break + // AND: it sources the script from the user profile + if let envScript { + var foundSourceLine = false + for p in [".profile", ".zprofile", ".bash_profile", ".bash_login", ".config/fish/conf.d/swiftly.fish"] { + let profile = SwiftlyTests.ctx.mockedHomeDir!.appendingPathComponent(p) + if profile.fileExists() { + if let profileContents = try? String(contentsOf: profile), profileContents.contains(envScript.path) { + foundSourceLine = true + break + } } } + #expect(foundSourceLine) } - XCTAssertTrue(foundSourceLine) } } } - func testInitOverwrite() async throws { - try await self.rollbackLocalChanges { + @Test func initOverwrite() async throws { + try await SwiftlyTests.withTestHome { // GIVEN: a user account with swiftly already installed - try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile) + try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) - var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) - try await initCmd.run() + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) // Add some customizations to files and directories - var config = try Config.load() + var config = try Config.load(SwiftlyTests.ctx) config.version = try SwiftlyVersion(parsing: "100.0.0") - try config.save() + try config.save(SwiftlyTests.ctx) - try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("foo.txt")) - try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir.appendingPathComponent("foo.txt")) + try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) + try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) // WHEN: swiftly is initialized with overwrite enabled - initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install", "--overwrite"]) - try await initCmd.run() + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install", "--overwrite"]) // THEN: everything is overwritten in initialization - config = try Config.load() - XCTAssertEqual(SwiftlyCore.version, config.version) - XCTAssertFalse(Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("foo.txt").fileExists()) - XCTAssertFalse(Swiftly.currentPlatform.swiftlyToolchainsDir.appendingPathComponent("foo.txt").fileExists()) + config = try Config.load(SwiftlyTests.ctx) + #expect(SwiftlyCore.version == config.version) + #expect(!Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) + #expect(!Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) } } - func testInitTwice() async throws { - try await self.rollbackLocalChanges { + @Test func initTwice() async throws { + try await SwiftlyTests.withTestHome { // GIVEN: a user account with swiftly already installed - try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile) + try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) - var initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) - try await initCmd.run() + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) // Add some customizations to files and directories - var config = try Config.load() + var config = try Config.load(SwiftlyTests.ctx) config.version = try SwiftlyVersion(parsing: "100.0.0") - try config.save() + try config.save(SwiftlyTests.ctx) - try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("foo.txt")) - try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir.appendingPathComponent("foo.txt")) + try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) + try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) // WHEN: swiftly init is invoked a second time - initCmd = try self.parseCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) var threw = false do { - try await initCmd.run() + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) } catch { threw = true } // THEN: init fails - XCTAssertTrue(threw) + #expect(threw) // AND: files were left intact - config = try Config.load() - XCTAssertEqual(try SwiftlyVersion(parsing: "100.0.0"), config.version) - XCTAssertTrue(Swiftly.currentPlatform.swiftlyHomeDir.appendingPathComponent("foo.txt").fileExists()) - XCTAssertTrue(Swiftly.currentPlatform.swiftlyToolchainsDir.appendingPathComponent("foo.txt").fileExists()) + config = try Config.load(SwiftlyTests.ctx) + #expect(try SwiftlyVersion(parsing: "100.0.0") == config.version) + #expect(Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) + #expect(Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) } } } diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index 74dbdc49..711cef12 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -1,104 +1,97 @@ import Foundation @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class InstallTests: SwiftlyTests { +@Suite struct InstallTests { /// Tests that `swiftly install latest` successfully installs the latest stable release of Swift. /// /// It stops short of verifying that it actually installs the _most_ recently released version, which is the intended /// behavior, since determining which version is the latest is non-trivial and would require duplicating code /// from within swiftly itself. - func testInstallLatest() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - var cmd = try self.parseCommand(Install.self, ["install", "latest", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + @Test func installLatest() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.runCommand(Install.self, ["install", "latest", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load(SwiftlyTests.ctx) guard !config.installedToolchains.isEmpty else { - XCTFail("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") + Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") return } let installedToolchain = config.installedToolchains.first! guard case let .stable(release) = installedToolchain else { - XCTFail("expected swiftly install latest to install release toolchain but got \(installedToolchain)") + Issue.record("expected swiftly install latest to install release toolchain but got \(installedToolchain)") return } // As of writing this, 5.8.0 is the latest stable release. Assert it is at least that new. - XCTAssertTrue(release >= ToolchainVersion.StableRelease(major: 5, minor: 8, patch: 0)) + #expect(release >= ToolchainVersion.StableRelease(major: 5, minor: 8, patch: 0)) - try await validateInstalledToolchains([installedToolchain], description: "install latest") + try await SwiftlyTests.validateInstalledToolchains([installedToolchain], description: "install latest") } } } /// Tests that `swiftly install a.b` installs the latest patch version of Swift a.b. - func testInstallLatestPatchVersion() async throws { - let snapshotsAvailable = try await self.snapshotsAvailable() - try XCTSkipIf(!snapshotsAvailable) - - guard try await self.baseTestConfig().platform.name != "ubi9" else { + @Test func installLatestPatchVersion() async throws { + guard try await SwiftlyTests.baseTestConfig().platform.name != "ubi9" else { print("Skipping test due to insufficient download availability for ubi9") return } - try await self.withTestHome { - try await self.withMockedToolchain { - var cmd = try self.parseCommand(Install.self, ["install", "5.7", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load(SwiftlyTests.ctx) guard !config.installedToolchains.isEmpty else { - XCTFail("expected swiftly install latest to install release toolchain but installed toolchains is empty in config") + Issue.record("expected swiftly install latest to install release toolchain but installed toolchains is empty in config") return } let installedToolchain = config.installedToolchains.first! guard case let .stable(release) = installedToolchain else { - XCTFail("expected swiftly install latest to install release toolchain but got \(installedToolchain)") + Issue.record("expected swiftly install latest to install release toolchain but got \(installedToolchain)") return } // As of writing this, 5.7.3 is the latest 5.7 patch release. Assert it is at least that new. - XCTAssertTrue(release >= ToolchainVersion.StableRelease(major: 5, minor: 7, patch: 3)) + #expect(release >= ToolchainVersion.StableRelease(major: 5, minor: 7, patch: 3)) - try await validateInstalledToolchains([installedToolchain], description: "install latest") + try await SwiftlyTests.validateInstalledToolchains([installedToolchain], description: "install latest") } } } /// Tests that swiftly can install different stable release versions by their full a.b.c versions. - func testInstallReleases() async throws { - guard try await self.baseTestConfig().platform.name != "ubi9" else { + @Test func installReleases() async throws { + guard try await SwiftlyTests.baseTestConfig().platform.name != "ubi9" else { print("Skipping test due to insufficient download availability for ubi9") return } - try await self.withTestHome { - try await self.withMockedToolchain { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { var installedToolchains: Set = [] - var cmd = try self.parseCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) installedToolchains.insert(ToolchainVersion(major: 5, minor: 7, patch: 0)) - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( installedToolchains, description: "install a stable release toolchain" ) - cmd = try self.parseCommand(Install.self, ["install", "5.7.2", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.2", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) installedToolchains.insert(ToolchainVersion(major: 5, minor: 7, patch: 2)) - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( installedToolchains, description: "install another stable release toolchain" ) @@ -107,26 +100,24 @@ final class InstallTests: SwiftlyTests { } /// Tests that swiftly can install main and release snapshots by their full snapshot names. - func testInstallSnapshots() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { + @Test func installSnapshots() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { var installedToolchains: Set = [] - var cmd = try self.parseCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) installedToolchains.insert(ToolchainVersion(snapshotBranch: .main, date: "2023-04-01")) - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( installedToolchains, description: "install a main snapshot toolchain" ) - cmd = try self.parseCommand(Install.self, ["install", "5.9-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.runCommand(Install.self, ["install", "5.9-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) installedToolchains.insert( ToolchainVersion(snapshotBranch: .release(major: 5, minor: 9), date: "2023-04-01")) - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( installedToolchains, description: "install a 5.9 snapshot toolchain" ) @@ -135,33 +126,29 @@ final class InstallTests: SwiftlyTests { } /// Tests that `swiftly install main-snapshot` installs the latest available main snapshot. - func testInstallLatestMainSnapshot() async throws { - let snapshotsAvailable = try await self.snapshotsAvailable() - try XCTSkipIf(!snapshotsAvailable) - - try await self.withTestHome { - try await self.withMockedToolchain { - var cmd = try self.parseCommand(Install.self, ["install", "main-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + @Test func installLatestMainSnapshot() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load(SwiftlyTests.ctx) guard !config.installedToolchains.isEmpty else { - XCTFail("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") + Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") return } let installedToolchain = config.installedToolchains.first! guard case let .snapshot(snapshot) = installedToolchain, snapshot.branch == .main else { - XCTFail("expected to install latest main snapshot toolchain but got \(installedToolchain)") + Issue.record("expected to install latest main snapshot toolchain but got \(installedToolchain)") return } // As of writing this, this is the date of the latest main snapshot. Assert it is at least that new. - XCTAssertTrue(snapshot.date >= "2023-04-01") + #expect(snapshot.date >= "2023-04-01") - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [installedToolchain], description: "install the latest main snapshot toolchain" ) @@ -170,33 +157,29 @@ final class InstallTests: SwiftlyTests { } /// Tests that `swiftly install a.b-snapshot` installs the latest available a.b release snapshot. - func testInstallLatestReleaseSnapshot() async throws { - let snapshotsAvailable = try await self.snapshotsAvailable() - try XCTSkipIf(!snapshotsAvailable) - - try await self.withTestHome { - try await self.withMockedToolchain { - var cmd = try self.parseCommand(Install.self, ["install", "6.0-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + @Test func installLatestReleaseSnapshot() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.runCommand(Install.self, ["install", "6.0-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load(SwiftlyTests.ctx) guard !config.installedToolchains.isEmpty else { - XCTFail("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") + Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") return } let installedToolchain = config.installedToolchains.first! guard case let .snapshot(snapshot) = installedToolchain, snapshot.branch == .release(major: 6, minor: 0) else { - XCTFail("expected swiftly install 6.0-snapshot to install snapshot toolchain but got \(installedToolchain)") + Issue.record("expected swiftly install 6.0-snapshot to install snapshot toolchain but got \(installedToolchain)") return } // As of writing this, this is the date of the latest 5.7 snapshot. Assert it is at least that new. - XCTAssertTrue(snapshot.date >= "2024-06-18") + #expect(snapshot.date >= "2024-06-18") - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [installedToolchain], description: "install the latest 6.0 snapshot toolchain" ) @@ -205,19 +188,16 @@ final class InstallTests: SwiftlyTests { } /// Tests that swiftly can install both stable release toolchains and snapshot toolchains. - func testInstallReleaseAndSnapshots() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - var cmd = try self.parseCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + @Test func installReleaseAndSnapshots() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - cmd = try self.parseCommand(Install.self, ["install", "5.9-snapshot-2023-03-28", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.runCommand(Install.self, ["install", "5.9-snapshot-2023-03-28", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - cmd = try self.parseCommand(Install.self, ["install", "5.8.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.runCommand(Install.self, ["install", "5.8.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [ ToolchainVersion(snapshotBranch: .main, date: "2023-04-01"), ToolchainVersion(snapshotBranch: .release(major: 5, minor: 9), date: "2023-03-28"), @@ -230,87 +210,76 @@ final class InstallTests: SwiftlyTests { } func duplicateTest(_ version: String) async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - var cmd = try self.parseCommand(Install.self, ["install", version, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.runCommand(Install.self, ["install", version, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let before = try Config.load() + let before = try Config.load(SwiftlyTests.ctx) let startTime = Date() - cmd = try self.parseCommand(Install.self, ["install", version, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.runCommand(Install.self, ["install", version, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) // Assert that swiftly didn't attempt to download a new toolchain. - XCTAssertTrue(startTime.timeIntervalSinceNow.magnitude < 10) + #expect(startTime.timeIntervalSinceNow.magnitude < 10) - let after = try Config.load() - XCTAssertEqual(before, after) + let after = try Config.load(SwiftlyTests.ctx) + #expect(before == after) } } } /// Tests that attempting to install stable releases that are already installed doesn't result in an error. - func testInstallDuplicateReleases() async throws { + @Test func installDuplicateReleases() async throws { try await self.duplicateTest("5.8.0") try await self.duplicateTest("latest") } /// Tests that attempting to install main snapshots that are already installed doesn't result in an error. - func testInstallDuplicateMainSnapshots() async throws { - let snapshotsAvailable = try await self.snapshotsAvailable() - try XCTSkipIf(!snapshotsAvailable) - + @Test func installDuplicateMainSnapshots() async throws { try await self.duplicateTest("main-snapshot-2023-04-01") try await self.duplicateTest("main-snapshot") } /// Tests that attempting to install release snapshots that are already installed doesn't result in an error. - func testInstallDuplicateReleaseSnapshots() async throws { - let snapshotsAvailable = try await self.snapshotsAvailable() - try XCTSkipIf(!snapshotsAvailable) - + @Test func installDuplicateReleaseSnapshots() async throws { try await self.duplicateTest("6.0-snapshot-2024-06-18") try await self.duplicateTest("6.0-snapshot") } /// Verify that the installed toolchain will be used if no toolchains currently are installed. - func testInstallUsesFirstToolchain() async throws { - guard try await self.baseTestConfig().platform.name != "ubi9" else { + @Test func installUsesFirstToolchain() async throws { + guard try await SwiftlyTests.baseTestConfig().platform.name != "ubi9" else { print("Skipping test due to insufficient download availability for ubi9") return } - try await self.withTestHome { - try await self.withMockedToolchain { - let config = try Config.load() - XCTAssertTrue(config.inUse == nil) - try await validateInUse(expected: nil) + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + let config = try Config.load(SwiftlyTests.ctx) + #expect(config.inUse == nil) + try await SwiftlyTests.validateInUse(expected: nil) - var cmd = try self.parseCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await cmd.run() + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await validateInUse(expected: ToolchainVersion(major: 5, minor: 7, patch: 0)) + try await SwiftlyTests.validateInUse(expected: ToolchainVersion(major: 5, minor: 7, patch: 0)) - var installOther = try self.parseCommand(Install.self, ["install", "5.7.1", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await installOther.run() + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.1", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) // Verify that 5.7.0 is still in use. - try await self.validateInUse(expected: ToolchainVersion(major: 5, minor: 7, patch: 0)) + try await SwiftlyTests.validateInUse(expected: ToolchainVersion(major: 5, minor: 7, patch: 0)) } } } /// Verify that the installed toolchain will be marked as in-use if the --use flag is specified. - func testInstallUseFlag() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - try await self.installMockedToolchain(toolchain: Self.oldStable) - var use = try self.parseCommand(Use.self, ["use", Self.oldStable.name]) - try await use.run() - try await validateInUse(expected: Self.oldStable) - try await self.installMockedToolchain(selector: Self.newStable.name, args: ["--use"]) - try await self.validateInUse(expected: Self.newStable) + @Test func installUseFlag() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(toolchain: SwiftlyTests.oldStable) + try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.oldStable.name]) + try await SwiftlyTests.validateInUse(expected: SwiftlyTests.oldStable) + try await SwiftlyTests.installMockedToolchain(selector: SwiftlyTests.newStable.name, args: ["--use"]) + try await SwiftlyTests.validateInUse(expected: SwiftlyTests.newStable) } } } diff --git a/Tests/SwiftlyTests/ListTests.swift b/Tests/SwiftlyTests/ListTests.swift index 6724ab97..31954ee3 100644 --- a/Tests/SwiftlyTests/ListTests.swift +++ b/Tests/SwiftlyTests/ListTests.swift @@ -1,34 +1,33 @@ import Foundation @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class ListTests: SwiftlyTests { +@Suite struct ListTests { static let homeName = "useTests" static let sortedReleaseToolchains: [ToolchainVersion] = [ - ListTests.newStable, - ListTests.oldStableNewPatch, - ListTests.oldStable, + SwiftlyTests.newStable, + SwiftlyTests.oldStableNewPatch, + SwiftlyTests.oldStable, ] static let sortedSnapshotToolchains: [ToolchainVersion] = [ - ListTests.newMainSnapshot, - ListTests.oldMainSnapshot, - ListTests.newReleaseSnapshot, - ListTests.oldReleaseSnapshot, + SwiftlyTests.newMainSnapshot, + SwiftlyTests.oldMainSnapshot, + SwiftlyTests.newReleaseSnapshot, + SwiftlyTests.oldReleaseSnapshot, ] /// Constructs a mock home directory with the toolchains listed above installed and runs the provided closure within /// the context of that home. func runListTest(f: () async throws -> Void) async throws { - try await self.withTestHome(name: Self.homeName) { - for toolchain in Self.allToolchains { - try await self.installMockedToolchain(toolchain: toolchain) + try await SwiftlyTests.withTestHome(name: Self.homeName) { + for toolchain in SwiftlyTests.allToolchains { + try await SwiftlyTests.installMockedToolchain(toolchain: toolchain) } - var use = try self.parseCommand(Use.self, ["use", "latest"]) - try await use.run() + try await SwiftlyTests.runCommand(Use.self, ["use", "latest"]) try await f() } @@ -42,11 +41,10 @@ final class ListTests: SwiftlyTests { args.append(selector) } - var list = try self.parseCommand(List.self, args) - let output = try await list.runWithMockedIO() + let output = try await SwiftlyTests.runWithMockedIO(List.self, args) let parsedToolchains = output.compactMap { outputLine in - Self.allToolchains.first { + SwiftlyTests.allToolchains.first { outputLine.contains(String(describing: $0)) } } @@ -61,93 +59,92 @@ final class ListTests: SwiftlyTests { /// Tests that running `swiftly list` without a selector prints all installed toolchains, sorted in descending /// order with release toolchains listed first. - func testList() async throws { + @Test func list() async throws { try await self.runListTest { let toolchains = try await self.runList(selector: nil) - XCTAssertEqual(toolchains, Self.sortedReleaseToolchains + Self.sortedSnapshotToolchains) + #expect(toolchains == Self.sortedReleaseToolchains + Self.sortedSnapshotToolchains) } } /// Tests that running `swiftly list` with a release version selector filters out unmatching toolchains and prints /// in descending order. - func testListReleaseToolchains() async throws { + @Test func listReleaseToolchains() async throws { try await self.runListTest { var toolchains = try await self.runList(selector: "5") - XCTAssertEqual(toolchains, Self.sortedReleaseToolchains) + #expect(toolchains == Self.sortedReleaseToolchains) - var selector = "\(Self.newStable.asStableRelease!.major).\(Self.newStable.asStableRelease!.minor)" + var selector = "\(SwiftlyTests.newStable.asStableRelease!.major).\(SwiftlyTests.newStable.asStableRelease!.minor)" toolchains = try await self.runList(selector: selector) - XCTAssertEqual(toolchains, [Self.newStable]) + #expect(toolchains == [SwiftlyTests.newStable]) - selector = "\(Self.oldStable.asStableRelease!.major).\(Self.oldStable.asStableRelease!.minor)" + selector = "\(SwiftlyTests.oldStable.asStableRelease!.major).\(SwiftlyTests.oldStable.asStableRelease!.minor)" toolchains = try await self.runList(selector: selector) - XCTAssertEqual(toolchains, [Self.oldStableNewPatch, Self.oldStable]) + #expect(toolchains == [SwiftlyTests.oldStableNewPatch, SwiftlyTests.oldStable]) for toolchain in Self.sortedReleaseToolchains { toolchains = try await self.runList(selector: toolchain.name) - XCTAssertEqual(toolchains, [toolchain]) + #expect(toolchains == [toolchain]) } toolchains = try await self.runList(selector: "4") - XCTAssertEqual(toolchains, []) + #expect(toolchains == []) } } /// Tests that running `swiftly list` with a snapshot selector filters out unmatching toolchains and prints /// in descending order. - func testListSnapshotToolchains() async throws { + @Test func listSnapshotToolchains() async throws { try await self.runListTest { var toolchains = try await self.runList(selector: "main-snapshot") - XCTAssertEqual(toolchains, [Self.newMainSnapshot, Self.oldMainSnapshot]) + #expect(toolchains == [SwiftlyTests.newMainSnapshot, SwiftlyTests.oldMainSnapshot]) - let snapshotBranch = Self.newReleaseSnapshot.asSnapshot!.branch + let snapshotBranch = SwiftlyTests.newReleaseSnapshot.asSnapshot!.branch toolchains = try await self.runList(selector: "\(snapshotBranch.major!).\(snapshotBranch.minor!)-snapshot") - XCTAssertEqual(toolchains, [Self.newReleaseSnapshot, Self.oldReleaseSnapshot]) + #expect(toolchains == [SwiftlyTests.newReleaseSnapshot, SwiftlyTests.oldReleaseSnapshot]) for toolchain in Self.sortedSnapshotToolchains { toolchains = try await self.runList(selector: toolchain.name) - XCTAssertEqual(toolchains, [toolchain]) + #expect(toolchains == [toolchain]) } toolchains = try await self.runList(selector: "1.2-snapshot") - XCTAssertEqual(toolchains, []) + #expect(toolchains == []) } } /// Tests that the "(in use)" marker is correctly printed when listing installed toolchains. - func testListInUse() async throws { + @Test func listInUse() async throws { func inUseTest(toolchain: ToolchainVersion, selector: String?) async throws { - var use = try self.parseCommand(Use.self, ["use", toolchain.name]) - try await use.run() + try await SwiftlyTests.runCommand(Use.self, ["use", toolchain.name]) var listArgs = ["list"] if let selector { listArgs.append(selector) } - var list = try self.parseCommand(List.self, listArgs) - let output = try await list.runWithMockedIO() + + let output = try await SwiftlyTests.runWithMockedIO(List.self, listArgs) let inUse = output.filter { $0.contains("in use") } - XCTAssertEqual(inUse, ["\(toolchain) (in use) (default)"]) + #expect(inUse == ["\(toolchain) (in use) (default)"]) } try await self.runListTest { - for toolchain in Self.allToolchains { + for toolchain in SwiftlyTests.allToolchains { try await inUseTest(toolchain: toolchain, selector: nil) try await inUseTest(toolchain: toolchain, selector: toolchain.name) } - let major = Self.oldStable.asStableRelease!.major + let major = SwiftlyTests.oldStable.asStableRelease!.major for toolchain in Self.sortedReleaseToolchains.filter({ $0.asStableRelease?.major == major }) { try await inUseTest(toolchain: toolchain, selector: "\(major)") } - for toolchain in Self.allToolchains.filter({ $0.asSnapshot?.branch == .main }) { + for toolchain in SwiftlyTests.allToolchains.filter({ $0.asSnapshot?.branch == .main }) { try await inUseTest(toolchain: toolchain, selector: "main-snapshot") } - let branch = Self.oldReleaseSnapshot.asSnapshot!.branch - let releaseSnapshots = Self.allToolchains.filter { + let branch = SwiftlyTests.oldReleaseSnapshot.asSnapshot!.branch + let releaseSnapshots = SwiftlyTests.allToolchains.filter { $0.asSnapshot?.branch == branch } for toolchain in releaseSnapshots { @@ -157,19 +154,19 @@ final class ListTests: SwiftlyTests { } /// Tests that `list` properly handles the case where no toolchains been installed yet. - func testListEmpty() async throws { - try await self.withTestHome { + @Test func listEmpty() async throws { + try await SwiftlyTests.withTestHome { var toolchains = try await self.runList(selector: nil) - XCTAssertEqual(toolchains, []) + #expect(toolchains == []) toolchains = try await self.runList(selector: "5") - XCTAssertEqual(toolchains, []) + #expect(toolchains == []) toolchains = try await self.runList(selector: "main-snapshot") - XCTAssertEqual(toolchains, []) + #expect(toolchains == []) toolchains = try await self.runList(selector: "5.7-snapshot") - XCTAssertEqual(toolchains, []) + #expect(toolchains == []) } } } diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index d99ff321..7d263866 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -1,11 +1,11 @@ import Foundation @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class PlatformTests: SwiftlyTests { +@Suite struct PlatformTests { func mockToolchainDownload(version: String) async throws -> (URL, ToolchainVersion) { - let mockDownloader = MockToolchainDownloader(executables: ["swift"], delegate: SwiftlyCore.httpRequestExecutor) + let mockDownloader = MockToolchainDownloader(executables: ["swift"], delegate: SwiftlyTests.ctx.httpClient.httpRequestExecutor) let version = try! ToolchainVersion(parsing: version) let ext = Swiftly.currentPlatform.toolchainFileExtension let tmpDir = Swiftly.currentPlatform.getTempFilePath() @@ -17,60 +17,60 @@ final class PlatformTests: SwiftlyTests { return (mockedToolchainFile, version) } - func testInstall() async throws { - try await self.rollbackLocalChanges { + @Test func install() async throws { + try await SwiftlyTests.withTestHome { // GIVEN: a toolchain has been downloaded var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.7.1") // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchain is extracted in the toolchains directory - var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil) - XCTAssertEqual(1, toolchains.count) + var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(1 == toolchains.count) // GIVEN: a second toolchain has been downloaded (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchain is added to the toolchains directory - toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil) - XCTAssertEqual(2, toolchains.count) + toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(2 == toolchains.count) // GIVEN: an identical toolchain has been downloaded (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchains directory remains the same - toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil) - XCTAssertEqual(2, toolchains.count) + toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(2 == toolchains.count) } } - func testUninstall() async throws { - try await self.rollbackLocalChanges { + @Test func uninstall() async throws { + try await SwiftlyTests.withTestHome { // GIVEN: toolchains have been downloaded, and installed var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.6.3") - try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true) + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // WHEN: one of the toolchains is uninstalled - try Swiftly.currentPlatform.uninstall(version, verbose: true) + try Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, version, verbose: true) // THEN: there is only one remaining toolchain installed - var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil) - XCTAssertEqual(1, toolchains.count) + var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(1 == toolchains.count) // GIVEN; there is only one toolchain installed // WHEN: a non-existent toolchain is uninstalled - try? Swiftly.currentPlatform.uninstall(ToolchainVersion(parsing: "5.9.1"), verbose: true) + try? Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.9.1"), verbose: true) // THEN: there is the one remaining toolchain that is still installed - toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil) - XCTAssertEqual(1, toolchains.count) + toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(1 == toolchains.count) // GIVEN: there is only one toolchain installed // WHEN: the last toolchain is uninstalled - try Swiftly.currentPlatform.uninstall(ToolchainVersion(parsing: "5.8.0"), verbose: true) + try Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.8.0"), verbose: true) // THEN: there are no toolchains installed - toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil) - XCTAssertEqual(0, toolchains.count) + toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(0 == toolchains.count) } } } diff --git a/Tests/SwiftlyTests/RunTests.swift b/Tests/SwiftlyTests/RunTests.swift index 6b42c63b..75416d1b 100644 --- a/Tests/SwiftlyTests/RunTests.swift +++ b/Tests/SwiftlyTests/RunTests.swift @@ -1,96 +1,92 @@ import Foundation @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class RunTests: SwiftlyTests { +@Suite struct RunTests { static let homeName = "runTests" /// Tests that the `run` command can switch between installed toolchains. - func testRunSelection() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { + @Test func runSelection() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { // GIVEN: a set of installed toolchains // WHEN: invoking the run command with a selector argument for that toolchain - var run = try self.parseCommand(Run.self, ["run", "swift", "--version", "+\(Self.newStable.name)"]) - var output = try await run.runWithMockedIO() + var output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version", "+\(SwiftlyTests.newStable.name)"]) // THEN: the output confirms that it ran with the selected toolchain - XCTAssert(output.contains(Self.newStable.name)) + #expect(output.contains(SwiftlyTests.newStable.name)) // GIVEN: a set of installed toolchains and one is selected with a .swift-version file - let versionFile = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(".swift-version") - try Self.oldStable.name.write(to: versionFile, atomically: true, encoding: .utf8) + let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") + try SwiftlyTests.oldStable.name.write(to: versionFile, atomically: true, encoding: .utf8) // WHEN: invoking the run command without any selector arguments for toolchains - run = try self.parseCommand(Run.self, ["run", "swift", "--version"]) - output = try await run.runWithMockedIO() + output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version"]) // THEN: the output confirms that it ran with the selected toolchain - XCTAssert(output.contains(Self.oldStable.name)) + #expect(output.contains(SwiftlyTests.oldStable.name)) // GIVEN: a set of installed toolchains // WHEN: invoking the run command with a selector argument for a toolchain that isn't installed - run = try self.parseCommand(Run.self, ["run", "swift", "+1.2.3", "--version"]) do { - try await run.run() - XCTAssert(false) + try await SwiftlyTests.runCommand(Run.self, ["run", "swift", "+1.2.3", "--version"]) + #expect(false) } catch let e as SwiftlyError { - XCTAssert(e.message.contains("didn't match any of the installed toolchains")) + #expect(e.message.contains("didn't match any of the installed toolchains")) } // THEN: an error is shown because there is no matching toolchain that is installed } } /// Tests the `run` command verifying that the environment is as expected - func testRunEnvironment() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { + @Test func runEnvironment() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { // The toolchains directory should be the fist entry on the path - var run = try self.parseCommand(Run.self, ["run", try await Swiftly.currentPlatform.getShell(), "-c", "echo $PATH"]) - let output = try await run.runWithMockedIO() - XCTAssert(output.count == 1) - XCTAssert(output[0].contains(Swiftly.currentPlatform.swiftlyToolchainsDir.path)) + let output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", try await Swiftly.currentPlatform.getShell(), "-c", "echo $PATH"]) + #expect(output.count == 1) + #expect(output[0].contains(Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).path)) } } /// Tests the extraction of proxy arguments from the run command arguments. - func testExtractProxyArguments() throws { - var (command, selector) = try extractProxyArguments(command: ["swift", "build"]) - XCTAssertEqual(["swift", "build"], command) - XCTAssertEqual(nil, selector) + @Test func extractProxyArguments() throws { + var (command, selector) = try Run.extractProxyArguments(command: ["swift", "build"]) + #expect(["swift", "build"] == command) + #expect(nil == selector) - (command, selector) = try extractProxyArguments(command: ["swift", "+1.2.3", "build"]) - XCTAssertEqual(["swift", "build"], command) - XCTAssertEqual(try! ToolchainSelector(parsing: "1.2.3"), selector) + (command, selector) = try Run.extractProxyArguments(command: ["swift", "+1.2.3", "build"]) + #expect(["swift", "build"] == command) + #expect(try! ToolchainSelector(parsing: "1.2.3") == selector) - (command, selector) = try extractProxyArguments(command: ["swift", "build", "+latest"]) - XCTAssertEqual(["swift", "build"], command) - XCTAssertEqual(try! ToolchainSelector(parsing: "latest"), selector) + (command, selector) = try Run.extractProxyArguments(command: ["swift", "build", "+latest"]) + #expect(["swift", "build"] == command) + #expect(try! ToolchainSelector(parsing: "latest") == selector) - (command, selector) = try extractProxyArguments(command: ["+5.6", "swift", "build"]) - XCTAssertEqual(["swift", "build"], command) - XCTAssertEqual(try! ToolchainSelector(parsing: "5.6"), selector) + (command, selector) = try Run.extractProxyArguments(command: ["+5.6", "swift", "build"]) + #expect(["swift", "build"] == command) + #expect(try! ToolchainSelector(parsing: "5.6") == selector) - (command, selector) = try extractProxyArguments(command: ["swift", "++1.2.3", "build"]) - XCTAssertEqual(["swift", "+1.2.3", "build"], command) - XCTAssertEqual(nil, selector) + (command, selector) = try Run.extractProxyArguments(command: ["swift", "++1.2.3", "build"]) + #expect(["swift", "+1.2.3", "build"] == command) + #expect(nil == selector) - (command, selector) = try extractProxyArguments(command: ["swift", "++", "+1.2.3", "build"]) - XCTAssertEqual(["swift", "+1.2.3", "build"], command) - XCTAssertEqual(nil, selector) + (command, selector) = try Run.extractProxyArguments(command: ["swift", "++", "+1.2.3", "build"]) + #expect(["swift", "+1.2.3", "build"] == command) + #expect(nil == selector) do { - let _ = try extractProxyArguments(command: ["+1.2.3"]) - XCTAssert(false) + let _ = try Run.extractProxyArguments(command: ["+1.2.3"]) + #expect(false) } catch {} do { - let _ = try extractProxyArguments(command: []) - XCTAssert(false) + let _ = try Run.extractProxyArguments(command: []) + #expect(false) } catch {} - (command, selector) = try extractProxyArguments(command: ["swift", "+1.2.3", "build"]) - XCTAssertEqual(["swift", "build"], command) - XCTAssertEqual(try! ToolchainSelector(parsing: "1.2.3"), selector) + (command, selector) = try Run.extractProxyArguments(command: ["swift", "+1.2.3", "build"]) + #expect(["swift", "build"] == command) + #expect(try! ToolchainSelector(parsing: "1.2.3") == selector) - (command, selector) = try extractProxyArguments(command: ["swift", "build"]) - XCTAssertEqual(["swift", "build"], command) - XCTAssertEqual(nil, selector) + (command, selector) = try Run.extractProxyArguments(command: ["swift", "build"]) + #expect(["swift", "build"] == command) + #expect(nil == selector) } } diff --git a/Tests/SwiftlyTests/SelfUpdateTests.swift b/Tests/SwiftlyTests/SelfUpdateTests.swift index 628ee9b8..0f5e355d 100644 --- a/Tests/SwiftlyTests/SelfUpdateTests.swift +++ b/Tests/SwiftlyTests/SelfUpdateTests.swift @@ -3,9 +3,9 @@ import Foundation import NIO @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class SelfUpdateTests: SwiftlyTests { +@Suite struct SelfUpdateTests { private static var newMajorVersion: SwiftlyVersion { SwiftlyVersion(major: SwiftlyCore.version.major + 1, minor: 0, patch: 0) } @@ -19,22 +19,22 @@ final class SelfUpdateTests: SwiftlyTests { } func runSelfUpdateTest(latestVersion: SwiftlyVersion) async throws { - try await self.withTestHome { - try await self.withMockedSwiftlyVersion(latestSwiftlyVersion: latestVersion) { - let updatedVersion = try await SelfUpdate.execute(verbose: true) - XCTAssertEqual(latestVersion, updatedVersion) + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedSwiftlyVersion(latestSwiftlyVersion: latestVersion) { + let updatedVersion = try await SelfUpdate.execute(SwiftlyTests.ctx, verbose: true) + #expect(latestVersion == updatedVersion) } } } - func testSelfUpdate() async throws { + @Test func selfUpdate() async throws { try await self.runSelfUpdateTest(latestVersion: Self.newPatchVersion) try await self.runSelfUpdateTest(latestVersion: Self.newMinorVersion) try await self.runSelfUpdateTest(latestVersion: Self.newMajorVersion) } /// Verify updating the most up-to-date toolchain has no effect. - func testSelfUpdateAlreadyUpToDate() async throws { + @Test func selfUpdateAlreadyUpToDate() async throws { try await self.runSelfUpdateTest(latestVersion: SwiftlyCore.version) } } diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 79b6cbf0..e9a45324 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -1,10 +1,11 @@ import _StringProcessing import ArgumentParser import AsyncHTTPClient +import Foundation import NIO @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing #if os(macOS) import MacOSPlatform @@ -17,23 +18,26 @@ struct SwiftlyTestError: LocalizedError { let message: String } -public class SwiftlyTests: XCTestCase { - override public class func tearDown() { -#if os(Linux) - let deleteTestGPGKeys = Process() - deleteTestGPGKeys.executableURL = URL(fileURLWithPath: "/usr/bin/env") - deleteTestGPGKeys.arguments = [ - "bash", - "-c", - """ - gpg --batch --yes --delete-secret-keys --fingerprint "A2A645E5249D25845C43954E7D210032D2F670B7" >/dev/null 2>&1 - gpg --batch --yes --delete-keys --fingerprint "A2A645E5249D25845C43954E7D210032D2F670B7" >/dev/null 2>&1 - """, - ] - try? deleteTestGPGKeys.run() - deleteTestGPGKeys.waitUntilExit() -#endif +struct OutputHandlerFail: OutputHandler { + func handleOutputLine(_: String) { + fatalError("core context was not mocked. put the test case in a SwiftlyTests.with() before running it.") } +} + +struct InputProviderFail: InputProvider { + func readLine() -> String? { + fatalError("core context was not mocked. put the test case in a SwiftlyTests.with() before running it.") + } +} + +public enum SwiftlyTests { + @TaskLocal static var ctx: SwiftlyCoreContext = .init( + mockedHomeDir: URL(fileURLWithPath: "/does/not/exist"), + // FIXME: place a request executor that fails on each request here + httpClient: SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()), + outputHandler: OutputHandlerFail(), + inputProvider: InputProviderFail() + ) // Below are some constants that can be used to write test cases. public static let oldStable = ToolchainVersion(major: 5, minor: 6, patch: 0) @@ -54,8 +58,8 @@ public class SwiftlyTests: XCTestCase { newReleaseSnapshot, ] - func baseTestConfig() async throws -> Config { - guard let pd = try? await Swiftly.currentPlatform.detectPlatform(disableConfirmation: true, platform: nil) else { + static func baseTestConfig() async throws -> Config { + guard let pd = try? await Swiftly.currentPlatform.detectPlatform(Self.ctx, disableConfirmation: true, platform: nil) else { throw SwiftlyTestError(message: "Unable to detect the current platform.") } @@ -66,19 +70,49 @@ public class SwiftlyTests: XCTestCase { ) } - func parseCommand(_ commandType: T.Type, _ arguments: [String]) throws -> T { + static func runCommand(_ commandType: T.Type, _ arguments: [String]) async throws { + let rawCmd = try Swiftly.parseAsRoot(arguments) + + guard var cmd = rawCmd as? T else { + throw SwiftlyTestError( + message: "expected \(arguments) to parse as \(commandType) but got \(rawCmd) instead" + ) + } + + try await cmd.run(Self.ctx) + } + + /// Run this command, using the provided input as the stdin (in lines). Returns an array of captured + /// output lines. + static func runWithMockedIO(_ commandType: T.Type, _ arguments: [String], quiet: Bool = false, input: [String]? = nil) async throws -> [String] { + let handler = TestOutputHandler(quiet: quiet) + let provider: (any InputProvider)? = if let input { + TestInputProvider(lines: input) + } else { + nil + } + + let ctx = SwiftlyCoreContext( + mockedHomeDir: SwiftlyTests.ctx.mockedHomeDir, + httpClient: SwiftlyTests.ctx.httpClient, + outputHandler: handler, + inputProvider: provider + ) + let rawCmd = try Swiftly.parseAsRoot(arguments) - guard let cmd = rawCmd as? T else { + guard var cmd = rawCmd as? T else { throw SwiftlyTestError( message: "expected \(arguments) to parse as \(commandType) but got \(rawCmd) instead" ) } - return cmd + try await cmd.run(ctx) + + return handler.lines } - class func getTestHomePath(name: String) -> URL { + static func getTestHomePath(name: String) -> URL { FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-tests-\(name)-\(UUID())") } @@ -86,51 +120,54 @@ public class SwiftlyTests: XCTestCase { /// Any swiftly commands executed in the closure will use this new home directory. /// /// The home directory will be deleted after the provided closure has been executed. - func withTestHome( + static func withTestHome( name: String = "testHome", _ f: () async throws -> Void ) async throws { let testHome = Self.getTestHomePath(name: name) - SwiftlyCore.mockedHomeDir = testHome + defer { - SwiftlyCore.mockedHomeDir = nil try? testHome.deleteIfExists() } - for dir in Swiftly.requiredDirectories { + + let ctx = SwiftlyCoreContext( + mockedHomeDir: testHome, + httpClient: SwiftlyTests.ctx.httpClient, + outputHandler: nil, + inputProvider: nil + ) + + for dir in Swiftly.requiredDirectories(ctx) { try dir.deleteIfExists() try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: false) } - let config = try await self.baseTestConfig() - try config.save() + let config = try await Self.baseTestConfig() + try config.save(ctx) - let cwd = FileManager.default.currentDirectoryPath - defer { - FileManager.default.changeCurrentDirectoryPath(cwd) + try await Self.$ctx.withValue(ctx) { + try await f() } - - FileManager.default.changeCurrentDirectoryPath(testHome.path) - - try await f() } - func withMockedHome( + /// Creates a mocked home directory with the supplied toolchains pre-installed as mocks, + /// and the provided inUse is the global default toolchain. + static func withMockedHome( homeName: String, toolchains: Set, inUse: ToolchainVersion? = nil, f: () async throws -> Void ) async throws { - try await self.withTestHome(name: homeName) { + try await Self.withTestHome(name: homeName) { for toolchain in toolchains { - try await self.installMockedToolchain(toolchain: toolchain) + try await Self.installMockedToolchain(toolchain: toolchain) } if !toolchains.isEmpty { - var use = try self.parseCommand(Use.self, ["use", inUse?.name ?? "latest"]) - try await use.run() + try await Self.runCommand(Use.self, ["use", inUse?.name ?? "latest"]) } else { try FileManager.default.createDirectory( - at: Swiftly.currentPlatform.swiftlyBinDir, + at: Swiftly.currentPlatform.swiftlyBinDir(Self.ctx), withIntermediateDirectories: true ) } @@ -139,133 +176,95 @@ public class SwiftlyTests: XCTestCase { } } - func withMockedSwiftlyVersion(latestSwiftlyVersion: SwiftlyVersion = SwiftlyCore.version, _ f: () async throws -> Void) async throws { - let prevExecutor = SwiftlyCore.httpRequestExecutor + /// Operate with a mocked swiftly version available when requested with the HTTP request executor. + static func withMockedSwiftlyVersion(latestSwiftlyVersion: SwiftlyVersion = SwiftlyCore.version, _ f: () async throws -> Void) async throws { + let prevExecutor = Self.ctx.httpClient.httpRequestExecutor let mockDownloader = MockToolchainDownloader(executables: ["swift"], latestSwiftlyVersion: latestSwiftlyVersion, delegate: prevExecutor) - SwiftlyCore.httpRequestExecutor = mockDownloader - defer { - SwiftlyCore.httpRequestExecutor = prevExecutor - } - try await f() - } + let ctx = SwiftlyCoreContext( + mockedHomeDir: SwiftlyTests.ctx.mockedHomeDir, + httpClient: SwiftlyHTTPClient(httpRequestExecutor: mockDownloader), + outputHandler: SwiftlyTests.ctx.outputHandler, + inputProvider: SwiftlyTests.ctx.inputProvider + ) - func withMockedToolchain(executables: [String]? = nil, f: () async throws -> Void) async throws { - let prevExecutor = SwiftlyCore.httpRequestExecutor - let mockDownloader = MockToolchainDownloader(executables: executables, delegate: prevExecutor) - SwiftlyCore.httpRequestExecutor = mockDownloader - defer { - SwiftlyCore.httpRequestExecutor = prevExecutor + try await SwiftlyTests.$ctx.withValue(ctx) { + try await f() } - - try await f() } - func withMockedHTTPRequests(_ handler: @escaping (HTTPClientRequest) async throws -> HTTPClientResponse, _ f: () async throws -> Void) async throws { - let prevExecutor = SwiftlyCore.httpRequestExecutor - let mockedRequestExecutor = MockHTTPRequestExecutor(handler: handler) - SwiftlyCore.httpRequestExecutor = mockedRequestExecutor - defer { - SwiftlyCore.httpRequestExecutor = prevExecutor - } + /// Operate with a mocked toolchain that has the provided list of executables in its bin directory. + static func withMockedToolchain(executables: [String]? = nil, f: () async throws -> Void) async throws { + let prevExecutor = SwiftlyTests.ctx.httpClient.httpRequestExecutor + let mockDownloader = MockToolchainDownloader(executables: executables, delegate: prevExecutor) - try await f() - } + let ctx = SwiftlyCoreContext( + mockedHomeDir: SwiftlyTests.ctx.mockedHomeDir, + httpClient: SwiftlyHTTPClient(httpRequestExecutor: mockDownloader), + outputHandler: SwiftlyTests.ctx.outputHandler, + inputProvider: SwiftlyTests.ctx.inputProvider + ) - /// Backup and rollback local changes to the user's installation. - /// - /// Backup the user's swiftly installation before running the provided - /// function and roll it all back afterwards. - func rollbackLocalChanges(_ f: () async throws -> Void) async throws { - let userHome = FileManager.default.homeDirectoryForCurrentUser - - // Backup user profile changes in case of init tests - let profiles = [".profile", ".zprofile", ".bash_profile", ".bash_login", ".config/fish/conf.d/swiftly.fish"] - for profile in profiles { - let config = userHome.appendingPathComponent(profile) - let backupConfig = config.appendingPathExtension("swiftly-test-backup") - _ = try? FileManager.default.copyItem(at: config, to: backupConfig) - } - defer { - for profile in profiles.reversed() { - let config = userHome.appendingPathComponent(profile) - let backupConfig = config.appendingPathExtension("swiftly-test-backup") - if backupConfig.fileExists() { - if config.fileExists() { - try? FileManager.default.removeItem(at: config) - } - try? FileManager.default.moveItem(at: backupConfig, to: config) - } else if config.fileExists() { - try? FileManager.default.removeItem(at: config) - } - } + try await SwiftlyTests.$ctx.withValue(ctx) { + try await f() } + } -#if os(macOS) - // In some environments, such as CI, we can't install directly to the user's home directory - try await self.withTestHome(name: "e2eHome") { try await f() } - return -#endif + /// Operate with a mocked HTTP request executor that calls the provided handler to handle the requests. + static func withMockedHTTPRequests(_ handler: @escaping (HTTPClientRequest) async throws -> HTTPClientResponse, _ f: () async throws -> Void) async throws { + let mockedRequestExecutor = MockHTTPRequestExecutor(handler: handler) - // Backup config, toolchain, and bin directory - let swiftlyFiles = [Swiftly.currentPlatform.swiftlyHomeDir, Swiftly.currentPlatform.swiftlyToolchainsDir, Swiftly.currentPlatform.swiftlyBinDir] - for file in swiftlyFiles { - let backupFile = file.appendingPathExtension("swiftly-test-backup") - _ = try? FileManager.default.moveItem(at: file, to: backupFile) + let ctx = SwiftlyCoreContext( + mockedHomeDir: SwiftlyTests.ctx.mockedHomeDir, + httpClient: SwiftlyHTTPClient(httpRequestExecutor: mockedRequestExecutor), + outputHandler: SwiftlyTests.ctx.outputHandler, + inputProvider: SwiftlyTests.ctx.inputProvider + ) - if file == Swiftly.currentPlatform.swiftlyConfigFile { - _ = try? FileManager.default.createDirectory(at: file.deletingLastPathComponent(), withIntermediateDirectories: true) - } else { - _ = try? FileManager.default.createDirectory(at: file, withIntermediateDirectories: true) - } - } - defer { - for file in swiftlyFiles.reversed() { - let backupFile = file.appendingPathExtension("swiftly-test-backup") - if backupFile.fileExists() { - if file.fileExists() { - try? FileManager.default.removeItem(at: file) - } - try? FileManager.default.moveItem(at: backupFile, to: file) - } else if file.fileExists() { - try? FileManager.default.removeItem(at: file) - } - } + try await SwiftlyTests.$ctx.withValue(ctx) { + try await f() } - - // create an empty config file and toolchains directory for the test - let c = try await self.baseTestConfig() - try c.save() - - try await f() } /// Validates that the provided toolchain is the one currently marked as "in use", both by checking the /// configuration file and by executing `swift --version` using the swift executable in the `bin` directory. /// If nil is provided, this validates that no toolchain is currently in use. - func validateInUse(expected: ToolchainVersion?) async throws { - let config = try Config.load() - XCTAssertEqual(config.inUse, expected) + static func validateInUse(expected: ToolchainVersion?) async throws { + let config = try Config.load(Self.ctx) + #expect(config.inUse == expected) } /// Validate that all of the provided toolchains have been installed. /// /// This method ensures that config.json reflects the expected installed toolchains and also /// validates that the toolchains on disk match their expected versions via `swift --version`. - func validateInstalledToolchains(_ toolchains: Set, description: String) async throws { - let config = try Config.load() + static func validateInstalledToolchains(_ toolchains: Set, description: String) async throws { + let config = try Config.load(Self.ctx) guard config.installedToolchains == toolchains else { throw SwiftlyTestError(message: "\(description): expected \(toolchains) but got \(config.installedToolchains)") } -#if os(Linux) +#if os(macOS) + for toolchain in toolchains { + let toolchainDir = Self.ctx.mockedHomeDir!.appendingPathComponent("Toolchains/\(toolchain.identifier).xctoolchain") + #expect(toolchainDir.fileExists()) + + let swiftBinary = toolchainDir + .appendingPathComponent("usr") + .appendingPathComponent("bin") + .appendingPathComponent("swift") + + let executable = SwiftExecutable(path: swiftBinary) + let actualVersion = try await executable.version() + #expect(actualVersion == toolchain) + } +#elseif os(Linux) // Verify that the toolchains on disk correspond to those in the config. for toolchain in toolchains { - let toolchainDir = Swiftly.currentPlatform.swiftlyHomeDir - .appendingPathComponent("toolchains") - .appendingPathComponent(toolchain.name) - XCTAssertTrue(toolchainDir.fileExists()) + let toolchainDir = Swiftly.currentPlatform.swiftlyHomeDir(Self.ctx) + .appendingPathComponent("toolchains/\(toolchain.name)") + #expect(toolchainDir.fileExists()) let swiftBinary = toolchainDir .appendingPathComponent("usr") @@ -274,7 +273,7 @@ public class SwiftlyTests: XCTestCase { let executable = SwiftExecutable(path: swiftBinary) let actualVersion = try await executable.version() - XCTAssertEqual(actualVersion, toolchain) + #expect(actualVersion == toolchain) } #endif } @@ -283,11 +282,9 @@ public class SwiftlyTests: XCTestCase { /// in its bin directory. /// /// When executed, the mocked executables will simply print the toolchain version and return. - func installMockedToolchain(selector: String, args: [String] = [], executables: [String]? = nil) async throws { - var install = try self.parseCommand(Install.self, ["install", "\(selector)", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] + args) - - try await self.withMockedToolchain(executables: executables) { - try await install.run() + static func installMockedToolchain(selector: String, args: [String] = [], executables: [String]? = nil) async throws { + try await Self.withMockedToolchain(executables: executables) { + try await Self.runCommand(Install.self, ["install", "\(selector)", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] + args) } } @@ -295,20 +292,20 @@ public class SwiftlyTests: XCTestCase { /// in its bin directory. /// /// When executed, the mocked executables will simply print the toolchain version and return. - func installMockedToolchain(toolchain: ToolchainVersion, executables: [String]? = nil) async throws { - try await self.installMockedToolchain(selector: "\(toolchain.name)", executables: executables) + static func installMockedToolchain(toolchain: ToolchainVersion, executables: [String]? = nil) async throws { + try await Self.installMockedToolchain(selector: "\(toolchain.name)", executables: executables) } /// Install a mocked toolchain associated with the given version that includes the provided list of executables /// in its bin directory. /// /// When executed, the mocked executables will simply print the toolchain version and return. - func installMockedToolchain(selector: ToolchainSelector, executables: [String]? = nil) async throws { - try await self.installMockedToolchain(selector: "\(selector)", executables: executables) + static func installMockedToolchain(selector: ToolchainSelector, executables: [String]? = nil) async throws { + try await Self.installMockedToolchain(selector: "\(selector)", executables: executables) } /// Get the toolchain version of a mocked executable installed via `installMockedToolchain` at the given URL. - func getMockedToolchainVersion(at url: URL) throws -> ToolchainVersion { + static func getMockedToolchainVersion(at url: URL) throws -> ToolchainVersion { let process = Process() process.executableURL = url @@ -325,20 +322,6 @@ public class SwiftlyTests: XCTestCase { let toolchainVersion = String(decoding: outputData, as: UTF8.self).trimmingCharacters(in: .newlines) return try ToolchainVersion(parsing: toolchainVersion) } - - func snapshotsAvailable() async throws -> Bool { - let pd = try await Swiftly.currentPlatform.detectPlatform(disableConfirmation: true, platform: nil) - - // Snapshots are currently unavailable for these platforms on swift.org - // TODO: remove these once snapshots are available for them - let snapshotsUnavailable: [PlatformDefinition] = [ - .ubuntu2404, - .fedora39, - .debian12, - ] - - return !snapshotsUnavailable.contains(pd) - } } public class TestOutputHandler: SwiftlyCore.OutputHandler { @@ -371,28 +354,6 @@ public class TestInputProvider: SwiftlyCore.InputProvider { } } -extension SwiftlyCommand { - /// Run this command, using the provided input as the stdin (in lines). Returns an array of captured - /// output lines. - mutating func runWithMockedIO(quiet: Bool = false, input: [String]? = nil) async throws -> [String] { - let handler = TestOutputHandler(quiet: quiet) - SwiftlyCore.outputHandler = handler - defer { - SwiftlyCore.outputHandler = nil - } - - if let input { - SwiftlyCore.inputProvider = TestInputProvider(lines: input) - } - defer { - SwiftlyCore.inputProvider = nil - } - - try await self.run() - return handler.lines - } -} - /// Wrapper around a `swift` executable used to execute swift commands. public struct SwiftExecutable { public let path: URL @@ -542,7 +503,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { } public func getReleaseToolchains() async throws -> [Components.Schemas.Release] { - let currentPlatform = try await Swiftly.currentPlatform.detectPlatform(disableConfirmation: true, platform: nil) + let currentPlatform = try await Swiftly.currentPlatform.detectPlatform(SwiftlyTests.ctx, disableConfirmation: true, platform: nil) let platformName = switch currentPlatform { case PlatformDefinition.ubuntu2004: @@ -586,7 +547,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { } public func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform _: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { - let currentPlatform = try await Swiftly.currentPlatform.detectPlatform(disableConfirmation: true, platform: nil) + let currentPlatform = try await Swiftly.currentPlatform.detectPlatform(SwiftlyTests.ctx, disableConfirmation: true, platform: nil) let releasesForBranch = self.snapshotToolchains.filter { snapshotVersion in switch snapshotVersion.branch { @@ -721,8 +682,10 @@ public class MockToolchainDownloader: HTTPRequestExecutor { let importKey = Process() importKey.executableURL = URL(fileURLWithPath: "/usr/bin/env") importKey.arguments = ["bash", "-c", """ - mkdir -p $HOME/.gnupg - touch $HOME/.gnupg/gpg.conf + export GNUPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" + export GPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" + mkdir -p "$GNUPG_HOME"/.gnupg + touch "$GNUPS_HOME"/.gnupg/gpg.conf gpg --batch --import \(gpgKeyFile.path) >/dev/null 2>&1 || echo -n """] try importKey.run() @@ -735,6 +698,8 @@ public class MockToolchainDownloader: HTTPRequestExecutor { detachSign.executableURL = URL(fileURLWithPath: "/usr/bin/env") detachSign.arguments = ["bash", "-c", """ export GPG_TTY=$(tty) + export GNUPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" + export GPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" gpg --version | grep '2.0.' > /dev/null if [ "$?" == "0" ]; then gpg --default-key "A2A645E5249D25845C43954E7D210032D2F670B7" --detach-sign "\(archive.path)" @@ -808,8 +773,10 @@ public class MockToolchainDownloader: HTTPRequestExecutor { let importKey = Process() importKey.executableURL = URL(fileURLWithPath: "/usr/bin/env") importKey.arguments = ["bash", "-c", """ - mkdir -p $HOME/.gnupg - touch $HOME/.gnupg/gpg.conf + export GNUPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" + export GPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" + mkdir -p "$GNUPG_HOME"/.gnupg + touch "$GNUPG_HOME"/.gnupg/gpg.conf gpg --batch --import \(gpgKeyFile.path) >/dev/null 2>&1 || echo -n """] try importKey.run() @@ -822,6 +789,8 @@ public class MockToolchainDownloader: HTTPRequestExecutor { detachSign.executableURL = URL(fileURLWithPath: "/usr/bin/env") detachSign.arguments = ["bash", "-c", """ export GPG_TTY=$(tty) + export GNUPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" + export GPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" gpg --version | grep '2.0.' > /dev/null if [ "$?" == "0" ]; then gpg --default-key "A2A645E5249D25845C43954E7D210032D2F670B7" --detach-sign "\(archive.path)" diff --git a/Tests/SwiftlyTests/ToolchainSelectorTests.swift b/Tests/SwiftlyTests/ToolchainSelectorTests.swift index 98eb3c4f..6388d188 100644 --- a/Tests/SwiftlyTests/ToolchainSelectorTests.swift +++ b/Tests/SwiftlyTests/ToolchainSelectorTests.swift @@ -1,25 +1,25 @@ import Foundation import SwiftlyCore -import XCTest +import Testing -final class ToolchainSelectorTests: SwiftlyTests { +@Suite struct ToolchainSelectorTests { func runTest(_ expected: ToolchainSelector, _ parses: [String]) throws { for string in parses { - XCTAssertEqual(try ToolchainSelector(parsing: string), expected) + #expect(try ToolchainSelector(parsing: string) == expected) } } - func testParseLatest() throws { + @Test func parseLatest() throws { try self.runTest(.latest, ["latest"]) } - func testParseRelease() throws { + @Test func parseRelease() throws { try self.runTest(.stable(major: 5, minor: 7, patch: 3), ["5.7.3"]) try self.runTest(.stable(major: 5, minor: 7, patch: nil), ["5.7"]) try self.runTest(.stable(major: 5, minor: nil, patch: nil), ["5"]) } - func testParseMainSnapshot() throws { + @Test func parseMainSnapshot() throws { let parses = [ "main-snapshot", "main-SNAPSHOT", @@ -28,7 +28,7 @@ final class ToolchainSelectorTests: SwiftlyTests { try runTest(.snapshot(branch: .main, date: nil), parses) } - func testParseMainSnapshotWithDate() throws { + @Test func parseMainSnapshotWithDate() throws { let parses = [ "main-snapshot-2023-06-05", "main-SNAPSHOT-2023-06-05", @@ -39,7 +39,7 @@ final class ToolchainSelectorTests: SwiftlyTests { try runTest(.snapshot(branch: .main, date: "2023-06-05"), parses) } - func testParseReleaseSnapshot() throws { + @Test func parseReleaseSnapshot() throws { let parses = [ "5.7-snapshot", "5.7-SNAPSHOT", @@ -51,7 +51,7 @@ final class ToolchainSelectorTests: SwiftlyTests { try runTest(.snapshot(branch: .release(major: 5, minor: 7), date: nil), parses) } - func testParseReleaseSnapshotWithDate() throws { + @Test func parseReleaseSnapshotWithDate() throws { let parses = [ "5.7-snapshot-2023-06-05", "5.7-SNAPSHOT-2023-06-05", diff --git a/Tests/SwiftlyTests/UninstallTests.swift b/Tests/SwiftlyTests/UninstallTests.swift index 2162e0dd..37841f58 100644 --- a/Tests/SwiftlyTests/UninstallTests.swift +++ b/Tests/SwiftlyTests/UninstallTests.swift @@ -1,18 +1,17 @@ import Foundation @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class UninstallTests: SwiftlyTests { +@Suite struct UninstallTests { static let homeName = "uninstallTests" /// Tests that `swiftly uninstall` successfully handles being invoked when no toolchains have been installed yet. - func testUninstallNoInstalledToolchains() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: []) { - var uninstall = try self.parseCommand(Uninstall.self, ["uninstall", "1.2.3"]) - _ = try await uninstall.runWithMockedIO(input: ["y"]) + @Test func uninstallNoInstalledToolchains() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: []) { + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", "1.2.3"], input: ["y"]) - try await self.validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [], description: "remove not-installed toolchain" ) @@ -20,48 +19,44 @@ final class UninstallTests: SwiftlyTests { } /// Tests that `swiftly uninstall latest` successfully uninstalls the latest stable release of Swift. - func testUninstallLatest() async throws { - let toolchains = Self.allToolchains.filter { $0.asStableRelease != nil } - try await self.withMockedHome(homeName: Self.homeName, toolchains: toolchains) { + @Test func uninstallLatest() async throws { + let toolchains = SwiftlyTests.allToolchains.filter { $0.asStableRelease != nil } + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains) { var installed = toolchains for i in 0.. ) async throws { - var uninstall = try self.parseCommand(Uninstall.self, ["uninstall", argument]) - let output = try await uninstall.runWithMockedIO(input: ["y"]) + let output = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", argument], input: ["y"]) installed.subtract(uninstalled) - try await self.validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( installed, description: "uninstall \(argument)" ) @@ -127,10 +119,10 @@ final class UninstallTests: SwiftlyTests { let outputToolchains = output.compactMap { try? ToolchainVersion(parsing: $0.trimmingCharacters(in: .whitespaces)) } - XCTAssertEqual(Set(outputToolchains), uninstalled) + #expect(Set(outputToolchains) == uninstalled) } - try await self.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { var installed = toolchains let mainSnapshots = installed.filter { toolchain in @@ -172,15 +164,15 @@ final class UninstallTests: SwiftlyTests { } /// Tests that uninstalling the toolchain that is currently "in use" has the expected behavior. - func testUninstallInUse() async throws { + @Test func uninstallInUse() async throws { let toolchains: Set = [ - Self.oldStable, - Self.oldStableNewPatch, - Self.newStable, - Self.oldMainSnapshot, - Self.newMainSnapshot, - Self.oldReleaseSnapshot, - Self.newReleaseSnapshot, + SwiftlyTests.oldStable, + SwiftlyTests.oldStableNewPatch, + SwiftlyTests.newStable, + SwiftlyTests.oldMainSnapshot, + SwiftlyTests.newMainSnapshot, + SwiftlyTests.oldReleaseSnapshot, + SwiftlyTests.newReleaseSnapshot, ] func uninstallInUseTest( @@ -188,109 +180,102 @@ final class UninstallTests: SwiftlyTests { toRemove: ToolchainVersion, expectedInUse: ToolchainVersion? ) async throws { - var uninstall = try self.parseCommand(Uninstall.self, ["uninstall", toRemove.name]) - let output = try await uninstall.runWithMockedIO(input: ["y"]) + let output = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", toRemove.name], input: ["y"]) installed.remove(toRemove) - try await self.validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( installed, description: "remove \(toRemove)" ) // Ensure the latest installed toolchain was used when the in-use one was uninstalled. - try await self.validateInUse(expected: expectedInUse) + try await SwiftlyTests.validateInUse(expected: expectedInUse) if let expectedInUse { // Ensure that something was printed indicating the latest toolchain was marked in use. - XCTAssert( + #expect( output.contains(where: { $0.contains(String(describing: expectedInUse)) }), "output did not contain \(expectedInUse)" ) } } - try await self.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: Self.oldStable) { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: SwiftlyTests.oldStable) { var installed = toolchains - try await uninstallInUseTest(&installed, toRemove: Self.oldStable, expectedInUse: Self.oldStableNewPatch) - try await uninstallInUseTest(&installed, toRemove: Self.oldStableNewPatch, expectedInUse: Self.newStable) - try await uninstallInUseTest(&installed, toRemove: Self.newStable, expectedInUse: Self.newMainSnapshot) + try await uninstallInUseTest(&installed, toRemove: SwiftlyTests.oldStable, expectedInUse: SwiftlyTests.oldStableNewPatch) + try await uninstallInUseTest(&installed, toRemove: SwiftlyTests.oldStableNewPatch, expectedInUse: SwiftlyTests.newStable) + try await uninstallInUseTest(&installed, toRemove: SwiftlyTests.newStable, expectedInUse: SwiftlyTests.newMainSnapshot) // Switch to the old main snapshot to ensure uninstalling it selects the new one. - var use = try self.parseCommand(Use.self, ["use", Self.oldMainSnapshot.name]) - try await use.run() - try await uninstallInUseTest(&installed, toRemove: Self.oldMainSnapshot, expectedInUse: Self.newMainSnapshot) + try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.oldMainSnapshot.name]) + try await uninstallInUseTest(&installed, toRemove: SwiftlyTests.oldMainSnapshot, expectedInUse: SwiftlyTests.newMainSnapshot) try await uninstallInUseTest( &installed, - toRemove: Self.newMainSnapshot, - expectedInUse: Self.newReleaseSnapshot + toRemove: SwiftlyTests.newMainSnapshot, + expectedInUse: SwiftlyTests.newReleaseSnapshot ) // Switch to the old release snapshot to ensure uninstalling it selects the new one. - use = try self.parseCommand(Use.self, ["use", Self.oldReleaseSnapshot.name]) - try await use.run() + try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.oldReleaseSnapshot.name]) try await uninstallInUseTest( &installed, - toRemove: Self.oldReleaseSnapshot, - expectedInUse: Self.newReleaseSnapshot + toRemove: SwiftlyTests.oldReleaseSnapshot, + expectedInUse: SwiftlyTests.newReleaseSnapshot ) try await uninstallInUseTest( &installed, - toRemove: Self.newReleaseSnapshot, + toRemove: SwiftlyTests.newReleaseSnapshot, expectedInUse: nil ) } } /// Tests that uninstalling the last toolchain is handled properly and cleans up any symlinks. - func testUninstallLastToolchain() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: [Self.oldStable], inUse: Self.oldStable) { - var uninstall = try self.parseCommand(Uninstall.self, ["uninstall", Self.oldStable.name]) - _ = try await uninstall.runWithMockedIO(input: ["y"]) - let config = try Config.load() - XCTAssertEqual(config.inUse, nil) + @Test func uninstallLastToolchain() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: [SwiftlyTests.oldStable], inUse: SwiftlyTests.oldStable) { + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", SwiftlyTests.oldStable.name], input: ["y"]) + let config = try Config.load(SwiftlyTests.ctx) + #expect(config.inUse == nil) // Ensure all symlinks have been cleaned up. let symlinks = try FileManager.default.contentsOfDirectory( - atPath: Swiftly.currentPlatform.swiftlyBinDir.path + atPath: Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx).path ) - XCTAssertEqual(symlinks, []) + #expect(symlinks == []) } } /// Tests that aborting an uninstall works correctly. - func testUninstallAbort() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains, inUse: Self.oldStable) { - let preConfig = try Config.load() - var uninstall = try self.parseCommand(Uninstall.self, ["uninstall", Self.oldStable.name]) - _ = try await uninstall.runWithMockedIO(input: ["n"]) - try await self.validateInstalledToolchains( - Self.allToolchains, + @Test func uninstallAbort() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains, inUse: SwiftlyTests.oldStable) { + let preConfig = try Config.load(SwiftlyTests.ctx) + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", SwiftlyTests.oldStable.name], input: ["n"]) + try await SwiftlyTests.validateInstalledToolchains( + SwiftlyTests.allToolchains, description: "abort uninstall" ) // Ensure config did not change. - XCTAssertEqual(try Config.load(), preConfig) + #expect(try Config.load(SwiftlyTests.ctx) == preConfig) } } /// Tests that providing the `-y` argument skips the confirmation prompt. - func testUninstallAssumeYes() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: [Self.oldStable, Self.newStable]) { - var uninstall = try self.parseCommand(Uninstall.self, ["uninstall", "-y", Self.oldStable.name]) - _ = try await uninstall.run() - try await self.validateInstalledToolchains( - [Self.newStable], + @Test func uninstallAssumeYes() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: [SwiftlyTests.oldStable, SwiftlyTests.newStable]) { + try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", SwiftlyTests.oldStable.name]) + try await SwiftlyTests.validateInstalledToolchains( + [SwiftlyTests.newStable], description: "uninstall did not succeed even with -y provided" ) } } /// Tests that providing "all" as an argument to uninstall will uninstall all toolchains. - func testUninstallAll() async throws { - let toolchains = Set([Self.oldStable, Self.newStable, Self.newMainSnapshot, Self.oldReleaseSnapshot]) - try await self.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: Self.newMainSnapshot) { - var uninstall = try self.parseCommand(Uninstall.self, ["uninstall", "-y", "all"]) - _ = try await uninstall.run() - try await self.validateInstalledToolchains( + @Test func uninstallAll() async throws { + let toolchains = Set([SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.newMainSnapshot, SwiftlyTests.oldReleaseSnapshot]) + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: SwiftlyTests.newMainSnapshot) { + try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", "all"]) + try await SwiftlyTests.validateInstalledToolchains( [], description: "uninstall did not uninstall all toolchains" ) @@ -298,18 +283,17 @@ final class UninstallTests: SwiftlyTests { } /// Tests that uninstalling a toolchain that is the global default, but is not in the list of installed toolchains. - func testUninstallNotInstalled() async throws { - let toolchains = Set([Self.oldStable, Self.newStable, Self.newMainSnapshot, Self.oldReleaseSnapshot]) - try await self.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: Self.newMainSnapshot) { - var config = try Config.load() - config.inUse = Self.newMainSnapshot - config.installedToolchains.remove(Self.newMainSnapshot) - try config.save() - - var uninstall = try self.parseCommand(Uninstall.self, ["uninstall", "-y", Self.newMainSnapshot.name]) - _ = try await uninstall.run() - try await self.validateInstalledToolchains( - [Self.oldStable, Self.newStable, Self.oldReleaseSnapshot], + @Test func uninstallNotInstalled() async throws { + let toolchains = Set([SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.newMainSnapshot, SwiftlyTests.oldReleaseSnapshot]) + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: SwiftlyTests.newMainSnapshot) { + var config = try Config.load(SwiftlyTests.ctx) + config.inUse = SwiftlyTests.newMainSnapshot + config.installedToolchains.remove(SwiftlyTests.newMainSnapshot) + try config.save(SwiftlyTests.ctx) + + try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", SwiftlyTests.newMainSnapshot.name]) + try await SwiftlyTests.validateInstalledToolchains( + [SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.oldReleaseSnapshot], description: "uninstall did not uninstall all toolchains" ) } diff --git a/Tests/SwiftlyTests/UpdateTests.swift b/Tests/SwiftlyTests/UpdateTests.swift index 4da2c9ec..c665890b 100644 --- a/Tests/SwiftlyTests/UpdateTests.swift +++ b/Tests/SwiftlyTests/UpdateTests.swift @@ -1,22 +1,21 @@ import Foundation @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class UpdateTests: SwiftlyTests { +@Suite struct UpdateTests { /// Verify updating the most up-to-date toolchain has no effect. - func testUpdateLatest() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: .latest) + @Test func updateLatest() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(selector: .latest) - let beforeUpdateConfig = try Config.load() + let beforeUpdateConfig = try Config.load(SwiftlyTests.ctx) - var update = try self.parseCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await update.run() + try await SwiftlyTests.runCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - XCTAssertEqual(try Config.load(), beforeUpdateConfig) - try await validateInstalledToolchains( + #expect(try Config.load(SwiftlyTests.ctx) == beforeUpdateConfig) + try await SwiftlyTests.validateInstalledToolchains( beforeUpdateConfig.installedToolchains, description: "Updating latest toolchain should have no effect" ) @@ -25,13 +24,12 @@ final class UpdateTests: SwiftlyTests { } /// Verify that attempting to update when no toolchains are installed has no effect. - func testUpdateLatestWithNoToolchains() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - var update = try self.parseCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await update.run() + @Test func updateLatestWithNoToolchains() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.runCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [], description: "Updating should not install any toolchains" ) @@ -40,18 +38,17 @@ final class UpdateTests: SwiftlyTests { } /// Verify that updating the latest installed toolchain updates it to the latest available toolchain. - func testUpdateLatestToLatest() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) - var update = try self.parseCommand(Update.self, ["update", "-y", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await update.run() - - let config = try Config.load() + @Test func updateLatestToLatest() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + let config = try Config.load(SwiftlyTests.ctx) let inUse = config.inUse!.asStableRelease! - XCTAssertGreaterThan(inUse, .init(major: 5, minor: 9, patch: 0)) - try await validateInstalledToolchains( + #expect(inUse > .init(major: 5, minor: 9, patch: 0)) + try await SwiftlyTests.validateInstalledToolchains( [config.inUse!], description: "Updating toolchain should properly install new toolchain and uninstall old" ) @@ -61,20 +58,19 @@ final class UpdateTests: SwiftlyTests { /// Verify that the latest installed toolchain for a given major version can be updated to the latest /// released minor version. - func testUpdateToLatestMinor() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) - var update = try self.parseCommand(Update.self, ["update", "-y", "5", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await update.run() - - let config = try Config.load() + @Test func updateToLatestMinor() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + let config = try Config.load(SwiftlyTests.ctx) let inUse = config.inUse!.asStableRelease! - XCTAssertEqual(inUse.major, 5) - XCTAssertGreaterThan(inUse.minor, 0) + #expect(inUse.major == 5) + #expect(inUse.minor > 0) - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [config.inUse!], description: "Updating toolchain should properly install new toolchain and uninstall old" ) @@ -83,25 +79,21 @@ final class UpdateTests: SwiftlyTests { } /// Verify that a toolchain can be updated to the latest patch version of that toolchain's minor version. - func testUpdateToLatestPatch() async throws { - let snapshotsAvailable = try await self.snapshotsAvailable() - try XCTSkipIf(!snapshotsAvailable) - - try await self.withTestHome { - try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: "5.9.0") + @Test func updateToLatestPatch() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(selector: "5.9.0") - var update = try self.parseCommand(Update.self, ["update", "-y", "5.9.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await update.run() + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5.9.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load(SwiftlyTests.ctx) let inUse = config.inUse!.asStableRelease! - XCTAssertEqual(inUse.major, 5) - XCTAssertEqual(inUse.minor, 9) - XCTAssertGreaterThan(inUse.patch, 0) + #expect(inUse.major == 5) + #expect(inUse.minor == 9) + #expect(inUse.patch > 0) - try await validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [config.inUse!], description: "Updating toolchain should properly install new toolchain and uninstall old" ) @@ -111,87 +103,81 @@ final class UpdateTests: SwiftlyTests { /// Verifies that updating the currently global default toolchain can be updated, and that after update the new toolchain /// will be the global default instead. - func testUpdateGlobalDefault() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: "6.0.0") + @Test func updateGlobalDefault() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") - var update = try self.parseCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await update.run() + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load(SwiftlyTests.ctx) let inUse = config.inUse!.asStableRelease! - XCTAssertGreaterThan(inUse, .init(major: 6, minor: 0, patch: 0)) - XCTAssertEqual(inUse.major, 6) - XCTAssertEqual(inUse.minor, 0) - XCTAssertGreaterThan(inUse.patch, 0) + #expect(inUse > .init(major: 6, minor: 0, patch: 0)) + #expect(inUse.major == 6) + #expect(inUse.minor == 0) + #expect(inUse.patch > 0) - try await self.validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [config.inUse!], description: "update should update the in use toolchain to latest patch" ) - try await self.validateInUse(expected: config.inUse!) + try await SwiftlyTests.validateInUse(expected: config.inUse!) } } } /// Verifies that updating the currently in-use toolchain can be updated, and that after update the new toolchain /// will be in-use with the swift version file updated. - func testUpdateInUse() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: "6.0.0") + @Test func updateInUse() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") - let versionFile = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(".swift-version") + let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") try "6.0.0".write(to: versionFile, atomically: true, encoding: .utf8) - var update = try self.parseCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await update.run() + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) let versionFileContents = try String(contentsOf: versionFile, encoding: .utf8) let inUse = try ToolchainVersion(parsing: versionFileContents) - XCTAssertGreaterThan(inUse, .init(major: 6, minor: 0, patch: 0)) + #expect(inUse > .init(major: 6, minor: 0, patch: 0)) // Since the global default was set to 6.0.0, and that toolchain is no longer installed // the update should have unset it to prevent the config from going into a bad state. - let config = try Config.load() - XCTAssertTrue(config.inUse == nil) + let config = try Config.load(SwiftlyTests.ctx) + #expect(config.inUse == nil) // The new toolchain should be installed - XCTAssertTrue(config.installedToolchains.contains(inUse)) + #expect(config.installedToolchains.contains(inUse)) } } } /// Verifies that snapshots, both from the main branch and from development branches, can be updated. - func testUpdateSnapshot() async throws { - let snapshotsAvailable = try await self.snapshotsAvailable() - try XCTSkipIf(!snapshotsAvailable) - + @Test func updateSnapshot() async throws { let branches: [ToolchainVersion.Snapshot.Branch] = [ .main, .release(major: 6, minor: 0), ] for branch in branches { - try await self.withTestHome { - try await self.withMockedToolchain { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { let date = branch == .main ? SwiftlyTests.oldMainSnapshot.asSnapshot!.date : SwiftlyTests.oldReleaseSnapshot.asSnapshot!.date - try await self.installMockedToolchain(selector: .snapshot(branch: branch, date: date)) + try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: date)) - var update = try self.parseCommand( + try await SwiftlyTests.runCommand( Update.self, ["update", "-y", "\(branch.name)-snapshot", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] ) - try await update.run() - let config = try Config.load() + let config = try Config.load(SwiftlyTests.ctx) let inUse = config.inUse!.asSnapshot! - XCTAssertGreaterThan(inUse, .init(branch: branch, date: date)) - XCTAssertEqual(inUse.branch, branch) - XCTAssertGreaterThan(inUse.date, date) + #expect(inUse > .init(branch: branch, date: date)) + #expect(inUse.branch == branch) + #expect(inUse.date > date) - try await self.validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [config.inUse!], description: "update should work with snapshots" ) @@ -201,22 +187,21 @@ final class UpdateTests: SwiftlyTests { } /// Verify that the latest of all the matching release toolchains is updated. - func testUpdateSelectsLatestMatchingStableRelease() async throws { - try await self.withTestHome { - try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: "6.0.1") - try await self.installMockedToolchain(selector: "6.0.0") + @Test func updateSelectsLatestMatchingStableRelease() async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(selector: "6.0.1") + try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") - var update = try self.parseCommand(Update.self, ["update", "-y", "6.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await update.run() + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "6.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load(SwiftlyTests.ctx) let inUse = config.inUse!.asStableRelease! - XCTAssertEqual(inUse.major, 6) - XCTAssertEqual(inUse.minor, 0) - XCTAssertGreaterThan(inUse.patch, 1) + #expect(inUse.major == 6) + #expect(inUse.minor == 0) + #expect(inUse.patch > 1) - try await self.validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [config.inUse!, .init(major: 6, minor: 0, patch: 0)], description: "update with ambiguous selector should update the latest matching toolchain" ) @@ -225,33 +210,29 @@ final class UpdateTests: SwiftlyTests { } /// Verify that the latest of all the matching snapshot toolchains is updated. - func testUpdateSelectsLatestMatchingSnapshotRelease() async throws { - let snapshotsAvailable = try await self.snapshotsAvailable() - try XCTSkipIf(!snapshotsAvailable) - + @Test func updateSelectsLatestMatchingSnapshotRelease() async throws { let branches: [ToolchainVersion.Snapshot.Branch] = [ .main, .release(major: 6, minor: 0), ] for branch in branches { - try await self.withTestHome { - try await self.withMockedToolchain { - try await self.installMockedToolchain(selector: .snapshot(branch: branch, date: "2024-06-19")) - try await self.installMockedToolchain(selector: .snapshot(branch: branch, date: "2024-06-18")) + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: "2024-06-19")) + try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: "2024-06-18")) - var update = try self.parseCommand( + try await SwiftlyTests.runCommand( Update.self, ["update", "-y", "\(branch.name)-snapshot", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] ) - try await update.run() - let config = try Config.load() + let config = try Config.load(SwiftlyTests.ctx) let inUse = config.inUse!.asSnapshot! - XCTAssertEqual(inUse.branch, branch) - XCTAssertGreaterThan(inUse.date, "2024-06-18") + #expect(inUse.branch == branch) + #expect(inUse.date > "2024-06-18") - try await self.validateInstalledToolchains( + try await SwiftlyTests.validateInstalledToolchains( [config.inUse!, .init(snapshotBranch: branch, date: "2024-06-18")], description: "update with ambiguous selector should update the latest matching toolchain" ) diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index 604af79a..e182ca0a 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -1,203 +1,200 @@ import Foundation @testable import Swiftly @testable import SwiftlyCore -import XCTest +import Testing -final class UseTests: SwiftlyTests { +@Suite struct UseTests { static let homeName = "useTests" /// Execute a `use` command with the provided argument. Then validate that the configuration is updated properly and /// the in-use swift executable prints the the provided expectedVersion. func useAndValidate(argument: String, expectedVersion: ToolchainVersion) async throws { - var use = try self.parseCommand(Use.self, ["use", "-g", argument]) - try await use.run() + try await SwiftlyTests.runCommand(Use.self, ["use", "-g", argument]) - XCTAssertEqual(try Config.load().inUse, expectedVersion) + #expect(try Config.load(SwiftlyTests.ctx).inUse == expectedVersion) } /// Tests that the `use` command can switch between installed stable release toolchains. - func testUseStable() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { - try await self.useAndValidate(argument: Self.oldStable.name, expectedVersion: Self.oldStable) - try await self.useAndValidate(argument: Self.newStable.name, expectedVersion: Self.newStable) - try await self.useAndValidate(argument: Self.newStable.name, expectedVersion: Self.newStable) + @Test func useStable() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) } } /// Tests that that "latest" can be provided to the `use` command to select the installed stable release /// toolchain with the most recent version. - func testUseLatestStable() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { + @Test func useLatestStable() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { // Use an older toolchain. - try await self.useAndValidate(argument: Self.oldStable.name, expectedVersion: Self.oldStable) + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) // Use latest, assert that it switched to the latest installed stable release. - try await self.useAndValidate(argument: "latest", expectedVersion: Self.newStable) + try await self.useAndValidate(argument: "latest", expectedVersion: SwiftlyTests.newStable) // Try to use latest again, assert no error was thrown and no changes were made. - try await self.useAndValidate(argument: "latest", expectedVersion: Self.newStable) + try await self.useAndValidate(argument: "latest", expectedVersion: SwiftlyTests.newStable) // Explicitly specify the current latest toolchain, assert no errors and no changes were made. - try await self.useAndValidate(argument: Self.newStable.name, expectedVersion: Self.newStable) + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) // Switch back to the old toolchain, verify it works. - try await self.useAndValidate(argument: Self.oldStable.name, expectedVersion: Self.oldStable) + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.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`). - func testUseLatestStablePatch() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { - try await self.useAndValidate(argument: Self.oldStable.name, expectedVersion: Self.oldStable) + @Test func useLatestStablePatch() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) - let oldStableVersion = Self.oldStable.asStableRelease! + let oldStableVersion = SwiftlyTests.oldStable.asStableRelease! // Drop the patch version and assert that the latest patch of the provided major.minor was chosen. try await self.useAndValidate( argument: "\(oldStableVersion.major).\(oldStableVersion.minor)", - expectedVersion: Self.oldStableNewPatch + expectedVersion: SwiftlyTests.oldStableNewPatch ) // Assert that selecting it again doesn't change anything. try await self.useAndValidate( argument: "\(oldStableVersion.major).\(oldStableVersion.minor)", - expectedVersion: Self.oldStableNewPatch + expectedVersion: SwiftlyTests.oldStableNewPatch ) // 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: Self.oldStable.name, expectedVersion: Self.oldStable) - let latestPatch = Self.oldStableNewPatch.asStableRelease!.patch + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) + let latestPatch = SwiftlyTests.oldStableNewPatch.asStableRelease!.patch try await self.useAndValidate( argument: "\(oldStableVersion.major).\(oldStableVersion.minor).\(latestPatch + 1)", - expectedVersion: Self.oldStable + expectedVersion: SwiftlyTests.oldStable ) } } /// Tests that the `use` command can switch between installed main snapshot toolchains. - func testUseMainSnapshot() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { + @Test func useMainSnapshot() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { // Switch to a non-snapshot. - try await self.useAndValidate(argument: Self.newStable.name, expectedVersion: Self.newStable) - try await self.useAndValidate(argument: Self.oldMainSnapshot.name, expectedVersion: Self.oldMainSnapshot) - try await self.useAndValidate(argument: Self.newMainSnapshot.name, expectedVersion: Self.newMainSnapshot) + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) + try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) + try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) // Verify that using the same snapshot again doesn't throw an error. - try await self.useAndValidate(argument: Self.newMainSnapshot.name, expectedVersion: Self.newMainSnapshot) - try await self.useAndValidate(argument: Self.oldMainSnapshot.name, expectedVersion: Self.oldMainSnapshot) + try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) + try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) } } /// Tests that the latest installed main snapshot toolchain can be selected by omitting the /// date (e.g. `use main-snapshot`). - func testUseLatestMainSnapshot() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { + @Test func useLatestMainSnapshot() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { // Switch to a non-snapshot. - try await self.useAndValidate(argument: Self.newStable.name, expectedVersion: Self.newStable) + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) // Switch to the latest main snapshot. - try await self.useAndValidate(argument: "main-snapshot", expectedVersion: Self.newMainSnapshot) + try await self.useAndValidate(argument: "main-snapshot", expectedVersion: SwiftlyTests.newMainSnapshot) // Switch to it again, assert no errors or changes were made. - try await self.useAndValidate(argument: "main-snapshot", expectedVersion: Self.newMainSnapshot) + try await self.useAndValidate(argument: "main-snapshot", expectedVersion: SwiftlyTests.newMainSnapshot) // Switch to it again, this time by name. Assert no errors or changes were made. - try await self.useAndValidate(argument: Self.newMainSnapshot.name, expectedVersion: Self.newMainSnapshot) + try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) // Switch to an older snapshot, verify it works. - try await self.useAndValidate(argument: Self.oldMainSnapshot.name, expectedVersion: Self.oldMainSnapshot) + try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) } } /// Tests that the `use` command can switch between installed release snapshot toolchains. - func testUseReleaseSnapshot() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { + @Test func useReleaseSnapshot() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { // Switch to a non-snapshot. - try await self.useAndValidate(argument: Self.newStable.name, expectedVersion: Self.newStable) + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) try await self.useAndValidate( - argument: Self.oldReleaseSnapshot.name, - expectedVersion: Self.oldReleaseSnapshot + argument: SwiftlyTests.oldReleaseSnapshot.name, + expectedVersion: SwiftlyTests.oldReleaseSnapshot ) try await self.useAndValidate( - argument: Self.newReleaseSnapshot.name, - expectedVersion: Self.newReleaseSnapshot + argument: SwiftlyTests.newReleaseSnapshot.name, + expectedVersion: SwiftlyTests.newReleaseSnapshot ) // Verify that using the same snapshot again doesn't throw an error. try await self.useAndValidate( - argument: Self.newReleaseSnapshot.name, - expectedVersion: Self.newReleaseSnapshot + argument: SwiftlyTests.newReleaseSnapshot.name, + expectedVersion: SwiftlyTests.newReleaseSnapshot ) try await self.useAndValidate( - argument: Self.oldReleaseSnapshot.name, - expectedVersion: Self.oldReleaseSnapshot + argument: SwiftlyTests.oldReleaseSnapshot.name, + expectedVersion: SwiftlyTests.oldReleaseSnapshot ) } } /// Tests that the latest installed release snapshot toolchain can be selected by omitting the /// date (e.g. `use 5.7-snapshot`). - func testUseLatestReleaseSnapshot() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { + @Test func useLatestReleaseSnapshot() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { // Switch to a non-snapshot. - try await self.useAndValidate(argument: Self.newStable.name, expectedVersion: Self.newStable) + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) // Switch to the latest snapshot for the given release. - guard case let .release(major, minor) = Self.newReleaseSnapshot.asSnapshot!.branch else { + guard case let .release(major, minor) = SwiftlyTests.newReleaseSnapshot.asSnapshot!.branch else { fatalError("expected release in snapshot release version") } try await self.useAndValidate( argument: "\(major).\(minor)-snapshot", - expectedVersion: Self.newReleaseSnapshot + expectedVersion: SwiftlyTests.newReleaseSnapshot ) // Switch to it again, assert no errors or changes were made. try await self.useAndValidate( argument: "\(major).\(minor)-snapshot", - expectedVersion: Self.newReleaseSnapshot + expectedVersion: SwiftlyTests.newReleaseSnapshot ) // Switch to it again, this time by name. Assert no errors or changes were made. try await self.useAndValidate( - argument: Self.newReleaseSnapshot.name, - expectedVersion: Self.newReleaseSnapshot + argument: SwiftlyTests.newReleaseSnapshot.name, + expectedVersion: SwiftlyTests.newReleaseSnapshot ) // Switch to an older snapshot, verify it works. try await self.useAndValidate( - argument: Self.oldReleaseSnapshot.name, - expectedVersion: Self.oldReleaseSnapshot + argument: SwiftlyTests.oldReleaseSnapshot.name, + expectedVersion: SwiftlyTests.oldReleaseSnapshot ) } } /// Tests that the `use` command gracefully exits when executed before any toolchains have been installed. - func testUseNoInstalledToolchains() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: []) { - var use = try self.parseCommand(Use.self, ["use", "-g", "latest"]) - try await use.run() + @Test func useNoInstalledToolchains() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: []) { + try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "latest"]) - var config = try Config.load() - XCTAssertEqual(config.inUse, nil) + var config = try Config.load(SwiftlyTests.ctx) + #expect(config.inUse == nil) - use = try self.parseCommand(Use.self, ["use", "-g", "5.6.0"]) - try await use.run() + try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "5.6.0"]) - config = try Config.load() - XCTAssertEqual(config.inUse, nil) + config = try Config.load(SwiftlyTests.ctx) + #expect(config.inUse == nil) } } /// Tests that the `use` command gracefully handles being executed with toolchain names that haven't been installed. - func testUseNonExistent() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { + @Test func useNonExistent() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { // Switch to a valid toolchain. - try await self.useAndValidate(argument: Self.oldStable.name, expectedVersion: Self.oldStable) + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) // Try various non-existent toolchains. - try await self.useAndValidate(argument: "1.2.3", expectedVersion: Self.oldStable) - try await self.useAndValidate(argument: "5.7-snapshot-1996-01-01", expectedVersion: Self.oldStable) - try await self.useAndValidate(argument: "6.7-snapshot", expectedVersion: Self.oldStable) - try await self.useAndValidate(argument: "main-snapshot-1996-01-01", expectedVersion: Self.oldStable) + try await self.useAndValidate(argument: "1.2.3", expectedVersion: SwiftlyTests.oldStable) + try await self.useAndValidate(argument: "5.7-snapshot-1996-01-01", expectedVersion: SwiftlyTests.oldStable) + try await self.useAndValidate(argument: "6.7-snapshot", expectedVersion: SwiftlyTests.oldStable) + try await self.useAndValidate(argument: "main-snapshot-1996-01-01", expectedVersion: SwiftlyTests.oldStable) } } /// Tests that the `use` command works with all the installed toolchains in this test harness. - func testUseAll() async throws { - try await self.withMockedHome(homeName: Self.homeName, toolchains: Self.allToolchains) { - let config = try Config.load() + @Test func useAll() async throws { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { + let config = try Config.load(SwiftlyTests.ctx) for toolchain in config.installedToolchains { try await self.useAndValidate( @@ -209,80 +206,73 @@ final class UseTests: SwiftlyTests { } /// Tests that running a use command without an argument prints the currently in-use toolchain. - func testPrintInUse() async throws { + @Test func printInUse() async throws { let toolchains = [ - Self.newStable, - Self.newMainSnapshot, - Self.newReleaseSnapshot, + SwiftlyTests.newStable, + SwiftlyTests.newMainSnapshot, + SwiftlyTests.newReleaseSnapshot, ] - try await self.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { for toolchain in toolchains { - var use = try self.parseCommand(Use.self, ["use", "-g", toolchain.name]) - try await use.run() + try await SwiftlyTests.runCommand(Use.self, ["use", "-g", toolchain.name]) - var useEmpty = try self.parseCommand(Use.self, ["use", "-g"]) - var output = try await useEmpty.runWithMockedIO() + var output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", "-g"]) - XCTAssert(output.contains(where: { $0.contains(String(describing: toolchain)) })) + #expect(output.contains(where: { $0.contains(String(describing: toolchain)) })) - useEmpty = try self.parseCommand(Use.self, ["use", "-g", "--print-location"]) - output = try await useEmpty.runWithMockedIO() + output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", "-g", "--print-location"]) - XCTAssert(output.contains(where: { $0.contains(Swiftly.currentPlatform.findToolchainLocation(toolchain).path) })) + #expect(output.contains(where: { $0.contains(Swiftly.currentPlatform.findToolchainLocation(SwiftlyTests.ctx, toolchain).path) })) } } } /// Tests in-use toolchain selected by the .swift-version file. - func testSwiftVersionFile() async throws { + @Test func swiftVersionFile() async throws { let toolchains = [ - Self.newStable, - Self.newMainSnapshot, - Self.newReleaseSnapshot, + SwiftlyTests.newStable, + SwiftlyTests.newMainSnapshot, + SwiftlyTests.newReleaseSnapshot, ] - try await self.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { - let versionFile = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(".swift-version") + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { + let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") // GIVEN: a directory with a swift version file that selects a particular toolchain - try Self.newStable.name.write(to: versionFile, atomically: true, encoding: .utf8) + try SwiftlyTests.newStable.name.write(to: versionFile, atomically: true, encoding: .utf8) // WHEN: checking which toolchain is selected with the use command - var useCmd = try self.parseCommand(Use.self, ["use"]) - var output = try await useCmd.runWithMockedIO() + var output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use"]) // THEN: the output shows this toolchain is in use with this working directory - XCTAssert(output.contains(where: { $0.contains(Self.newStable.name) })) + #expect(output.contains(where: { $0.contains(SwiftlyTests.newStable.name) })) // GIVEN: a directory with a swift version file that selects a particular toolchain // WHEN: using another toolchain version - useCmd = try self.parseCommand(Use.self, ["use", Self.newMainSnapshot.name]) - output = try await useCmd.runWithMockedIO() + output = try await SwiftlyTests.runWithMockedIO(Use.self, ["use", SwiftlyTests.newMainSnapshot.name]) // THEN: the swift version file is updated to this toolchain version var versionFileContents = try String(contentsOf: versionFile, encoding: .utf8) - XCTAssertEqual(Self.newMainSnapshot.name, versionFileContents) + #expect(SwiftlyTests.newMainSnapshot.name == versionFileContents) // THEN: the use command reports this toolchain to be in use - XCTAssert(output.contains(where: { $0.contains(Self.newMainSnapshot.name) })) + #expect(output.contains(where: { $0.contains(SwiftlyTests.newMainSnapshot.name) })) // GIVEN: a directory with no swift version file at the top of a git repository try FileManager.default.removeItem(atPath: versionFile.path) - let gitDir = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(".git") + let gitDir = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".git") try FileManager.default.createDirectory(atPath: gitDir.path, withIntermediateDirectories: false) // WHEN: using a toolchain version - useCmd = try self.parseCommand(Use.self, ["use", Self.newReleaseSnapshot.name]) - try await useCmd.run() + try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.newReleaseSnapshot.name]) // THEN: a swift version file is created - XCTAssert(FileManager.default.fileExists(atPath: versionFile.path)) + #expect(FileManager.default.fileExists(atPath: versionFile.path)) // THEN: the version file contains the specified version versionFileContents = try String(contentsOf: versionFile, encoding: .utf8) - XCTAssertEqual(Self.newReleaseSnapshot.name, versionFileContents) + #expect(SwiftlyTests.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, encoding: .utf8) // 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: Self.newStable.asStableRelease!.major, minor: nil, patch: nil) - useCmd = try self.parseCommand(Use.self, ["use", broadSelector.description]) - try await useCmd.run() + let broadSelector = ToolchainSelector.stable(major: SwiftlyTests.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, encoding: .utf8) - XCTAssertEqual(Self.newStable.name, versionFileContents) + #expect(SwiftlyTests.newStable.name == versionFileContents) } } } From 29f6b4b786d822e68252d644960a23f5403ff9da Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 4 Apr 2025 20:25:48 -0400 Subject: [PATCH 02/12] Fix typo in touch command --- Tests/SwiftlyTests/SwiftlyTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index e9a45324..20fadcf0 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -685,7 +685,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { export GNUPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" export GPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" mkdir -p "$GNUPG_HOME"/.gnupg - touch "$GNUPS_HOME"/.gnupg/gpg.conf + touch "$GNUPG_HOME"/.gnupg/gpg.conf gpg --batch --import \(gpgKeyFile.path) >/dev/null 2>&1 || echo -n """] try importKey.run() From 76ba5401a980910aa96b5b19faeece1ffc3e63e5 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sat, 5 Apr 2025 10:29:03 -0400 Subject: [PATCH 03/12] Streamline passing of test contexts to common Swiftly API Remove extraneous HTTP requests during most testing Fully sandbox and mock GPG key import and verification --- Sources/LinuxPlatform/Linux.swift | 47 ++++---- Sources/MacOSPlatform/MacOS.swift | 14 +-- Sources/Swiftly/Init.swift | 14 +-- Sources/Swiftly/Install.swift | 26 ++--- Sources/Swiftly/List.swift | 16 +-- Sources/Swiftly/ListAvailable.swift | 6 +- Sources/Swiftly/Proxy.swift | 8 +- Sources/Swiftly/SelfUpdate.swift | 8 +- Sources/Swiftly/Swiftly.swift | 2 +- Sources/Swiftly/Uninstall.swift | 20 ++-- Sources/Swiftly/Update.swift | 18 ++-- Sources/Swiftly/Use.swift | 14 +-- Sources/SwiftlyCore/Platform.swift | 6 +- Sources/SwiftlyCore/SwiftlyCore.swift | 71 ++++++------ Sources/SwiftlyCore/Utils.swift | 24 ----- Tests/SwiftlyTests/HTTPClientTests.swift | 38 +++++-- Tests/SwiftlyTests/InitTests.swift | 14 +-- Tests/SwiftlyTests/InstallTests.swift | 14 +-- Tests/SwiftlyTests/PlatformTests.swift | 2 +- Tests/SwiftlyTests/SwiftlyTests.swift | 131 ++++++++++------------- Tests/SwiftlyTests/UninstallTests.swift | 10 +- Tests/SwiftlyTests/UpdateTests.swift | 20 ++-- Tests/SwiftlyTests/UseTests.swift | 8 +- 23 files changed, 261 insertions(+), 270 deletions(-) diff --git a/Sources/LinuxPlatform/Linux.swift b/Sources/LinuxPlatform/Linux.swift index f435617b..3c725b7d 100644 --- a/Sources/LinuxPlatform/Linux.swift +++ b/Sources/LinuxPlatform/Linux.swift @@ -1,8 +1,6 @@ import Foundation import SwiftlyCore -var swiftGPGKeysRefreshed = false - /// `Platform` implementation for Linux systems. /// This implementation can be reused for any supported Linux platform. /// TODO: replace dummy implementations @@ -69,7 +67,7 @@ public struct Linux: Platform { } } - public func verifySystemPrerequisitesForInstall(httpClient: SwiftlyHTTPClient, platformName: String, version _: ToolchainVersion, requireSignatureValidation: Bool) async throws -> String? { + public func verifySystemPrerequisitesForInstall(_ ctx: SwiftlyCoreContext, platformName: String, version _: ToolchainVersion, requireSignatureValidation: Bool) async throws -> String? { // TODO: these are hard-coded until we have a place to query for these based on the toolchain version // These lists were copied from the dockerfile sources here: https://github.com/apple/swift-docker/tree/ea035798755cce4ec41e0c6dbdd320904cef0421/5.10 let packages: [String] = switch platformName { @@ -260,22 +258,21 @@ public struct Linux: Platform { throw SwiftlyError(message: msg) } - // Import the latest swift keys, but only once per session, which will help with the performance in tests - if !swiftGPGKeysRefreshed { - let tmpFile = self.getTempFilePath() - let _ = FileManager.default.createFile(atPath: tmpFile.path, contents: nil, attributes: [.posixPermissions: 0o600]) - defer { - try? FileManager.default.removeItem(at: tmpFile) - } + let tmpFile = self.getTempFilePath() + let _ = FileManager.default.createFile(atPath: tmpFile.path, contents: nil, attributes: [.posixPermissions: 0o600]) + defer { + try? FileManager.default.removeItem(at: tmpFile) + } - guard let url = URL(string: "https://www.swift.org/keys/all-keys.asc") else { - throw SwiftlyError(message: "malformed URL to the swift gpg keys") - } + guard let url = URL(string: "https://www.swift.org/keys/all-keys.asc") else { + throw SwiftlyError(message: "malformed URL to the swift gpg keys") + } - try await httpClient.downloadFile(url: url, to: tmpFile) + try await ctx.httpClient.downloadFile(url: url, to: tmpFile) + if let mockedHomeDir = ctx.mockedHomeDir { + try self.runProgram("gpg", "--import", tmpFile.path, quiet: true, env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path]) + } else { try self.runProgram("gpg", "--import", tmpFile.path, quiet: true) - - swiftGPGKeysRefreshed = true } } @@ -335,7 +332,7 @@ public struct Linux: Platform { try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir(ctx), withIntermediateDirectories: false) } - SwiftlyCore.print(ctx, "Extracting toolchain...") + ctx.print("Extracting toolchain...") let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent(version.name) if toolchainDir.fileExists() { @@ -350,7 +347,7 @@ public struct Linux: Platform { let destination = toolchainDir.appendingPathComponent(String(relativePath)) if verbose { - SwiftlyCore.print(ctx, "\(destination.path)") + ctx.print("\(destination.path)") } // prepend /path/to/swiftlyHomeDir/toolchains/ to each file name @@ -366,7 +363,7 @@ public struct Linux: Platform { let tmpDir = self.getTempFilePath() try FileManager.default.createDirectory(atPath: tmpDir.path, withIntermediateDirectories: true) - SwiftlyCore.print(ctx, "Extracting new swiftly...") + ctx.print("Extracting new swiftly...") try extractArchive(atPath: archive) { name in // Extract to the temporary directory tmpDir.appendingPathComponent(String(name)) @@ -392,7 +389,7 @@ public struct Linux: Platform { public func verifySignature(_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws { if verbose { - SwiftlyCore.print(ctx, "Downloading toolchain signature...") + ctx.print("Downloading toolchain signature...") } let sigFile = self.getTempFilePath() @@ -406,9 +403,13 @@ public struct Linux: Platform { to: sigFile ) - SwiftlyCore.print(ctx, "Verifying toolchain signature...") + ctx.print("Verifying toolchain signature...") do { - try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: !verbose) + if let mockedHomeDir = ctx.mockedHomeDir { + try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: false, env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path]) + } else { + try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: !verbose) + } } catch { throw SwiftlyError(message: "Signature verification failed: \(error).") } @@ -430,7 +431,7 @@ public struct Linux: Platform { \(selections) """) - let choice = SwiftlyCore.readLine(ctx, prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ") ?? "0" + let choice = ctx.readLine(prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ") ?? "0" guard let choiceNum = Int(choice) else { fatalError("Installation canceled") diff --git a/Sources/MacOSPlatform/MacOS.swift b/Sources/MacOSPlatform/MacOS.swift index 12a28b2a..c299439c 100644 --- a/Sources/MacOSPlatform/MacOS.swift +++ b/Sources/MacOSPlatform/MacOS.swift @@ -39,7 +39,7 @@ public struct MacOS: Platform { // All system prerequisites are there for swiftly on macOS } - public func verifySystemPrerequisitesForInstall(httpClient _: SwiftlyHTTPClient, platformName _: String, version _: ToolchainVersion, requireSignatureValidation _: Bool) async throws -> String? { + public func verifySystemPrerequisitesForInstall(_: SwiftlyCoreContext, platformName _: String, version _: ToolchainVersion, requireSignatureValidation _: Bool) async throws -> String? { // All system prerequisites should be there for macOS nil } @@ -54,12 +54,12 @@ public struct MacOS: Platform { } if ctx.mockedHomeDir == nil { - SwiftlyCore.print(ctx, "Installing package in user home directory...") + ctx.print("Installing package in user home directory...") try runProgram("installer", "-verbose", "-pkg", tmpFile.path, "-target", "CurrentUserHomeDirectory", quiet: !verbose) } else { // In the case of a mock for testing purposes we won't use the installer, perferring a manual process because // the installer will not install to an arbitrary path, only a volume or user home directory. - SwiftlyCore.print(ctx, "Expanding pkg...") + ctx.print("Expanding pkg...") let tmpDir = self.getTempFilePath() let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(version.identifier).xctoolchain", isDirectory: true) if !toolchainDir.fileExists() { @@ -73,7 +73,7 @@ public struct MacOS: Platform { payload = tmpDir.appendingPathComponent("\(version.identifier)-osx-package.pkg/Payload") } - SwiftlyCore.print(ctx, "Untarring pkg Payload...") + ctx.print("Untarring pkg Payload...") try runProgram("tar", "-C", toolchainDir.path, "-xvf", payload.path, quiet: !verbose) } } @@ -88,7 +88,7 @@ public struct MacOS: Platform { if ctx.mockedHomeDir == nil { homeDir = FileManager.default.homeDirectoryForCurrentUser - SwiftlyCore.print(ctx, "Extracting the swiftly package...") + ctx.print("Extracting the swiftly package...") try runProgram("installer", "-pkg", archive.path, "-target", "CurrentUserHomeDirectory") try? runProgram("pkgutil", "--volume", homeDir.path, "--forget", "org.swift.swiftly") } else { @@ -109,7 +109,7 @@ public struct MacOS: Platform { throw SwiftlyError(message: "Payload file could not be found at \(tmpDir).") } - SwiftlyCore.print(ctx, "Extracting the swiftly package into \(installDir.path)...") + ctx.print("Extracting the swiftly package into \(installDir.path)...") try runProgram("tar", "-C", installDir.path, "-xvf", payload.path, quiet: false) } @@ -117,7 +117,7 @@ public struct MacOS: Platform { } public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose: Bool) throws { - SwiftlyCore.print(ctx, "Uninstalling package in user home directory...") + ctx.print("Uninstalling package in user home directory...") let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.identifier).xctoolchain", isDirectory: true) diff --git a/Sources/Swiftly/Init.swift b/Sources/Swiftly/Init.swift index 739055fb..dc18113d 100644 --- a/Sources/Swiftly/Init.swift +++ b/Sources/Swiftly/Init.swift @@ -113,9 +113,9 @@ struct Init: SwiftlyCommand { """ } - SwiftlyCore.print(ctx, msg) + ctx.print(msg) - guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: true) else { + guard ctx.promptForConfirmation(defaultBehavior: true) else { throw SwiftlyError(message: "swiftly installation has been cancelled") } } @@ -180,7 +180,7 @@ struct Init: SwiftlyCommand { try Swiftly.currentPlatform.installSwiftlyBin(ctx) if overwrite || !FileManager.default.fileExists(atPath: envFile.path) { - SwiftlyCore.print(ctx, "Creating shell environment file for the user...") + ctx.print("Creating shell environment file for the user...") var env = "" if shell.hasSuffix("fish") { env = """ @@ -206,7 +206,7 @@ struct Init: SwiftlyCommand { } if !noModifyProfile { - SwiftlyCore.print(ctx, "Updating profile...") + ctx.print("Updating profile...") let userHome = ctx.mockedHomeDir ?? FileManager.default.homeDirectoryForCurrentUser @@ -262,7 +262,7 @@ struct Init: SwiftlyCommand { try Data(sourceLine.utf8).append(to: profileHome) if !quietShellFollowup { - SwiftlyCore.print(ctx, """ + ctx.print(""" To begin using installed swiftly from your current shell, first run the following command: \(sourceLine) @@ -272,7 +272,7 @@ struct Init: SwiftlyCommand { // Fish doesn't have path caching, so this might only be needed for bash/zsh if pathChanged && !quietShellFollowup && !shell.hasSuffix("fish") { - SwiftlyCore.print(ctx, """ + ctx.print(""" Your shell caches items on your path for better performance. Swiftly has added items to your path that may not get picked up right away. You can update your shell's environment by running @@ -285,7 +285,7 @@ struct Init: SwiftlyCommand { } if let postInstall { - SwiftlyCore.print(ctx, """ + ctx.print(""" There are some dependencies that should be installed before using this toolchain. You can run the following script as the system administrator (e.g. root) to prepare your system: diff --git a/Sources/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index 649cf975..f0aa8ad2 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -116,7 +116,7 @@ struct Install: SwiftlyCommand { // Fish doesn't cache its path, so this instruction is not necessary. if pathChanged && !shell.hasSuffix("fish") { - SwiftlyCore.print(ctx, """ + ctx.print(""" NOTE: Swiftly has updated some elements in your path and your shell may not yet be aware of the changes. You can update your shell's environment by running @@ -153,16 +153,16 @@ struct Install: SwiftlyCommand { assumeYes: Bool ) async throws -> (postInstall: String?, pathChanged: Bool) { guard !config.installedToolchains.contains(version) else { - SwiftlyCore.print(ctx, "\(version) is already installed.") + ctx.print("\(version) is already installed.") return (nil, false) } // Ensure the system is set up correctly before downloading it. Problems that prevent installation // will throw, while problems that prevent use of the toolchain will be written out as a post install // script for the user to run afterwards. - let postInstallScript = try await Swiftly.currentPlatform.verifySystemPrerequisitesForInstall(httpClient: ctx.httpClient, platformName: config.platform.name, version: version, requireSignatureValidation: verifySignature) + let postInstallScript = try await Swiftly.currentPlatform.verifySystemPrerequisitesForInstall(ctx, platformName: config.platform.name, version: version, requireSignatureValidation: verifySignature) - SwiftlyCore.print(ctx, "Installing \(version)") + ctx.print("Installing \(version)") let tmpFile = Swiftly.currentPlatform.getTempFilePath() FileManager.default.createFile(atPath: tmpFile.path, contents: nil) @@ -275,19 +275,19 @@ struct Install: SwiftlyCommand { let overwrite = Set(toolchainBinDirContents).subtracting(existingProxies).intersection(swiftlyBinDirContents) if !overwrite.isEmpty && !assumeYes { - SwiftlyCore.print(ctx, "The following existing executables will be overwritten:") + ctx.print("The following existing executables will be overwritten:") for executable in overwrite { - SwiftlyCore.print(ctx, " \(swiftlyBinDir.appendingPathComponent(executable).path)") + ctx.print(" \(swiftlyBinDir.appendingPathComponent(executable).path)") } - guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: false) else { + guard ctx.promptForConfirmation(defaultBehavior: false) else { throw SwiftlyError(message: "Toolchain installation has been cancelled") } } if verbose { - SwiftlyCore.print(ctx, "Setting up toolchain proxies...") + ctx.print("Setting up toolchain proxies...") } let proxiesToCreate = Set(toolchainBinDirContents).subtracting(swiftlyBinDirContents).union(overwrite) @@ -323,10 +323,10 @@ struct Install: SwiftlyCommand { if config.inUse == nil { config.inUse = version try config.save(ctx) - SwiftlyCore.print(ctx, "The global default toolchain has been set to `\(version)`") + ctx.print("The global default toolchain has been set to `\(version)`") } - SwiftlyCore.print(ctx, "\(version) installed successfully!") + ctx.print("\(version) installed successfully!") return (postInstallScript, pathChanged) } @@ -334,7 +334,7 @@ struct Install: SwiftlyCommand { public static func resolve(_ ctx: SwiftlyCoreContext, config: Config, selector: ToolchainSelector) async throws -> ToolchainVersion { switch selector { case .latest: - SwiftlyCore.print(ctx, "Fetching the latest stable Swift release...") + ctx.print("Fetching the latest stable Swift release...") guard let release = try await ctx.httpClient.getReleaseToolchains(platform: config.platform, limit: 1).first else { throw SwiftlyError(message: "couldn't get latest releases") @@ -352,7 +352,7 @@ struct Install: SwiftlyCommand { return .stable(ToolchainVersion.StableRelease(major: major, minor: minor, patch: patch)) } - SwiftlyCore.print(ctx, "Fetching the latest stable Swift \(major).\(minor) release...") + ctx.print("Fetching the latest stable Swift \(major).\(minor) release...") // If a patch was not provided, perform a lookup to get the latest patch release // of the provided major/minor version pair. let toolchain = try await ctx.httpClient.getReleaseToolchains(platform: config.platform, limit: 1) { release in @@ -370,7 +370,7 @@ struct Install: SwiftlyCommand { return ToolchainVersion(snapshotBranch: branch, date: date) } - SwiftlyCore.print(ctx, "Fetching the latest \(branch) branch snapshot...") + ctx.print("Fetching the latest \(branch) branch snapshot...") // If a date was not provided, perform a lookup to find the most recent snapshot // for the given branch. diff --git a/Sources/Swiftly/List.swift b/Sources/Swiftly/List.swift index a9ed84ba..718f35ad 100644 --- a/Sources/Swiftly/List.swift +++ b/Sources/Swiftly/List.swift @@ -56,7 +56,7 @@ struct List: SwiftlyCommand { if toolchain == config.inUse { message += " (default)" } - SwiftlyCore.print(ctx, message) + ctx.print(message) } if let selector { @@ -76,14 +76,14 @@ struct List: SwiftlyCommand { } let message = "Installed \(modifier) toolchains" - SwiftlyCore.print(ctx, message) - SwiftlyCore.print(ctx, String(repeating: "-", count: message.count)) + ctx.print(message) + ctx.print(String(repeating: "-", count: message.count)) for toolchain in toolchains { printToolchain(toolchain) } } else { - SwiftlyCore.print(ctx, "Installed release toolchains") - SwiftlyCore.print(ctx, "----------------------------") + ctx.print("Installed release toolchains") + ctx.print("----------------------------") for toolchain in toolchains { guard toolchain.isStableRelease() else { continue @@ -91,9 +91,9 @@ struct List: SwiftlyCommand { printToolchain(toolchain) } - SwiftlyCore.print(ctx, "") - SwiftlyCore.print(ctx, "Installed snapshot toolchains") - SwiftlyCore.print(ctx, "-----------------------------") + ctx.print("") + ctx.print("Installed snapshot toolchains") + ctx.print("-----------------------------") for toolchain in toolchains where toolchain.isSnapshot() { printToolchain(toolchain) } diff --git a/Sources/Swiftly/ListAvailable.swift b/Sources/Swiftly/ListAvailable.swift index fd6eb8a8..653ddc26 100644 --- a/Sources/Swiftly/ListAvailable.swift +++ b/Sources/Swiftly/ListAvailable.swift @@ -80,7 +80,7 @@ struct ListAvailable: SwiftlyCommand { } else if installedToolchains.contains(toolchain) { message += " (installed)" } - SwiftlyCore.print(ctx, message) + ctx.print(message) } if let selector { @@ -100,8 +100,8 @@ struct ListAvailable: SwiftlyCommand { } let message = "Available \(modifier) toolchains" - SwiftlyCore.print(ctx, message) - SwiftlyCore.print(ctx, String(repeating: "-", count: message.count)) + ctx.print(message) + ctx.print(String(repeating: "-", count: message.count)) for toolchain in toolchains { printToolchain(toolchain) } diff --git a/Sources/Swiftly/Proxy.swift b/Sources/Swiftly/Proxy.swift index b960cc4d..bc9ce3ed 100644 --- a/Sources/Swiftly/Proxy.swift +++ b/Sources/Swiftly/Proxy.swift @@ -4,7 +4,7 @@ import SwiftlyCore @main public enum Proxy { static func main() async throws { - let ctx = SwiftlyCoreContext(httpClient: SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl())) + let ctx = SwiftlyCoreContext() do { let zero = CommandLine.arguments[0] @@ -13,8 +13,6 @@ public enum Proxy { } guard binName != "swiftly" else { - let ctx = SwiftlyCoreContext(httpClient: SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl())) - // Treat this as a swiftly invocation, but first check that we are installed, bootstrapping // the installation process if we aren't. let configResult = Result { try Config.load(ctx) } @@ -70,10 +68,10 @@ public enum Proxy { } catch let terminated as RunProgramError { exit(terminated.exitCode) } catch let error as SwiftlyError { - SwiftlyCore.print(ctx, error.message) + ctx.print(error.message) exit(1) } catch { - SwiftlyCore.print(ctx, "\(error)") + ctx.print("\(error)") exit(1) } } diff --git a/Sources/Swiftly/SelfUpdate.swift b/Sources/Swiftly/SelfUpdate.swift index 982be9d6..e2d796c9 100644 --- a/Sources/Swiftly/SelfUpdate.swift +++ b/Sources/Swiftly/SelfUpdate.swift @@ -32,12 +32,12 @@ struct SelfUpdate: SwiftlyCommand { } public static func execute(_ ctx: SwiftlyCoreContext, verbose: Bool) async throws -> SwiftlyVersion { - SwiftlyCore.print(ctx, "Checking for swiftly updates...") + ctx.print("Checking for swiftly updates...") let swiftlyRelease = try await ctx.httpClient.getCurrentSwiftlyRelease() guard try swiftlyRelease.swiftlyVersion > SwiftlyCore.version else { - SwiftlyCore.print(ctx, "Already up to date.") + ctx.print("Already up to date.") return SwiftlyCore.version } @@ -66,7 +66,7 @@ struct SelfUpdate: SwiftlyCommand { let version = try swiftlyRelease.swiftlyVersion - SwiftlyCore.print(ctx, "A new version is available: \(version)") + ctx.print("A new version is available: \(version)") let tmpFile = Swiftly.currentPlatform.getTempFilePath() FileManager.default.createFile(atPath: tmpFile.path, contents: nil) @@ -102,7 +102,7 @@ struct SelfUpdate: SwiftlyCommand { try await Swiftly.currentPlatform.verifySignature(ctx, archiveDownloadURL: downloadURL, archive: tmpFile, verbose: verbose) try Swiftly.currentPlatform.extractSwiftlyAndInstall(ctx, from: tmpFile) - SwiftlyCore.print(ctx, "Successfully updated swiftly to \(version) (was \(SwiftlyCore.version))") + ctx.print("Successfully updated swiftly to \(version) (was \(SwiftlyCore.version))") return version } } diff --git a/Sources/Swiftly/Swiftly.swift b/Sources/Swiftly/Swiftly.swift index 92b34192..abdaacb6 100644 --- a/Sources/Swiftly/Swiftly.swift +++ b/Sources/Swiftly/Swiftly.swift @@ -37,7 +37,7 @@ public struct Swiftly: SwiftlyCommand { ) public static func createDefaultContext() -> SwiftlyCoreContext { - SwiftlyCoreContext(httpClient: SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl())) + SwiftlyCoreContext() } /// The list of directories that swiftly needs to exist in order to execute. diff --git a/Sources/Swiftly/Uninstall.swift b/Sources/Swiftly/Uninstall.swift index 0a3e3d4c..472eb568 100644 --- a/Sources/Swiftly/Uninstall.swift +++ b/Sources/Swiftly/Uninstall.swift @@ -69,24 +69,24 @@ struct Uninstall: SwiftlyCommand { } guard !toolchains.isEmpty else { - SwiftlyCore.print(ctx, "No toolchains matched \"\(self.toolchain)\"") + ctx.print("No toolchains matched \"\(self.toolchain)\"") return } if !self.root.assumeYes { - SwiftlyCore.print(ctx, "The following toolchains will be uninstalled:") + ctx.print("The following toolchains will be uninstalled:") for toolchain in toolchains { - SwiftlyCore.print(ctx, " \(toolchain)") + ctx.print(" \(toolchain)") } - guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: true) else { - SwiftlyCore.print(ctx, "Aborting uninstall") + guard ctx.promptForConfirmation(defaultBehavior: true) else { + ctx.print("Aborting uninstall") return } } - SwiftlyCore.print(ctx) + ctx.print() for toolchain in toolchains { var config = try Config.load(ctx) @@ -120,12 +120,12 @@ struct Uninstall: SwiftlyCommand { try await Self.execute(ctx, toolchain, &config, verbose: self.root.verbose) } - SwiftlyCore.print(ctx) - SwiftlyCore.print(ctx, "\(toolchains.count) toolchain(s) successfully uninstalled") + ctx.print() + ctx.print("\(toolchains.count) toolchain(s) successfully uninstalled") } static func execute(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, _ config: inout Config, verbose: Bool) async throws { - SwiftlyCore.print(ctx, "Uninstalling \(toolchain)...", terminator: "") + ctx.print("Uninstalling \(toolchain)...", terminator: "") config.installedToolchains.remove(toolchain) // This is here to prevent the inUse from referencing a toolchain that is not installed if config.inUse == toolchain { @@ -134,6 +134,6 @@ struct Uninstall: SwiftlyCommand { try config.save(ctx) try Swiftly.currentPlatform.uninstall(ctx, toolchain, verbose: verbose) - SwiftlyCore.print(ctx, "done") + ctx.print("done") } } diff --git a/Sources/Swiftly/Update.swift b/Sources/Swiftly/Update.swift index 095c23b9..cb73efb3 100644 --- a/Sources/Swiftly/Update.swift +++ b/Sources/Swiftly/Update.swift @@ -87,27 +87,27 @@ struct Update: SwiftlyCommand { guard let parameters = try await self.resolveUpdateParameters(ctx, &config) else { if let toolchain = self.toolchain { - SwiftlyCore.print(ctx, "No installed toolchain matched \"\(toolchain)\"") + ctx.print("No installed toolchain matched \"\(toolchain)\"") } else { - SwiftlyCore.print(ctx, "No toolchains are currently installed") + ctx.print("No toolchains are currently installed") } return } guard let newToolchain = try await self.lookupNewToolchain(ctx, config, parameters) else { - SwiftlyCore.print(ctx, "\(parameters.oldToolchain) is already up to date") + ctx.print("\(parameters.oldToolchain) is already up to date") return } guard !config.installedToolchains.contains(newToolchain) else { - SwiftlyCore.print(ctx, "The newest version of \(parameters.oldToolchain) (\(newToolchain)) is already installed") + ctx.print("The newest version of \(parameters.oldToolchain) (\(newToolchain)) is already installed") return } if !self.root.assumeYes { - SwiftlyCore.print(ctx, "Update \(parameters.oldToolchain) -> \(newToolchain)?") - guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: true) else { - SwiftlyCore.print(ctx, "Aborting") + ctx.print("Update \(parameters.oldToolchain) -> \(newToolchain)?") + guard ctx.promptForConfirmation(defaultBehavior: true) else { + ctx.print("Aborting") return } } @@ -123,7 +123,7 @@ struct Update: SwiftlyCommand { ) try await Uninstall.execute(ctx, parameters.oldToolchain, &config, verbose: self.root.verbose) - SwiftlyCore.print(ctx, "Successfully updated \(parameters.oldToolchain) ⟶ \(newToolchain)") + ctx.print("Successfully updated \(parameters.oldToolchain) ⟶ \(newToolchain)") if let postInstallScript { guard let postInstallFile = self.postInstallFile else { @@ -141,7 +141,7 @@ struct Update: SwiftlyCommand { } if pathChanged { - SwiftlyCore.print(ctx, """ + ctx.print(""" NOTE: Swiftly has updated some elements in your path and your shell may not yet be aware of the changes. You can update your shell's environment by running diff --git a/Sources/Swiftly/Use.swift b/Sources/Swiftly/Use.swift index 764df27c..37bf6fd7 100644 --- a/Sources/Swiftly/Use.swift +++ b/Sources/Swiftly/Use.swift @@ -78,7 +78,7 @@ struct Use: SwiftlyCommand { if self.printLocation { // Print the toolchain location and exit - SwiftlyCore.print(ctx, "\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion).path)") + ctx.print("\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion).path)") return } @@ -91,7 +91,7 @@ struct Use: SwiftlyCommand { message += " (default)" } - SwiftlyCore.print(ctx, message) + ctx.print(message) return } @@ -103,7 +103,7 @@ struct Use: SwiftlyCommand { let selector = try ToolchainSelector(parsing: toolchain) guard let toolchain = config.listInstalledToolchains(selector: selector).max() else { - SwiftlyCore.print(ctx, "No installed toolchains match \"\(toolchain)\"") + ctx.print("No installed toolchains match \"\(toolchain)\"") return } @@ -123,10 +123,10 @@ struct Use: SwiftlyCommand { message = "The file `\(versionFile.path)` has been set to `\(toolchain)`" } else if let newVersionFile = findNewVersionFile(ctx), !globalDefault { if !assumeYes { - SwiftlyCore.print(ctx, "A new file `\(newVersionFile)` will be created to set the new in-use toolchain for this project. Alternatively, you can set your default globally with the `--global-default` flag. Proceed with creating this file?") + ctx.print("A new file `\(newVersionFile)` will be created to set the new in-use toolchain for this project. Alternatively, you can set your default globally with the `--global-default` flag. Proceed with creating this file?") - guard SwiftlyCore.promptForConfirmation(ctx, defaultBehavior: true) else { - SwiftlyCore.print(ctx, "Aborting setting in-use toolchain") + guard ctx.promptForConfirmation(defaultBehavior: true) else { + ctx.print("Aborting setting in-use toolchain") return } } @@ -144,7 +144,7 @@ struct Use: SwiftlyCommand { message += " (was \(selectedVersion.name))" } - SwiftlyCore.print(ctx, message) + ctx.print(message) } static func findNewVersionFile(_ ctx: SwiftlyCoreContext) -> URL? { diff --git a/Sources/SwiftlyCore/Platform.swift b/Sources/SwiftlyCore/Platform.swift index 652d65a4..f28171e9 100644 --- a/Sources/SwiftlyCore/Platform.swift +++ b/Sources/SwiftlyCore/Platform.swift @@ -90,7 +90,7 @@ public protocol Platform { /// can run to install these dependencies, possibly with super user permissions. /// /// Throws if system does not meet the requirements to perform the install. - func verifySystemPrerequisitesForInstall(httpClient: SwiftlyHTTPClient, platformName: String, version: ToolchainVersion, requireSignatureValidation: Bool) async throws -> String? + func verifySystemPrerequisitesForInstall(_ ctx: SwiftlyCoreContext, platformName: String, version: ToolchainVersion, requireSignatureValidation: Bool) async throws -> String? /// Downloads the signature file associated with the archive and verifies it matches the downloaded archive. /// Throws an error if the signature does not match. @@ -311,7 +311,7 @@ extension Platform { return } - SwiftlyCore.print(ctx, "Installing swiftly in \(swiftlyHomeBin)...") + ctx.print("Installing swiftly in \(swiftlyHomeBin)...") if FileManager.default.fileExists(atPath: swiftlyHomeBin) { try FileManager.default.removeItem(atPath: swiftlyHomeBin) @@ -321,7 +321,7 @@ extension Platform { try FileManager.default.moveItem(atPath: cmdAbsolute, toPath: swiftlyHomeBin) } catch { try FileManager.default.copyItem(atPath: cmdAbsolute, toPath: swiftlyHomeBin) - SwiftlyCore.print(ctx, "Swiftly has been copied into the installation directory. You can remove '\(cmdAbsolute)'. It is no longer needed.") + ctx.print("Swiftly has been copied into the installation directory. You can remove '\(cmdAbsolute)'. It is no longer needed.") } } diff --git a/Sources/SwiftlyCore/SwiftlyCore.swift b/Sources/SwiftlyCore/SwiftlyCore.swift index 7900a83c..f81c29a7 100644 --- a/Sources/SwiftlyCore/SwiftlyCore.swift +++ b/Sources/SwiftlyCore/SwiftlyCore.swift @@ -38,45 +38,56 @@ public struct SwiftlyCoreContext { /// The input probider to use, if any public var inputProvider: (any InputProvider)? - public init(httpClient: SwiftlyHTTPClient) { - self.httpClient = httpClient + public init() { + self.httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()) self.currentDirectory = URL.currentDirectory() } - public init( - mockedHomeDir: URL?, - httpClient: SwiftlyHTTPClient, - outputHandler: (any OutputHandler)?, - inputProvider: (any InputProvider)? - ) { - self.mockedHomeDir = mockedHomeDir - self.currentDirectory = mockedHomeDir ?? URL.currentDirectory() - self.httpClient = httpClient - self.outputHandler = outputHandler - self.inputProvider = inputProvider + /// Pass the provided string to the set output handler if any. + /// If no output handler has been set, just print to stdout. + public func print(_ string: String = "", terminator: String? = nil) { + guard let handler = self.outputHandler else { + if let terminator { + Swift.print(string, terminator: terminator) + } else { + Swift.print(string) + } + return + } + handler.handleOutputLine(string + (terminator ?? "")) } -} -/// Pass the provided string to the set output handler if any. -/// If no output handler has been set, just print to stdout. -public func print(_ ctx: SwiftlyCoreContext, _ string: String = "", terminator: String? = nil) { - guard let handler = ctx.outputHandler else { - if let terminator { - Swift.print(string, terminator: terminator) - } else { - Swift.print(string) + public func readLine(prompt: String) -> String? { + self.print(prompt, terminator: ": \n") + guard let provider = self.inputProvider else { + return Swift.readLine(strippingNewline: true) } - return + return provider.readLine() } - handler.handleOutputLine(string + (terminator ?? "")) -} -public func readLine(_ ctx: SwiftlyCoreContext, prompt: String) -> String? { - print(prompt, terminator: ": \n") - guard let provider = ctx.inputProvider else { - return Swift.readLine(strippingNewline: true) + public func promptForConfirmation(defaultBehavior: Bool) -> Bool { + let options: String + if defaultBehavior { + options = "(Y/n)" + } else { + options = "(y/N)" + } + + while true { + let answer = (self.readLine(prompt: "Proceed? \(options)") ?? (defaultBehavior ? "y" : "n")).lowercased() + + guard ["y", "n", ""].contains(answer) else { + self.print("Please input either \"y\" or \"n\", or press ENTER to use the default.") + continue + } + + if answer.isEmpty { + return defaultBehavior + } else { + return answer == "y" + } + } } - return provider.readLine() } #if arch(x86_64) diff --git a/Sources/SwiftlyCore/Utils.swift b/Sources/SwiftlyCore/Utils.swift index 93f815fe..285fe90c 100644 --- a/Sources/SwiftlyCore/Utils.swift +++ b/Sources/SwiftlyCore/Utils.swift @@ -15,27 +15,3 @@ extension URL { } } } - -public func promptForConfirmation(_ ctx: SwiftlyCoreContext, defaultBehavior: Bool) -> Bool { - let options: String - if defaultBehavior { - options = "(Y/n)" - } else { - options = "(y/N)" - } - - while true { - let answer = (SwiftlyCore.readLine(ctx, prompt: "Proceed? \(options)") ?? (defaultBehavior ? "y" : "n")).lowercased() - - guard ["y", "n", ""].contains(answer) else { - SwiftlyCore.print(ctx, "Please input either \"y\" or \"n\", or press ENTER to use the default.") - continue - } - - if answer.isEmpty { - return defaultBehavior - } else { - return answer == "y" - } - } -} diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index a440bdc5..ae7c4418 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -1,8 +1,36 @@ +import AsyncHTTPClient +import Foundation @testable import Swiftly @testable import SwiftlyCore import Testing @Suite struct HTTPClientTests { + @Test func getSwiftOrgGPGKeys() async throws { + let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()) + + let tmpFile = FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") + _ = FileManager.default.createFile(atPath: tmpFile.path, contents: nil) + defer { + try? FileManager.default.removeItem(at: tmpFile) + } + + let gpgKeysUrl = URL(string: "https://www.swift.org/keys/all-keys.asc")! + + try await httpClient.downloadFile(url: gpgKeysUrl, to: tmpFile) + +#if os(Linux) + // With linux, we can ask gpg to try an import to see if the file is valid + // in a sandbox home directory to avoid contaminating the system + let gpgHome = FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") + try FileManager.default.createDirectory(atPath: gpgHome.path, withIntermediateDirectories: true) + defer { + try? FileManager.default.removeItem(at: gpgHome) + } + + try Swiftly.currentPlatform.runProgram("gpg", "--import", tmpFile.path, quiet: false, env: ["GNUPGHOME": gpgHome.path]) +#endif + } + @Test func getSwiftlyReleaseMetadataFromSwiftOrg() async throws { let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()) let currentRelease = try await httpClient.getCurrentSwiftlyRelease() @@ -24,15 +52,9 @@ import Testing .debian12, ] - let newPlatforms: [PlatformDefinition] = [ - .ubuntu2404, - .fedora39, - .debian12, - ] - let branches: [ToolchainVersion.Snapshot.Branch] = [ .main, - .release(major: 6, minor: 0), // This is available in swift.org API + .release(major: 6, minor: 1), // This is available in swift.org API ] for arch in [Components.Schemas.Architecture.x8664, Components.Schemas.Architecture.aarch64] { @@ -43,8 +65,6 @@ import Testing // THEN: we get at least 1 release #expect(1 <= releases.count) - if newPlatforms.contains(platform) { continue } // Newer distros don't have main snapshots yet - for branch in branches { // GIVEN: we have a swiftly http client with swift.org metadata capability // WHEN: we ask for the first five snapshots on a branch for a supported platform and arch diff --git a/Tests/SwiftlyTests/InitTests.swift b/Tests/SwiftlyTests/InitTests.swift index dfd85fb6..5069709d 100644 --- a/Tests/SwiftlyTests/InitTests.swift +++ b/Tests/SwiftlyTests/InitTests.swift @@ -32,7 +32,7 @@ import Testing try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) // THEN: it creates a valid configuration at the correct version - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() #expect(SwiftlyCore.version == config.version) // AND: it creates an environment script suited for the type of shell @@ -72,9 +72,9 @@ import Testing try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) // Add some customizations to files and directories - var config = try Config.load(SwiftlyTests.ctx) + var config = try Config.load() config.version = try SwiftlyVersion(parsing: "100.0.0") - try config.save(SwiftlyTests.ctx) + try config.save() try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) @@ -83,7 +83,7 @@ import Testing try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install", "--overwrite"]) // THEN: everything is overwritten in initialization - config = try Config.load(SwiftlyTests.ctx) + config = try Config.load() #expect(SwiftlyCore.version == config.version) #expect(!Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) #expect(!Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) @@ -98,9 +98,9 @@ import Testing try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) // Add some customizations to files and directories - var config = try Config.load(SwiftlyTests.ctx) + var config = try Config.load() config.version = try SwiftlyVersion(parsing: "100.0.0") - try config.save(SwiftlyTests.ctx) + try config.save() try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) @@ -117,7 +117,7 @@ import Testing #expect(threw) // AND: files were left intact - config = try Config.load(SwiftlyTests.ctx) + config = try Config.load() #expect(try SwiftlyVersion(parsing: "100.0.0") == config.version) #expect(Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) #expect(Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index 711cef12..8bcdc372 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -14,7 +14,7 @@ import Testing try await SwiftlyTests.withMockedToolchain { try await SwiftlyTests.runCommand(Install.self, ["install", "latest", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() guard !config.installedToolchains.isEmpty else { Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") @@ -47,7 +47,7 @@ import Testing try await SwiftlyTests.withMockedToolchain { try await SwiftlyTests.runCommand(Install.self, ["install", "5.7", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() guard !config.installedToolchains.isEmpty else { Issue.record("expected swiftly install latest to install release toolchain but installed toolchains is empty in config") @@ -131,7 +131,7 @@ import Testing try await SwiftlyTests.withMockedToolchain { try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() guard !config.installedToolchains.isEmpty else { Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") @@ -162,7 +162,7 @@ import Testing try await SwiftlyTests.withMockedToolchain { try await SwiftlyTests.runCommand(Install.self, ["install", "6.0-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() guard !config.installedToolchains.isEmpty else { Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") @@ -214,7 +214,7 @@ import Testing try await SwiftlyTests.withMockedToolchain { try await SwiftlyTests.runCommand(Install.self, ["install", version, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let before = try Config.load(SwiftlyTests.ctx) + let before = try Config.load() let startTime = Date() try await SwiftlyTests.runCommand(Install.self, ["install", version, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) @@ -222,7 +222,7 @@ import Testing // Assert that swiftly didn't attempt to download a new toolchain. #expect(startTime.timeIntervalSinceNow.magnitude < 10) - let after = try Config.load(SwiftlyTests.ctx) + let after = try Config.load() #expect(before == after) } } @@ -255,7 +255,7 @@ import Testing try await SwiftlyTests.withTestHome { try await SwiftlyTests.withMockedToolchain { - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() #expect(config.inUse == nil) try await SwiftlyTests.validateInUse(expected: nil) diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index 7d263866..8f78531b 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -5,7 +5,7 @@ import Testing @Suite struct PlatformTests { func mockToolchainDownload(version: String) async throws -> (URL, ToolchainVersion) { - let mockDownloader = MockToolchainDownloader(executables: ["swift"], delegate: SwiftlyTests.ctx.httpClient.httpRequestExecutor) + let mockDownloader = MockToolchainDownloader(executables: ["swift"]) let version = try! ToolchainVersion(parsing: version) let ext = Swiftly.currentPlatform.toolchainFileExtension let tmpDir = Swiftly.currentPlatform.getTempFilePath() diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 20fadcf0..0b867127 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -30,11 +30,46 @@ struct InputProviderFail: InputProvider { } } +struct HTTPRequestExecutorFail: HTTPRequestExecutor { + func execute(_: HTTPClientRequest, timeout _: TimeAmount) async throws -> HTTPClientResponse { fatalError("http request executor was not mocked. put test case in a SwiftlyTests.withMocked() before running it.") } + func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { fatalError("http request executor was not mocked. put test case in a SwiftlyTests.withMocked() before running it.") } + func getReleaseToolchains() async throws -> [Components.Schemas.Release] { fatalError("http request executor was not mocked. put test case in a SwiftlyTests.withMocked() before running it.") } + func getSnapshotToolchains(branch _: Components.Schemas.SourceBranch, platform _: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { fatalError("http request executor was not mocked. put test case in a SwiftlyTests.withMocked() before running it.") } +} + +// Convenience extensions to common Swiftly and SwiftlyCore types to set the correct context + +extension Config { + public static func load() throws -> Config { + try Config.load(SwiftlyTests.ctx) + } + + public func save() throws { + try self.save(SwiftlyTests.ctx) + } +} + +extension SwiftlyCoreContext { + public init( + mockedHomeDir: URL?, + httpRequestExecutor: HTTPRequestExecutor, + outputHandler: (any OutputHandler)?, + inputProvider: (any InputProvider)? + ) { + self.init() + + self.mockedHomeDir = mockedHomeDir + self.currentDirectory = mockedHomeDir ?? URL.currentDirectory() + self.httpClient = SwiftlyHTTPClient(httpRequestExecutor: httpRequestExecutor) + self.outputHandler = outputHandler + self.inputProvider = inputProvider + } +} + public enum SwiftlyTests { @TaskLocal static var ctx: SwiftlyCoreContext = .init( mockedHomeDir: URL(fileURLWithPath: "/does/not/exist"), - // FIXME: place a request executor that fails on each request here - httpClient: SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()), + httpRequestExecutor: HTTPRequestExecutorFail(), outputHandler: OutputHandlerFail(), inputProvider: InputProviderFail() ) @@ -94,7 +129,7 @@ public enum SwiftlyTests { let ctx = SwiftlyCoreContext( mockedHomeDir: SwiftlyTests.ctx.mockedHomeDir, - httpClient: SwiftlyTests.ctx.httpClient, + httpRequestExecutor: SwiftlyTests.ctx.httpClient.httpRequestExecutor, outputHandler: handler, inputProvider: provider ) @@ -132,7 +167,7 @@ public enum SwiftlyTests { let ctx = SwiftlyCoreContext( mockedHomeDir: testHome, - httpClient: SwiftlyTests.ctx.httpClient, + httpRequestExecutor: SwiftlyTests.ctx.httpClient.httpRequestExecutor, outputHandler: nil, inputProvider: nil ) @@ -178,12 +213,11 @@ public enum SwiftlyTests { /// Operate with a mocked swiftly version available when requested with the HTTP request executor. static func withMockedSwiftlyVersion(latestSwiftlyVersion: SwiftlyVersion = SwiftlyCore.version, _ f: () async throws -> Void) async throws { - let prevExecutor = Self.ctx.httpClient.httpRequestExecutor - let mockDownloader = MockToolchainDownloader(executables: ["swift"], latestSwiftlyVersion: latestSwiftlyVersion, delegate: prevExecutor) + let mockDownloader = MockToolchainDownloader(executables: ["swift"], latestSwiftlyVersion: latestSwiftlyVersion) let ctx = SwiftlyCoreContext( mockedHomeDir: SwiftlyTests.ctx.mockedHomeDir, - httpClient: SwiftlyHTTPClient(httpRequestExecutor: mockDownloader), + httpRequestExecutor: mockDownloader, outputHandler: SwiftlyTests.ctx.outputHandler, inputProvider: SwiftlyTests.ctx.inputProvider ) @@ -195,28 +229,11 @@ public enum SwiftlyTests { /// Operate with a mocked toolchain that has the provided list of executables in its bin directory. static func withMockedToolchain(executables: [String]? = nil, f: () async throws -> Void) async throws { - let prevExecutor = SwiftlyTests.ctx.httpClient.httpRequestExecutor - let mockDownloader = MockToolchainDownloader(executables: executables, delegate: prevExecutor) - - let ctx = SwiftlyCoreContext( - mockedHomeDir: SwiftlyTests.ctx.mockedHomeDir, - httpClient: SwiftlyHTTPClient(httpRequestExecutor: mockDownloader), - outputHandler: SwiftlyTests.ctx.outputHandler, - inputProvider: SwiftlyTests.ctx.inputProvider - ) - - try await SwiftlyTests.$ctx.withValue(ctx) { - try await f() - } - } - - /// Operate with a mocked HTTP request executor that calls the provided handler to handle the requests. - static func withMockedHTTPRequests(_ handler: @escaping (HTTPClientRequest) async throws -> HTTPClientResponse, _ f: () async throws -> Void) async throws { - let mockedRequestExecutor = MockHTTPRequestExecutor(handler: handler) + let mockDownloader = MockToolchainDownloader(executables: executables) let ctx = SwiftlyCoreContext( mockedHomeDir: SwiftlyTests.ctx.mockedHomeDir, - httpClient: SwiftlyHTTPClient(httpRequestExecutor: mockedRequestExecutor), + httpRequestExecutor: mockDownloader, outputHandler: SwiftlyTests.ctx.outputHandler, inputProvider: SwiftlyTests.ctx.inputProvider ) @@ -230,7 +247,7 @@ public enum SwiftlyTests { /// configuration file and by executing `swift --version` using the swift executable in the `bin` directory. /// If nil is provided, this validates that no toolchain is currently in use. static func validateInUse(expected: ToolchainVersion?) async throws { - let config = try Config.load(Self.ctx) + let config = try Config.load() #expect(config.inUse == expected) } @@ -239,7 +256,7 @@ public enum SwiftlyTests { /// This method ensures that config.json reflects the expected installed toolchains and also /// validates that the toolchains on disk match their expected versions via `swift --version`. static func validateInstalledToolchains(_ toolchains: Set, description: String) async throws { - let config = try Config.load(Self.ctx) + let config = try Config.load() guard config.installedToolchains == toolchains else { throw SwiftlyTestError(message: "\(description): expected \(toolchains) but got \(config.installedToolchains)") @@ -413,31 +430,6 @@ public struct SwiftExecutable { } } -/// An `HTTPRequestExecutor` that responds to all HTTP requests by invoking the provided closure. -private struct MockHTTPRequestExecutor: HTTPRequestExecutor { - private let handler: (HTTPClientRequest) async throws -> HTTPClientResponse - - public init(handler: @escaping (HTTPClientRequest) async throws -> HTTPClientResponse) { - self.handler = handler - } - - public func execute(_ request: HTTPClientRequest, timeout _: TimeAmount) async throws -> HTTPClientResponse { - try await self.handler(request) - } - - public func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { - throw SwiftlyTestError(message: "Mocking of fetching the current swiftly release is not implemented in MockHTTPRequestExecutor.") - } - - public func getReleaseToolchains() async throws -> [Components.Schemas.Release] { - throw SwiftlyTestError(message: "Mocking of fetching the release toolchains is not implemented in MockHTTPRequestExecutor.") - } - - public func getSnapshotToolchains(branch _: Components.Schemas.SourceBranch, platform _: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { - throw SwiftlyTestError(message: "Mocking of fetching the snapshot toolchains is not implemented in MockHTTPRequestExecutor.") - } -} - /// An `HTTPRequestExecutor` which will return a mocked response to any toolchain download requests. /// All other requests are performed using an actual HTTP client. public class MockToolchainDownloader: HTTPRequestExecutor { @@ -450,7 +442,6 @@ public class MockToolchainDownloader: HTTPRequestExecutor { #if os(Linux) private var signatures: [String: URL] #endif - private let delegate: HTTPRequestExecutor private let latestSwiftlyVersion: SwiftlyVersion @@ -477,14 +468,12 @@ public class MockToolchainDownloader: HTTPRequestExecutor { SwiftlyTests.newMainSnapshot.asSnapshot!, SwiftlyTests.oldReleaseSnapshot.asSnapshot!, SwiftlyTests.newReleaseSnapshot.asSnapshot!, - ], - delegate: HTTPRequestExecutor + ] ) { self.executables = executables ?? ["swift"] #if os(Linux) self.signatures = [:] #endif - self.delegate = delegate self.latestSwiftlyVersion = latestSwiftlyVersion self.releaseToolchains = releaseToolchains self.snapshotToolchains = snapshotToolchains @@ -582,7 +571,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { } } - public func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse { + public func execute(_ request: HTTPClientRequest, timeout _: TimeAmount) async throws -> HTTPClientResponse { guard let url = URL(string: request.url) else { throw SwiftlyTestError(message: "invalid request URL: \(request.url)") } @@ -593,9 +582,8 @@ public class MockToolchainDownloader: HTTPRequestExecutor { } else if url.host == "download.swift.org" && (url.path.hasPrefix("/swift-") || url.path.hasPrefix("/development")) { // Download a toolchain return try self.makeToolchainDownloadResponse(from: url) - } else if url.host == "www.swift.org" { - // Delegate any API requests to swift.org - return try await self.delegate.execute(request, timeout: timeout) + } else if url.host == "www.swift.org" && url.path == "/keys/all-keys.asc" { + return try self.makeGPGKeysResponse(from: url) } else { throw SwiftlyTestError(message: "unmocked URL: \(request)") } @@ -630,6 +618,11 @@ public class MockToolchainDownloader: HTTPRequestExecutor { return HTTPClientResponse(body: .bytes(ByteBuffer(data: mockedSwiftly))) } + private func makeGPGKeysResponse(from _: URL) throws -> HTTPClientResponse { + // Give GPG the test's private signature here as trusted + HTTPClientResponse(body: .bytes(ByteBuffer(data: Data(PackageResources.mock_signing_key_private_pgp)))) + } + #if os(Linux) public func makeMockedSwiftly(from url: URL) throws -> Data { // Check our cache if this is a signature request @@ -682,10 +675,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { let importKey = Process() importKey.executableURL = URL(fileURLWithPath: "/usr/bin/env") importKey.arguments = ["bash", "-c", """ - export GNUPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" - export GPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" - mkdir -p "$GNUPG_HOME"/.gnupg - touch "$GNUPG_HOME"/.gnupg/gpg.conf + export GNUPGHOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)/.gnupg" gpg --batch --import \(gpgKeyFile.path) >/dev/null 2>&1 || echo -n """] try importKey.run() @@ -698,8 +688,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { detachSign.executableURL = URL(fileURLWithPath: "/usr/bin/env") detachSign.arguments = ["bash", "-c", """ export GPG_TTY=$(tty) - export GNUPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" - export GPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" + export GNUPGHOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)/.gnupg" gpg --version | grep '2.0.' > /dev/null if [ "$?" == "0" ]; then gpg --default-key "A2A645E5249D25845C43954E7D210032D2F670B7" --detach-sign "\(archive.path)" @@ -773,10 +762,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { let importKey = Process() importKey.executableURL = URL(fileURLWithPath: "/usr/bin/env") importKey.arguments = ["bash", "-c", """ - export GNUPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" - export GPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" - mkdir -p "$GNUPG_HOME"/.gnupg - touch "$GNUPG_HOME"/.gnupg/gpg.conf + export GNUPGHOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)/.gnupg" gpg --batch --import \(gpgKeyFile.path) >/dev/null 2>&1 || echo -n """] try importKey.run() @@ -789,8 +775,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { detachSign.executableURL = URL(fileURLWithPath: "/usr/bin/env") detachSign.arguments = ["bash", "-c", """ export GPG_TTY=$(tty) - export GNUPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" - export GPG_HOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)" + export GNUPGHOME="\(SwiftlyTests.ctx.mockedHomeDir!.path)/.gnupg" gpg --version | grep '2.0.' > /dev/null if [ "$?" == "0" ]; then gpg --default-key "A2A645E5249D25845C43954E7D210032D2F670B7" --detach-sign "\(archive.path)" diff --git a/Tests/SwiftlyTests/UninstallTests.swift b/Tests/SwiftlyTests/UninstallTests.swift index 37841f58..fb861080 100644 --- a/Tests/SwiftlyTests/UninstallTests.swift +++ b/Tests/SwiftlyTests/UninstallTests.swift @@ -233,7 +233,7 @@ import Testing @Test func uninstallLastToolchain() async throws { try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: [SwiftlyTests.oldStable], inUse: SwiftlyTests.oldStable) { _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", SwiftlyTests.oldStable.name], input: ["y"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() #expect(config.inUse == nil) // Ensure all symlinks have been cleaned up. @@ -247,7 +247,7 @@ import Testing /// Tests that aborting an uninstall works correctly. @Test func uninstallAbort() async throws { try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains, inUse: SwiftlyTests.oldStable) { - let preConfig = try Config.load(SwiftlyTests.ctx) + let preConfig = try Config.load() _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", SwiftlyTests.oldStable.name], input: ["n"]) try await SwiftlyTests.validateInstalledToolchains( SwiftlyTests.allToolchains, @@ -255,7 +255,7 @@ import Testing ) // Ensure config did not change. - #expect(try Config.load(SwiftlyTests.ctx) == preConfig) + #expect(try Config.load() == preConfig) } } @@ -286,10 +286,10 @@ import Testing @Test func uninstallNotInstalled() async throws { let toolchains = Set([SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.newMainSnapshot, SwiftlyTests.oldReleaseSnapshot]) try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: SwiftlyTests.newMainSnapshot) { - var config = try Config.load(SwiftlyTests.ctx) + var config = try Config.load() config.inUse = SwiftlyTests.newMainSnapshot config.installedToolchains.remove(SwiftlyTests.newMainSnapshot) - try config.save(SwiftlyTests.ctx) + try config.save() try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", SwiftlyTests.newMainSnapshot.name]) try await SwiftlyTests.validateInstalledToolchains( diff --git a/Tests/SwiftlyTests/UpdateTests.swift b/Tests/SwiftlyTests/UpdateTests.swift index c665890b..eec7700a 100644 --- a/Tests/SwiftlyTests/UpdateTests.swift +++ b/Tests/SwiftlyTests/UpdateTests.swift @@ -10,11 +10,11 @@ import Testing try await SwiftlyTests.withMockedToolchain { try await SwiftlyTests.installMockedToolchain(selector: .latest) - let beforeUpdateConfig = try Config.load(SwiftlyTests.ctx) + let beforeUpdateConfig = try Config.load() try await SwiftlyTests.runCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - #expect(try Config.load(SwiftlyTests.ctx) == beforeUpdateConfig) + #expect(try Config.load() == beforeUpdateConfig) try await SwiftlyTests.validateInstalledToolchains( beforeUpdateConfig.installedToolchains, description: "Updating latest toolchain should have no effect" @@ -44,7 +44,7 @@ import Testing try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() let inUse = config.inUse!.asStableRelease! #expect(inUse > .init(major: 5, minor: 9, patch: 0)) @@ -64,7 +64,7 @@ import Testing try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() let inUse = config.inUse!.asStableRelease! #expect(inUse.major == 5) @@ -86,7 +86,7 @@ import Testing try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5.9.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() let inUse = config.inUse!.asStableRelease! #expect(inUse.major == 5) @@ -110,7 +110,7 @@ import Testing try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() let inUse = config.inUse!.asStableRelease! #expect(inUse > .init(major: 6, minor: 0, patch: 0)) #expect(inUse.major == 6) @@ -145,7 +145,7 @@ import Testing // Since the global default was set to 6.0.0, and that toolchain is no longer installed // the update should have unset it to prevent the config from going into a bad state. - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() #expect(config.inUse == nil) // The new toolchain should be installed @@ -171,7 +171,7 @@ import Testing Update.self, ["update", "-y", "\(branch.name)-snapshot", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] ) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() let inUse = config.inUse!.asSnapshot! #expect(inUse > .init(branch: branch, date: date)) #expect(inUse.branch == branch) @@ -195,7 +195,7 @@ import Testing try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "6.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() let inUse = config.inUse!.asStableRelease! #expect(inUse.major == 6) #expect(inUse.minor == 0) @@ -226,7 +226,7 @@ import Testing Update.self, ["update", "-y", "\(branch.name)-snapshot", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] ) - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() let inUse = config.inUse!.asSnapshot! #expect(inUse.branch == branch) diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index e182ca0a..d8759247 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -11,7 +11,7 @@ import Testing func useAndValidate(argument: String, expectedVersion: ToolchainVersion) async throws { try await SwiftlyTests.runCommand(Use.self, ["use", "-g", argument]) - #expect(try Config.load(SwiftlyTests.ctx).inUse == expectedVersion) + #expect(try Config.load().inUse == expectedVersion) } /// Tests that the `use` command can switch between installed stable release toolchains. @@ -167,12 +167,12 @@ import Testing try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: []) { try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "latest"]) - var config = try Config.load(SwiftlyTests.ctx) + var config = try Config.load() #expect(config.inUse == nil) try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "5.6.0"]) - config = try Config.load(SwiftlyTests.ctx) + config = try Config.load() #expect(config.inUse == nil) } } @@ -194,7 +194,7 @@ import Testing /// Tests that the `use` command works with all the installed toolchains in this test harness. @Test func useAll() async throws { try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - let config = try Config.load(SwiftlyTests.ctx) + let config = try Config.load() for toolchain in config.installedToolchains { try await self.useAndValidate( From 312e6aeab8d7a72ffda9305e4cb5ca3b51b5970d Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sat, 5 Apr 2025 15:47:11 -0400 Subject: [PATCH 04/12] Introduce test scoping traits to remove many of the with... closures needed for mocking --- Tests/SwiftlyTests/InitTests.swift | 182 ++++++++------- Tests/SwiftlyTests/InstallTests.swift | 295 ++++++++++--------------- Tests/SwiftlyTests/ListTests.swift | 22 +- Tests/SwiftlyTests/PlatformTests.swift | 94 ++++---- Tests/SwiftlyTests/RunTests.swift | 56 +++-- Tests/SwiftlyTests/SwiftlyTests.swift | 71 +++++- Tests/SwiftlyTests/UpdateTests.swift | 236 +++++++++----------- Tests/SwiftlyTests/UseTests.swift | 276 +++++++++++------------ 8 files changed, 586 insertions(+), 646 deletions(-) diff --git a/Tests/SwiftlyTests/InitTests.swift b/Tests/SwiftlyTests/InitTests.swift index 5069709d..84d024a9 100644 --- a/Tests/SwiftlyTests/InitTests.swift +++ b/Tests/SwiftlyTests/InitTests.swift @@ -4,123 +4,117 @@ import Foundation import Testing @Suite struct InitTests { - @Test func initFresh() async throws { - try await SwiftlyTests.withTestHome { - // GIVEN: a fresh user account without Swiftly installed - try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) - - // AND: the user is using the bash shell - let shell = "/bin/bash" - var ctx = SwiftlyTests.ctx - ctx.mockedShell = shell - - try await SwiftlyTests.$ctx.withValue(ctx) { - let envScript: URL? - if shell.hasSuffix("bash") || shell.hasSuffix("zsh") { - envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("env.sh") - } else if shell.hasSuffix("fish") { - envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("env.fish") - } else { - envScript = nil - } + @Test(.testHome) func initFresh() async throws { + // GIVEN: a fresh user account without Swiftly installed + try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) + + // AND: the user is using the bash shell + let shell = "/bin/bash" + var ctx = SwiftlyTests.ctx + ctx.mockedShell = shell + + try await SwiftlyTests.$ctx.withValue(ctx) { + let envScript: URL? + if shell.hasSuffix("bash") || shell.hasSuffix("zsh") { + envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("env.sh") + } else if shell.hasSuffix("fish") { + envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("env.fish") + } else { + envScript = nil + } - if let envScript { - #expect(!envScript.fileExists()) - } + if let envScript { + #expect(!envScript.fileExists()) + } - // WHEN: swiftly is invoked to init the user account and finish swiftly installation - try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) - - // THEN: it creates a valid configuration at the correct version - let config = try Config.load() - #expect(SwiftlyCore.version == config.version) - - // AND: it creates an environment script suited for the type of shell - if let envScript { - #expect(envScript.fileExists()) - if let scriptContents = try? String(contentsOf: envScript) { - #expect(scriptContents.contains("SWIFTLY_HOME_DIR")) - #expect(scriptContents.contains("SWIFTLY_BIN_DIR")) - #expect(scriptContents.contains(Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).path)) - #expect(scriptContents.contains(Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx).path)) - } + // WHEN: swiftly is invoked to init the user account and finish swiftly installation + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) + + // THEN: it creates a valid configuration at the correct version + let config = try Config.load() + #expect(SwiftlyCore.version == config.version) + + // AND: it creates an environment script suited for the type of shell + if let envScript { + #expect(envScript.fileExists()) + if let scriptContents = try? String(contentsOf: envScript) { + #expect(scriptContents.contains("SWIFTLY_HOME_DIR")) + #expect(scriptContents.contains("SWIFTLY_BIN_DIR")) + #expect(scriptContents.contains(Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).path)) + #expect(scriptContents.contains(Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx).path)) } + } - // AND: it sources the script from the user profile - if let envScript { - var foundSourceLine = false - for p in [".profile", ".zprofile", ".bash_profile", ".bash_login", ".config/fish/conf.d/swiftly.fish"] { - let profile = SwiftlyTests.ctx.mockedHomeDir!.appendingPathComponent(p) - if profile.fileExists() { - if let profileContents = try? String(contentsOf: profile), profileContents.contains(envScript.path) { - foundSourceLine = true - break - } + // AND: it sources the script from the user profile + if let envScript { + var foundSourceLine = false + for p in [".profile", ".zprofile", ".bash_profile", ".bash_login", ".config/fish/conf.d/swiftly.fish"] { + let profile = SwiftlyTests.ctx.mockedHomeDir!.appendingPathComponent(p) + if profile.fileExists() { + if let profileContents = try? String(contentsOf: profile), profileContents.contains(envScript.path) { + foundSourceLine = true + break } } - #expect(foundSourceLine) } + #expect(foundSourceLine) } } } - @Test func initOverwrite() async throws { - try await SwiftlyTests.withTestHome { - // GIVEN: a user account with swiftly already installed - try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) + @Test(.testHome) func initOverwrite() async throws { + // GIVEN: a user account with swiftly already installed + try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) - try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) - // Add some customizations to files and directories - var config = try Config.load() - config.version = try SwiftlyVersion(parsing: "100.0.0") - try config.save() + // Add some customizations to files and directories + var config = try Config.load() + config.version = try SwiftlyVersion(parsing: "100.0.0") + try config.save() - try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) - try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) + try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) + try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) - // WHEN: swiftly is initialized with overwrite enabled - try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install", "--overwrite"]) + // WHEN: swiftly is initialized with overwrite enabled + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install", "--overwrite"]) - // THEN: everything is overwritten in initialization - config = try Config.load() - #expect(SwiftlyCore.version == config.version) - #expect(!Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) - #expect(!Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) - } + // THEN: everything is overwritten in initialization + config = try Config.load() + #expect(SwiftlyCore.version == config.version) + #expect(!Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) + #expect(!Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) } - @Test func initTwice() async throws { - try await SwiftlyTests.withTestHome { - // GIVEN: a user account with swiftly already installed - try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) + @Test(.testHome) func initTwice() async throws { + // GIVEN: a user account with swiftly already installed + try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) - try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) - // Add some customizations to files and directories - var config = try Config.load() - config.version = try SwiftlyVersion(parsing: "100.0.0") - try config.save() + // Add some customizations to files and directories + var config = try Config.load() + config.version = try SwiftlyVersion(parsing: "100.0.0") + try config.save() - try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) - try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) + try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) + try Data("".utf8).append(to: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt")) - // WHEN: swiftly init is invoked a second time - var threw = false - do { - try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) - } catch { - threw = true - } + // WHEN: swiftly init is invoked a second time + var threw = false + do { + try await SwiftlyTests.runCommand(Init.self, ["init", "--assume-yes", "--skip-install"]) + } catch { + threw = true + } - // THEN: init fails - #expect(threw) + // THEN: init fails + #expect(threw) - // AND: files were left intact - config = try Config.load() - #expect(try SwiftlyVersion(parsing: "100.0.0") == config.version) - #expect(Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) - #expect(Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) - } + // AND: files were left intact + config = try Config.load() + #expect(try SwiftlyVersion(parsing: "100.0.0") == config.version) + #expect(Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) + #expect(Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) } } diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index 8bcdc372..25645205 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -9,204 +9,166 @@ import Testing /// It stops short of verifying that it actually installs the _most_ recently released version, which is the intended /// behavior, since determining which version is the latest is non-trivial and would require duplicating code /// from within swiftly itself. - @Test func installLatest() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.runCommand(Install.self, ["install", "latest", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + @Test(.testHomeMockedToolchain) func installLatest() async throws { + try await SwiftlyTests.runCommand(Install.self, ["install", "latest", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load() - guard !config.installedToolchains.isEmpty else { - Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") - return - } + guard !config.installedToolchains.isEmpty else { + Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") + return + } - let installedToolchain = config.installedToolchains.first! + let installedToolchain = config.installedToolchains.first! - guard case let .stable(release) = installedToolchain else { - Issue.record("expected swiftly install latest to install release toolchain but got \(installedToolchain)") - return - } + guard case let .stable(release) = installedToolchain else { + Issue.record("expected swiftly install latest to install release toolchain but got \(installedToolchain)") + return + } - // As of writing this, 5.8.0 is the latest stable release. Assert it is at least that new. - #expect(release >= ToolchainVersion.StableRelease(major: 5, minor: 8, patch: 0)) + // As of writing this, 5.8.0 is the latest stable release. Assert it is at least that new. + #expect(release >= ToolchainVersion.StableRelease(major: 5, minor: 8, patch: 0)) - try await SwiftlyTests.validateInstalledToolchains([installedToolchain], description: "install latest") - } - } + try await SwiftlyTests.validateInstalledToolchains([installedToolchain], description: "install latest") } /// Tests that `swiftly install a.b` installs the latest patch version of Swift a.b. - @Test func installLatestPatchVersion() async throws { - guard try await SwiftlyTests.baseTestConfig().platform.name != "ubi9" else { - print("Skipping test due to insufficient download availability for ubi9") - return - } - - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.runCommand(Install.self, ["install", "5.7", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + @Test(.testHomeMockedToolchain) func installLatestPatchVersion() async throws { + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load() - guard !config.installedToolchains.isEmpty else { - Issue.record("expected swiftly install latest to install release toolchain but installed toolchains is empty in config") - return - } + guard !config.installedToolchains.isEmpty else { + Issue.record("expected swiftly install latest to install release toolchain but installed toolchains is empty in config") + return + } - let installedToolchain = config.installedToolchains.first! + let installedToolchain = config.installedToolchains.first! - guard case let .stable(release) = installedToolchain else { - Issue.record("expected swiftly install latest to install release toolchain but got \(installedToolchain)") - return - } + guard case let .stable(release) = installedToolchain else { + Issue.record("expected swiftly install latest to install release toolchain but got \(installedToolchain)") + return + } - // As of writing this, 5.7.3 is the latest 5.7 patch release. Assert it is at least that new. - #expect(release >= ToolchainVersion.StableRelease(major: 5, minor: 7, patch: 3)) + // As of writing this, 5.7.3 is the latest 5.7 patch release. Assert it is at least that new. + #expect(release >= ToolchainVersion.StableRelease(major: 5, minor: 7, patch: 3)) - try await SwiftlyTests.validateInstalledToolchains([installedToolchain], description: "install latest") - } - } + try await SwiftlyTests.validateInstalledToolchains([installedToolchain], description: "install latest") } /// Tests that swiftly can install different stable release versions by their full a.b.c versions. - @Test func installReleases() async throws { - guard try await SwiftlyTests.baseTestConfig().platform.name != "ubi9" else { - print("Skipping test due to insufficient download availability for ubi9") - return - } - - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - var installedToolchains: Set = [] + @Test(.testHomeMockedToolchain) func installReleases() async throws { + var installedToolchains: Set = [] - try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - installedToolchains.insert(ToolchainVersion(major: 5, minor: 7, patch: 0)) - try await SwiftlyTests.validateInstalledToolchains( - installedToolchains, - description: "install a stable release toolchain" - ) + installedToolchains.insert(ToolchainVersion(major: 5, minor: 7, patch: 0)) + try await SwiftlyTests.validateInstalledToolchains( + installedToolchains, + description: "install a stable release toolchain" + ) - try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.2", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.2", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - installedToolchains.insert(ToolchainVersion(major: 5, minor: 7, patch: 2)) - try await SwiftlyTests.validateInstalledToolchains( - installedToolchains, - description: "install another stable release toolchain" - ) - } - } + installedToolchains.insert(ToolchainVersion(major: 5, minor: 7, patch: 2)) + try await SwiftlyTests.validateInstalledToolchains( + installedToolchains, + description: "install another stable release toolchain" + ) } /// Tests that swiftly can install main and release snapshots by their full snapshot names. - @Test func installSnapshots() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - var installedToolchains: Set = [] + @Test(.testHomeMockedToolchain) func installSnapshots() async throws { + var installedToolchains: Set = [] - try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - installedToolchains.insert(ToolchainVersion(snapshotBranch: .main, date: "2023-04-01")) - try await SwiftlyTests.validateInstalledToolchains( - installedToolchains, - description: "install a main snapshot toolchain" - ) + installedToolchains.insert(ToolchainVersion(snapshotBranch: .main, date: "2023-04-01")) + try await SwiftlyTests.validateInstalledToolchains( + installedToolchains, + description: "install a main snapshot toolchain" + ) - try await SwiftlyTests.runCommand(Install.self, ["install", "5.9-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.runCommand(Install.self, ["install", "5.9-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - installedToolchains.insert( - ToolchainVersion(snapshotBranch: .release(major: 5, minor: 9), date: "2023-04-01")) - try await SwiftlyTests.validateInstalledToolchains( - installedToolchains, - description: "install a 5.9 snapshot toolchain" - ) - } - } + installedToolchains.insert( + ToolchainVersion(snapshotBranch: .release(major: 5, minor: 9), date: "2023-04-01")) + try await SwiftlyTests.validateInstalledToolchains( + installedToolchains, + description: "install a 5.9 snapshot toolchain" + ) } /// Tests that `swiftly install main-snapshot` installs the latest available main snapshot. - @Test func installLatestMainSnapshot() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + @Test(.testHomeMockedToolchain) func installLatestMainSnapshot() async throws { + try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load() - guard !config.installedToolchains.isEmpty else { - Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") - return - } + guard !config.installedToolchains.isEmpty else { + Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") + return + } - let installedToolchain = config.installedToolchains.first! + let installedToolchain = config.installedToolchains.first! - guard case let .snapshot(snapshot) = installedToolchain, snapshot.branch == .main else { - Issue.record("expected to install latest main snapshot toolchain but got \(installedToolchain)") - return - } + guard case let .snapshot(snapshot) = installedToolchain, snapshot.branch == .main else { + Issue.record("expected to install latest main snapshot toolchain but got \(installedToolchain)") + return + } - // As of writing this, this is the date of the latest main snapshot. Assert it is at least that new. - #expect(snapshot.date >= "2023-04-01") + // As of writing this, this is the date of the latest main snapshot. Assert it is at least that new. + #expect(snapshot.date >= "2023-04-01") - try await SwiftlyTests.validateInstalledToolchains( - [installedToolchain], - description: "install the latest main snapshot toolchain" - ) - } - } + try await SwiftlyTests.validateInstalledToolchains( + [installedToolchain], + description: "install the latest main snapshot toolchain" + ) } /// Tests that `swiftly install a.b-snapshot` installs the latest available a.b release snapshot. - @Test func installLatestReleaseSnapshot() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.runCommand(Install.self, ["install", "6.0-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + @Test(.testHomeMockedToolchain) func installLatestReleaseSnapshot() async throws { + try await SwiftlyTests.runCommand(Install.self, ["install", "6.0-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() + let config = try Config.load() - guard !config.installedToolchains.isEmpty else { - Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") - return - } + guard !config.installedToolchains.isEmpty else { + Issue.record("expected to install latest main snapshot toolchain but installed toolchains is empty in the config") + return + } - let installedToolchain = config.installedToolchains.first! + let installedToolchain = config.installedToolchains.first! - guard case let .snapshot(snapshot) = installedToolchain, snapshot.branch == .release(major: 6, minor: 0) else { - Issue.record("expected swiftly install 6.0-snapshot to install snapshot toolchain but got \(installedToolchain)") - return - } + guard case let .snapshot(snapshot) = installedToolchain, snapshot.branch == .release(major: 6, minor: 0) else { + Issue.record("expected swiftly install 6.0-snapshot to install snapshot toolchain but got \(installedToolchain)") + return + } - // As of writing this, this is the date of the latest 5.7 snapshot. Assert it is at least that new. - #expect(snapshot.date >= "2024-06-18") + // As of writing this, this is the date of the latest 5.7 snapshot. Assert it is at least that new. + #expect(snapshot.date >= "2024-06-18") - try await SwiftlyTests.validateInstalledToolchains( - [installedToolchain], - description: "install the latest 6.0 snapshot toolchain" - ) - } - } + try await SwiftlyTests.validateInstalledToolchains( + [installedToolchain], + description: "install the latest 6.0 snapshot toolchain" + ) } /// Tests that swiftly can install both stable release toolchains and snapshot toolchains. - @Test func installReleaseAndSnapshots() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + @Test(.testHomeMockedToolchain) func installReleaseAndSnapshots() async throws { + try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await SwiftlyTests.runCommand(Install.self, ["install", "5.9-snapshot-2023-03-28", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.runCommand(Install.self, ["install", "5.9-snapshot-2023-03-28", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await SwiftlyTests.runCommand(Install.self, ["install", "5.8.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.runCommand(Install.self, ["install", "5.8.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await SwiftlyTests.validateInstalledToolchains( - [ - ToolchainVersion(snapshotBranch: .main, date: "2023-04-01"), - ToolchainVersion(snapshotBranch: .release(major: 5, minor: 9), date: "2023-03-28"), - ToolchainVersion(major: 5, minor: 8, patch: 0), - ], - description: "install both snapshots and releases" - ) - } - } + try await SwiftlyTests.validateInstalledToolchains( + [ + ToolchainVersion(snapshotBranch: .main, date: "2023-04-01"), + ToolchainVersion(snapshotBranch: .release(major: 5, minor: 9), date: "2023-03-28"), + ToolchainVersion(major: 5, minor: 8, patch: 0), + ], + description: "install both snapshots and releases" + ) } func duplicateTest(_ version: String) async throws { @@ -247,40 +209,27 @@ import Testing } /// Verify that the installed toolchain will be used if no toolchains currently are installed. - @Test func installUsesFirstToolchain() async throws { - guard try await SwiftlyTests.baseTestConfig().platform.name != "ubi9" else { - print("Skipping test due to insufficient download availability for ubi9") - return - } + @Test(.testHomeMockedToolchain) func installUsesFirstToolchain() async throws { + let config = try Config.load() + #expect(config.inUse == nil) + try await SwiftlyTests.validateInUse(expected: nil) - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - let config = try Config.load() - #expect(config.inUse == nil) - try await SwiftlyTests.validateInUse(expected: nil) + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.validateInUse(expected: ToolchainVersion(major: 5, minor: 7, patch: 0)) - try await SwiftlyTests.validateInUse(expected: ToolchainVersion(major: 5, minor: 7, patch: 0)) + try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.1", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.1", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - - // Verify that 5.7.0 is still in use. - try await SwiftlyTests.validateInUse(expected: ToolchainVersion(major: 5, minor: 7, patch: 0)) - } - } + // Verify that 5.7.0 is still in use. + try await SwiftlyTests.validateInUse(expected: ToolchainVersion(major: 5, minor: 7, patch: 0)) } /// Verify that the installed toolchain will be marked as in-use if the --use flag is specified. - @Test func installUseFlag() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.installMockedToolchain(toolchain: SwiftlyTests.oldStable) - try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.oldStable.name]) - try await SwiftlyTests.validateInUse(expected: SwiftlyTests.oldStable) - try await SwiftlyTests.installMockedToolchain(selector: SwiftlyTests.newStable.name, args: ["--use"]) - try await SwiftlyTests.validateInUse(expected: SwiftlyTests.newStable) - } - } + @Test(.testHomeMockedToolchain) func installUseFlag() async throws { + try await SwiftlyTests.installMockedToolchain(toolchain: SwiftlyTests.oldStable) + try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.oldStable.name]) + try await SwiftlyTests.validateInUse(expected: SwiftlyTests.oldStable) + try await SwiftlyTests.installMockedToolchain(selector: SwiftlyTests.newStable.name, args: ["--use"]) + try await SwiftlyTests.validateInUse(expected: SwiftlyTests.newStable) } } diff --git a/Tests/SwiftlyTests/ListTests.swift b/Tests/SwiftlyTests/ListTests.swift index 31954ee3..e38e132b 100644 --- a/Tests/SwiftlyTests/ListTests.swift +++ b/Tests/SwiftlyTests/ListTests.swift @@ -153,20 +153,18 @@ import Testing } } - /// Tests that `list` properly handles the case where no toolchains been installed yet. - @Test func listEmpty() async throws { - try await SwiftlyTests.withTestHome { - var toolchains = try await self.runList(selector: nil) - #expect(toolchains == []) + /// Tests that `list` properly handles the case where no toolchains have been installed yet. + @Test(.testHome) func listEmpty() async throws { + var toolchains = try await self.runList(selector: nil) + #expect(toolchains == []) - toolchains = try await self.runList(selector: "5") - #expect(toolchains == []) + toolchains = try await self.runList(selector: "5") + #expect(toolchains == []) - toolchains = try await self.runList(selector: "main-snapshot") - #expect(toolchains == []) + toolchains = try await self.runList(selector: "main-snapshot") + #expect(toolchains == []) - toolchains = try await self.runList(selector: "5.7-snapshot") - #expect(toolchains == []) - } + toolchains = try await self.runList(selector: "5.7-snapshot") + #expect(toolchains == []) } } diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index 8f78531b..5374d24f 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -17,60 +17,56 @@ import Testing return (mockedToolchainFile, version) } - @Test func install() async throws { - try await SwiftlyTests.withTestHome { - // GIVEN: a toolchain has been downloaded - var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.7.1") - // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) - // THEN: the toolchain is extracted in the toolchains directory - var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) - #expect(1 == toolchains.count) + @Test(.testHome) func install() async throws { + // GIVEN: a toolchain has been downloaded + var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.7.1") + // WHEN: the platform installs the toolchain + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + // THEN: the toolchain is extracted in the toolchains directory + var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(1 == toolchains.count) - // GIVEN: a second toolchain has been downloaded - (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") - // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) - // THEN: the toolchain is added to the toolchains directory - toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) - #expect(2 == toolchains.count) + // GIVEN: a second toolchain has been downloaded + (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") + // WHEN: the platform installs the toolchain + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + // THEN: the toolchain is added to the toolchains directory + toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(2 == toolchains.count) - // GIVEN: an identical toolchain has been downloaded - (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") - // WHEN: the platform installs the toolchain - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) - // THEN: the toolchains directory remains the same - toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) - #expect(2 == toolchains.count) - } + // GIVEN: an identical toolchain has been downloaded + (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") + // WHEN: the platform installs the toolchain + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + // THEN: the toolchains directory remains the same + toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(2 == toolchains.count) } - @Test func uninstall() async throws { - try await SwiftlyTests.withTestHome { - // GIVEN: toolchains have been downloaded, and installed - var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) - (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.6.3") - try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) - // WHEN: one of the toolchains is uninstalled - try Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, version, verbose: true) - // THEN: there is only one remaining toolchain installed - var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) - #expect(1 == toolchains.count) + @Test(.testHome) func uninstall() async throws { + // GIVEN: toolchains have been downloaded, and installed + var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.6.3") + try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) + // WHEN: one of the toolchains is uninstalled + try Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, version, verbose: true) + // THEN: there is only one remaining toolchain installed + var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(1 == toolchains.count) - // GIVEN; there is only one toolchain installed - // WHEN: a non-existent toolchain is uninstalled - try? Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.9.1"), verbose: true) - // THEN: there is the one remaining toolchain that is still installed - toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) - #expect(1 == toolchains.count) + // GIVEN; there is only one toolchain installed + // WHEN: a non-existent toolchain is uninstalled + try? Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.9.1"), verbose: true) + // THEN: there is the one remaining toolchain that is still installed + toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(1 == toolchains.count) - // GIVEN: there is only one toolchain installed - // WHEN: the last toolchain is uninstalled - try Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.8.0"), verbose: true) - // THEN: there are no toolchains installed - toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) - #expect(0 == toolchains.count) - } + // GIVEN: there is only one toolchain installed + // WHEN: the last toolchain is uninstalled + try Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, ToolchainVersion(parsing: "5.8.0"), verbose: true) + // THEN: there are no toolchains installed + toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx), includingPropertiesForKeys: nil) + #expect(0 == toolchains.count) } } diff --git a/Tests/SwiftlyTests/RunTests.swift b/Tests/SwiftlyTests/RunTests.swift index 75416d1b..9f8984e8 100644 --- a/Tests/SwiftlyTests/RunTests.swift +++ b/Tests/SwiftlyTests/RunTests.swift @@ -7,42 +7,38 @@ import Testing static let homeName = "runTests" /// Tests that the `run` command can switch between installed toolchains. - @Test func runSelection() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - // GIVEN: a set of installed toolchains - // WHEN: invoking the run command with a selector argument for that toolchain - var output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version", "+\(SwiftlyTests.newStable.name)"]) - // THEN: the output confirms that it ran with the selected toolchain - #expect(output.contains(SwiftlyTests.newStable.name)) + @Test(.mockHomeAllToolchains) func runSelection() async throws { + // GIVEN: a set of installed toolchains + // WHEN: invoking the run command with a selector argument for that toolchain + var output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version", "+\(SwiftlyTests.newStable.name)"]) + // THEN: the output confirms that it ran with the selected toolchain + #expect(output.contains(SwiftlyTests.newStable.name)) - // GIVEN: a set of installed toolchains and one is selected with a .swift-version file - let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") - try SwiftlyTests.oldStable.name.write(to: versionFile, atomically: true, encoding: .utf8) - // WHEN: invoking the run command without any selector arguments for toolchains - output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version"]) - // THEN: the output confirms that it ran with the selected toolchain - #expect(output.contains(SwiftlyTests.oldStable.name)) + // GIVEN: a set of installed toolchains and one is selected with a .swift-version file + let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") + try SwiftlyTests.oldStable.name.write(to: versionFile, atomically: true, encoding: .utf8) + // WHEN: invoking the run command without any selector arguments for toolchains + output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version"]) + // THEN: the output confirms that it ran with the selected toolchain + #expect(output.contains(SwiftlyTests.oldStable.name)) - // GIVEN: a set of installed toolchains - // WHEN: invoking the run command with a selector argument for a toolchain that isn't installed - do { - try await SwiftlyTests.runCommand(Run.self, ["run", "swift", "+1.2.3", "--version"]) - #expect(false) - } catch let e as SwiftlyError { - #expect(e.message.contains("didn't match any of the installed toolchains")) - } - // THEN: an error is shown because there is no matching toolchain that is installed + // GIVEN: a set of installed toolchains + // WHEN: invoking the run command with a selector argument for a toolchain that isn't installed + do { + try await SwiftlyTests.runCommand(Run.self, ["run", "swift", "+1.2.3", "--version"]) + #expect(false) + } catch let e as SwiftlyError { + #expect(e.message.contains("didn't match any of the installed toolchains")) } + // THEN: an error is shown because there is no matching toolchain that is installed } /// Tests the `run` command verifying that the environment is as expected - @Test func runEnvironment() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - // The toolchains directory should be the fist entry on the path - let output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", try await Swiftly.currentPlatform.getShell(), "-c", "echo $PATH"]) - #expect(output.count == 1) - #expect(output[0].contains(Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).path)) - } + @Test(.mockHomeAllToolchains) func runEnvironment() async throws { + // The toolchains directory should be the fist entry on the path + let output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", try await Swiftly.currentPlatform.getShell(), "-c", "echo $PATH"]) + #expect(output.count == 1) + #expect(output[0].contains(Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).path)) } /// Tests the extraction of proxy arguments from the run command arguments. diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 0b867127..7118a090 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -18,23 +18,25 @@ struct SwiftlyTestError: LocalizedError { let message: String } +let unmockedMsg = "All swiftly test case logic must be mocked in order to prevent mutation of the system running the test. This test must either run swiftly components inside a SwiftlyTests.with... closure, or it must have one of the @Test traits, such as @Test(.testHome), or @Test(.mock...)" + struct OutputHandlerFail: OutputHandler { func handleOutputLine(_: String) { - fatalError("core context was not mocked. put the test case in a SwiftlyTests.with() before running it.") + fatalError(unmockedMsg) } } struct InputProviderFail: InputProvider { func readLine() -> String? { - fatalError("core context was not mocked. put the test case in a SwiftlyTests.with() before running it.") + fatalError(unmockedMsg) } } struct HTTPRequestExecutorFail: HTTPRequestExecutor { - func execute(_: HTTPClientRequest, timeout _: TimeAmount) async throws -> HTTPClientResponse { fatalError("http request executor was not mocked. put test case in a SwiftlyTests.withMocked() before running it.") } - func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { fatalError("http request executor was not mocked. put test case in a SwiftlyTests.withMocked() before running it.") } - func getReleaseToolchains() async throws -> [Components.Schemas.Release] { fatalError("http request executor was not mocked. put test case in a SwiftlyTests.withMocked() before running it.") } - func getSnapshotToolchains(branch _: Components.Schemas.SourceBranch, platform _: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { fatalError("http request executor was not mocked. put test case in a SwiftlyTests.withMocked() before running it.") } + func execute(_: HTTPClientRequest, timeout _: TimeAmount) async throws -> HTTPClientResponse { fatalError(unmockedMsg) } + func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { fatalError(unmockedMsg) } + func getReleaseToolchains() async throws -> [Components.Schemas.Release] { fatalError(unmockedMsg) } + func getSnapshotToolchains(branch _: Components.Schemas.SourceBranch, platform _: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { fatalError(unmockedMsg) } } // Convenience extensions to common Swiftly and SwiftlyCore types to set the correct context @@ -66,6 +68,63 @@ extension SwiftlyCoreContext { } } +// Convenience test scoping traits + +struct TestHomeTrait: TestTrait, TestScoping { + func provideScope(for _: Test, testCase _: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { + try await SwiftlyTests.withTestHome { + try await function() + } + } +} + +extension Trait where Self == TestHomeTrait { + /// Run the test with a test home directory. + static var testHome: Self { Self() } +} + +struct MockHomeAllToolchainsTrait: TestTrait, TestScoping { + func provideScope(for _: Test, testCase _: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { + try await SwiftlyTests.withMockedHome(homeName: "testHome", toolchains: SwiftlyTests.allToolchains) { + try await function() + } + } +} + +extension Trait where Self == MockHomeAllToolchainsTrait { + /// Run the test with this trait to get a mocked home directory with a predefined collection of toolchains already installed. + static var mockHomeAllToolchains: Self { Self() } +} + +struct MockHomeNoToolchainsTrait: TestTrait, TestScoping { + func provideScope(for _: Test, testCase _: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { + try await SwiftlyTests.withMockedHome(homeName: UseTests.homeName, toolchains: []) { + try await function() + } + } +} + +extension Trait where Self == MockHomeNoToolchainsTrait { + /// Run the test with this trait to get a mocked home directory without any toolchains installed there yet. + static var mockHomeNoToolchains: Self { Self() } +} + +struct TestHomeMockedToolchainTrait: TestTrait, TestScoping { + func provideScope(for _: Test, testCase _: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { + try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withMockedToolchain { + try await function() + } + } + } +} + +extension Trait where Self == TestHomeMockedToolchainTrait { + /// Run the test with this trait to get a test home directory and a mocked + /// toolchain can be installed by request, at any version. + static var testHomeMockedToolchain: Self { Self() } +} + public enum SwiftlyTests { @TaskLocal static var ctx: SwiftlyCoreContext = .init( mockedHomeDir: URL(fileURLWithPath: "/does/not/exist"), diff --git a/Tests/SwiftlyTests/UpdateTests.swift b/Tests/SwiftlyTests/UpdateTests.swift index eec7700a..dc7f4aa1 100644 --- a/Tests/SwiftlyTests/UpdateTests.swift +++ b/Tests/SwiftlyTests/UpdateTests.swift @@ -5,153 +5,125 @@ import Testing @Suite struct UpdateTests { /// Verify updating the most up-to-date toolchain has no effect. - @Test func updateLatest() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.installMockedToolchain(selector: .latest) + @Test(.testHomeMockedToolchain) func updateLatest() async throws { + try await SwiftlyTests.installMockedToolchain(selector: .latest) - let beforeUpdateConfig = try Config.load() + let beforeUpdateConfig = try Config.load() - try await SwiftlyTests.runCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.runCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - #expect(try Config.load() == beforeUpdateConfig) - try await SwiftlyTests.validateInstalledToolchains( - beforeUpdateConfig.installedToolchains, - description: "Updating latest toolchain should have no effect" - ) - } - } + #expect(try Config.load() == beforeUpdateConfig) + try await SwiftlyTests.validateInstalledToolchains( + beforeUpdateConfig.installedToolchains, + description: "Updating latest toolchain should have no effect" + ) } /// Verify that attempting to update when no toolchains are installed has no effect. - @Test func updateLatestWithNoToolchains() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.runCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - - try await SwiftlyTests.validateInstalledToolchains( - [], - description: "Updating should not install any toolchains" - ) - } - } + @Test(.testHomeMockedToolchain) func updateLatestWithNoToolchains() async throws { + try await SwiftlyTests.runCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + try await SwiftlyTests.validateInstalledToolchains( + [], + description: "Updating should not install any toolchains" + ) } /// Verify that updating the latest installed toolchain updates it to the latest available toolchain. - @Test func updateLatestToLatest() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) - try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - - let config = try Config.load() - let inUse = config.inUse!.asStableRelease! - - #expect(inUse > .init(major: 5, minor: 9, patch: 0)) - try await SwiftlyTests.validateInstalledToolchains( - [config.inUse!], - description: "Updating toolchain should properly install new toolchain and uninstall old" - ) - } - } + @Test(.testHomeMockedToolchain) func updateLatestToLatest() async throws { + try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + let config = try Config.load() + let inUse = config.inUse!.asStableRelease! + + #expect(inUse > .init(major: 5, minor: 9, patch: 0)) + try await SwiftlyTests.validateInstalledToolchains( + [config.inUse!], + description: "Updating toolchain should properly install new toolchain and uninstall old" + ) } /// Verify that the latest installed toolchain for a given major version can be updated to the latest /// released minor version. - @Test func updateToLatestMinor() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) - try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - - let config = try Config.load() - let inUse = config.inUse!.asStableRelease! - - #expect(inUse.major == 5) - #expect(inUse.minor > 0) - - try await SwiftlyTests.validateInstalledToolchains( - [config.inUse!], - description: "Updating toolchain should properly install new toolchain and uninstall old" - ) - } - } + @Test(.testHomeMockedToolchain) func updateToLatestMinor() async throws { + try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + let config = try Config.load() + let inUse = config.inUse!.asStableRelease! + + #expect(inUse.major == 5) + #expect(inUse.minor > 0) + + try await SwiftlyTests.validateInstalledToolchains( + [config.inUse!], + description: "Updating toolchain should properly install new toolchain and uninstall old" + ) } /// Verify that a toolchain can be updated to the latest patch version of that toolchain's minor version. - @Test func updateToLatestPatch() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.installMockedToolchain(selector: "5.9.0") + @Test(.testHomeMockedToolchain) func updateToLatestPatch() async throws { + try await SwiftlyTests.installMockedToolchain(selector: "5.9.0") - try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5.9.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5.9.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let config = try Config.load() - let inUse = config.inUse!.asStableRelease! + let config = try Config.load() + let inUse = config.inUse!.asStableRelease! - #expect(inUse.major == 5) - #expect(inUse.minor == 9) - #expect(inUse.patch > 0) + #expect(inUse.major == 5) + #expect(inUse.minor == 9) + #expect(inUse.patch > 0) - try await SwiftlyTests.validateInstalledToolchains( - [config.inUse!], - description: "Updating toolchain should properly install new toolchain and uninstall old" - ) - } - } + try await SwiftlyTests.validateInstalledToolchains( + [config.inUse!], + description: "Updating toolchain should properly install new toolchain and uninstall old" + ) } /// Verifies that updating the currently global default toolchain can be updated, and that after update the new toolchain /// will be the global default instead. - @Test func updateGlobalDefault() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") - - try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - - let config = try Config.load() - let inUse = config.inUse!.asStableRelease! - #expect(inUse > .init(major: 6, minor: 0, patch: 0)) - #expect(inUse.major == 6) - #expect(inUse.minor == 0) - #expect(inUse.patch > 0) - - try await SwiftlyTests.validateInstalledToolchains( - [config.inUse!], - description: "update should update the in use toolchain to latest patch" - ) - - try await SwiftlyTests.validateInUse(expected: config.inUse!) - } - } + @Test(.testHomeMockedToolchain) func updateGlobalDefault() async throws { + try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") + + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + let config = try Config.load() + let inUse = config.inUse!.asStableRelease! + #expect(inUse > .init(major: 6, minor: 0, patch: 0)) + #expect(inUse.major == 6) + #expect(inUse.minor == 0) + #expect(inUse.patch > 0) + + try await SwiftlyTests.validateInstalledToolchains( + [config.inUse!], + description: "update should update the in use toolchain to latest patch" + ) + + try await SwiftlyTests.validateInUse(expected: config.inUse!) } /// Verifies that updating the currently in-use toolchain can be updated, and that after update the new toolchain /// will be in-use with the swift version file updated. - @Test func updateInUse() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") + @Test(.testHomeMockedToolchain) func updateInUse() async throws { + try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") - let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") - try "6.0.0".write(to: versionFile, atomically: true, encoding: .utf8) + let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") + try "6.0.0".write(to: versionFile, atomically: true, encoding: .utf8) - try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - let versionFileContents = try String(contentsOf: versionFile, encoding: .utf8) - let inUse = try ToolchainVersion(parsing: versionFileContents) - #expect(inUse > .init(major: 6, minor: 0, patch: 0)) + let versionFileContents = try String(contentsOf: versionFile, encoding: .utf8) + let inUse = try ToolchainVersion(parsing: versionFileContents) + #expect(inUse > .init(major: 6, minor: 0, patch: 0)) - // Since the global default was set to 6.0.0, and that toolchain is no longer installed - // the update should have unset it to prevent the config from going into a bad state. - let config = try Config.load() - #expect(config.inUse == nil) + // Since the global default was set to 6.0.0, and that toolchain is no longer installed + // the update should have unset it to prevent the config from going into a bad state. + let config = try Config.load() + #expect(config.inUse == nil) - // The new toolchain should be installed - #expect(config.installedToolchains.contains(inUse)) - } - } + // The new toolchain should be installed + #expect(config.installedToolchains.contains(inUse)) } /// Verifies that snapshots, both from the main branch and from development branches, can be updated. @@ -187,26 +159,22 @@ import Testing } /// Verify that the latest of all the matching release toolchains is updated. - @Test func updateSelectsLatestMatchingStableRelease() async throws { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.installMockedToolchain(selector: "6.0.1") - try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") - - try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "6.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) - - let config = try Config.load() - let inUse = config.inUse!.asStableRelease! - #expect(inUse.major == 6) - #expect(inUse.minor == 0) - #expect(inUse.patch > 1) - - try await SwiftlyTests.validateInstalledToolchains( - [config.inUse!, .init(major: 6, minor: 0, patch: 0)], - description: "update with ambiguous selector should update the latest matching toolchain" - ) - } - } + @Test(.testHomeMockedToolchain) func updateSelectsLatestMatchingStableRelease() async throws { + try await SwiftlyTests.installMockedToolchain(selector: "6.0.1") + try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") + + try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "6.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + let config = try Config.load() + let inUse = config.inUse!.asStableRelease! + #expect(inUse.major == 6) + #expect(inUse.minor == 0) + #expect(inUse.patch > 1) + + try await SwiftlyTests.validateInstalledToolchains( + [config.inUse!, .init(major: 6, minor: 0, patch: 0)], + description: "update with ambiguous selector should update the latest matching toolchain" + ) } /// Verify that the latest of all the matching snapshot toolchains is updated. diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index d8759247..a6651ee0 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -15,193 +15,173 @@ import Testing } /// Tests that the `use` command can switch between installed stable release toolchains. - @Test func useStable() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) - } + @Test(.mockHomeAllToolchains) func useStable() async throws { + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.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 func useLatestStable() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - // Use an older toolchain. - try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) + @Test(.mockHomeAllToolchains) func useLatestStable() async throws { + // Use an older toolchain. + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) - // Use latest, assert that it switched to the latest installed stable release. - try await self.useAndValidate(argument: "latest", expectedVersion: SwiftlyTests.newStable) + // Use latest, assert that it switched to the latest installed stable release. + try await self.useAndValidate(argument: "latest", expectedVersion: SwiftlyTests.newStable) - // Try to use latest again, assert no error was thrown and no changes were made. - try await self.useAndValidate(argument: "latest", expectedVersion: SwiftlyTests.newStable) + // Try to use latest again, assert no error was thrown and no changes were made. + try await self.useAndValidate(argument: "latest", expectedVersion: SwiftlyTests.newStable) - // Explicitly specify the current latest toolchain, assert no errors and no changes were made. - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) + // Explicitly specify the current latest toolchain, assert no errors and no changes were made. + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) - // Switch back to the old toolchain, verify it works. - try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) - } + // Switch back to the old toolchain, verify it works. + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.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 func useLatestStablePatch() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) - - let oldStableVersion = SwiftlyTests.oldStable.asStableRelease! - - // Drop the patch version and assert that the latest patch of the provided major.minor was chosen. - try await self.useAndValidate( - argument: "\(oldStableVersion.major).\(oldStableVersion.minor)", - expectedVersion: SwiftlyTests.oldStableNewPatch - ) - - // Assert that selecting it again doesn't change anything. - try await self.useAndValidate( - argument: "\(oldStableVersion.major).\(oldStableVersion.minor)", - expectedVersion: SwiftlyTests.oldStableNewPatch - ) - - // 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: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) - let latestPatch = SwiftlyTests.oldStableNewPatch.asStableRelease!.patch - try await self.useAndValidate( - argument: "\(oldStableVersion.major).\(oldStableVersion.minor).\(latestPatch + 1)", - expectedVersion: SwiftlyTests.oldStable - ) - } + @Test(.mockHomeAllToolchains) func useLatestStablePatch() async throws { + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) + + let oldStableVersion = SwiftlyTests.oldStable.asStableRelease! + + // Drop the patch version and assert that the latest patch of the provided major.minor was chosen. + try await self.useAndValidate( + argument: "\(oldStableVersion.major).\(oldStableVersion.minor)", + expectedVersion: SwiftlyTests.oldStableNewPatch + ) + + // Assert that selecting it again doesn't change anything. + try await self.useAndValidate( + argument: "\(oldStableVersion.major).\(oldStableVersion.minor)", + expectedVersion: SwiftlyTests.oldStableNewPatch + ) + + // 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: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) + let latestPatch = SwiftlyTests.oldStableNewPatch.asStableRelease!.patch + try await self.useAndValidate( + argument: "\(oldStableVersion.major).\(oldStableVersion.minor).\(latestPatch + 1)", + expectedVersion: SwiftlyTests.oldStable + ) } /// Tests that the `use` command can switch between installed main snapshot toolchains. - @Test func useMainSnapshot() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - // Switch to a non-snapshot. - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) - try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) - try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) - // Verify that using the same snapshot again doesn't throw an error. - try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) - try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) - } + @Test(.mockHomeAllToolchains) func useMainSnapshot() async throws { + // Switch to a non-snapshot. + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) + try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) + try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) + // Verify that using the same snapshot again doesn't throw an error. + try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) + try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) } /// Tests that the latest installed main snapshot toolchain can be selected by omitting the /// date (e.g. `use main-snapshot`). - @Test func useLatestMainSnapshot() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - // Switch to a non-snapshot. - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) - // Switch to the latest main snapshot. - try await self.useAndValidate(argument: "main-snapshot", expectedVersion: SwiftlyTests.newMainSnapshot) - // Switch to it again, assert no errors or changes were made. - try await self.useAndValidate(argument: "main-snapshot", expectedVersion: SwiftlyTests.newMainSnapshot) - // Switch to it again, this time by name. Assert no errors or changes were made. - try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) - // Switch to an older snapshot, verify it works. - try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) - } + @Test(.mockHomeAllToolchains) func useLatestMainSnapshot() async throws { + // Switch to a non-snapshot. + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) + // Switch to the latest main snapshot. + try await self.useAndValidate(argument: "main-snapshot", expectedVersion: SwiftlyTests.newMainSnapshot) + // Switch to it again, assert no errors or changes were made. + try await self.useAndValidate(argument: "main-snapshot", expectedVersion: SwiftlyTests.newMainSnapshot) + // Switch to it again, this time by name. Assert no errors or changes were made. + try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) + // Switch to an older snapshot, verify it works. + try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) } /// Tests that the `use` command can switch between installed release snapshot toolchains. - @Test func useReleaseSnapshot() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - // Switch to a non-snapshot. - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) - try await self.useAndValidate( - argument: SwiftlyTests.oldReleaseSnapshot.name, - expectedVersion: SwiftlyTests.oldReleaseSnapshot - ) - try await self.useAndValidate( - argument: SwiftlyTests.newReleaseSnapshot.name, - expectedVersion: SwiftlyTests.newReleaseSnapshot - ) - // Verify that using the same snapshot again doesn't throw an error. - try await self.useAndValidate( - argument: SwiftlyTests.newReleaseSnapshot.name, - expectedVersion: SwiftlyTests.newReleaseSnapshot - ) - try await self.useAndValidate( - argument: SwiftlyTests.oldReleaseSnapshot.name, - expectedVersion: SwiftlyTests.oldReleaseSnapshot - ) - } + @Test(.mockHomeAllToolchains) func useReleaseSnapshot() async throws { + // Switch to a non-snapshot. + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) + try await self.useAndValidate( + argument: SwiftlyTests.oldReleaseSnapshot.name, + expectedVersion: SwiftlyTests.oldReleaseSnapshot + ) + try await self.useAndValidate( + argument: SwiftlyTests.newReleaseSnapshot.name, + expectedVersion: SwiftlyTests.newReleaseSnapshot + ) + // Verify that using the same snapshot again doesn't throw an error. + try await self.useAndValidate( + argument: SwiftlyTests.newReleaseSnapshot.name, + expectedVersion: SwiftlyTests.newReleaseSnapshot + ) + try await self.useAndValidate( + argument: SwiftlyTests.oldReleaseSnapshot.name, + expectedVersion: SwiftlyTests.oldReleaseSnapshot + ) } /// Tests that the latest installed release snapshot toolchain can be selected by omitting the /// date (e.g. `use 5.7-snapshot`). - @Test func useLatestReleaseSnapshot() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - // Switch to a non-snapshot. - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) - // Switch to the latest snapshot for the given release. - guard case let .release(major, minor) = SwiftlyTests.newReleaseSnapshot.asSnapshot!.branch else { - fatalError("expected release in snapshot release version") - } - try await self.useAndValidate( - argument: "\(major).\(minor)-snapshot", - expectedVersion: SwiftlyTests.newReleaseSnapshot - ) - // Switch to it again, assert no errors or changes were made. - try await self.useAndValidate( - argument: "\(major).\(minor)-snapshot", - expectedVersion: SwiftlyTests.newReleaseSnapshot - ) - // Switch to it again, this time by name. Assert no errors or changes were made. - try await self.useAndValidate( - argument: SwiftlyTests.newReleaseSnapshot.name, - expectedVersion: SwiftlyTests.newReleaseSnapshot - ) - // Switch to an older snapshot, verify it works. - try await self.useAndValidate( - argument: SwiftlyTests.oldReleaseSnapshot.name, - expectedVersion: SwiftlyTests.oldReleaseSnapshot - ) + @Test(.mockHomeAllToolchains) func useLatestReleaseSnapshot() async throws { + // Switch to a non-snapshot. + try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) + // Switch to the latest snapshot for the given release. + guard case let .release(major, minor) = SwiftlyTests.newReleaseSnapshot.asSnapshot!.branch else { + fatalError("expected release in snapshot release version") } + try await self.useAndValidate( + argument: "\(major).\(minor)-snapshot", + expectedVersion: SwiftlyTests.newReleaseSnapshot + ) + // Switch to it again, assert no errors or changes were made. + try await self.useAndValidate( + argument: "\(major).\(minor)-snapshot", + expectedVersion: SwiftlyTests.newReleaseSnapshot + ) + // Switch to it again, this time by name. Assert no errors or changes were made. + try await self.useAndValidate( + argument: SwiftlyTests.newReleaseSnapshot.name, + expectedVersion: SwiftlyTests.newReleaseSnapshot + ) + // Switch to an older snapshot, verify it works. + try await self.useAndValidate( + argument: SwiftlyTests.oldReleaseSnapshot.name, + expectedVersion: SwiftlyTests.oldReleaseSnapshot + ) } /// Tests that the `use` command gracefully exits when executed before any toolchains have been installed. - @Test func useNoInstalledToolchains() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: []) { - try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "latest"]) + @Test(.mockHomeNoToolchains) func useNoInstalledToolchains() async throws { + try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "latest"]) - var config = try Config.load() - #expect(config.inUse == nil) + var config = try Config.load() + #expect(config.inUse == nil) - try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "5.6.0"]) + try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "5.6.0"]) - config = try Config.load() - #expect(config.inUse == nil) - } + config = try Config.load() + #expect(config.inUse == nil) } /// Tests that the `use` command gracefully handles being executed with toolchain names that haven't been installed. - @Test func useNonExistent() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - // Switch to a valid toolchain. - try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) - - // Try various non-existent toolchains. - try await self.useAndValidate(argument: "1.2.3", expectedVersion: SwiftlyTests.oldStable) - try await self.useAndValidate(argument: "5.7-snapshot-1996-01-01", expectedVersion: SwiftlyTests.oldStable) - try await self.useAndValidate(argument: "6.7-snapshot", expectedVersion: SwiftlyTests.oldStable) - try await self.useAndValidate(argument: "main-snapshot-1996-01-01", expectedVersion: SwiftlyTests.oldStable) - } + @Test(.mockHomeAllToolchains) func useNonExistent() async throws { + // Switch to a valid toolchain. + try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) + + // Try various non-existent toolchains. + try await self.useAndValidate(argument: "1.2.3", expectedVersion: SwiftlyTests.oldStable) + try await self.useAndValidate(argument: "5.7-snapshot-1996-01-01", expectedVersion: SwiftlyTests.oldStable) + try await self.useAndValidate(argument: "6.7-snapshot", expectedVersion: SwiftlyTests.oldStable) + try await self.useAndValidate(argument: "main-snapshot-1996-01-01", expectedVersion: SwiftlyTests.oldStable) } /// Tests that the `use` command works with all the installed toolchains in this test harness. - @Test func useAll() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - let config = try Config.load() - - for toolchain in config.installedToolchains { - try await self.useAndValidate( - argument: toolchain.name, - expectedVersion: toolchain - ) - } + @Test(.mockHomeAllToolchains) func useAll() async throws { + let config = try Config.load() + + for toolchain in config.installedToolchains { + try await self.useAndValidate( + argument: toolchain.name, + expectedVersion: toolchain + ) } } From d97e2036838939ebd0b82b544d5ce61ca5f0e387 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sat, 5 Apr 2025 16:22:08 -0400 Subject: [PATCH 05/12] Make test scoping traits more versatile and fewer by adding functions to Trait instead of var --- Tests/SwiftlyTests/InitTests.swift | 6 +- Tests/SwiftlyTests/InstallTests.swift | 18 +-- Tests/SwiftlyTests/ListTests.swift | 2 +- Tests/SwiftlyTests/PlatformTests.swift | 4 +- Tests/SwiftlyTests/RunTests.swift | 4 +- Tests/SwiftlyTests/SwiftlyTests.swift | 43 +++---- Tests/SwiftlyTests/UninstallTests.swift | 146 +++++++++++------------- Tests/SwiftlyTests/UpdateTests.swift | 16 +-- Tests/SwiftlyTests/UseTests.swift | 20 ++-- 9 files changed, 123 insertions(+), 136 deletions(-) diff --git a/Tests/SwiftlyTests/InitTests.swift b/Tests/SwiftlyTests/InitTests.swift index 84d024a9..0de9059e 100644 --- a/Tests/SwiftlyTests/InitTests.swift +++ b/Tests/SwiftlyTests/InitTests.swift @@ -4,7 +4,7 @@ import Foundation import Testing @Suite struct InitTests { - @Test(.testHome) func initFresh() async throws { + @Test(.testHome()) func initFresh() async throws { // GIVEN: a fresh user account without Swiftly installed try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) @@ -62,7 +62,7 @@ import Testing } } - @Test(.testHome) func initOverwrite() async throws { + @Test(.testHome()) func initOverwrite() async throws { // GIVEN: a user account with swiftly already installed try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) @@ -86,7 +86,7 @@ import Testing #expect(!Swiftly.currentPlatform.swiftlyToolchainsDir(SwiftlyTests.ctx).appendingPathComponent("foo.txt").fileExists()) } - @Test(.testHome) func initTwice() async throws { + @Test(.testHome()) func initTwice() async throws { // GIVEN: a user account with swiftly already installed try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index 25645205..f45ab062 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -9,7 +9,7 @@ import Testing /// It stops short of verifying that it actually installs the _most_ recently released version, which is the intended /// behavior, since determining which version is the latest is non-trivial and would require duplicating code /// from within swiftly itself. - @Test(.testHomeMockedToolchain) func installLatest() async throws { + @Test(.testHomeMockedToolchain()) func installLatest() async throws { try await SwiftlyTests.runCommand(Install.self, ["install", "latest", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) let config = try Config.load() @@ -33,7 +33,7 @@ import Testing } /// Tests that `swiftly install a.b` installs the latest patch version of Swift a.b. - @Test(.testHomeMockedToolchain) func installLatestPatchVersion() async throws { + @Test(.testHomeMockedToolchain()) func installLatestPatchVersion() async throws { try await SwiftlyTests.runCommand(Install.self, ["install", "5.7", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) let config = try Config.load() @@ -57,7 +57,7 @@ import Testing } /// Tests that swiftly can install different stable release versions by their full a.b.c versions. - @Test(.testHomeMockedToolchain) func installReleases() async throws { + @Test(.testHomeMockedToolchain()) func installReleases() async throws { var installedToolchains: Set = [] try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) @@ -78,7 +78,7 @@ import Testing } /// Tests that swiftly can install main and release snapshots by their full snapshot names. - @Test(.testHomeMockedToolchain) func installSnapshots() async throws { + @Test(.testHomeMockedToolchain()) func installSnapshots() async throws { var installedToolchains: Set = [] try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) @@ -100,7 +100,7 @@ import Testing } /// Tests that `swiftly install main-snapshot` installs the latest available main snapshot. - @Test(.testHomeMockedToolchain) func installLatestMainSnapshot() async throws { + @Test(.testHomeMockedToolchain()) func installLatestMainSnapshot() async throws { try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) let config = try Config.load() @@ -127,7 +127,7 @@ import Testing } /// Tests that `swiftly install a.b-snapshot` installs the latest available a.b release snapshot. - @Test(.testHomeMockedToolchain) func installLatestReleaseSnapshot() async throws { + @Test(.testHomeMockedToolchain()) func installLatestReleaseSnapshot() async throws { try await SwiftlyTests.runCommand(Install.self, ["install", "6.0-snapshot", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) let config = try Config.load() @@ -154,7 +154,7 @@ import Testing } /// Tests that swiftly can install both stable release toolchains and snapshot toolchains. - @Test(.testHomeMockedToolchain) func installReleaseAndSnapshots() async throws { + @Test(.testHomeMockedToolchain()) func installReleaseAndSnapshots() async throws { try await SwiftlyTests.runCommand(Install.self, ["install", "main-snapshot-2023-04-01", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) try await SwiftlyTests.runCommand(Install.self, ["install", "5.9-snapshot-2023-03-28", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) @@ -209,7 +209,7 @@ import Testing } /// Verify that the installed toolchain will be used if no toolchains currently are installed. - @Test(.testHomeMockedToolchain) func installUsesFirstToolchain() async throws { + @Test(.testHomeMockedToolchain()) func installUsesFirstToolchain() async throws { let config = try Config.load() #expect(config.inUse == nil) try await SwiftlyTests.validateInUse(expected: nil) @@ -225,7 +225,7 @@ import Testing } /// Verify that the installed toolchain will be marked as in-use if the --use flag is specified. - @Test(.testHomeMockedToolchain) func installUseFlag() async throws { + @Test(.testHomeMockedToolchain()) func installUseFlag() async throws { try await SwiftlyTests.installMockedToolchain(toolchain: SwiftlyTests.oldStable) try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.oldStable.name]) try await SwiftlyTests.validateInUse(expected: SwiftlyTests.oldStable) diff --git a/Tests/SwiftlyTests/ListTests.swift b/Tests/SwiftlyTests/ListTests.swift index e38e132b..472ed831 100644 --- a/Tests/SwiftlyTests/ListTests.swift +++ b/Tests/SwiftlyTests/ListTests.swift @@ -154,7 +154,7 @@ import Testing } /// Tests that `list` properly handles the case where no toolchains have been installed yet. - @Test(.testHome) func listEmpty() async throws { + @Test(.testHome(Self.homeName)) func listEmpty() async throws { var toolchains = try await self.runList(selector: nil) #expect(toolchains == []) diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index 5374d24f..7505f152 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -17,7 +17,7 @@ import Testing return (mockedToolchainFile, version) } - @Test(.testHome) func install() async throws { + @Test(.testHome()) func install() async throws { // GIVEN: a toolchain has been downloaded var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.7.1") // WHEN: the platform installs the toolchain @@ -43,7 +43,7 @@ import Testing #expect(2 == toolchains.count) } - @Test(.testHome) func uninstall() async throws { + @Test(.testHome()) func uninstall() async throws { // GIVEN: toolchains have been downloaded, and installed var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) diff --git a/Tests/SwiftlyTests/RunTests.swift b/Tests/SwiftlyTests/RunTests.swift index 9f8984e8..0649af7a 100644 --- a/Tests/SwiftlyTests/RunTests.swift +++ b/Tests/SwiftlyTests/RunTests.swift @@ -7,7 +7,7 @@ import Testing static let homeName = "runTests" /// Tests that the `run` command can switch between installed toolchains. - @Test(.mockHomeAllToolchains) func runSelection() async throws { + @Test(.mockHomeToolchains()) func runSelection() async throws { // GIVEN: a set of installed toolchains // WHEN: invoking the run command with a selector argument for that toolchain var output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version", "+\(SwiftlyTests.newStable.name)"]) @@ -34,7 +34,7 @@ import Testing } /// Tests the `run` command verifying that the environment is as expected - @Test(.mockHomeAllToolchains) func runEnvironment() async throws { + @Test(.mockHomeToolchains()) func runEnvironment() async throws { // The toolchains directory should be the fist entry on the path let output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", try await Swiftly.currentPlatform.getShell(), "-c", "echo $PATH"]) #expect(output.count == 1) diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 7118a090..5c26d530 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -71,8 +71,12 @@ extension SwiftlyCoreContext { // Convenience test scoping traits struct TestHomeTrait: TestTrait, TestScoping { + var name: String = "testHome" + + init(_ name: String) { self.name = name } + func provideScope(for _: Test, testCase _: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { - try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withTestHome(name: self.name) { try await function() } } @@ -80,38 +84,37 @@ struct TestHomeTrait: TestTrait, TestScoping { extension Trait where Self == TestHomeTrait { /// Run the test with a test home directory. - static var testHome: Self { Self() } + static func testHome(_ name: String = "testHome") -> Self { Self(name) } } -struct MockHomeAllToolchainsTrait: TestTrait, TestScoping { - func provideScope(for _: Test, testCase _: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { - try await SwiftlyTests.withMockedHome(homeName: "testHome", toolchains: SwiftlyTests.allToolchains) { - try await function() - } - } -} +struct MockHomeToolchainsTrait: TestTrait, TestScoping { + var name: String = "testHome" + var toolchains: Set = SwiftlyTests.allToolchains -extension Trait where Self == MockHomeAllToolchainsTrait { - /// Run the test with this trait to get a mocked home directory with a predefined collection of toolchains already installed. - static var mockHomeAllToolchains: Self { Self() } -} + init(_ name: String, toolchains: Set) { + self.name = name + self.toolchains = toolchains + } -struct MockHomeNoToolchainsTrait: TestTrait, TestScoping { func provideScope(for _: Test, testCase _: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { - try await SwiftlyTests.withMockedHome(homeName: UseTests.homeName, toolchains: []) { + try await SwiftlyTests.withMockedHome(homeName: self.name, toolchains: self.toolchains) { try await function() } } } -extension Trait where Self == MockHomeNoToolchainsTrait { - /// Run the test with this trait to get a mocked home directory without any toolchains installed there yet. - static var mockHomeNoToolchains: Self { Self() } +extension Trait where Self == MockHomeToolchainsTrait { + /// Run the test with this trait to get a mocked home directory with a predefined collection of toolchains already installed. + static func mockHomeToolchains(_ homeName: String = "testHome", toolchains: Set = SwiftlyTests.allToolchains) -> Self { Self(homeName, toolchains: toolchains) } } struct TestHomeMockedToolchainTrait: TestTrait, TestScoping { + var name: String = "testHome" + + init(_ name: String) { self.name = name } + func provideScope(for _: Test, testCase _: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { - try await SwiftlyTests.withTestHome { + try await SwiftlyTests.withTestHome(name: self.name) { try await SwiftlyTests.withMockedToolchain { try await function() } @@ -122,7 +125,7 @@ struct TestHomeMockedToolchainTrait: TestTrait, TestScoping { extension Trait where Self == TestHomeMockedToolchainTrait { /// Run the test with this trait to get a test home directory and a mocked /// toolchain can be installed by request, at any version. - static var testHomeMockedToolchain: Self { Self() } + static func testHomeMockedToolchain(_ name: String = "testHome") -> Self { Self(name) } } public enum SwiftlyTests { diff --git a/Tests/SwiftlyTests/UninstallTests.swift b/Tests/SwiftlyTests/UninstallTests.swift index fb861080..a069ab70 100644 --- a/Tests/SwiftlyTests/UninstallTests.swift +++ b/Tests/SwiftlyTests/UninstallTests.swift @@ -7,15 +7,13 @@ import Testing static let homeName = "uninstallTests" /// Tests that `swiftly uninstall` successfully handles being invoked when no toolchains have been installed yet. - @Test func uninstallNoInstalledToolchains() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: []) { - _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", "1.2.3"], input: ["y"]) + @Test(.mockHomeToolchains(Self.homeName, toolchains: [])) func uninstallNoInstalledToolchains() async throws { + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", "1.2.3"], input: ["y"]) - try await SwiftlyTests.validateInstalledToolchains( - [], - description: "remove not-installed toolchain" - ) - } + try await SwiftlyTests.validateInstalledToolchains( + [], + description: "remove not-installed toolchain" + ) } /// Tests that `swiftly uninstall latest` successfully uninstalls the latest stable release of Swift. @@ -40,51 +38,47 @@ import Testing } /// Tests that a fully-qualified stable release version can be supplied to `swiftly uninstall`. - @Test func uninstallStableRelease() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - var installed = SwiftlyTests.allToolchains - - for toolchain in SwiftlyTests.allToolchains.filter({ $0.isStableRelease() }) { - _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", toolchain.name], input: ["y"]) - installed.remove(toolchain) - - try await SwiftlyTests.validateInstalledToolchains( - installed, - description: "remove \(toolchain)" - ) - } + @Test(.mockHomeToolchains(Self.homeName)) func uninstallStableRelease() async throws { + var installed = SwiftlyTests.allToolchains - _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", "1.2.3"], input: ["y"]) + for toolchain in SwiftlyTests.allToolchains.filter({ $0.isStableRelease() }) { + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", toolchain.name], input: ["y"]) + installed.remove(toolchain) try await SwiftlyTests.validateInstalledToolchains( installed, - description: "remove not-installed toolchain" + description: "remove \(toolchain)" ) } - } - /// Tests that a fully-qualified snapshot version can be supplied to `swiftly uninstall`. - @Test func uninstallSnapshot() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains) { - var installed = SwiftlyTests.allToolchains + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", "1.2.3"], input: ["y"]) - for toolchain in SwiftlyTests.allToolchains.filter({ $0.isSnapshot() }) { - _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", toolchain.name], input: ["y"]) - installed.remove(toolchain) + try await SwiftlyTests.validateInstalledToolchains( + installed, + description: "remove not-installed toolchain" + ) + } - try await SwiftlyTests.validateInstalledToolchains( - installed, - description: "remove \(toolchain)" - ) - } + /// Tests that a fully-qualified snapshot version can be supplied to `swiftly uninstall`. + @Test(.mockHomeToolchains(Self.homeName)) func uninstallSnapshot() async throws { + var installed = SwiftlyTests.allToolchains - _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", "main-snapshot-2022-01-01"], input: ["y"]) + for toolchain in SwiftlyTests.allToolchains.filter({ $0.isSnapshot() }) { + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", toolchain.name], input: ["y"]) + installed.remove(toolchain) try await SwiftlyTests.validateInstalledToolchains( installed, - description: "remove not-installed toolchain" + description: "remove \(toolchain)" ) } + + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", "main-snapshot-2022-01-01"], input: ["y"]) + + try await SwiftlyTests.validateInstalledToolchains( + installed, + description: "remove not-installed toolchain" + ) } /// Tests that multiple toolchains can be installed at once. @@ -230,18 +224,16 @@ import Testing } /// Tests that uninstalling the last toolchain is handled properly and cleans up any symlinks. - @Test func uninstallLastToolchain() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: [SwiftlyTests.oldStable], inUse: SwiftlyTests.oldStable) { - _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", SwiftlyTests.oldStable.name], input: ["y"]) - let config = try Config.load() - #expect(config.inUse == nil) - - // Ensure all symlinks have been cleaned up. - let symlinks = try FileManager.default.contentsOfDirectory( - atPath: Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx).path - ) - #expect(symlinks == []) - } + @Test(.mockHomeToolchains(Self.homeName, toolchains: [SwiftlyTests.oldStable])) func uninstallLastToolchain() async throws { + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", SwiftlyTests.oldStable.name], input: ["y"]) + let config = try Config.load() + #expect(config.inUse == nil) + + // Ensure all symlinks have been cleaned up. + let symlinks = try FileManager.default.contentsOfDirectory( + atPath: Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx).path + ) + #expect(symlinks == []) } /// Tests that aborting an uninstall works correctly. @@ -260,42 +252,34 @@ import Testing } /// Tests that providing the `-y` argument skips the confirmation prompt. - @Test func uninstallAssumeYes() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: [SwiftlyTests.oldStable, SwiftlyTests.newStable]) { - try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", SwiftlyTests.oldStable.name]) - try await SwiftlyTests.validateInstalledToolchains( - [SwiftlyTests.newStable], - description: "uninstall did not succeed even with -y provided" - ) - } + @Test(.mockHomeToolchains(Self.homeName, toolchains: [SwiftlyTests.oldStable, SwiftlyTests.newStable])) func uninstallAssumeYes() async throws { + try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", SwiftlyTests.oldStable.name]) + try await SwiftlyTests.validateInstalledToolchains( + [SwiftlyTests.newStable], + description: "uninstall did not succeed even with -y provided" + ) } /// Tests that providing "all" as an argument to uninstall will uninstall all toolchains. - @Test func uninstallAll() async throws { - let toolchains = Set([SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.newMainSnapshot, SwiftlyTests.oldReleaseSnapshot]) - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: SwiftlyTests.newMainSnapshot) { - try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", "all"]) - try await SwiftlyTests.validateInstalledToolchains( - [], - description: "uninstall did not uninstall all toolchains" - ) - } + @Test(.mockHomeToolchains(Self.homeName, toolchains: [SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.newMainSnapshot, SwiftlyTests.oldReleaseSnapshot])) func uninstallAll() async throws { + try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", "all"]) + try await SwiftlyTests.validateInstalledToolchains( + [], + description: "uninstall did not uninstall all toolchains" + ) } /// Tests that uninstalling a toolchain that is the global default, but is not in the list of installed toolchains. - @Test func uninstallNotInstalled() async throws { - let toolchains = Set([SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.newMainSnapshot, SwiftlyTests.oldReleaseSnapshot]) - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: SwiftlyTests.newMainSnapshot) { - var config = try Config.load() - config.inUse = SwiftlyTests.newMainSnapshot - config.installedToolchains.remove(SwiftlyTests.newMainSnapshot) - try config.save() - - try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", SwiftlyTests.newMainSnapshot.name]) - try await SwiftlyTests.validateInstalledToolchains( - [SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.oldReleaseSnapshot], - description: "uninstall did not uninstall all toolchains" - ) - } + @Test(.mockHomeToolchains(Self.homeName, toolchains: [SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.newMainSnapshot, SwiftlyTests.oldReleaseSnapshot])) func uninstallNotInstalled() async throws { + var config = try Config.load() + config.inUse = SwiftlyTests.newMainSnapshot + config.installedToolchains.remove(SwiftlyTests.newMainSnapshot) + try config.save() + + try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", SwiftlyTests.newMainSnapshot.name]) + try await SwiftlyTests.validateInstalledToolchains( + [SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.oldReleaseSnapshot], + description: "uninstall did not uninstall all toolchains" + ) } } diff --git a/Tests/SwiftlyTests/UpdateTests.swift b/Tests/SwiftlyTests/UpdateTests.swift index dc7f4aa1..7a5af2fc 100644 --- a/Tests/SwiftlyTests/UpdateTests.swift +++ b/Tests/SwiftlyTests/UpdateTests.swift @@ -5,7 +5,7 @@ import Testing @Suite struct UpdateTests { /// Verify updating the most up-to-date toolchain has no effect. - @Test(.testHomeMockedToolchain) func updateLatest() async throws { + @Test(.testHomeMockedToolchain()) func updateLatest() async throws { try await SwiftlyTests.installMockedToolchain(selector: .latest) let beforeUpdateConfig = try Config.load() @@ -20,7 +20,7 @@ import Testing } /// Verify that attempting to update when no toolchains are installed has no effect. - @Test(.testHomeMockedToolchain) func updateLatestWithNoToolchains() async throws { + @Test(.testHomeMockedToolchain()) func updateLatestWithNoToolchains() async throws { try await SwiftlyTests.runCommand(Update.self, ["update", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) try await SwiftlyTests.validateInstalledToolchains( @@ -30,7 +30,7 @@ import Testing } /// Verify that updating the latest installed toolchain updates it to the latest available toolchain. - @Test(.testHomeMockedToolchain) func updateLatestToLatest() async throws { + @Test(.testHomeMockedToolchain()) func updateLatestToLatest() async throws { try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "latest", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) @@ -46,7 +46,7 @@ import Testing /// Verify that the latest installed toolchain for a given major version can be updated to the latest /// released minor version. - @Test(.testHomeMockedToolchain) func updateToLatestMinor() async throws { + @Test(.testHomeMockedToolchain()) func updateToLatestMinor() async throws { try await SwiftlyTests.installMockedToolchain(selector: .stable(major: 5, minor: 9, patch: 0)) try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) @@ -63,7 +63,7 @@ import Testing } /// Verify that a toolchain can be updated to the latest patch version of that toolchain's minor version. - @Test(.testHomeMockedToolchain) func updateToLatestPatch() async throws { + @Test(.testHomeMockedToolchain()) func updateToLatestPatch() async throws { try await SwiftlyTests.installMockedToolchain(selector: "5.9.0") try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "5.9.0", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) @@ -83,7 +83,7 @@ import Testing /// Verifies that updating the currently global default toolchain can be updated, and that after update the new toolchain /// will be the global default instead. - @Test(.testHomeMockedToolchain) func updateGlobalDefault() async throws { + @Test(.testHomeMockedToolchain()) func updateGlobalDefault() async throws { try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") try await SwiftlyTests.runCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) @@ -105,7 +105,7 @@ import Testing /// Verifies that updating the currently in-use toolchain can be updated, and that after update the new toolchain /// will be in-use with the swift version file updated. - @Test(.testHomeMockedToolchain) func updateInUse() async throws { + @Test(.testHomeMockedToolchain()) func updateInUse() async throws { try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") @@ -159,7 +159,7 @@ import Testing } /// Verify that the latest of all the matching release toolchains is updated. - @Test(.testHomeMockedToolchain) func updateSelectsLatestMatchingStableRelease() async throws { + @Test(.testHomeMockedToolchain()) func updateSelectsLatestMatchingStableRelease() async throws { try await SwiftlyTests.installMockedToolchain(selector: "6.0.1") try await SwiftlyTests.installMockedToolchain(selector: "6.0.0") diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index a6651ee0..2eb6588c 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -15,7 +15,7 @@ import Testing } /// Tests that the `use` command can switch between installed stable release toolchains. - @Test(.mockHomeAllToolchains) func useStable() async throws { + @Test(.mockHomeToolchains()) func useStable() async throws { try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) @@ -23,7 +23,7 @@ import Testing /// Tests that that "latest" can be provided to the `use` command to select the installed stable release /// toolchain with the most recent version. - @Test(.mockHomeAllToolchains) func useLatestStable() async throws { + @Test(.mockHomeToolchains()) func useLatestStable() async throws { // Use an older toolchain. try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) @@ -42,7 +42,7 @@ import Testing /// 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(.mockHomeAllToolchains) func useLatestStablePatch() async throws { + @Test(.mockHomeToolchains()) func useLatestStablePatch() async throws { try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) let oldStableVersion = SwiftlyTests.oldStable.asStableRelease! @@ -70,7 +70,7 @@ import Testing } /// Tests that the `use` command can switch between installed main snapshot toolchains. - @Test(.mockHomeAllToolchains) func useMainSnapshot() async throws { + @Test(.mockHomeToolchains()) func useMainSnapshot() async throws { // Switch to a non-snapshot. try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) @@ -82,7 +82,7 @@ import Testing /// Tests that the latest installed main snapshot toolchain can be selected by omitting the /// date (e.g. `use main-snapshot`). - @Test(.mockHomeAllToolchains) func useLatestMainSnapshot() async throws { + @Test(.mockHomeToolchains()) func useLatestMainSnapshot() async throws { // Switch to a non-snapshot. try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) // Switch to the latest main snapshot. @@ -96,7 +96,7 @@ import Testing } /// Tests that the `use` command can switch between installed release snapshot toolchains. - @Test(.mockHomeAllToolchains) func useReleaseSnapshot() async throws { + @Test(.mockHomeToolchains()) func useReleaseSnapshot() async throws { // Switch to a non-snapshot. try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) try await self.useAndValidate( @@ -120,7 +120,7 @@ import Testing /// Tests that the latest installed release snapshot toolchain can be selected by omitting the /// date (e.g. `use 5.7-snapshot`). - @Test(.mockHomeAllToolchains) func useLatestReleaseSnapshot() async throws { + @Test(.mockHomeToolchains()) func useLatestReleaseSnapshot() async throws { // Switch to a non-snapshot. try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) // Switch to the latest snapshot for the given release. @@ -149,7 +149,7 @@ import Testing } /// Tests that the `use` command gracefully exits when executed before any toolchains have been installed. - @Test(.mockHomeNoToolchains) func useNoInstalledToolchains() async throws { + @Test(.mockHomeToolchains(toolchains: [])) func useNoInstalledToolchains() async throws { try await SwiftlyTests.runCommand(Use.self, ["use", "-g", "latest"]) var config = try Config.load() @@ -162,7 +162,7 @@ import Testing } /// Tests that the `use` command gracefully handles being executed with toolchain names that haven't been installed. - @Test(.mockHomeAllToolchains) func useNonExistent() async throws { + @Test(.mockHomeToolchains()) func useNonExistent() async throws { // Switch to a valid toolchain. try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) @@ -174,7 +174,7 @@ import Testing } /// Tests that the `use` command works with all the installed toolchains in this test harness. - @Test(.mockHomeAllToolchains) func useAll() async throws { + @Test(.mockHomeToolchains()) func useAll() async throws { let config = try Config.load() for toolchain in config.installedToolchains { From a107360c3d00f79bdd64666bae38f946996d8661 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sat, 5 Apr 2025 17:18:30 -0400 Subject: [PATCH 06/12] Move the toolchain constants into ToolchainVersion and Set for shorter lists and static references --- Tests/SwiftlyTests/InstallTests.swift | 10 +- Tests/SwiftlyTests/ListTests.swift | 42 ++++---- Tests/SwiftlyTests/RunTests.swift | 8 +- Tests/SwiftlyTests/SwiftlyTests.swift | 59 +++++------ Tests/SwiftlyTests/UninstallTests.swift | 76 +++++++-------- Tests/SwiftlyTests/UpdateTests.swift | 2 +- Tests/SwiftlyTests/UseTests.swift | 124 ++++++++++++------------ 7 files changed, 162 insertions(+), 159 deletions(-) diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index f45ab062..316aa569 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -226,10 +226,10 @@ import Testing /// Verify that the installed toolchain will be marked as in-use if the --use flag is specified. @Test(.testHomeMockedToolchain()) func installUseFlag() async throws { - try await SwiftlyTests.installMockedToolchain(toolchain: SwiftlyTests.oldStable) - try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.oldStable.name]) - try await SwiftlyTests.validateInUse(expected: SwiftlyTests.oldStable) - try await SwiftlyTests.installMockedToolchain(selector: SwiftlyTests.newStable.name, args: ["--use"]) - try await SwiftlyTests.validateInUse(expected: SwiftlyTests.newStable) + try await SwiftlyTests.installMockedToolchain(toolchain: .oldStable) + try await SwiftlyTests.runCommand(Use.self, ["use", ToolchainVersion.oldStable.name]) + try await SwiftlyTests.validateInUse(expected: .oldStable) + try await SwiftlyTests.installMockedToolchain(selector: ToolchainVersion.newStable.name, args: ["--use"]) + try await SwiftlyTests.validateInUse(expected: .newStable) } } diff --git a/Tests/SwiftlyTests/ListTests.swift b/Tests/SwiftlyTests/ListTests.swift index 472ed831..d1d28ee0 100644 --- a/Tests/SwiftlyTests/ListTests.swift +++ b/Tests/SwiftlyTests/ListTests.swift @@ -7,23 +7,23 @@ import Testing static let homeName = "useTests" static let sortedReleaseToolchains: [ToolchainVersion] = [ - SwiftlyTests.newStable, - SwiftlyTests.oldStableNewPatch, - SwiftlyTests.oldStable, + .newStable, + .oldStableNewPatch, + .oldStable, ] static let sortedSnapshotToolchains: [ToolchainVersion] = [ - SwiftlyTests.newMainSnapshot, - SwiftlyTests.oldMainSnapshot, - SwiftlyTests.newReleaseSnapshot, - SwiftlyTests.oldReleaseSnapshot, + .newMainSnapshot, + .oldMainSnapshot, + .newReleaseSnapshot, + .oldReleaseSnapshot, ] /// Constructs a mock home directory with the toolchains listed above installed and runs the provided closure within /// the context of that home. func runListTest(f: () async throws -> Void) async throws { try await SwiftlyTests.withTestHome(name: Self.homeName) { - for toolchain in SwiftlyTests.allToolchains { + for toolchain in Set.allToolchains() { try await SwiftlyTests.installMockedToolchain(toolchain: toolchain) } @@ -44,7 +44,7 @@ import Testing let output = try await SwiftlyTests.runWithMockedIO(List.self, args) let parsedToolchains = output.compactMap { outputLine in - SwiftlyTests.allToolchains.first { + Set.allToolchains().first { outputLine.contains(String(describing: $0)) } } @@ -73,13 +73,13 @@ import Testing var toolchains = try await self.runList(selector: "5") #expect(toolchains == Self.sortedReleaseToolchains) - var selector = "\(SwiftlyTests.newStable.asStableRelease!.major).\(SwiftlyTests.newStable.asStableRelease!.minor)" + var selector = "\(ToolchainVersion.newStable.asStableRelease!.major).\(ToolchainVersion.newStable.asStableRelease!.minor)" toolchains = try await self.runList(selector: selector) - #expect(toolchains == [SwiftlyTests.newStable]) + #expect(toolchains == [ToolchainVersion.newStable]) - selector = "\(SwiftlyTests.oldStable.asStableRelease!.major).\(SwiftlyTests.oldStable.asStableRelease!.minor)" + selector = "\(ToolchainVersion.oldStable.asStableRelease!.major).\(ToolchainVersion.oldStable.asStableRelease!.minor)" toolchains = try await self.runList(selector: selector) - #expect(toolchains == [SwiftlyTests.oldStableNewPatch, SwiftlyTests.oldStable]) + #expect(toolchains == [ToolchainVersion.oldStableNewPatch, ToolchainVersion.oldStable]) for toolchain in Self.sortedReleaseToolchains { toolchains = try await self.runList(selector: toolchain.name) @@ -96,11 +96,11 @@ import Testing @Test func listSnapshotToolchains() async throws { try await self.runListTest { var toolchains = try await self.runList(selector: "main-snapshot") - #expect(toolchains == [SwiftlyTests.newMainSnapshot, SwiftlyTests.oldMainSnapshot]) + #expect(toolchains == [ToolchainVersion.newMainSnapshot, ToolchainVersion.oldMainSnapshot]) - let snapshotBranch = SwiftlyTests.newReleaseSnapshot.asSnapshot!.branch + let snapshotBranch = ToolchainVersion.newReleaseSnapshot.asSnapshot!.branch toolchains = try await self.runList(selector: "\(snapshotBranch.major!).\(snapshotBranch.minor!)-snapshot") - #expect(toolchains == [SwiftlyTests.newReleaseSnapshot, SwiftlyTests.oldReleaseSnapshot]) + #expect(toolchains == [ToolchainVersion.newReleaseSnapshot, ToolchainVersion.oldReleaseSnapshot]) for toolchain in Self.sortedSnapshotToolchains { toolchains = try await self.runList(selector: toolchain.name) @@ -129,22 +129,22 @@ import Testing } try await self.runListTest { - for toolchain in SwiftlyTests.allToolchains { + for toolchain in Set.allToolchains() { try await inUseTest(toolchain: toolchain, selector: nil) try await inUseTest(toolchain: toolchain, selector: toolchain.name) } - let major = SwiftlyTests.oldStable.asStableRelease!.major + let major = ToolchainVersion.oldStable.asStableRelease!.major for toolchain in Self.sortedReleaseToolchains.filter({ $0.asStableRelease?.major == major }) { try await inUseTest(toolchain: toolchain, selector: "\(major)") } - for toolchain in SwiftlyTests.allToolchains.filter({ $0.asSnapshot?.branch == .main }) { + for toolchain in Set.allToolchains().filter({ $0.asSnapshot?.branch == .main }) { try await inUseTest(toolchain: toolchain, selector: "main-snapshot") } - let branch = SwiftlyTests.oldReleaseSnapshot.asSnapshot!.branch - let releaseSnapshots = SwiftlyTests.allToolchains.filter { + let branch = ToolchainVersion.oldReleaseSnapshot.asSnapshot!.branch + let releaseSnapshots = Set.allToolchains().filter { $0.asSnapshot?.branch == branch } for toolchain in releaseSnapshots { diff --git a/Tests/SwiftlyTests/RunTests.swift b/Tests/SwiftlyTests/RunTests.swift index 0649af7a..1146d313 100644 --- a/Tests/SwiftlyTests/RunTests.swift +++ b/Tests/SwiftlyTests/RunTests.swift @@ -10,17 +10,17 @@ import Testing @Test(.mockHomeToolchains()) func runSelection() async throws { // GIVEN: a set of installed toolchains // WHEN: invoking the run command with a selector argument for that toolchain - var output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version", "+\(SwiftlyTests.newStable.name)"]) + var output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version", "+\(ToolchainVersion.newStable.name)"]) // THEN: the output confirms that it ran with the selected toolchain - #expect(output.contains(SwiftlyTests.newStable.name)) + #expect(output.contains(ToolchainVersion.newStable.name)) // GIVEN: a set of installed toolchains and one is selected with a .swift-version file let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") - try SwiftlyTests.oldStable.name.write(to: versionFile, atomically: true, encoding: .utf8) + try ToolchainVersion.oldStable.name.write(to: versionFile, atomically: true, encoding: .utf8) // WHEN: invoking the run command without any selector arguments for toolchains output = try await SwiftlyTests.runWithMockedIO(Run.self, ["run", "swift", "--version"]) // THEN: the output confirms that it ran with the selected toolchain - #expect(output.contains(SwiftlyTests.oldStable.name)) + #expect(output.contains(ToolchainVersion.oldStable.name)) // GIVEN: a set of installed toolchains // WHEN: invoking the run command with a selector argument for a toolchain that isn't installed diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 5c26d530..d211abdc 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -68,6 +68,28 @@ extension SwiftlyCoreContext { } } +extension ToolchainVersion { + public static let oldStable = ToolchainVersion(major: 5, minor: 6, patch: 0) + public static let oldStableNewPatch = ToolchainVersion(major: 5, minor: 6, patch: 3) + public static let newStable = ToolchainVersion(major: 5, minor: 7, patch: 0) + public static let oldMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2025-03-10") + public static let newMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2025-03-14") + public static let oldReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 6, minor: 0), date: "2025-02-09") + public static let newReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 6, minor: 0), date: "2025-02-11") +} + +extension Set where Element == ToolchainVersion { + static func allToolchains() -> Set { [ + .oldStable, + .oldStableNewPatch, + .newStable, + .oldMainSnapshot, + .newMainSnapshot, + .oldReleaseSnapshot, + .newReleaseSnapshot, + ] } +} + // Convenience test scoping traits struct TestHomeTrait: TestTrait, TestScoping { @@ -89,7 +111,7 @@ extension Trait where Self == TestHomeTrait { struct MockHomeToolchainsTrait: TestTrait, TestScoping { var name: String = "testHome" - var toolchains: Set = SwiftlyTests.allToolchains + var toolchains: Set = .allToolchains() init(_ name: String, toolchains: Set) { self.name = name @@ -105,7 +127,7 @@ struct MockHomeToolchainsTrait: TestTrait, TestScoping { extension Trait where Self == MockHomeToolchainsTrait { /// Run the test with this trait to get a mocked home directory with a predefined collection of toolchains already installed. - static func mockHomeToolchains(_ homeName: String = "testHome", toolchains: Set = SwiftlyTests.allToolchains) -> Self { Self(homeName, toolchains: toolchains) } + static func mockHomeToolchains(_ homeName: String = "testHome", toolchains: Set = .allToolchains()) -> Self { Self(homeName, toolchains: toolchains) } } struct TestHomeMockedToolchainTrait: TestTrait, TestScoping { @@ -136,25 +158,6 @@ public enum SwiftlyTests { inputProvider: InputProviderFail() ) - // Below are some constants that can be used to write test cases. - public static let oldStable = ToolchainVersion(major: 5, minor: 6, patch: 0) - public static let oldStableNewPatch = ToolchainVersion(major: 5, minor: 6, patch: 3) - public static let newStable = ToolchainVersion(major: 5, minor: 7, patch: 0) - public static let oldMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2025-03-10") - public static let newMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2025-03-14") - public static let oldReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 6, minor: 0), date: "2025-02-09") - public static let newReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 6, minor: 0), date: "2025-02-11") - - static let allToolchains: Set = [ - oldStable, - oldStableNewPatch, - newStable, - oldMainSnapshot, - newMainSnapshot, - oldReleaseSnapshot, - newReleaseSnapshot, - ] - static func baseTestConfig() async throws -> Config { guard let pd = try? await Swiftly.currentPlatform.detectPlatform(Self.ctx, disableConfirmation: true, platform: nil) else { throw SwiftlyTestError(message: "Unable to detect the current platform.") @@ -515,9 +518,9 @@ public class MockToolchainDownloader: HTTPRequestExecutor { executables: [String]? = nil, latestSwiftlyVersion: SwiftlyVersion = SwiftlyCore.version, releaseToolchains: [ToolchainVersion.StableRelease] = [ - SwiftlyTests.oldStable.asStableRelease!, - SwiftlyTests.newStable.asStableRelease!, - SwiftlyTests.oldStableNewPatch.asStableRelease!, + ToolchainVersion.oldStable.asStableRelease!, + ToolchainVersion.newStable.asStableRelease!, + ToolchainVersion.oldStableNewPatch.asStableRelease!, ToolchainVersion.StableRelease(major: 5, minor: 7, patch: 4), // Some tests look for a patch in the 5.7.x series larger than 5.0.3 ToolchainVersion.StableRelease(major: 5, minor: 9, patch: 0), // Some tests try to update from 5.9.0 ToolchainVersion.StableRelease(major: 5, minor: 9, patch: 1), @@ -526,10 +529,10 @@ public class MockToolchainDownloader: HTTPRequestExecutor { ToolchainVersion.StableRelease(major: 6, minor: 0, patch: 2), // Some tests try to update from 6.0.1 ], snapshotToolchains: [ToolchainVersion.Snapshot] = [ - SwiftlyTests.oldMainSnapshot.asSnapshot!, - SwiftlyTests.newMainSnapshot.asSnapshot!, - SwiftlyTests.oldReleaseSnapshot.asSnapshot!, - SwiftlyTests.newReleaseSnapshot.asSnapshot!, + ToolchainVersion.oldMainSnapshot.asSnapshot!, + ToolchainVersion.newMainSnapshot.asSnapshot!, + ToolchainVersion.oldReleaseSnapshot.asSnapshot!, + ToolchainVersion.newReleaseSnapshot.asSnapshot!, ] ) { self.executables = executables ?? ["swift"] diff --git a/Tests/SwiftlyTests/UninstallTests.swift b/Tests/SwiftlyTests/UninstallTests.swift index a069ab70..5c9496ea 100644 --- a/Tests/SwiftlyTests/UninstallTests.swift +++ b/Tests/SwiftlyTests/UninstallTests.swift @@ -18,7 +18,7 @@ import Testing /// Tests that `swiftly uninstall latest` successfully uninstalls the latest stable release of Swift. @Test func uninstallLatest() async throws { - let toolchains = SwiftlyTests.allToolchains.filter { $0.asStableRelease != nil } + let toolchains = Set.allToolchains().filter { $0.asStableRelease != nil } try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains) { var installed = toolchains @@ -39,9 +39,9 @@ import Testing /// Tests that a fully-qualified stable release version can be supplied to `swiftly uninstall`. @Test(.mockHomeToolchains(Self.homeName)) func uninstallStableRelease() async throws { - var installed = SwiftlyTests.allToolchains + var installed: Set = .allToolchains() - for toolchain in SwiftlyTests.allToolchains.filter({ $0.isStableRelease() }) { + for toolchain in Set.allToolchains().filter({ $0.isStableRelease() }) { _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", toolchain.name], input: ["y"]) installed.remove(toolchain) @@ -61,9 +61,9 @@ import Testing /// Tests that a fully-qualified snapshot version can be supplied to `swiftly uninstall`. @Test(.mockHomeToolchains(Self.homeName)) func uninstallSnapshot() async throws { - var installed = SwiftlyTests.allToolchains + var installed: Set = .allToolchains() - for toolchain in SwiftlyTests.allToolchains.filter({ $0.isSnapshot() }) { + for toolchain in Set.allToolchains().filter({ $0.isSnapshot() }) { _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", toolchain.name], input: ["y"]) installed.remove(toolchain) @@ -160,13 +160,13 @@ import Testing /// Tests that uninstalling the toolchain that is currently "in use" has the expected behavior. @Test func uninstallInUse() async throws { let toolchains: Set = [ - SwiftlyTests.oldStable, - SwiftlyTests.oldStableNewPatch, - SwiftlyTests.newStable, - SwiftlyTests.oldMainSnapshot, - SwiftlyTests.newMainSnapshot, - SwiftlyTests.oldReleaseSnapshot, - SwiftlyTests.newReleaseSnapshot, + .oldStable, + .oldStableNewPatch, + .newStable, + .oldMainSnapshot, + .newMainSnapshot, + .oldReleaseSnapshot, + .newReleaseSnapshot, ] func uninstallInUseTest( @@ -193,39 +193,39 @@ import Testing } } - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: SwiftlyTests.oldStable) { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: .oldStable) { var installed = toolchains - try await uninstallInUseTest(&installed, toRemove: SwiftlyTests.oldStable, expectedInUse: SwiftlyTests.oldStableNewPatch) - try await uninstallInUseTest(&installed, toRemove: SwiftlyTests.oldStableNewPatch, expectedInUse: SwiftlyTests.newStable) - try await uninstallInUseTest(&installed, toRemove: SwiftlyTests.newStable, expectedInUse: SwiftlyTests.newMainSnapshot) + try await uninstallInUseTest(&installed, toRemove: .oldStable, expectedInUse: .oldStableNewPatch) + try await uninstallInUseTest(&installed, toRemove: .oldStableNewPatch, expectedInUse: .newStable) + try await uninstallInUseTest(&installed, toRemove: .newStable, expectedInUse: .newMainSnapshot) // Switch to the old main snapshot to ensure uninstalling it selects the new one. - try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.oldMainSnapshot.name]) - try await uninstallInUseTest(&installed, toRemove: SwiftlyTests.oldMainSnapshot, expectedInUse: SwiftlyTests.newMainSnapshot) + try await SwiftlyTests.runCommand(Use.self, ["use", ToolchainVersion.oldMainSnapshot.name]) + try await uninstallInUseTest(&installed, toRemove: .oldMainSnapshot, expectedInUse: .newMainSnapshot) try await uninstallInUseTest( &installed, - toRemove: SwiftlyTests.newMainSnapshot, - expectedInUse: SwiftlyTests.newReleaseSnapshot + toRemove: .newMainSnapshot, + expectedInUse: .newReleaseSnapshot ) // Switch to the old release snapshot to ensure uninstalling it selects the new one. - try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.oldReleaseSnapshot.name]) + try await SwiftlyTests.runCommand(Use.self, ["use", ToolchainVersion.oldReleaseSnapshot.name]) try await uninstallInUseTest( &installed, - toRemove: SwiftlyTests.oldReleaseSnapshot, - expectedInUse: SwiftlyTests.newReleaseSnapshot + toRemove: .oldReleaseSnapshot, + expectedInUse: .newReleaseSnapshot ) try await uninstallInUseTest( &installed, - toRemove: SwiftlyTests.newReleaseSnapshot, + toRemove: .newReleaseSnapshot, expectedInUse: nil ) } } /// Tests that uninstalling the last toolchain is handled properly and cleans up any symlinks. - @Test(.mockHomeToolchains(Self.homeName, toolchains: [SwiftlyTests.oldStable])) func uninstallLastToolchain() async throws { - _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", SwiftlyTests.oldStable.name], input: ["y"]) + @Test(.mockHomeToolchains(Self.homeName, toolchains: [.oldStable])) func uninstallLastToolchain() async throws { + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", ToolchainVersion.oldStable.name], input: ["y"]) let config = try Config.load() #expect(config.inUse == nil) @@ -238,11 +238,11 @@ import Testing /// Tests that aborting an uninstall works correctly. @Test func uninstallAbort() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: SwiftlyTests.allToolchains, inUse: SwiftlyTests.oldStable) { + try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: .allToolchains(), inUse: .oldStable) { let preConfig = try Config.load() - _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", SwiftlyTests.oldStable.name], input: ["n"]) + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", ToolchainVersion.oldStable.name], input: ["n"]) try await SwiftlyTests.validateInstalledToolchains( - SwiftlyTests.allToolchains, + .allToolchains(), description: "abort uninstall" ) @@ -252,16 +252,16 @@ import Testing } /// Tests that providing the `-y` argument skips the confirmation prompt. - @Test(.mockHomeToolchains(Self.homeName, toolchains: [SwiftlyTests.oldStable, SwiftlyTests.newStable])) func uninstallAssumeYes() async throws { - try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", SwiftlyTests.oldStable.name]) + @Test(.mockHomeToolchains(Self.homeName, toolchains: [.oldStable, .newStable])) func uninstallAssumeYes() async throws { + try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", ToolchainVersion.oldStable.name]) try await SwiftlyTests.validateInstalledToolchains( - [SwiftlyTests.newStable], + [.newStable], description: "uninstall did not succeed even with -y provided" ) } /// Tests that providing "all" as an argument to uninstall will uninstall all toolchains. - @Test(.mockHomeToolchains(Self.homeName, toolchains: [SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.newMainSnapshot, SwiftlyTests.oldReleaseSnapshot])) func uninstallAll() async throws { + @Test(.mockHomeToolchains(Self.homeName, toolchains: [.oldStable, .newStable, .newMainSnapshot, .oldReleaseSnapshot])) func uninstallAll() async throws { try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", "all"]) try await SwiftlyTests.validateInstalledToolchains( [], @@ -270,15 +270,15 @@ import Testing } /// Tests that uninstalling a toolchain that is the global default, but is not in the list of installed toolchains. - @Test(.mockHomeToolchains(Self.homeName, toolchains: [SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.newMainSnapshot, SwiftlyTests.oldReleaseSnapshot])) func uninstallNotInstalled() async throws { + @Test(.mockHomeToolchains(Self.homeName, toolchains: [.oldStable, .newStable, .newMainSnapshot, .oldReleaseSnapshot])) func uninstallNotInstalled() async throws { var config = try Config.load() - config.inUse = SwiftlyTests.newMainSnapshot - config.installedToolchains.remove(SwiftlyTests.newMainSnapshot) + config.inUse = .newMainSnapshot + config.installedToolchains.remove(.newMainSnapshot) try config.save() - try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", SwiftlyTests.newMainSnapshot.name]) + try await SwiftlyTests.runCommand(Uninstall.self, ["uninstall", "-y", ToolchainVersion.newMainSnapshot.name]) try await SwiftlyTests.validateInstalledToolchains( - [SwiftlyTests.oldStable, SwiftlyTests.newStable, SwiftlyTests.oldReleaseSnapshot], + [.oldStable, .newStable, .oldReleaseSnapshot], description: "uninstall did not uninstall all toolchains" ) } diff --git a/Tests/SwiftlyTests/UpdateTests.swift b/Tests/SwiftlyTests/UpdateTests.swift index 7a5af2fc..b9fcb8de 100644 --- a/Tests/SwiftlyTests/UpdateTests.swift +++ b/Tests/SwiftlyTests/UpdateTests.swift @@ -136,7 +136,7 @@ import Testing for branch in branches { try await SwiftlyTests.withTestHome { try await SwiftlyTests.withMockedToolchain { - let date = branch == .main ? SwiftlyTests.oldMainSnapshot.asSnapshot!.date : SwiftlyTests.oldReleaseSnapshot.asSnapshot!.date + let date = branch == .main ? ToolchainVersion.oldMainSnapshot.asSnapshot!.date : ToolchainVersion.oldReleaseSnapshot.asSnapshot!.date try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: date)) try await SwiftlyTests.runCommand( diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index 2eb6588c..e960da04 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -16,105 +16,105 @@ import Testing /// Tests that the `use` command can switch between installed stable release toolchains. @Test(.mockHomeToolchains()) func useStable() async throws { - try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.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(.mockHomeToolchains()) func useLatestStable() async throws { // Use an older toolchain. - try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.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: SwiftlyTests.newStable) + try await self.useAndValidate(argument: "latest", expectedVersion: .newStable) // Try to use latest again, assert no error was thrown and no changes were made. - try await self.useAndValidate(argument: "latest", expectedVersion: SwiftlyTests.newStable) + 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: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.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: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.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(.mockHomeToolchains()) func useLatestStablePatch() async throws { - try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) + try await self.useAndValidate(argument: ToolchainVersion.oldStable.name, expectedVersion: .oldStable) - let oldStableVersion = SwiftlyTests.oldStable.asStableRelease! + let oldStableVersion = ToolchainVersion.oldStable.asStableRelease! // Drop the patch version and assert that the latest patch of the provided major.minor was chosen. try await self.useAndValidate( argument: "\(oldStableVersion.major).\(oldStableVersion.minor)", - expectedVersion: SwiftlyTests.oldStableNewPatch + expectedVersion: .oldStableNewPatch ) // Assert that selecting it again doesn't change anything. try await self.useAndValidate( argument: "\(oldStableVersion.major).\(oldStableVersion.minor)", - expectedVersion: SwiftlyTests.oldStableNewPatch + expectedVersion: .oldStableNewPatch ) // 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: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.oldStable) - let latestPatch = SwiftlyTests.oldStableNewPatch.asStableRelease!.patch + 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)", - expectedVersion: SwiftlyTests.oldStable + expectedVersion: .oldStable ) } /// Tests that the `use` command can switch between installed main snapshot toolchains. @Test(.mockHomeToolchains()) func useMainSnapshot() async throws { // Switch to a non-snapshot. - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) - try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) - try await self.useAndValidate(argument: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.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: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) - try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.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(.mockHomeToolchains()) func useLatestMainSnapshot() async throws { // Switch to a non-snapshot. - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.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: SwiftlyTests.newMainSnapshot) + 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: SwiftlyTests.newMainSnapshot) + 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: SwiftlyTests.newMainSnapshot.name, expectedVersion: SwiftlyTests.newMainSnapshot) + try await self.useAndValidate(argument: ToolchainVersion.newMainSnapshot.name, expectedVersion: .newMainSnapshot) // Switch to an older snapshot, verify it works. - try await self.useAndValidate(argument: SwiftlyTests.oldMainSnapshot.name, expectedVersion: SwiftlyTests.oldMainSnapshot) + try await self.useAndValidate(argument: ToolchainVersion.oldMainSnapshot.name, expectedVersion: .oldMainSnapshot) } /// Tests that the `use` command can switch between installed release snapshot toolchains. @Test(.mockHomeToolchains()) func useReleaseSnapshot() async throws { // Switch to a non-snapshot. - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.newStable) + try await self.useAndValidate(argument: ToolchainVersion.newStable.name, expectedVersion: .newStable) try await self.useAndValidate( - argument: SwiftlyTests.oldReleaseSnapshot.name, - expectedVersion: SwiftlyTests.oldReleaseSnapshot + argument: ToolchainVersion.oldReleaseSnapshot.name, + expectedVersion: .oldReleaseSnapshot ) try await self.useAndValidate( - argument: SwiftlyTests.newReleaseSnapshot.name, - expectedVersion: SwiftlyTests.newReleaseSnapshot + argument: ToolchainVersion.newReleaseSnapshot.name, + expectedVersion: .newReleaseSnapshot ) // Verify that using the same snapshot again doesn't throw an error. try await self.useAndValidate( - argument: SwiftlyTests.newReleaseSnapshot.name, - expectedVersion: SwiftlyTests.newReleaseSnapshot + argument: ToolchainVersion.newReleaseSnapshot.name, + expectedVersion: .newReleaseSnapshot ) try await self.useAndValidate( - argument: SwiftlyTests.oldReleaseSnapshot.name, - expectedVersion: SwiftlyTests.oldReleaseSnapshot + argument: ToolchainVersion.oldReleaseSnapshot.name, + expectedVersion: .oldReleaseSnapshot ) } @@ -122,29 +122,29 @@ import Testing /// date (e.g. `use 5.7-snapshot`). @Test(.mockHomeToolchains()) func useLatestReleaseSnapshot() async throws { // Switch to a non-snapshot. - try await self.useAndValidate(argument: SwiftlyTests.newStable.name, expectedVersion: SwiftlyTests.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) = SwiftlyTests.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( argument: "\(major).\(minor)-snapshot", - expectedVersion: SwiftlyTests.newReleaseSnapshot + expectedVersion: .newReleaseSnapshot ) // Switch to it again, assert no errors or changes were made. try await self.useAndValidate( argument: "\(major).\(minor)-snapshot", - expectedVersion: SwiftlyTests.newReleaseSnapshot + expectedVersion: .newReleaseSnapshot ) // Switch to it again, this time by name. Assert no errors or changes were made. try await self.useAndValidate( - argument: SwiftlyTests.newReleaseSnapshot.name, - expectedVersion: SwiftlyTests.newReleaseSnapshot + argument: ToolchainVersion.newReleaseSnapshot.name, + expectedVersion: .newReleaseSnapshot ) // Switch to an older snapshot, verify it works. try await self.useAndValidate( - argument: SwiftlyTests.oldReleaseSnapshot.name, - expectedVersion: SwiftlyTests.oldReleaseSnapshot + argument: ToolchainVersion.oldReleaseSnapshot.name, + expectedVersion: .oldReleaseSnapshot ) } @@ -164,13 +164,13 @@ import Testing /// Tests that the `use` command gracefully handles being executed with toolchain names that haven't been installed. @Test(.mockHomeToolchains()) func useNonExistent() async throws { // Switch to a valid toolchain. - try await self.useAndValidate(argument: SwiftlyTests.oldStable.name, expectedVersion: SwiftlyTests.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: SwiftlyTests.oldStable) - try await self.useAndValidate(argument: "5.7-snapshot-1996-01-01", expectedVersion: SwiftlyTests.oldStable) - try await self.useAndValidate(argument: "6.7-snapshot", expectedVersion: SwiftlyTests.oldStable) - try await self.useAndValidate(argument: "main-snapshot-1996-01-01", expectedVersion: SwiftlyTests.oldStable) + 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: "6.7-snapshot", 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. @@ -188,9 +188,9 @@ import Testing /// Tests that running a use command without an argument prints the currently in-use toolchain. @Test func printInUse() async throws { let toolchains = [ - SwiftlyTests.newStable, - SwiftlyTests.newMainSnapshot, - SwiftlyTests.newReleaseSnapshot, + ToolchainVersion.newStable, + ToolchainVersion.newMainSnapshot, + ToolchainVersion.newReleaseSnapshot, ] try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { for toolchain in toolchains { @@ -210,49 +210,49 @@ import Testing /// Tests in-use toolchain selected by the .swift-version file. @Test func swiftVersionFile() async throws { let toolchains = [ - SwiftlyTests.newStable, - SwiftlyTests.newMainSnapshot, - SwiftlyTests.newReleaseSnapshot, + ToolchainVersion.newStable, + ToolchainVersion.newMainSnapshot, + ToolchainVersion.newReleaseSnapshot, ] try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") // GIVEN: a directory with a swift version file that selects a particular toolchain - try SwiftlyTests.newStable.name.write(to: versionFile, atomically: true, encoding: .utf8) + try ToolchainVersion.newStable.name.write(to: versionFile, atomically: true, encoding: .utf8) // 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(SwiftlyTests.newStable.name) })) + #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", SwiftlyTests.newMainSnapshot.name]) + 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, encoding: .utf8) - #expect(SwiftlyTests.newMainSnapshot.name == versionFileContents) + #expect(ToolchainVersion.newMainSnapshot.name == versionFileContents) // THEN: the use command reports this toolchain to be in use - #expect(output.contains(where: { $0.contains(SwiftlyTests.newMainSnapshot.name) })) + #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 FileManager.default.removeItem(atPath: versionFile.path) let gitDir = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".git") try FileManager.default.createDirectory(atPath: gitDir.path, withIntermediateDirectories: false) // WHEN: using a toolchain version - try await SwiftlyTests.runCommand(Use.self, ["use", SwiftlyTests.newReleaseSnapshot.name]) + try await SwiftlyTests.runCommand(Use.self, ["use", ToolchainVersion.newReleaseSnapshot.name]) // THEN: a swift version file is created #expect(FileManager.default.fileExists(atPath: versionFile.path)) // THEN: the version file contains the specified version versionFileContents = try String(contentsOf: versionFile, encoding: .utf8) - #expect(SwiftlyTests.newReleaseSnapshot.name == versionFileContents) + #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, encoding: .utf8) // 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: SwiftlyTests.newStable.asStableRelease!.major, minor: nil, patch: nil) + 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, encoding: .utf8) - #expect(SwiftlyTests.newStable.name == versionFileContents) + #expect(ToolchainVersion.newStable.name == versionFileContents) } } } From f9192156f0d3500113cf49055e6b1c116be1682c Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sat, 5 Apr 2025 20:55:55 -0400 Subject: [PATCH 07/12] Introduce test parameters and test scoping traits to reduce nesting within test cases Increase HTTP timeouts to help with timeouts in CI --- Sources/SwiftlyCore/HTTPClient.swift | 8 +-- Tests/SwiftlyTests/HTTPClientTests.swift | 44 +++++------- Tests/SwiftlyTests/InitTests.swift | 5 +- Tests/SwiftlyTests/InstallTests.swift | 48 ++++++++++--- Tests/SwiftlyTests/SwiftlyTests.swift | 10 +-- Tests/SwiftlyTests/UninstallTests.swift | 20 +++--- Tests/SwiftlyTests/UpdateTests.swift | 88 +++++++++++------------- Tests/SwiftlyTests/UseTests.swift | 8 +-- 8 files changed, 120 insertions(+), 111 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 77228705..e3f00fc2 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -124,7 +124,7 @@ public class HTTPRequestExecutorImpl: HTTPRequestExecutor { } public func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { - let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30)) + let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(45)) let swiftlyUserAgent = SwiftlyUserAgentMiddleware() let client = Client( @@ -138,7 +138,7 @@ public class HTTPRequestExecutorImpl: HTTPRequestExecutor { } public func getReleaseToolchains() async throws -> [Components.Schemas.Release] { - let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30)) + let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(45)) let swiftlyUserAgent = SwiftlyUserAgentMiddleware() let client = Client( @@ -153,7 +153,7 @@ public class HTTPRequestExecutorImpl: HTTPRequestExecutor { } public func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { - let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30)) + let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(45)) let swiftlyUserAgent = SwiftlyUserAgentMiddleware() let client = Client( @@ -457,7 +457,7 @@ public struct SwiftlyHTTPClient { } let request = makeRequest(url: url.absoluteString) - let response = try await self.httpRequestExecutor.execute(request, timeout: .seconds(30)) + let response = try await self.httpRequestExecutor.execute(request, timeout: .seconds(45)) switch response.status { case .ok: diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index ae7c4418..7cd8d6ca 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -37,42 +37,30 @@ import Testing #expect(throws: Never.self) { try currentRelease.swiftlyVersion } } - @Test func getToolchainMetdataFromSwiftOrg() async throws { + @Test( + arguments: + [PlatformDefinition.macOS, .ubuntu2404, .ubuntu2204, .rhel9, .fedora39, .amazonlinux2, .debian12], + [Components.Schemas.Architecture.x8664, .aarch64] + ) func getToolchainMetdataFromSwiftOrg(_ platform: PlatformDefinition, _ arch: Components.Schemas.Architecture) async throws { let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()) - let supportedPlatforms: [PlatformDefinition] = [ - .macOS, - .ubuntu2404, - .ubuntu2204, - .ubuntu2004, - // .ubuntu1804, // There are no releases for Ubuntu 18.04 in the branches being tested below - .rhel9, - .fedora39, - .amazonlinux2, - .debian12, - ] - let branches: [ToolchainVersion.Snapshot.Branch] = [ .main, .release(major: 6, minor: 1), // This is available in swift.org API ] - for arch in [Components.Schemas.Architecture.x8664, Components.Schemas.Architecture.aarch64] { - for platform in supportedPlatforms { - // GIVEN: we have a swiftly http client with swift.org metadata capability - // WHEN: we ask for the first five releases of a supported platform in a supported arch - let releases = try await httpClient.getReleaseToolchains(platform: platform, arch: arch, limit: 5) - // THEN: we get at least 1 release - #expect(1 <= releases.count) + // GIVEN: we have a swiftly http client with swift.org metadata capability + // WHEN: we ask for the first five releases of a supported platform in a supported arch + let releases = try await httpClient.getReleaseToolchains(platform: platform, arch: arch, limit: 5) + // THEN: we get at least 1 release + #expect(1 <= releases.count) - for branch in branches { - // GIVEN: we have a swiftly http client with swift.org metadata capability - // WHEN: we ask for the first five snapshots on a branch for a supported platform and arch - let snapshots = try await httpClient.getSnapshotToolchains(platform: platform, arch: arch.value2!, branch: branch, limit: 5) - // THEN: we get at least 3 releases - #expect(3 <= snapshots.count) - } - } + for branch in branches { + // GIVEN: we have a swiftly http client with swift.org metadata capability + // WHEN: we ask for the first five snapshots on a branch for a supported platform and arch + let snapshots = try await httpClient.getSnapshotToolchains(platform: platform, arch: arch.value2!, branch: branch, limit: 5) + // THEN: we get at least 3 releases + #expect(3 <= snapshots.count) } } } diff --git a/Tests/SwiftlyTests/InitTests.swift b/Tests/SwiftlyTests/InitTests.swift index 0de9059e..e5fe510a 100644 --- a/Tests/SwiftlyTests/InitTests.swift +++ b/Tests/SwiftlyTests/InitTests.swift @@ -4,12 +4,11 @@ import Foundation import Testing @Suite struct InitTests { - @Test(.testHome()) func initFresh() async throws { - // GIVEN: a fresh user account without Swiftly installed + @Test(.testHome(), arguments: ["/bin/bash", "/bin/zsh", "/bin/fish"]) func initFresh(_ shell: String) async throws { + // GIVEN: a fresh user account without swiftly installed try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyConfigFile(SwiftlyTests.ctx)) // AND: the user is using the bash shell - let shell = "/bin/bash" var ctx = SwiftlyTests.ctx ctx.mockedShell = shell diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index 316aa569..ebee9c2b 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -191,21 +191,51 @@ import Testing } /// Tests that attempting to install stable releases that are already installed doesn't result in an error. - @Test func installDuplicateReleases() async throws { - try await self.duplicateTest("5.8.0") - try await self.duplicateTest("latest") + @Test(.testHomeMockedToolchain(), arguments: ["5.8.0", "latest"]) func installDuplicateReleases(_ installVersion: String) async throws { + try await SwiftlyTests.runCommand(Install.self, ["install", installVersion, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + let before = try Config.load() + + let startTime = Date() + try await SwiftlyTests.runCommand(Install.self, ["install", installVersion, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + // Assert that swiftly didn't attempt to download a new toolchain. + #expect(startTime.timeIntervalSinceNow.magnitude < 10) + + let after = try Config.load() + #expect(before == after) } /// Tests that attempting to install main snapshots that are already installed doesn't result in an error. - @Test func installDuplicateMainSnapshots() async throws { - try await self.duplicateTest("main-snapshot-2023-04-01") - try await self.duplicateTest("main-snapshot") + @Test(.testHomeMockedToolchain(), arguments: ["main-snapshot-2023-04-01", "main-snapshot"]) func installDuplicateMainSnapshots(_ installVersion: String) async throws { + try await SwiftlyTests.runCommand(Install.self, ["install", installVersion, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + let before = try Config.load() + + let startTime = Date() + try await SwiftlyTests.runCommand(Install.self, ["install", installVersion, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + // Assert that swiftly didn't attempt to download a new toolchain. + #expect(startTime.timeIntervalSinceNow.magnitude < 10) + + let after = try Config.load() + #expect(before == after) } /// Tests that attempting to install release snapshots that are already installed doesn't result in an error. - @Test func installDuplicateReleaseSnapshots() async throws { - try await self.duplicateTest("6.0-snapshot-2024-06-18") - try await self.duplicateTest("6.0-snapshot") + @Test(.testHomeMockedToolchain(), arguments: ["6.0-snapshot-2024-06-18", "6.0-snapshot"]) func installDuplicateReleaseSnapshots(_ installVersion: String) async throws { + try await SwiftlyTests.runCommand(Install.self, ["install", installVersion, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + let before = try Config.load() + + let startTime = Date() + try await SwiftlyTests.runCommand(Install.self, ["install", installVersion, "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"]) + + // Assert that swiftly didn't attempt to download a new toolchain. + #expect(startTime.timeIntervalSinceNow.magnitude < 10) + + let after = try Config.load() + #expect(before == after) } /// Verify that the installed toolchain will be used if no toolchains currently are installed. diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index d211abdc..63b92d40 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -112,14 +112,16 @@ extension Trait where Self == TestHomeTrait { struct MockHomeToolchainsTrait: TestTrait, TestScoping { var name: String = "testHome" var toolchains: Set = .allToolchains() + var inUse: ToolchainVersion? - init(_ name: String, toolchains: Set) { + init(_ name: String, toolchains: Set, inUse: ToolchainVersion?) { self.name = name self.toolchains = toolchains + self.inUse = inUse } func provideScope(for _: Test, testCase _: Test.Case?, performing function: @Sendable () async throws -> Void) async throws { - try await SwiftlyTests.withMockedHome(homeName: self.name, toolchains: self.toolchains) { + try await SwiftlyTests.withMockedHome(homeName: self.name, toolchains: self.toolchains, inUse: self.inUse) { try await function() } } @@ -127,7 +129,7 @@ struct MockHomeToolchainsTrait: TestTrait, TestScoping { extension Trait where Self == MockHomeToolchainsTrait { /// Run the test with this trait to get a mocked home directory with a predefined collection of toolchains already installed. - static func mockHomeToolchains(_ homeName: String = "testHome", toolchains: Set = .allToolchains()) -> Self { Self(homeName, toolchains: toolchains) } + static func mockHomeToolchains(_ homeName: String = "testHome", toolchains: Set = .allToolchains(), inUse: ToolchainVersion? = nil) -> Self { Self(homeName, toolchains: toolchains, inUse: inUse) } } struct TestHomeMockedToolchainTrait: TestTrait, TestScoping { @@ -146,7 +148,7 @@ struct TestHomeMockedToolchainTrait: TestTrait, TestScoping { extension Trait where Self == TestHomeMockedToolchainTrait { /// Run the test with this trait to get a test home directory and a mocked - /// toolchain can be installed by request, at any version. + /// toolchain. static func testHomeMockedToolchain(_ name: String = "testHome") -> Self { Self(name) } } diff --git a/Tests/SwiftlyTests/UninstallTests.swift b/Tests/SwiftlyTests/UninstallTests.swift index 5c9496ea..2c4ff401 100644 --- a/Tests/SwiftlyTests/UninstallTests.swift +++ b/Tests/SwiftlyTests/UninstallTests.swift @@ -237,18 +237,16 @@ import Testing } /// Tests that aborting an uninstall works correctly. - @Test func uninstallAbort() async throws { - try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: .allToolchains(), inUse: .oldStable) { - let preConfig = try Config.load() - _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", ToolchainVersion.oldStable.name], input: ["n"]) - try await SwiftlyTests.validateInstalledToolchains( - .allToolchains(), - description: "abort uninstall" - ) + @Test(.mockHomeToolchains(Self.homeName, toolchains: .allToolchains(), inUse: .oldStable)) func uninstallAbort() async throws { + let preConfig = try Config.load() + _ = try await SwiftlyTests.runWithMockedIO(Uninstall.self, ["uninstall", ToolchainVersion.oldStable.name], input: ["n"]) + try await SwiftlyTests.validateInstalledToolchains( + .allToolchains(), + description: "abort uninstall" + ) - // Ensure config did not change. - #expect(try Config.load() == preConfig) - } + // Ensure config did not change. + #expect(try Config.load() == preConfig) } /// Tests that providing the `-y` argument skips the confirmation prompt. diff --git a/Tests/SwiftlyTests/UpdateTests.swift b/Tests/SwiftlyTests/UpdateTests.swift index b9fcb8de..a1166368 100644 --- a/Tests/SwiftlyTests/UpdateTests.swift +++ b/Tests/SwiftlyTests/UpdateTests.swift @@ -127,35 +127,30 @@ import Testing } /// Verifies that snapshots, both from the main branch and from development branches, can be updated. - @Test func updateSnapshot() async throws { - let branches: [ToolchainVersion.Snapshot.Branch] = [ - .main, + @Test( + .testHomeMockedToolchain(), + arguments: [ + ToolchainVersion.Snapshot.Branch.main, .release(major: 6, minor: 0), ] + ) func updateSnapshot(_ branch: ToolchainVersion.Snapshot.Branch) async throws { + let date = branch == .main ? ToolchainVersion.oldMainSnapshot.asSnapshot!.date : ToolchainVersion.oldReleaseSnapshot.asSnapshot!.date + try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: date)) - for branch in branches { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - let date = branch == .main ? ToolchainVersion.oldMainSnapshot.asSnapshot!.date : ToolchainVersion.oldReleaseSnapshot.asSnapshot!.date - try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: date)) - - try await SwiftlyTests.runCommand( - Update.self, ["update", "-y", "\(branch.name)-snapshot", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] - ) - - let config = try Config.load() - let inUse = config.inUse!.asSnapshot! - #expect(inUse > .init(branch: branch, date: date)) - #expect(inUse.branch == branch) - #expect(inUse.date > date) - - try await SwiftlyTests.validateInstalledToolchains( - [config.inUse!], - description: "update should work with snapshots" - ) - } - } - } + try await SwiftlyTests.runCommand( + Update.self, ["update", "-y", "\(branch.name)-snapshot", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] + ) + + let config = try Config.load() + let inUse = config.inUse!.asSnapshot! + #expect(inUse > .init(branch: branch, date: date)) + #expect(inUse.branch == branch) + #expect(inUse.date > date) + + try await SwiftlyTests.validateInstalledToolchains( + [config.inUse!], + description: "update should work with snapshots" + ) } /// Verify that the latest of all the matching release toolchains is updated. @@ -178,34 +173,31 @@ import Testing } /// Verify that the latest of all the matching snapshot toolchains is updated. - @Test func updateSelectsLatestMatchingSnapshotRelease() async throws { - let branches: [ToolchainVersion.Snapshot.Branch] = [ - .main, + @Test( + .testHomeMockedToolchain(), + arguments: [ + ToolchainVersion.Snapshot.Branch.main, .release(major: 6, minor: 0), ] + ) func updateSelectsLatestMatchingSnapshotRelease(_ branch: ToolchainVersion.Snapshot.Branch) async throws { + try await SwiftlyTests.withMockedToolchain { + try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: "2024-06-19")) + try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: "2024-06-18")) - for branch in branches { - try await SwiftlyTests.withTestHome { - try await SwiftlyTests.withMockedToolchain { - try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: "2024-06-19")) - try await SwiftlyTests.installMockedToolchain(selector: .snapshot(branch: branch, date: "2024-06-18")) - - try await SwiftlyTests.runCommand( - Update.self, ["update", "-y", "\(branch.name)-snapshot", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] - ) + try await SwiftlyTests.runCommand( + Update.self, ["update", "-y", "\(branch.name)-snapshot", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"] + ) - let config = try Config.load() - let inUse = config.inUse!.asSnapshot! + let config = try Config.load() + let inUse = config.inUse!.asSnapshot! - #expect(inUse.branch == branch) - #expect(inUse.date > "2024-06-18") + #expect(inUse.branch == branch) + #expect(inUse.date > "2024-06-18") - try await SwiftlyTests.validateInstalledToolchains( - [config.inUse!, .init(snapshotBranch: branch, date: "2024-06-18")], - description: "update with ambiguous selector should update the latest matching toolchain" - ) - } - } + try await SwiftlyTests.validateInstalledToolchains( + [config.inUse!, .init(snapshotBranch: branch, date: "2024-06-18")], + description: "update with ambiguous selector should update the latest matching toolchain" + ) } } } diff --git a/Tests/SwiftlyTests/UseTests.swift b/Tests/SwiftlyTests/UseTests.swift index e960da04..0e216b88 100644 --- a/Tests/SwiftlyTests/UseTests.swift +++ b/Tests/SwiftlyTests/UseTests.swift @@ -189,8 +189,8 @@ import Testing @Test func printInUse() async throws { let toolchains = [ ToolchainVersion.newStable, - ToolchainVersion.newMainSnapshot, - ToolchainVersion.newReleaseSnapshot, + .newMainSnapshot, + .newReleaseSnapshot, ] try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { for toolchain in toolchains { @@ -211,8 +211,8 @@ import Testing @Test func swiftVersionFile() async throws { let toolchains = [ ToolchainVersion.newStable, - ToolchainVersion.newMainSnapshot, - ToolchainVersion.newReleaseSnapshot, + .newMainSnapshot, + .newReleaseSnapshot, ] try await SwiftlyTests.withMockedHome(homeName: Self.homeName, toolchains: Set(toolchains)) { let versionFile = SwiftlyTests.ctx.currentDirectory.appendingPathComponent(".swift-version") From 7979a6674f7959742f3988c444d898642cfdab77 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sat, 5 Apr 2025 21:38:08 -0400 Subject: [PATCH 08/12] Improve test temporary file cleanup Run HTTPClientTests serially to avoid network contention --- Tests/SwiftlyTests/HTTPClientTests.swift | 1 + Tests/SwiftlyTests/SwiftlyTests.swift | 54 ++++++++++++++++++------ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index 7cd8d6ca..2136be05 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -38,6 +38,7 @@ import Testing } @Test( + .serialized, arguments: [PlatformDefinition.macOS, .ubuntu2404, .ubuntu2204, .rhel9, .fedora39, .amazonlinux2, .debian12], [Components.Schemas.Architecture.x8664, .aarch64] diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 63b92d40..5c0078e4 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -229,7 +229,7 @@ public enum SwiftlyTests { let testHome = Self.getTestHomePath(name: name) defer { - try? testHome.deleteIfExists() + try? FileManager.default.removeItem(atPath: testHome.path) } let ctx = SwiftlyCoreContext( @@ -244,6 +244,12 @@ public enum SwiftlyTests { try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: false) } + defer { + for dir in Swiftly.requiredDirectories(ctx) { + try? FileManager.default.removeItem(at: dir) + } + } + let config = try await Self.baseTestConfig() try config.save(ctx) @@ -272,6 +278,10 @@ public enum SwiftlyTests { at: Swiftly.currentPlatform.swiftlyBinDir(Self.ctx), withIntermediateDirectories: true ) + + defer { + try? FileManager.default.removeItem(at: Swiftly.currentPlatform.swiftlyBinDir(Self.ctx)) + } } try await f() @@ -507,7 +517,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { private let executables: [String] #if os(Linux) - private var signatures: [String: URL] + private var signatures: [String: Data] #endif private let latestSwiftlyVersion: SwiftlyVersion @@ -699,10 +709,13 @@ public class MockToolchainDownloader: HTTPRequestExecutor { throw SwiftlyTestError(message: "swiftly signature wasn't found in the cache") } - return try Data(contentsOf: signature) + return signature } let tmp = FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") + defer { + try? FileManager.default.removeItem(at: tmp) + } let swiftlyDir = tmp.appendingPathComponent("swiftly", isDirectory: true) try FileManager.default.createDirectory( @@ -738,7 +751,12 @@ public class MockToolchainDownloader: HTTPRequestExecutor { // Extra step involves generating a gpg signature and putting that in a cache for a later request. We will // use a local key for this to avoid running into entropy problems in CI. let gpgKeyFile = FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") + try Data(PackageResources.mock_signing_key_private_pgp).write(to: gpgKeyFile) + defer { + try? FileManager.default.removeItem(at: gpgKeyFile) + } + let importKey = Process() importKey.executableURL = URL(fileURLWithPath: "/usr/bin/env") importKey.arguments = ["bash", "-c", """ @@ -770,7 +788,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { throw SwiftlyTestError(message: "unable to sign archive using the test user's gpg key") } - self.signatures["swiftly"] = archive.appendingPathExtension("sig") + self.signatures["swiftly"] = try Data(contentsOf: archive.appendingPathExtension("sig")) return try Data(contentsOf: archive) } @@ -783,10 +801,14 @@ public class MockToolchainDownloader: HTTPRequestExecutor { throw SwiftlyTestError(message: "signature wasn't found in the cache") } - return try Data(contentsOf: signature) + return signature } let tmp = FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") + defer { + try? FileManager.default.removeItem(at: tmp) + } + let toolchainDir = tmp.appendingPathComponent("toolchain", isDirectory: true) let toolchainBinDir = toolchainDir .appendingPathComponent("usr", isDirectory: true) @@ -826,6 +848,10 @@ public class MockToolchainDownloader: HTTPRequestExecutor { // use a local key for this to avoid running into entropy problems in CI. let gpgKeyFile = FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") try Data(PackageResources.mock_signing_key_private_pgp).write(to: gpgKeyFile) + defer { + try? FileManager.default.removeItem(at: gpgKeyFile) + } + let importKey = Process() importKey.executableURL = URL(fileURLWithPath: "/usr/bin/env") importKey.arguments = ["bash", "-c", """ @@ -857,7 +883,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { throw SwiftlyTestError(message: "unable to sign archive using the test user's gpg key") } - self.signatures[toolchain.name] = archive.appendingPathExtension("sig") + self.signatures[toolchain.name] = try Data(contentsOf: archive.appendingPathExtension("sig")) return try Data(contentsOf: archive) } @@ -865,6 +891,10 @@ public class MockToolchainDownloader: HTTPRequestExecutor { #elseif os(macOS) public func makeMockedSwiftly(from _: URL) throws -> Data { let tmp = FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") + defer { + try? FileManager.default.removeItem(at: tmp) + } + let swiftlyDir = tmp.appendingPathComponent(".swiftly", isDirectory: true) let swiftlyBinDir = swiftlyDir.appendingPathComponent("bin") @@ -873,10 +903,6 @@ public class MockToolchainDownloader: HTTPRequestExecutor { withIntermediateDirectories: true ) - defer { - try? FileManager.default.removeItem(at: tmp) - } - for executable in ["swiftly"] { let executablePath = swiftlyBinDir.appendingPathComponent(executable) @@ -917,6 +943,10 @@ public class MockToolchainDownloader: HTTPRequestExecutor { public func makeMockedToolchain(toolchain: ToolchainVersion, name _: String) throws -> Data { let tmp = FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())") + defer { + try? FileManager.default.removeItem(at: tmp) + } + let toolchainDir = tmp.appendingPathComponent("toolchain", isDirectory: true) let toolchainBinDir = toolchainDir.appendingPathComponent("usr/bin", isDirectory: true) @@ -925,10 +955,6 @@ public class MockToolchainDownloader: HTTPRequestExecutor { withIntermediateDirectories: true ) - defer { - try? FileManager.default.removeItem(at: tmp) - } - for executable in self.executables { let executablePath = toolchainBinDir.appendingPathComponent(executable) From 1b3aef5cd405c170d6382fd0d9c14506a7c736a3 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sat, 5 Apr 2025 21:54:41 -0400 Subject: [PATCH 09/12] More swiftly test cleanup routines Bump the default http client read timeout --- Sources/LinuxPlatform/Linux.swift | 3 +++ Sources/SwiftlyCore/HTTPClient.swift | 8 +++---- Tests/SwiftlyTests/PlatformTests.swift | 30 ++++++++++++++++++++------ 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Sources/LinuxPlatform/Linux.swift b/Sources/LinuxPlatform/Linux.swift index 3c725b7d..124a8b23 100644 --- a/Sources/LinuxPlatform/Linux.swift +++ b/Sources/LinuxPlatform/Linux.swift @@ -361,6 +361,9 @@ public struct Linux: Platform { } let tmpDir = self.getTempFilePath() + defer { + try? FileManager.default.removeItem(at: tmpDir) + } try FileManager.default.createDirectory(atPath: tmpDir.path, withIntermediateDirectories: true) ctx.print("Extracting new swiftly...") diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index e3f00fc2..8beba11b 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -124,7 +124,7 @@ public class HTTPRequestExecutorImpl: HTTPRequestExecutor { } public func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { - let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(45)) + let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(60)) let swiftlyUserAgent = SwiftlyUserAgentMiddleware() let client = Client( @@ -138,7 +138,7 @@ public class HTTPRequestExecutorImpl: HTTPRequestExecutor { } public func getReleaseToolchains() async throws -> [Components.Schemas.Release] { - let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(45)) + let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(60)) let swiftlyUserAgent = SwiftlyUserAgentMiddleware() let client = Client( @@ -153,7 +153,7 @@ public class HTTPRequestExecutorImpl: HTTPRequestExecutor { } public func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { - let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(45)) + let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(60)) let swiftlyUserAgent = SwiftlyUserAgentMiddleware() let client = Client( @@ -457,7 +457,7 @@ public struct SwiftlyHTTPClient { } let request = makeRequest(url: url.absoluteString) - let response = try await self.httpRequestExecutor.execute(request, timeout: .seconds(45)) + let response = try await self.httpRequestExecutor.execute(request, timeout: .seconds(60)) switch response.status { case .ok: diff --git a/Tests/SwiftlyTests/PlatformTests.swift b/Tests/SwiftlyTests/PlatformTests.swift index 7505f152..6a548010 100644 --- a/Tests/SwiftlyTests/PlatformTests.swift +++ b/Tests/SwiftlyTests/PlatformTests.swift @@ -4,7 +4,7 @@ import Foundation import Testing @Suite struct PlatformTests { - func mockToolchainDownload(version: String) async throws -> (URL, ToolchainVersion) { + func mockToolchainDownload(version: String) async throws -> (URL, ToolchainVersion, URL) { let mockDownloader = MockToolchainDownloader(executables: ["swift"]) let version = try! ToolchainVersion(parsing: version) let ext = Swiftly.currentPlatform.toolchainFileExtension @@ -14,12 +14,19 @@ import Testing let mockedToolchain = try mockDownloader.makeMockedToolchain(toolchain: version, name: tmpDir.path) try mockedToolchain.write(to: mockedToolchainFile) - return (mockedToolchainFile, version) + return (mockedToolchainFile, version, tmpDir) } @Test(.testHome()) func install() async throws { // GIVEN: a toolchain has been downloaded - var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.7.1") + var (mockedToolchainFile, version, tmpDir) = try await self.mockToolchainDownload(version: "5.7.1") + var cleanup = [tmpDir] + defer { + for dir in cleanup { + try? FileManager.default.removeItem(at: dir) + } + } + // WHEN: the platform installs the toolchain try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchain is extracted in the toolchains directory @@ -27,7 +34,8 @@ import Testing #expect(1 == toolchains.count) // GIVEN: a second toolchain has been downloaded - (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") + (mockedToolchainFile, version, tmpDir) = try await self.mockToolchainDownload(version: "5.8.0") + cleanup += [tmpDir] // WHEN: the platform installs the toolchain try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchain is added to the toolchains directory @@ -35,7 +43,8 @@ import Testing #expect(2 == toolchains.count) // GIVEN: an identical toolchain has been downloaded - (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") + (mockedToolchainFile, version, tmpDir) = try await self.mockToolchainDownload(version: "5.8.0") + cleanup += [tmpDir] // WHEN: the platform installs the toolchain try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // THEN: the toolchains directory remains the same @@ -45,9 +54,16 @@ import Testing @Test(.testHome()) func uninstall() async throws { // GIVEN: toolchains have been downloaded, and installed - var (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.8.0") + var (mockedToolchainFile, version, tmpDir) = try await self.mockToolchainDownload(version: "5.8.0") + var cleanup = [tmpDir] + defer { + for dir in cleanup { + try? FileManager.default.removeItem(at: dir) + } + } try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) - (mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.6.3") + (mockedToolchainFile, version, tmpDir) = try await self.mockToolchainDownload(version: "5.6.3") + cleanup += [tmpDir] try Swiftly.currentPlatform.install(SwiftlyTests.ctx, from: mockedToolchainFile, version: version, verbose: true) // WHEN: one of the toolchains is uninstalled try Swiftly.currentPlatform.uninstall(SwiftlyTests.ctx, version, verbose: true) From ff4a70741ea8cb5bfb35f38adb2dfd788314c834 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sat, 5 Apr 2025 22:08:03 -0400 Subject: [PATCH 10/12] Serialize the entire HTTPClientTests suite --- Tests/SwiftlyTests/HTTPClientTests.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index 2136be05..dbb49877 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -4,7 +4,7 @@ import Foundation @testable import SwiftlyCore import Testing -@Suite struct HTTPClientTests { +@Suite(.serialized) struct HTTPClientTests { @Test func getSwiftOrgGPGKeys() async throws { let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()) @@ -38,7 +38,6 @@ import Testing } @Test( - .serialized, arguments: [PlatformDefinition.macOS, .ubuntu2404, .ubuntu2204, .rhel9, .fedora39, .amazonlinux2, .debian12], [Components.Schemas.Architecture.x8664, .aarch64] From c13cfa6ce90d36336b615cdcfc956c9cfb58e4d0 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sun, 6 Apr 2025 06:59:53 -0400 Subject: [PATCH 11/12] Add retries to HTTPClientTests to work around CI issues --- Tests/SwiftlyTests/HTTPClientTests.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index dbb49877..08bf15fe 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -16,7 +16,12 @@ import Testing let gpgKeysUrl = URL(string: "https://www.swift.org/keys/all-keys.asc")! - try await httpClient.downloadFile(url: gpgKeysUrl, to: tmpFile) + do { + try await httpClient.downloadFile(url: gpgKeysUrl, to: tmpFile) + } catch { + // Retry once to improve CI resiliency + try await httpClient.downloadFile(url: gpgKeysUrl, to: tmpFile) + } #if os(Linux) // With linux, we can ask gpg to try an import to see if the file is valid @@ -33,8 +38,13 @@ import Testing @Test func getSwiftlyReleaseMetadataFromSwiftOrg() async throws { let httpClient = SwiftlyHTTPClient(httpRequestExecutor: HTTPRequestExecutorImpl()) - let currentRelease = try await httpClient.getCurrentSwiftlyRelease() - #expect(throws: Never.self) { try currentRelease.swiftlyVersion } + do { + let currentRelease = try await httpClient.getCurrentSwiftlyRelease() + #expect(throws: Never.self) { try currentRelease.swiftlyVersion } + } catch { + let currentRelease = try await httpClient.getCurrentSwiftlyRelease() + #expect(throws: Never.self) { try currentRelease.swiftlyVersion } + } } @Test( From 837e8e972b2bcd525fc6957b3a21c79b03d9efd6 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Sun, 6 Apr 2025 12:42:54 -0400 Subject: [PATCH 12/12] Create OpenAPI client in a single place for better consistency and remove unused Response --- Sources/SwiftlyCore/HTTPClient.swift | 44 ++++++++-------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 8beba11b..eee76dba 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -123,47 +123,32 @@ public class HTTPRequestExecutorImpl: HTTPRequestExecutor { try await self.httpClient.execute(request, timeout: timeout) } - public func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { - let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(60)) + private func client() throws -> Client { let swiftlyUserAgent = SwiftlyUserAgentMiddleware() + let transport: ClientTransport + + let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30)) + transport = AsyncHTTPClientTransport(configuration: config) - let client = Client( + return Client( serverURL: try Servers.Server1.url(), - transport: AsyncHTTPClientTransport(configuration: config), + transport: transport, middlewares: [swiftlyUserAgent] ) + } - let response = try await client.getCurrentSwiftlyRelease() + public func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { + let response = try await self.client().getCurrentSwiftlyRelease() return try response.ok.body.json } public func getReleaseToolchains() async throws -> [Components.Schemas.Release] { - let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(60)) - let swiftlyUserAgent = SwiftlyUserAgentMiddleware() - - let client = Client( - serverURL: try Servers.Server1.url(), - transport: AsyncHTTPClientTransport(configuration: config), - middlewares: [swiftlyUserAgent] - ) - - let response = try await client.listReleases() - + let response = try await self.client().listReleases() return try response.ok.body.json } public func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { - let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(60)) - let swiftlyUserAgent = SwiftlyUserAgentMiddleware() - - let client = Client( - serverURL: try Servers.Server1.url(), - transport: AsyncHTTPClientTransport(configuration: config), - middlewares: [swiftlyUserAgent] - ) - - let response = try await client.listDevToolchains(.init(path: .init(branch: branch, platform: platform))) - + let response = try await self.client().listDevToolchains(.init(path: .init(branch: branch, platform: platform))) return try response.ok.body.json } } @@ -301,11 +286,6 @@ public struct SwiftlyHTTPClient { self.httpRequestExecutor = httpRequestExecutor } - private struct Response { - let status: HTTPResponseStatus - let buffer: ByteBuffer - } - public struct JSONNotFoundError: LocalizedError { public var url: String }