diff --git a/Sources/SPMBuildCore/Plugins/PluginInvocation.swift b/Sources/SPMBuildCore/Plugins/PluginInvocation.swift index e7414507105..66eef0d25f9 100644 --- a/Sources/SPMBuildCore/Plugins/PluginInvocation.swift +++ b/Sources/SPMBuildCore/Plugins/PluginInvocation.swift @@ -24,6 +24,46 @@ public enum PluginAction { } extension PluginTarget { + public func invoke( + action: PluginAction, + buildEnvironment: BuildEnvironment, + scriptRunner: PluginScriptRunner, + workingDirectory: AbsolutePath, + outputDirectory: AbsolutePath, + toolSearchDirectories: [AbsolutePath], + accessibleTools: [String: (path: AbsolutePath, triples: [String]?)], + writableDirectories: [AbsolutePath], + readOnlyDirectories: [AbsolutePath], + allowNetworkConnections: [SandboxNetworkPermission], + pkgConfigDirectories: [AbsolutePath], + sdkRootPath: AbsolutePath?, + fileSystem: FileSystem, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + delegate: PluginInvocationDelegate + ) async throws -> Bool { + try await safe_async { + self.invoke( + action: action, + buildEnvironment: buildEnvironment, + scriptRunner: scriptRunner, + workingDirectory: workingDirectory, + outputDirectory: outputDirectory, + toolSearchDirectories: toolSearchDirectories, + accessibleTools: accessibleTools, + writableDirectories: writableDirectories, + readOnlyDirectories: readOnlyDirectories, + allowNetworkConnections: allowNetworkConnections, + pkgConfigDirectories: pkgConfigDirectories, + sdkRootPath: sdkRootPath, + fileSystem: fileSystem, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue, + delegate: delegate, + completion: $0 + ) + } + } /// Invokes the plugin by compiling its source code (if needed) and then running it as a subprocess. The specified /// plugin action determines which entry point is called in the subprocess, and the package and the tool mapping /// determine the context that is available to the plugin. @@ -47,6 +87,7 @@ extension PluginTarget { /// - fileSystem: The file system to which all of the paths refers. /// /// - Returns: A PluginInvocationResult that contains the results of invoking the plugin. + @available(*, noasync, message: "Use the async alternative") public func invoke( action: PluginAction, buildEnvironment: BuildEnvironment, diff --git a/Sources/SPMBuildCore/Plugins/PluginScriptRunner.swift b/Sources/SPMBuildCore/Plugins/PluginScriptRunner.swift index b833f7d10ae..a898851f23d 100644 --- a/Sources/SPMBuildCore/Plugins/PluginScriptRunner.swift +++ b/Sources/SPMBuildCore/Plugins/PluginScriptRunner.swift @@ -20,6 +20,7 @@ import PackageGraph public protocol PluginScriptRunner { /// Public protocol function that starts compiling the plugin script to an executable. The name is used as the basename for the executable and auxiliary files. The tools version controls the availability of APIs in PackagePlugin, and should be set to the tools version of the package that defines the plugin (not of the target to which it is being applied). This function returns immediately and then calls the completion handler on the callback queue when compilation ends. + @available(*, noasync, message: "Use the async alternative") func compilePluginScript( sourceFiles: [AbsolutePath], pluginName: String, @@ -61,6 +62,29 @@ public protocol PluginScriptRunner { var hostTriple: Triple { get throws } } +public extension PluginScriptRunner { + func compilePluginScript( + sourceFiles: [AbsolutePath], + pluginName: String, + toolsVersion: ToolsVersion, + observabilityScope: ObservabilityScope, + callbackQueue: DispatchQueue, + delegate: PluginScriptCompilerDelegate + ) async throws -> PluginCompilationResult { + try await safe_async { + self.compilePluginScript( + sourceFiles: sourceFiles, + pluginName: pluginName, + toolsVersion: toolsVersion, + observabilityScope: observabilityScope, + callbackQueue: callbackQueue, + delegate: delegate, + completion: $0 + ) + } + } +} + /// Protocol by which `PluginScriptRunner` communicates back to the caller as it compiles plugins. public protocol PluginScriptCompilerDelegate { /// Called immediately before compiling a plugin. Will not be called if the plugin didn't have to be compiled. This call is always followed by a `didCompilePlugin()` but is mutually exclusive with a `skippedCompilingPlugin()` call. diff --git a/Sources/SPMTestSupport/misc.swift b/Sources/SPMTestSupport/misc.swift index 8164a923b6c..bec9b39ace7 100644 --- a/Sources/SPMTestSupport/misc.swift +++ b/Sources/SPMTestSupport/misc.swift @@ -48,22 +48,23 @@ public func testWithTemporaryDirectory( ) } -public func testWithTemporaryDirectory( +@discardableResult +public func testWithTemporaryDirectory( function: StaticString = #function, - body: (AbsolutePath) async throws -> Void -) async throws { + body: (AbsolutePath) async throws -> Result +) async throws -> Result { let cleanedFunction = function.description .replacingOccurrences(of: "(", with: "") .replacingOccurrences(of: ")", with: "") .replacingOccurrences(of: ".", with: "") .replacingOccurrences(of: ":", with: "_") - try await withTemporaryDirectory(prefix: "spm-tests-\(cleanedFunction)") { tmpDirPath in + return try await withTemporaryDirectory(prefix: "spm-tests-\(cleanedFunction)") { tmpDirPath in defer { // Unblock and remove the tmp dir on deinit. try? localFileSystem.chmod(.userWritable, path: tmpDirPath, options: [.recursive]) try? localFileSystem.removeFileTree(tmpDirPath) } - try await body(tmpDirPath) + return try await body(tmpDirPath) } } diff --git a/Sources/SourceControl/RepositoryManager.swift b/Sources/SourceControl/RepositoryManager.swift index 4d31db93c9e..8e9551e5f54 100644 --- a/Sources/SourceControl/RepositoryManager.swift +++ b/Sources/SourceControl/RepositoryManager.swift @@ -92,6 +92,27 @@ public class RepositoryManager: Cancellable { self.concurrencySemaphore = DispatchSemaphore(value: maxConcurrentOperations) } + public func lookup( + package: PackageIdentity, + repository: RepositorySpecifier, + updateStrategy: RepositoryUpdateStrategy, + observabilityScope: ObservabilityScope, + delegateQueue: DispatchQueue, + callbackQueue: DispatchQueue + ) async throws -> RepositoryHandle { + try await safe_async { + self.lookup( + package: package, + repository: repository, + updateStrategy: updateStrategy, + observabilityScope: observabilityScope, + delegateQueue: delegateQueue, + callbackQueue: callbackQueue, + completion: $0 + ) + } + } + /// Get a handle to a repository. /// /// This will initiate a clone of the repository automatically, if necessary. @@ -107,6 +128,7 @@ public class RepositoryManager: Cancellable { /// - delegateQueue: Dispatch queue for delegate events /// - callbackQueue: Dispatch queue for callbacks /// - completion: The completion block that should be called after lookup finishes. + @available(*, noasync, message: "Use the async alternative") public func lookup( package: PackageIdentity, repository: RepositorySpecifier, diff --git a/Tests/CommandsTests/PackageRegistryToolTests.swift b/Tests/CommandsTests/PackageRegistryToolTests.swift index 3b08ec12513..fd0a33e23c9 100644 --- a/Tests/CommandsTests/PackageRegistryToolTests.swift +++ b/Tests/CommandsTests/PackageRegistryToolTests.swift @@ -643,7 +643,7 @@ final class PackageRegistryToolTests: CommandsTestCase { // Validate signatures var verifierConfiguration = VerifierConfiguration() - verifierConfiguration.trustedRoots = try temp_await { self.testRoots(callback: $0) } + verifierConfiguration.trustedRoots = try testRoots() // archive signature let archivePath = workingDirectory.appending("\(packageIdentity)-\(version).zip") @@ -753,7 +753,7 @@ final class PackageRegistryToolTests: CommandsTestCase { // Validate signatures var verifierConfiguration = VerifierConfiguration() - verifierConfiguration.trustedRoots = try temp_await { self.testRoots(callback: $0) } + verifierConfiguration.trustedRoots = try testRoots() // archive signature let archivePath = workingDirectory.appending("\(packageIdentity)-\(version).zip") @@ -860,7 +860,7 @@ final class PackageRegistryToolTests: CommandsTestCase { // Validate signatures var verifierConfiguration = VerifierConfiguration() - verifierConfiguration.trustedRoots = try temp_await { self.testRoots(callback: $0) } + verifierConfiguration.trustedRoots = try testRoots() // archive signature let archivePath = workingDirectory.appending("\(packageIdentity)-\(version).zip") @@ -920,15 +920,11 @@ final class PackageRegistryToolTests: CommandsTestCase { XCTAssertEqual(try SwiftPackageRegistryTool.Login.loginURL(from: registryURL, loginAPIPath: "/secret-sign-in").absoluteString, "https://packages.example.com:8081/secret-sign-in") } - private func testRoots(callback: (Result<[[UInt8]], Error>) -> Void) { - do { - try fixture(name: "Signing", createGitRepo: false) { fixturePath in - let rootCA = try localFileSystem - .readFileContents(fixturePath.appending(components: "Certificates", "TestRootCA.cer")).contents - callback(.success([rootCA])) - } - } catch { - callback(.failure(error)) + private func testRoots() throws -> [[UInt8]] { + try fixture(name: "Signing", createGitRepo: false) { fixturePath in + let rootCA = try localFileSystem + .readFileContents(fixturePath.appending(components: "Certificates", "TestRootCA.cer")).contents + return [rootCA] } } diff --git a/Tests/CommandsTests/PackageToolTests.swift b/Tests/CommandsTests/PackageToolTests.swift index 82ddc313da9..9850bdaa4ce 100644 --- a/Tests/CommandsTests/PackageToolTests.swift +++ b/Tests/CommandsTests/PackageToolTests.swift @@ -2903,11 +2903,11 @@ final class PackageToolTests: CommandsTestCase { } } - func testSinglePluginTarget() throws { + func testSinglePluginTarget() async throws { // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try testWithTemporaryDirectory { tmpPath in + try await testWithTemporaryDirectory { tmpPath in // Create a sample package with a library target and a plugin. let packageDir = tmpPath.appending(components: "MyPackage") try localFileSystem.createDirectory(packageDir, recursive: true) @@ -2956,13 +2956,10 @@ final class PackageToolTests: CommandsTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. diff --git a/Tests/FunctionalTests/PluginTests.swift b/Tests/FunctionalTests/PluginTests.swift index 43da00c4590..8e2e0361f7c 100644 --- a/Tests/FunctionalTests/PluginTests.swift +++ b/Tests/FunctionalTests/PluginTests.swift @@ -168,14 +168,14 @@ class PluginTests: XCTestCase { } } - func testCommandPluginInvocation() throws { + func testCommandPluginInvocation() async throws { try XCTSkipIf(true, "test is disabled because it isn't stable, see rdar://117870608") // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") // FIXME: This test is getting quite long — we should add some support functionality for creating synthetic plugin tests and factor this out into separate tests. - try testWithTemporaryDirectory { tmpPath in + try await testWithTemporaryDirectory { tmpPath in // Create a sample package with a library target and a plugin. It depends on a sample package. let packageDir = tmpPath.appending(components: "MyPackage") let manifestFile = packageDir.appending("Package.swift") @@ -366,13 +366,10 @@ class PluginTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. @@ -434,7 +431,7 @@ class PluginTests: XCTestCase { line: UInt = #line, expectFailure: Bool = false, diagnosticsChecker: (DiagnosticsTestResult) throws -> Void - ) { + ) async { // Find the named plugin. let plugins = package.targets.compactMap{ $0.underlyingTarget as? PluginTarget } guard let plugin = plugins.first(where: { $0.name == pluginName }) else { @@ -462,7 +459,7 @@ class PluginTests: XCTestCase { ) let toolSearchDirectories = [try UserToolchain.default.swiftCompilerPath.parentDirectory] - let success = try temp_await { plugin.invoke( + let success = try await safe_async { plugin.invoke( action: .performCommand(package: package, arguments: arguments), buildEnvironment: BuildEnvironment(platform: .macOS, configuration: .debug), scriptRunner: scriptRunner, @@ -479,7 +476,8 @@ class PluginTests: XCTestCase { observabilityScope: observability.topScope, callbackQueue: delegateQueue, delegate: delegate, - completion: $0) } + completion: $0) + } if expectFailure { XCTAssertFalse(success, "expected command to fail, but it succeeded", file: file, line: line) } @@ -499,19 +497,19 @@ class PluginTests: XCTestCase { } // Invoke the command plugin that prints out various things it was given, and check them. - testCommand(package: package, plugin: "PluginPrintingInfo", targets: ["MyLibrary"], arguments: ["veni", "vidi", "vici"]) { output in + await testCommand(package: package, plugin: "PluginPrintingInfo", targets: ["MyLibrary"], arguments: ["veni", "vidi", "vici"]) { output in output.check(diagnostic: .equal("Root package is MyPackage."), severity: .info) output.check(diagnostic: .and(.prefix("Found the swiftc tool"), .suffix(".")), severity: .info) } // Invoke the command plugin that throws an unhandled error at the top level. - testCommand(package: package, plugin: "PluginFailingWithError", targets: [], arguments: [], expectFailure: true) { output in + await testCommand(package: package, plugin: "PluginFailingWithError", targets: [], arguments: [], expectFailure: true) { output in output.check(diagnostic: .equal("This text should appear before the uncaught thrown error."), severity: .info) output.check(diagnostic: .equal("This is the uncaught thrown error."), severity: .error) } // Invoke the command plugin that exits with code 1 without returning an error. - testCommand(package: package, plugin: "PluginFailingWithoutError", targets: [], arguments: [], expectFailure: true) { output in + await testCommand(package: package, plugin: "PluginFailingWithoutError", targets: [], arguments: [], expectFailure: true) { output in output.check(diagnostic: .equal("This text should appear before we exit."), severity: .info) output.check(diagnostic: .equal("Plugin ended with exit code 1"), severity: .error) } @@ -534,10 +532,10 @@ class PluginTests: XCTestCase { } } - func testPluginUsageDoesntAffectTestTargetMappings() throws { + func testPluginUsageDoesntAffectTestTargetMappings() async throws { try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try fixture(name: "Miscellaneous/Plugins/MySourceGenPlugin") { packageDir in + try await fixture(name: "Miscellaneous/Plugins/MySourceGenPlugin") { packageDir in // Load a workspace from the package. let observability = ObservabilitySystem.makeForTesting() let workspace = try Workspace( @@ -549,13 +547,10 @@ class PluginTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. @@ -570,11 +565,11 @@ class PluginTests: XCTestCase { } } - func testCommandPluginCancellation() throws { + func testCommandPluginCancellation() async throws { // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try testWithTemporaryDirectory { tmpPath in + try await testWithTemporaryDirectory { (tmpPath: AbsolutePath) -> Void in // Create a sample package with a couple of plugins a other targets and products. let packageDir = tmpPath.appending(components: "MyPackage") try localFileSystem.createDirectory(packageDir, recursive: true) @@ -646,13 +641,10 @@ class PluginTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. @@ -732,64 +724,81 @@ class PluginTests: XCTestCase { toolchain: try UserToolchain.default ) let delegate = PluginDelegate(delegateQueue: delegateQueue) - let sync = DispatchSemaphore(value: 0) - plugin.invoke( - action: .performCommand(package: package, arguments: []), - buildEnvironment: BuildEnvironment(platform: .macOS, configuration: .debug), - scriptRunner: scriptRunner, - workingDirectory: package.path, - outputDirectory: pluginDir.appending("output"), - toolSearchDirectories: [try UserToolchain.default.swiftCompilerPath.parentDirectory], - accessibleTools: [:], - writableDirectories: [pluginDir.appending("output")], - readOnlyDirectories: [package.path], - allowNetworkConnections: [], - pkgConfigDirectories: [], - sdkRootPath: try UserToolchain.default.sdkRootPath, - fileSystem: localFileSystem, - observabilityScope: observability.topScope, - callbackQueue: delegateQueue, - delegate: delegate, - completion: { _ in - sync.signal() + try await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + // TODO: have invoke natively support task cancelation instead + try await withTaskCancellationHandler { + _ = try await plugin.invoke( + action: .performCommand(package: package, arguments: []), + buildEnvironment: BuildEnvironment(platform: .macOS, configuration: .debug), + scriptRunner: scriptRunner, + workingDirectory: package.path, + outputDirectory: pluginDir.appending("output"), + toolSearchDirectories: [try UserToolchain.default.swiftCompilerPath.parentDirectory], + accessibleTools: [:], + writableDirectories: [pluginDir.appending("output")], + readOnlyDirectories: [package.path], + allowNetworkConnections: [], + pkgConfigDirectories: [], + sdkRootPath: try UserToolchain.default.sdkRootPath, + fileSystem: localFileSystem, + observabilityScope: observability.topScope, + callbackQueue: delegateQueue, + delegate: delegate + ) + } onCancel: { + do { + try scriptRunner.cancel(deadline: .now() + .seconds(5)) + } catch { + XCTFail("Cancelling script runner should not fail: \(error)") + } + } } - ) - - // Wait for three seconds. - let result = sync.wait(timeout: .now() + 3) - XCTAssertEqual(result, .timedOut, "expected the plugin to time out") - - // At this point we should have parsed out the process identifier. But it's possible we don't always — this is being investigated in rdar://88792829. - var pid: Int? = .none - delegateQueue.sync { - pid = delegate.parsedProcessIdentifier - } - guard let pid else { - throw XCTSkip("skipping test because no pid was received from the plugin; being investigated as rdar://88792829\n\(delegate.diagnostics.description)") + group.addTask { + do { + try await Task.sleep(nanoseconds: UInt64(DispatchTimeInterval.seconds(3).nanoseconds()!)) + } catch { + XCTFail("The plugin should not finish within 3 seconds") + } + } + + try await group.next() + + + // At this point we should have parsed out the process identifier. But it's possible we don't always — this is being investigated in rdar://88792829. + var pid: Int? = .none + delegateQueue.sync { + pid = delegate.parsedProcessIdentifier + } + guard let pid else { + throw XCTSkip("skipping test because no pid was received from the plugin; being investigated as rdar://88792829\n\(delegate.diagnostics.description)") + } + + // Check that it's running (we do this by asking for its priority — this only works on some platforms). + #if os(macOS) + errno = 0 + getpriority(Int32(PRIO_PROCESS), UInt32(pid)) + XCTAssertEqual(errno, 0, "unexpectedly got errno \(errno) when trying to check process \(pid)") + #endif + + // Ask the plugin running to cancel all plugins. + group.cancelAll() + + // Check that it's no longer running (we do this by asking for its priority — this only works on some platforms). + #if os(macOS) + errno = 0 + getpriority(Int32(PRIO_PROCESS), UInt32(pid)) + XCTAssertEqual(errno, ESRCH, "unexpectedly got errno \(errno) when trying to check process \(pid)") + #endif } - - // Check that it's running (we do this by asking for its priority — this only works on some platforms). - #if os(macOS) - errno = 0 - getpriority(Int32(PRIO_PROCESS), UInt32(pid)) - XCTAssertEqual(errno, 0, "unexpectedly got errno \(errno) when trying to check process \(pid)") - #endif - - // Ask the plugin running to cancel all plugins. - try scriptRunner.cancel(deadline: .now() + .seconds(5)) - - // Check that it's no longer running (we do this by asking for its priority — this only works on some platforms). - #if os(macOS) - errno = 0 - getpriority(Int32(PRIO_PROCESS), UInt32(pid)) - XCTAssertEqual(errno, ESRCH, "unexpectedly got errno \(errno) when trying to check process \(pid)") - #endif + + } } - func testUnusedPluginProductWarnings() throws { + func testUnusedPluginProductWarnings() async throws { // Test the warnings we get around unused plugin products in package dependencies. - try testWithTemporaryDirectory { tmpPath in + try await testWithTemporaryDirectory { tmpPath in // Create a sample package that uses three packages that vend plugins. let packageDir = tmpPath.appending(components: "MyPackage") try localFileSystem.createDirectory(packageDir, recursive: true) @@ -942,13 +951,10 @@ class PluginTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. diff --git a/Tests/PackageFingerprintTests/FilePackageFingerprintStorageTests.swift b/Tests/PackageFingerprintTests/FilePackageFingerprintStorageTests.swift index 40941151672..0f8cfd70c98 100644 --- a/Tests/PackageFingerprintTests/FilePackageFingerprintStorageTests.swift +++ b/Tests/PackageFingerprintTests/FilePackageFingerprintStorageTests.swift @@ -22,7 +22,7 @@ import class TSCBasic.InMemoryFileSystem import struct TSCUtility.Version final class FilePackageFingerprintStorageTests: XCTestCase { - func testHappyCase() throws { + func testHappyCase() async throws { let mockFileSystem = InMemoryFileSystem() let directoryPath = AbsolutePath("/fingerprints") let storage = FilePackageFingerprintStorage(fileSystem: mockFileSystem, directoryPath: directoryPath) @@ -31,12 +31,12 @@ final class FilePackageFingerprintStorageTests: XCTestCase { // Add fingerprints for mona.LinkedList let package = PackageIdentity.plain("mona.LinkedList") - try storage.put( + try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init(origin: .registry(registryURL), value: "checksum-1.0.0", contentType: .sourceCode) ) - try storage.put( + try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init( @@ -45,7 +45,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { contentType: .sourceCode ) ) - try storage.put( + try await storage.put( package: package, version: Version("1.1.0"), fingerprint: .init(origin: .registry(registryURL), value: "checksum-1.1.0", contentType: .sourceCode) @@ -53,7 +53,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { // Fingerprint for another package let otherPackage = PackageIdentity.plain("other.LinkedList") - try storage.put( + try await storage.put( package: otherPackage, version: Version("1.0.0"), fingerprint: .init(origin: .registry(registryURL), value: "checksum-1.0.0", contentType: .sourceCode) @@ -68,7 +68,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { // Fingerprints should be saved do { - let fingerprints = try storage.get(package: package, version: Version("1.0.0")) + let fingerprints = try await storage.get(package: package, version: Version("1.0.0")) XCTAssertEqual(fingerprints.count, 2) let registryFingerprints = fingerprints[.registry] @@ -83,7 +83,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { } do { - let fingerprints = try storage.get(package: package, version: Version("1.1.0")) + let fingerprints = try await storage.get(package: package, version: Version("1.1.0")) XCTAssertEqual(fingerprints.count, 1) let registryFingerprints = fingerprints[.registry] @@ -93,7 +93,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { } do { - let fingerprints = try storage.get(package: otherPackage, version: Version("1.0.0")) + let fingerprints = try await storage.get(package: otherPackage, version: Version("1.0.0")) XCTAssertEqual(fingerprints.count, 1) let registryFingerprints = fingerprints[.registry] @@ -103,21 +103,21 @@ final class FilePackageFingerprintStorageTests: XCTestCase { } } - func testNotFound() throws { + func testNotFound() async throws { let mockFileSystem = InMemoryFileSystem() let directoryPath = AbsolutePath("/fingerprints") let storage = FilePackageFingerprintStorage(fileSystem: mockFileSystem, directoryPath: directoryPath) let registryURL = URL("https://example.packages.com") let package = PackageIdentity.plain("mona.LinkedList") - try storage.put( + try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init(origin: .registry(registryURL), value: "checksum-1.0.0", contentType: .sourceCode) ) // No fingerprints found for the content type - XCTAssertThrowsError(try storage.get( + await XCTAssertAsyncThrowsError(try await storage.get( package: package, version: Version("1.0.0"), kind: .registry, @@ -129,7 +129,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { } // No fingerprints found for the version - XCTAssertThrowsError(try storage.get(package: package, version: Version("1.1.0"))) { error in + await XCTAssertAsyncThrowsError(try await storage.get(package: package, version: Version("1.1.0"))) { error in guard case PackageFingerprintStorageError.notFound = error else { return XCTFail("Expected PackageFingerprintStorageError.notFound, got \(error)") } @@ -137,14 +137,14 @@ final class FilePackageFingerprintStorageTests: XCTestCase { // No fingerprints found for the package let otherPackage = PackageIdentity.plain("other.LinkedList") - XCTAssertThrowsError(try storage.get(package: otherPackage, version: Version("1.0.0"))) { error in + await XCTAssertAsyncThrowsError(try await storage.get(package: otherPackage, version: Version("1.0.0"))) { error in guard case PackageFingerprintStorageError.notFound = error else { return XCTFail("Expected PackageFingerprintStorageError.notFound, got \(error)") } } } - func testSingleFingerprintPerKindAndContentType() throws { + func testSingleFingerprintPerKindAndContentType() async throws { let mockFileSystem = InMemoryFileSystem() let directoryPath = AbsolutePath("/fingerprints") let storage = FilePackageFingerprintStorage(fileSystem: mockFileSystem, directoryPath: directoryPath) @@ -152,14 +152,14 @@ final class FilePackageFingerprintStorageTests: XCTestCase { let package = PackageIdentity.plain("mona.LinkedList") // Write registry checksum for v1.0.0 - try storage.put( + try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init(origin: .registry(registryURL), value: "checksum-1.0.0", contentType: .sourceCode) ) // Writing for the same version and kind and content type but different checksum should fail - XCTAssertThrowsError(try storage.put( + await XCTAssertAsyncThrowsError(try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init( @@ -174,7 +174,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { } // Writing for the same version and kind and content type same checksum should not fail - XCTAssertNoThrow(try storage.put( + _ = try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init( @@ -182,10 +182,10 @@ final class FilePackageFingerprintStorageTests: XCTestCase { value: "checksum-1.0.0", contentType: .sourceCode ) - )) + ) } - func testHappyCase_PackageReferenceAPI() throws { + func testHappyCase_PackageReferenceAPI() async throws { let mockFileSystem = InMemoryFileSystem() let directoryPath = AbsolutePath("/fingerprints") let storage = FilePackageFingerprintStorage(fileSystem: mockFileSystem, directoryPath: directoryPath) @@ -195,7 +195,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { url: sourceControlURL ) - try storage.put( + try await storage.put( package: packageRef, version: Version("1.0.0"), fingerprint: .init( @@ -204,7 +204,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { contentType: .sourceCode ) ) - try storage.put( + try await storage.put( package: packageRef, version: Version("1.1.0"), fingerprint: .init( @@ -215,7 +215,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { ) // Fingerprints should be saved - let fingerprints = try storage.get(package: packageRef, version: Version("1.1.0")) + let fingerprints = try await storage.get(package: packageRef, version: Version("1.1.0")) XCTAssertEqual(fingerprints.count, 1) let scmFingerprints = fingerprints[.sourceControl] @@ -225,7 +225,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { XCTAssertEqual(scmFingerprints?[.sourceCode]?.value, "gitHash-1.1.0") } - func testDifferentRepoURLsThatHaveSameIdentity() throws { + func testDifferentRepoURLsThatHaveSameIdentity() async throws { let mockFileSystem = InMemoryFileSystem() let directoryPath = AbsolutePath("/fingerprints") let storage = FilePackageFingerprintStorage(fileSystem: mockFileSystem, directoryPath: directoryPath) @@ -236,13 +236,13 @@ final class FilePackageFingerprintStorageTests: XCTestCase { let fooRef = PackageReference.remoteSourceControl(identity: PackageIdentity(url: fooURL), url: fooURL) let barRef = PackageReference.remoteSourceControl(identity: PackageIdentity(url: barURL), url: barURL) - try storage.put( + try await storage.put( package: fooRef, version: Version("1.0.0"), fingerprint: .init(origin: .sourceControl(fooURL), value: "abcde-foo", contentType: .sourceCode) ) // This should succeed because they get written to different files - try storage.put( + try await storage.put( package: barRef, version: Version("1.0.0"), fingerprint: .init(origin: .sourceControl(barURL), value: "abcde-bar", contentType: .sourceCode) @@ -261,7 +261,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { ) // This should fail because fingerprint for 1.0.0 already exists and it's different - XCTAssertThrowsError(try storage.put( + await XCTAssertAsyncThrowsError(try await storage.put( package: fooRef, version: Version("1.0.0"), fingerprint: .init( @@ -276,14 +276,14 @@ final class FilePackageFingerprintStorageTests: XCTestCase { } // This should succeed because fingerprint for 2.0.0 doesn't exist yet - try storage.put( + try await storage.put( package: fooRef, version: Version("2.0.0"), fingerprint: .init(origin: .sourceControl(fooURL), value: "abcde-foo", contentType: .sourceCode) ) } - func testConvertingFromV1ToV2() throws { + func testConvertingFromV1ToV2() async throws { let mockFileSystem = InMemoryFileSystem() let directoryPath = AbsolutePath("/fingerprints") try mockFileSystem.createDirectory(directoryPath, recursive: true) @@ -314,7 +314,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { try mockFileSystem.writeFileContents(fingerprintsPath, string: v1Fingerprints) // v1 fingerprints file should be converted to v2 when read - let fingerprints = try storage.get(package: package, version: Version("1.0.3")) + let fingerprints = try await storage.get(package: package, version: Version("1.0.3")) XCTAssertEqual(fingerprints.count, 1) let scmFingerprints = fingerprints[.sourceControl] @@ -324,7 +324,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { XCTAssertEqual(scmFingerprints?[.sourceCode]?.value, "e394bf350e38cb100b6bc4172834770ede1b7232") } - func testFingerprintsOfDifferentContentTypes() throws { + func testFingerprintsOfDifferentContentTypes() async throws { let mockFileSystem = InMemoryFileSystem() let directoryPath = AbsolutePath("/fingerprints") let storage = FilePackageFingerprintStorage(fileSystem: mockFileSystem, directoryPath: directoryPath) @@ -333,7 +333,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { // Add fingerprints for 1.0.0 source archive/code let package = PackageIdentity.plain("mona.LinkedList") - try storage.put( + try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init( @@ -342,7 +342,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { contentType: .sourceCode ) ) - try storage.put( + try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init( @@ -353,7 +353,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { ) // Add fingerprints for 1.0.0 manifests - try storage.put( + try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init( @@ -362,7 +362,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { contentType: .manifest(.none) ) ) - try storage.put( + try await storage.put( package: package, version: Version("1.0.0"), fingerprint: .init( @@ -373,7 +373,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { ) // Add fingerprint for 1.1.0 source archive - try storage.put( + try await storage.put( package: package, version: Version("1.1.0"), fingerprint: .init( @@ -383,7 +383,7 @@ final class FilePackageFingerprintStorageTests: XCTestCase { ) ) - let fingerprints = try storage.get(package: package, version: Version("1.0.0")) + let fingerprints = try await storage.get(package: package, version: Version("1.0.0")) XCTAssertEqual(fingerprints.count, 2) let registryFingerprints = fingerprints[.registry] @@ -406,8 +406,8 @@ extension PackageFingerprintStorage { fileprivate func get( package: PackageIdentity, version: Version - ) throws -> [Fingerprint.Kind: [Fingerprint.ContentType: Fingerprint]] { - try temp_await { + ) async throws -> [Fingerprint.Kind: [Fingerprint.ContentType: Fingerprint]] { + try await safe_async { self.get( package: package, version: version, @@ -423,8 +423,8 @@ extension PackageFingerprintStorage { version: Version, kind: Fingerprint.Kind, contentType: Fingerprint.ContentType - ) throws -> Fingerprint { - try temp_await { + ) async throws -> Fingerprint { + try await safe_async { self.get( package: package, version: version, @@ -441,8 +441,8 @@ extension PackageFingerprintStorage { package: PackageIdentity, version: Version, fingerprint: Fingerprint - ) throws { - try temp_await { + ) async throws { + try await safe_async { self.put( package: package, version: version, @@ -457,8 +457,8 @@ extension PackageFingerprintStorage { fileprivate func get( package: PackageReference, version: Version - ) throws -> [Fingerprint.Kind: [Fingerprint.ContentType: Fingerprint]] { - try temp_await { + ) async throws -> [Fingerprint.Kind: [Fingerprint.ContentType: Fingerprint]] { + try await safe_async { self.get( package: package, version: version, @@ -474,8 +474,8 @@ extension PackageFingerprintStorage { version: Version, kind: Fingerprint.Kind, contentType: Fingerprint.ContentType - ) throws -> Fingerprint { - try temp_await { + ) async throws -> Fingerprint { + try await safe_async { self.get( package: package, version: version, @@ -492,8 +492,8 @@ extension PackageFingerprintStorage { package: PackageReference, version: Version, fingerprint: Fingerprint - ) throws { - try temp_await { + ) async throws { + try await safe_async { self.put( package: package, version: version, diff --git a/Tests/PackageLoadingTests/PD_4_2_LoadingTests.swift b/Tests/PackageLoadingTests/PD_4_2_LoadingTests.swift index e00141d8479..ddd8f7e402a 100644 --- a/Tests/PackageLoadingTests/PD_4_2_LoadingTests.swift +++ b/Tests/PackageLoadingTests/PD_4_2_LoadingTests.swift @@ -602,8 +602,8 @@ class PackageDescription4_2LoadingTests: PackageDescriptionLoadingTests { } // run this with TSAN/ASAN to detect concurrency issues - func testConcurrencyWithWarmup() throws { - try testWithTemporaryDirectory { path in + func testConcurrencyWithWarmup() async throws { + try await testWithTemporaryDirectory { path in let total = 1000 let manifestPath = path.appending(components: "pkg", "Package.swift") try localFileSystem.createDirectory(manifestPath.parentDirectory) @@ -630,63 +630,53 @@ class PackageDescription4_2LoadingTests: PackageDescriptionLoadingTests { // warm up caches delegate.prepare() - let manifest = try temp_await { - manifestLoader.load( - manifestPath: manifestPath, - manifestToolsVersion: .v4_2, - packageIdentity: .plain("Trivial"), - packageKind: .fileSystem(manifestPath.parentDirectory), - packageLocation: manifestPath.pathString, - packageVersion: nil, - identityResolver: identityResolver, - dependencyMapper: dependencyMapper, - fileSystem: localFileSystem, - observabilityScope: observability.topScope, - delegateQueue: .sharedConcurrent, - callbackQueue: .sharedConcurrent, - completion: $0 - ) - } + let manifest = try await manifestLoader.load( + manifestPath: manifestPath, + manifestToolsVersion: .v4_2, + packageIdentity: .plain("Trivial"), + packageKind: .fileSystem(manifestPath.parentDirectory), + packageLocation: manifestPath.pathString, + packageVersion: nil, + identityResolver: identityResolver, + dependencyMapper: dependencyMapper, + fileSystem: localFileSystem, + observabilityScope: observability.topScope, + delegateQueue: .sharedConcurrent, + callbackQueue: .sharedConcurrent + ) XCTAssertNoDiagnostics(observability.diagnostics) XCTAssertEqual(manifest.displayName, "Trivial") XCTAssertEqual(manifest.targets[0].name, "foo") - let sync = DispatchGroup() - for _ in 0 ..< total { - sync.enter() - delegate.prepare(expectParsing: false) - manifestLoader.load( - manifestPath: manifestPath, - manifestToolsVersion: .v4_2, - packageIdentity: .plain("Trivial"), - packageKind: .fileSystem(manifestPath.parentDirectory), - packageLocation: manifestPath.pathString, - packageVersion: nil, - identityResolver: identityResolver, - dependencyMapper: dependencyMapper, - fileSystem: localFileSystem, - observabilityScope: observability.topScope, - delegateQueue: .sharedConcurrent, - callbackQueue: .sharedConcurrent - ) { result in - defer { - sync.leave() - } - - switch result { - case .failure(let error): - XCTFail("\(error)") - case .success(let manifest): - XCTAssertNoDiagnostics(observability.diagnostics) - XCTAssertEqual(manifest.displayName, "Trivial") - XCTAssertEqual(manifest.targets[0].name, "foo") + await withTaskGroup(of:Void.self) { group in + for _ in 0 ..< total { + delegate.prepare(expectParsing: false) + group.addTask { + do { + let manifest = try await manifestLoader.load( + manifestPath: manifestPath, + manifestToolsVersion: .v4_2, + packageIdentity: .plain("Trivial"), + packageKind: .fileSystem(manifestPath.parentDirectory), + packageLocation: manifestPath.pathString, + packageVersion: nil, + identityResolver: identityResolver, + dependencyMapper: dependencyMapper, + fileSystem: localFileSystem, + observabilityScope: observability.topScope, + delegateQueue: .sharedConcurrent, + callbackQueue: .sharedConcurrent + ) + XCTAssertNoDiagnostics(observability.diagnostics) + XCTAssertEqual(manifest.displayName, "Trivial") + XCTAssertEqual(manifest.targets[0].name, "foo") + } catch { + XCTFail("\(error)") + } } } - } - - if case .timedOut = sync.wait(timeout: .now() + 30) { - XCTFail("timeout") + await group.waitForAll() } XCTAssertEqual(try delegate.loaded(timeout: .now() + 1).count, total+1) @@ -696,7 +686,7 @@ class PackageDescription4_2LoadingTests: PackageDescriptionLoadingTests { } // run this with TSAN/ASAN to detect concurrency issues - func testConcurrencyNoWarmUp() throws { + func testConcurrencyNoWarmUp() async throws { #if os(Windows) // FIXME: does this actually trigger only on Windows or are other // platforms just getting lucky? I'm feeling lucky. @@ -704,7 +694,7 @@ class PackageDescription4_2LoadingTests: PackageDescriptionLoadingTests { #else try XCTSkipIfCI() - try testWithTemporaryDirectory { path in + try await testWithTemporaryDirectory { path in let total = 100 let observability = ObservabilitySystem.makeForTesting() let delegate = ManifestTestDelegate() @@ -712,60 +702,52 @@ class PackageDescription4_2LoadingTests: PackageDescriptionLoadingTests { let identityResolver = DefaultIdentityResolver() let dependencyMapper = DefaultDependencyMapper(identityResolver: identityResolver) - let sync = DispatchGroup() - for _ in 0 ..< total { - let random = Int.random(in: 0 ... total / 4) - let manifestPath = path.appending(components: "pkg-\(random)", "Package.swift") - if !localFileSystem.exists(manifestPath) { - try localFileSystem.createDirectory(manifestPath.parentDirectory) - try localFileSystem.writeFileContents( - manifestPath, - string: """ - import PackageDescription - let package = Package( - name: "Trivial-\(random)", - targets: [ - .target( - name: "foo-\(random)", - dependencies: []), - ] + try await withThrowingTaskGroup(of: Void.self) { group in + for _ in 0 ..< total { + let random = Int.random(in: 0 ... total / 4) + let manifestPath = path.appending(components: "pkg-\(random)", "Package.swift") + if !localFileSystem.exists(manifestPath) { + try localFileSystem.createDirectory(manifestPath.parentDirectory) + try localFileSystem.writeFileContents( + manifestPath, + string: """ + import PackageDescription + let package = Package( + name: "Trivial-\(random)", + targets: [ + .target( + name: "foo-\(random)", + dependencies: []), + ] + ) + """ ) - """ - ) - } - - sync.enter() - delegate.prepare() - manifestLoader.load( - manifestPath: manifestPath, - manifestToolsVersion: .v4_2, - packageIdentity: .plain("Trivial-\(random)"), - packageKind: .fileSystem(manifestPath.parentDirectory), - packageLocation: manifestPath.pathString, - packageVersion: nil, - identityResolver: identityResolver, - dependencyMapper: dependencyMapper, - fileSystem: localFileSystem, - observabilityScope: observability.topScope, - delegateQueue: .sharedConcurrent, - callbackQueue: .sharedConcurrent - ) { result in - defer { - sync.leave() } - - switch result { - case .failure(let error): - XCTFail("\(error)") - case .success(let manifest): - XCTAssertEqual(manifest.displayName, "Trivial-\(random)") - XCTAssertEqual(manifest.targets[0].name, "foo-\(random)") + group.addTask { + do { + delegate.prepare() + let manifest = try await manifestLoader.load( + manifestPath: manifestPath, + manifestToolsVersion: .v4_2, + packageIdentity: .plain("Trivial-\(random)"), + packageKind: .fileSystem(manifestPath.parentDirectory), + packageLocation: manifestPath.pathString, + packageVersion: nil, + identityResolver: identityResolver, + dependencyMapper: dependencyMapper, + fileSystem: localFileSystem, + observabilityScope: observability.topScope, + delegateQueue: .sharedConcurrent, + callbackQueue: .sharedConcurrent + ) + XCTAssertEqual(manifest.displayName, "Trivial-\(random)") + XCTAssertEqual(manifest.targets[0].name, "foo-\(random)") + } catch { + XCTFail("\(error)") + } } } - } - - if case .timedOut = sync.wait(timeout: .now() + 60) { - XCTFail("timeout") + try await group.waitForAll() } XCTAssertEqual(try delegate.loaded(timeout: .now() + 1).count, total) diff --git a/Tests/PackageRegistryTests/RegistryDownloadsManagerTests.swift b/Tests/PackageRegistryTests/RegistryDownloadsManagerTests.swift index b5ffd16a3c3..75919374b76 100644 --- a/Tests/PackageRegistryTests/RegistryDownloadsManagerTests.swift +++ b/Tests/PackageRegistryTests/RegistryDownloadsManagerTests.swift @@ -254,7 +254,7 @@ class RegistryDownloadsManagerTests: XCTestCase { } } - func testConcurrency() throws { + func testConcurrency() async throws { let observability = ObservabilitySystem.makeForTesting() let fs = InMemoryFileSystem() @@ -291,19 +291,15 @@ class RegistryDownloadsManagerTests: XCTestCase { source: packageSource ) - let group = DispatchGroup() - let results = ThreadSafeKeyValueStore>() - for packageVersion in packageVersions { - group.enter() - delegate.prepare(fetchExpected: true) - manager.lookup(package: package, version: packageVersion, observabilityScope: observability.topScope, delegateQueue: .sharedConcurrent, callbackQueue: .sharedConcurrent) { result in - results[packageVersion] = result - group.leave() + let results = ThreadSafeKeyValueStore() + try await withThrowingTaskGroup(of: Void.self) { group in + for packageVersion in packageVersions { + group.addTask { + delegate.prepare(fetchExpected: true) + results[packageVersion] = try await manager.lookup(package: package, version: packageVersion, observabilityScope: observability.topScope, delegateQueue: .sharedConcurrent, callbackQueue: .sharedConcurrent) + } } - } - - if case .timedOut = group.wait(timeout: .now() + 60) { - return XCTFail("timeout") + try await group.waitForAll() } try delegate.wait(timeout: .now() + 2) @@ -313,7 +309,7 @@ class RegistryDownloadsManagerTests: XCTestCase { XCTAssertEqual(results.count, concurrency) for packageVersion in packageVersions { let expectedPath = try downloadsPath.appending(package.downloadPath(version: packageVersion)) - XCTAssertEqual(try results[packageVersion]?.get(), expectedPath) + XCTAssertEqual(results[packageVersion], expectedPath) } } @@ -334,20 +330,16 @@ class RegistryDownloadsManagerTests: XCTestCase { ) delegate.reset() - let group = DispatchGroup() - let results = ThreadSafeKeyValueStore>() - for index in 0 ..< concurrency { - group.enter() - delegate.prepare(fetchExpected: index < concurrency / repeatRatio) - let packageVersion = Version(index % (concurrency / repeatRatio), 0 , 0) - manager.lookup(package: package, version: packageVersion, observabilityScope: observability.topScope, delegateQueue: .sharedConcurrent, callbackQueue: .sharedConcurrent) { result in - results[packageVersion] = result - group.leave() + let results = ThreadSafeKeyValueStore() + try await withThrowingTaskGroup(of: Void.self) { group in + for index in 0 ..< concurrency { + group.addTask { + delegate.prepare(fetchExpected: index < concurrency / repeatRatio) + let packageVersion = Version(index % (concurrency / repeatRatio), 0 , 0) + results[packageVersion] = try await manager.lookup(package: package, version: packageVersion, observabilityScope: observability.topScope, delegateQueue: .sharedConcurrent, callbackQueue: .sharedConcurrent) + } } - } - - if case .timedOut = group.wait(timeout: .now() + 60) { - return XCTFail("timeout") + try await group.waitForAll() } try delegate.wait(timeout: .now() + 2) @@ -357,7 +349,7 @@ class RegistryDownloadsManagerTests: XCTestCase { XCTAssertEqual(results.count, concurrency / repeatRatio) for packageVersion in packageVersions { let expectedPath = try downloadsPath.appending(package.downloadPath(version: packageVersion)) - XCTAssertEqual(try results[packageVersion]?.get(), expectedPath) + XCTAssertEqual(results[packageVersion], expectedPath) } } } diff --git a/Tests/SPMBuildCoreTests/PluginInvocationTests.swift b/Tests/SPMBuildCoreTests/PluginInvocationTests.swift index 83cf4fca585..bfffa9e60f5 100644 --- a/Tests/SPMBuildCoreTests/PluginInvocationTests.swift +++ b/Tests/SPMBuildCoreTests/PluginInvocationTests.swift @@ -229,8 +229,8 @@ class PluginInvocationTests: XCTestCase { XCTAssertEqual(evalFirstResult.textOutput, "Hello Plugin!") } - func testCompilationDiagnostics() throws { - try testWithTemporaryDirectory { tmpPath in + func testCompilationDiagnostics() async throws { + try await testWithTemporaryDirectory { tmpPath in // Create a sample package with a library target and a plugin. let packageDir = tmpPath.appending(components: "MyPackage") try localFileSystem.createDirectory(packageDir, recursive: true) @@ -285,13 +285,10 @@ class PluginInvocationTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. @@ -335,16 +332,14 @@ class PluginInvocationTests: XCTestCase { // Try to compile the broken plugin script. do { let delegate = Delegate() - let result = try temp_await { - pluginScriptRunner.compilePluginScript( - sourceFiles: buildToolPlugin.sources.paths, - pluginName: buildToolPlugin.name, - toolsVersion: buildToolPlugin.apiVersion, - observabilityScope: observability.topScope, - callbackQueue: DispatchQueue.sharedConcurrent, - delegate: delegate, - completion: $0) - } + let result = try await pluginScriptRunner.compilePluginScript( + sourceFiles: buildToolPlugin.sources.paths, + pluginName: buildToolPlugin.name, + toolsVersion: buildToolPlugin.apiVersion, + observabilityScope: observability.topScope, + callbackQueue: DispatchQueue.sharedConcurrent, + delegate: delegate + ) // This should invoke the compiler but should fail. XCTAssert(result.succeeded == false) @@ -389,16 +384,14 @@ class PluginInvocationTests: XCTestCase { let firstExecModTime: Date do { let delegate = Delegate() - let result = try temp_await { - pluginScriptRunner.compilePluginScript( - sourceFiles: buildToolPlugin.sources.paths, - pluginName: buildToolPlugin.name, - toolsVersion: buildToolPlugin.apiVersion, - observabilityScope: observability.topScope, - callbackQueue: DispatchQueue.sharedConcurrent, - delegate: delegate, - completion: $0) - } + let result = try await pluginScriptRunner.compilePluginScript( + sourceFiles: buildToolPlugin.sources.paths, + pluginName: buildToolPlugin.name, + toolsVersion: buildToolPlugin.apiVersion, + observabilityScope: observability.topScope, + callbackQueue: DispatchQueue.sharedConcurrent, + delegate: delegate + ) // This should invoke the compiler and this time should succeed. XCTAssert(result.succeeded == true) @@ -441,16 +434,14 @@ class PluginInvocationTests: XCTestCase { let secondExecModTime: Date do { let delegate = Delegate() - let result = try temp_await { - pluginScriptRunner.compilePluginScript( - sourceFiles: buildToolPlugin.sources.paths, - pluginName: buildToolPlugin.name, - toolsVersion: buildToolPlugin.apiVersion, - observabilityScope: observability.topScope, - callbackQueue: DispatchQueue.sharedConcurrent, - delegate: delegate, - completion: $0) - } + let result = try await pluginScriptRunner.compilePluginScript( + sourceFiles: buildToolPlugin.sources.paths, + pluginName: buildToolPlugin.name, + toolsVersion: buildToolPlugin.apiVersion, + observabilityScope: observability.topScope, + callbackQueue: DispatchQueue.sharedConcurrent, + delegate: delegate + ) // This should not invoke the compiler (just reuse the cached executable). XCTAssert(result.succeeded == true) @@ -499,22 +490,20 @@ class PluginInvocationTests: XCTestCase { // NTFS does not have nanosecond granularity (nor is this is a guaranteed file // system feature on all file systems). Add a sleep before the execution to ensure that we have sufficient // precision to read a difference. - Thread.sleep(forTimeInterval: 1) + try await Task.sleep(nanoseconds: UInt64(SendableTimeInterval.seconds(1).nanoseconds()!)) // Recompile the plugin again. let thirdExecModTime: Date do { let delegate = Delegate() - let result = try temp_await { - pluginScriptRunner.compilePluginScript( - sourceFiles: buildToolPlugin.sources.paths, - pluginName: buildToolPlugin.name, - toolsVersion: buildToolPlugin.apiVersion, - observabilityScope: observability.topScope, - callbackQueue: DispatchQueue.sharedConcurrent, - delegate: delegate, - completion: $0) - } + let result = try await pluginScriptRunner.compilePluginScript( + sourceFiles: buildToolPlugin.sources.paths, + pluginName: buildToolPlugin.name, + toolsVersion: buildToolPlugin.apiVersion, + observabilityScope: observability.topScope, + callbackQueue: DispatchQueue.sharedConcurrent, + delegate: delegate + ) // This should invoke the compiler and not use the cache. XCTAssert(result.succeeded == true) @@ -560,16 +549,14 @@ class PluginInvocationTests: XCTestCase { // Recompile the plugin again. do { let delegate = Delegate() - let result = try temp_await { - pluginScriptRunner.compilePluginScript( - sourceFiles: buildToolPlugin.sources.paths, - pluginName: buildToolPlugin.name, - toolsVersion: buildToolPlugin.apiVersion, - observabilityScope: observability.topScope, - callbackQueue: DispatchQueue.sharedConcurrent, - delegate: delegate, - completion: $0) - } + let result = try await pluginScriptRunner.compilePluginScript( + sourceFiles: buildToolPlugin.sources.paths, + pluginName: buildToolPlugin.name, + toolsVersion: buildToolPlugin.apiVersion, + observabilityScope: observability.topScope, + callbackQueue: DispatchQueue.sharedConcurrent, + delegate: delegate + ) // This should again invoke the compiler but should fail. XCTAssert(result.succeeded == false) @@ -598,11 +585,11 @@ class PluginInvocationTests: XCTestCase { } } - func testUnsupportedDependencyProduct() throws { + func testUnsupportedDependencyProduct() async throws { // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try testWithTemporaryDirectory { tmpPath in + try await testWithTemporaryDirectory { tmpPath in // Create a sample package with a library product and a plugin. let packageDir = tmpPath.appending(components: "MyPackage") try localFileSystem.createDirectory(packageDir, recursive: true) @@ -675,13 +662,10 @@ class PluginInvocationTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. @@ -696,11 +680,11 @@ class PluginInvocationTests: XCTestCase { } } - func testUnsupportedDependencyTarget() throws { + func testUnsupportedDependencyTarget() async throws { // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try testWithTemporaryDirectory { tmpPath in + try await testWithTemporaryDirectory { tmpPath in // Create a sample package with a library target and a plugin. let packageDir = tmpPath.appending(components: "MyPackage") try localFileSystem.createDirectory(packageDir, recursive: true) @@ -754,13 +738,10 @@ class PluginInvocationTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. @@ -775,11 +756,11 @@ class PluginInvocationTests: XCTestCase { } } - func testPrebuildPluginShouldNotUseExecTarget() throws { + func testPrebuildPluginShouldNotUseExecTarget() async throws { // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try testWithTemporaryDirectory { tmpPath in + try await testWithTemporaryDirectory { tmpPath in // Create a sample package with a library target and a plugin. let packageDir = tmpPath.appending(components: "mypkg") try localFileSystem.createDirectory(packageDir, recursive: true) @@ -864,13 +845,10 @@ class PluginInvocationTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. @@ -1048,13 +1026,10 @@ class PluginInvocationTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") let graph = try workspace.loadPackageGraph(rootInput: rootInput, observabilityScope: observability.topScope) @@ -1090,11 +1065,11 @@ class PluginInvocationTests: XCTestCase { } } - func checkParseArtifactsPlatformCompatibility(artifactSupportedTriples: [Triple], hostTriple: Triple, pluginResultChecker: ([ResolvedTarget: [BuildToolPluginInvocationResult]]) throws -> ()) throws { + func checkParseArtifactsPlatformCompatibility(artifactSupportedTriples: [Triple], hostTriple: Triple) async throws -> [ResolvedTarget: [BuildToolPluginInvocationResult]] { // Only run the test if the environment in which we're running actually supports Swift concurrency (which the plugin APIs require). try XCTSkipIf(!UserToolchain.default.supportsSwiftConcurrency(), "skipping because test environment doesn't support concurrency") - try testWithTemporaryDirectory { tmpPath in + return try await testWithTemporaryDirectory { tmpPath in // Create a sample package with a library target and a plugin. let packageDir = tmpPath.appending(components: "MyPackage") try localFileSystem.createDirectory(packageDir, recursive: true) @@ -1192,13 +1167,10 @@ class PluginInvocationTests: XCTestCase { // Load the root manifest. let rootInput = PackageGraphRootInput(packages: [packageDir], dependencies: []) - let rootManifests = try temp_await { - workspace.loadRootManifests( - packages: rootInput.packages, - observabilityScope: observability.topScope, - completion: $0 - ) - } + let rootManifests = try await workspace.loadRootManifests( + packages: rootInput.packages, + observabilityScope: observability.topScope + ) XCTAssert(rootManifests.count == 1, "\(rootManifests)") // Load the package graph. @@ -1235,7 +1207,7 @@ class PluginInvocationTests: XCTestCase { // Invoke build tool plugin let outputDir = packageDir.appending(".build") let builtToolsDir = outputDir.appending("debug") - let result = try packageGraph.invokeBuildToolPlugins( + return try packageGraph.invokeBuildToolPlugins( outputDir: outputDir, builtToolsDir: builtToolsDir, buildEnvironment: BuildEnvironment(platform: .macOS, configuration: .debug), @@ -1246,54 +1218,50 @@ class PluginInvocationTests: XCTestCase { observabilityScope: observability.topScope, fileSystem: localFileSystem ) - try pluginResultChecker(result) } } - func testParseArtifactNotSupportedOnTargetPlatform() throws { + func testParseArtifactNotSupportedOnTargetPlatform() async throws { let hostTriple = try UserToolchain.default.targetTriple let artifactSupportedTriples = try [Triple("riscv64-apple-windows-android")] var checked = false - try checkParseArtifactsPlatformCompatibility(artifactSupportedTriples: artifactSupportedTriples, hostTriple: hostTriple) { result in - if let pluginResult = result.first, - let diag = pluginResult.value.first?.diagnostics, - diag.description == "[[error]: Tool ‘LocalBinaryTool’ is not supported on the target platform]" { - checked = true - } + let result = try await checkParseArtifactsPlatformCompatibility(artifactSupportedTriples: artifactSupportedTriples, hostTriple: hostTriple) + if let pluginResult = result.first, + let diag = pluginResult.value.first?.diagnostics, + diag.description == "[[error]: Tool ‘LocalBinaryTool’ is not supported on the target platform]" { + checked = true } XCTAssertTrue(checked) } - func testParseArtifactsDoesNotCheckPlatformVersion() throws { + func testParseArtifactsDoesNotCheckPlatformVersion() async throws { #if !os(macOS) throw XCTSkip("platform versions are only available if the host is macOS") #else let hostTriple = try UserToolchain.default.targetTriple let artifactSupportedTriples = try [Triple("\(hostTriple.withoutVersion().tripleString)20.0")] - try checkParseArtifactsPlatformCompatibility(artifactSupportedTriples: artifactSupportedTriples, hostTriple: hostTriple) { result in - result.forEach { - $0.value.forEach { - XCTAssertTrue($0.succeeded, "plugin unexpectedly failed") - XCTAssertEqual($0.diagnostics.map { $0.message }, [], "plugin produced unexpected diagnostics") - } + let result = try await checkParseArtifactsPlatformCompatibility(artifactSupportedTriples: artifactSupportedTriples, hostTriple: hostTriple) + result.forEach { + $0.value.forEach { + XCTAssertTrue($0.succeeded, "plugin unexpectedly failed") + XCTAssertEqual($0.diagnostics.map { $0.message }, [], "plugin produced unexpected diagnostics") } } #endif } - func testParseArtifactsConsidersAllSupportedTriples() throws { + func testParseArtifactsConsidersAllSupportedTriples() async throws { let hostTriple = try UserToolchain.default.targetTriple let artifactSupportedTriples = [hostTriple, try Triple("riscv64-apple-windows-android")] - try checkParseArtifactsPlatformCompatibility(artifactSupportedTriples: artifactSupportedTriples, hostTriple: hostTriple) { result in - result.forEach { - $0.value.forEach { - XCTAssertTrue($0.succeeded, "plugin unexpectedly failed") - XCTAssertEqual($0.diagnostics.map { $0.message }, [], "plugin produced unexpected diagnostics") - XCTAssertEqual($0.buildCommands.first?.configuration.executable.basename, "LocalBinaryTool\(hostTriple.tripleString).sh") - } + let result = try await checkParseArtifactsPlatformCompatibility(artifactSupportedTriples: artifactSupportedTriples, hostTriple: hostTriple) + result.forEach { + $0.value.forEach { + XCTAssertTrue($0.succeeded, "plugin unexpectedly failed") + XCTAssertEqual($0.diagnostics.map { $0.message }, [], "plugin produced unexpected diagnostics") + XCTAssertEqual($0.buildCommands.first?.configuration.executable.basename, "LocalBinaryTool\(hostTriple.tripleString).sh") } } } diff --git a/Tests/SourceControlTests/RepositoryManagerTests.swift b/Tests/SourceControlTests/RepositoryManagerTests.swift index 0b73a926661..87bd1fc7fdd 100644 --- a/Tests/SourceControlTests/RepositoryManagerTests.swift +++ b/Tests/SourceControlTests/RepositoryManagerTests.swift @@ -20,11 +20,11 @@ import class TSCBasic.InMemoryFileSystem import enum TSCBasic.ProcessEnv class RepositoryManagerTests: XCTestCase { - func testBasics() throws { + func testBasics() async throws { let fs = localFileSystem let observability = ObservabilitySystem.makeForTesting() - try testWithTemporaryDirectory { path in + try await testWithTemporaryDirectory { path in let provider = DummyRepositoryProvider(fileSystem: fs) let delegate = DummyRepositoryManagerDelegate() @@ -43,7 +43,7 @@ class RepositoryManagerTests: XCTestCase { do { delegate.prepare(fetchExpected: true, updateExpected: false) - let handle = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + let handle = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) prevHandle = handle @@ -69,7 +69,7 @@ class RepositoryManagerTests: XCTestCase { do { delegate.prepare(fetchExpected: true, updateExpected: false) - XCTAssertThrowsError(try manager.lookup(repository: badDummyRepo, observabilityScope: observability.topScope)) { error in + await XCTAssertAsyncThrowsError(try await manager.lookup(repository: badDummyRepo, observabilityScope: observability.topScope)) { error in XCTAssertEqual(error as? DummyError, DummyError.invalidRepository) } @@ -85,7 +85,7 @@ class RepositoryManagerTests: XCTestCase { do { delegate.prepare(fetchExpected: false, updateExpected: true) - let handle = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + let handle = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) XCTAssertEqual(handle.repository, dummyRepo) XCTAssertEqual(handle.repository, prevHandle?.repository) @@ -111,7 +111,7 @@ class RepositoryManagerTests: XCTestCase { // We should get a new handle now because we deleted the existing repository. delegate.prepare(fetchExpected: true, updateExpected: false) - let handle = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + let handle = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) XCTAssertEqual(handle.repository, dummyRepo) @@ -125,11 +125,11 @@ class RepositoryManagerTests: XCTestCase { } } - func testCache() throws { + func testCache() async throws { let fs = localFileSystem let observability = ObservabilitySystem.makeForTesting() - try fixture(name: "DependencyResolution/External/Simple") { (fixturePath: AbsolutePath) in + try await fixture(name: "DependencyResolution/External/Simple") { (fixturePath: AbsolutePath) in let cachePath = fixturePath.appending("cache") let repositoriesPath = fixturePath.appending("repositories") let repo = RepositorySpecifier(path: fixturePath.appending("Foo")) @@ -148,7 +148,7 @@ class RepositoryManagerTests: XCTestCase { // fetch packages and populate cache delegate.prepare(fetchExpected: true, updateExpected: false) - _ = try manager.lookup(repository: repo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: repo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try XCTAssertDirectoryExists(cachePath.appending(repo.storagePath())) try XCTAssertDirectoryExists(repositoriesPath.appending(repo.storagePath())) @@ -163,7 +163,7 @@ class RepositoryManagerTests: XCTestCase { // fetch packages from the cache delegate.prepare(fetchExpected: true, updateExpected: false) - _ = try manager.lookup(repository: repo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: repo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try XCTAssertDirectoryExists(repositoriesPath.appending(repo.storagePath())) try delegate.wait(timeout: .now() + 2) @@ -178,7 +178,7 @@ class RepositoryManagerTests: XCTestCase { // fetch packages and populate cache delegate.prepare(fetchExpected: true, updateExpected: false) - _ = try manager.lookup(repository: repo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: repo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try XCTAssertDirectoryExists(cachePath.appending(repo.storagePath())) try XCTAssertDirectoryExists(repositoriesPath.appending(repo.storagePath())) @@ -190,7 +190,7 @@ class RepositoryManagerTests: XCTestCase { // update packages from the cache delegate.prepare(fetchExpected: false, updateExpected: true) - _ = try manager.lookup(repository: repo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: repo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try delegate.wait(timeout: .now() + 2) try XCTAssertEqual(delegate.willUpdate[0].storagePath(), repo.storagePath()) @@ -198,11 +198,11 @@ class RepositoryManagerTests: XCTestCase { } } - func testReset() throws { + func testReset() async throws { let fs = localFileSystem let observability = ObservabilitySystem.makeForTesting() - try testWithTemporaryDirectory { path in + try await testWithTemporaryDirectory { path in let repos = path.appending("repo") let provider = DummyRepositoryProvider(fileSystem: fs) let delegate = DummyRepositoryManagerDelegate() @@ -218,10 +218,10 @@ class RepositoryManagerTests: XCTestCase { let dummyRepo = RepositorySpecifier(path: "/dummy") delegate.prepare(fetchExpected: true, updateExpected: false) - _ = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) delegate.prepare(fetchExpected: false, updateExpected: true) - _ = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try delegate.wait(timeout: .now() + 2) XCTAssertEqual(delegate.willFetch.count, 1) @@ -234,7 +234,7 @@ class RepositoryManagerTests: XCTestCase { try fs.createDirectory(repos, recursive: true) delegate.prepare(fetchExpected: true, updateExpected: false) - _ = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try delegate.wait(timeout: .now() + 2) XCTAssertEqual(delegate.willFetch.count, 2) @@ -243,11 +243,11 @@ class RepositoryManagerTests: XCTestCase { } /// Check that the manager is persistent. - func testPersistence() throws { + func testPersistence() async throws { let fs = localFileSystem let observability = ObservabilitySystem.makeForTesting() - try testWithTemporaryDirectory { path in + try await testWithTemporaryDirectory { path in let provider = DummyRepositoryProvider(fileSystem: fs) let dummyRepo = RepositorySpecifier(path: "/dummy") @@ -262,7 +262,7 @@ class RepositoryManagerTests: XCTestCase { ) delegate.prepare(fetchExpected: true, updateExpected: false) - _ = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try delegate.wait(timeout: .now() + 2) XCTAssertEqual(delegate.willFetch.map { $0.repository }, [dummyRepo]) @@ -283,7 +283,7 @@ class RepositoryManagerTests: XCTestCase { ) delegate.prepare(fetchExpected: true, updateExpected: false) - _ = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) // This time fetch shouldn't be called. try delegate.wait(timeout: .now() + 2) @@ -312,7 +312,7 @@ class RepositoryManagerTests: XCTestCase { let dummyRepo = RepositorySpecifier(path: "/dummy") delegate.prepare(fetchExpected: true, updateExpected: false) - _ = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try delegate.wait(timeout: .now() + 2) XCTAssertEqual(delegate.willFetch.map { $0.repository }, [dummyRepo]) @@ -335,11 +335,11 @@ class RepositoryManagerTests: XCTestCase { } } - func testConcurrency() throws { + func testConcurrency() async throws { let fs = localFileSystem let observability = ObservabilitySystem.makeForTesting() - try testWithTemporaryDirectory { path in + try await testWithTemporaryDirectory { path in let provider = DummyRepositoryProvider(fileSystem: fs) let delegate = DummyRepositoryManagerDelegate() let manager = RepositoryManager( @@ -350,27 +350,23 @@ class RepositoryManagerTests: XCTestCase { ) let dummyRepo = RepositorySpecifier(path: "/dummy") - let group = DispatchGroup() - let results = ThreadSafeKeyValueStore>() + let results = ThreadSafeKeyValueStore() let concurrency = 10000 - for index in 0 ..< concurrency { - group.enter() - delegate.prepare(fetchExpected: index == 0, updateExpected: index > 0) - manager.lookup( - package: .init(url: SourceControlURL(dummyRepo.url)), - repository: dummyRepo, - updateStrategy: .always, - observabilityScope: observability.topScope, - delegateQueue: .sharedConcurrent, - callbackQueue: .sharedConcurrent - ) { result in - results[index] = result - group.leave() + try await withThrowingTaskGroup(of: Void.self) { group in + for index in 0 ..< concurrency { + group.addTask { + delegate.prepare(fetchExpected: index == 0, updateExpected: index > 0) + results[index] = try await manager.lookup( + package: .init(url: SourceControlURL(dummyRepo.url)), + repository: dummyRepo, + updateStrategy: .always, + observabilityScope: observability.topScope, + delegateQueue: .sharedConcurrent, + callbackQueue: .sharedConcurrent + ) + } } - } - - if case .timedOut = group.wait(timeout: .now() + 60) { - return XCTFail("timeout") + try await group.waitForAll() } XCTAssertNoDiagnostics(observability.diagnostics) @@ -383,16 +379,16 @@ class RepositoryManagerTests: XCTestCase { XCTAssertEqual(results.count, concurrency) for index in 0 ..< concurrency { - XCTAssertEqual(try results[index]?.get().repository, dummyRepo) + XCTAssertEqual(results[index]?.repository, dummyRepo) } } } - func testSkipUpdate() throws { + func testSkipUpdate() async throws { let fs = localFileSystem let observability = ObservabilitySystem.makeForTesting() - try testWithTemporaryDirectory { path in + try await testWithTemporaryDirectory { path in let repos = path.appending("repo") let provider = DummyRepositoryProvider(fileSystem: fs) let delegate = DummyRepositoryManagerDelegate() @@ -408,7 +404,7 @@ class RepositoryManagerTests: XCTestCase { let dummyRepo = RepositorySpecifier(path: "/dummy") delegate.prepare(fetchExpected: true, updateExpected: false) - _ = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try delegate.wait(timeout: .now() + 2) XCTAssertEqual(delegate.willFetch.count, 1) @@ -417,10 +413,10 @@ class RepositoryManagerTests: XCTestCase { XCTAssertEqual(delegate.didUpdate.count, 0) delegate.prepare(fetchExpected: false, updateExpected: true) - _ = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) delegate.prepare(fetchExpected: false, updateExpected: true) - _ = try manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try delegate.wait(timeout: .now() + 2) XCTAssertEqual(delegate.willFetch.count, 1) @@ -429,7 +425,7 @@ class RepositoryManagerTests: XCTestCase { XCTAssertEqual(delegate.didUpdate.count, 2) delegate.prepare(fetchExpected: false, updateExpected: false) - _ = try manager.lookup(repository: dummyRepo, updateStrategy: .never, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: dummyRepo, updateStrategy: .never, observabilityScope: observability.topScope) XCTAssertNoDiagnostics(observability.diagnostics) try delegate.wait(timeout: .now() + 2) XCTAssertEqual(delegate.willFetch.count, 1) @@ -571,11 +567,11 @@ class RepositoryManagerTests: XCTestCase { } } - func testInvalidRepositoryOnDisk() throws { + func testInvalidRepositoryOnDisk() async throws { let fileSystem = localFileSystem let observability = ObservabilitySystem.makeForTesting() - try testWithTemporaryDirectory { path in + try await testWithTemporaryDirectory { path in let repositoriesDirectory = path.appending("repositories") try fileSystem.createDirectory(repositoriesDirectory, recursive: true) @@ -589,7 +585,7 @@ class RepositoryManagerTests: XCTestCase { delegate: nil ) - _ = try manager.lookup(repository: testRepository, observabilityScope: observability.topScope) + _ = try await manager.lookup(repository: testRepository, observabilityScope: observability.topScope) testDiagnostics(observability.diagnostics) { result in result.check( diagnostic: .contains("is not valid git repository for '\(testRepository)', will fetch again"), @@ -717,8 +713,8 @@ extension RepositoryManager { repository: RepositorySpecifier, updateStrategy: RepositoryUpdateStrategy = .always, observabilityScope: ObservabilityScope - ) throws -> RepositoryHandle { - return try temp_await { + ) async throws -> RepositoryHandle { + return try await safe_async { self.lookup( package: .init(url: SourceControlURL(repository.url)), repository: repository,