diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..0ca09b4 --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,33 @@ +name: Checks + +on: + push: + branches: + - main + pull_request: {} + +concurrency: + group: checks-${{ github.head_ref }} + cancel-in-progress: true + +jobs: + test: + name: "Test on macOS" + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - name: Test + run: swift test + + test_linux: + name: "Test on Linux" + runs-on: ubuntu-latest + container: swift:6.0.0 + steps: + - uses: actions/checkout@v4 + - name: Install SQLite + run: | + apt-get update + apt-get install -y libsqlite3-dev sqlite3 + - name: Test + run: swift test diff --git a/Sources/PackageCollectionGenerator/PackageCollectionGenerate.swift b/Sources/PackageCollectionGenerator/PackageCollectionGenerate.swift index 6a6fbcf..5a320c3 100644 --- a/Sources/PackageCollectionGenerator/PackageCollectionGenerate.swift +++ b/Sources/PackageCollectionGenerator/PackageCollectionGenerate.swift @@ -23,7 +23,7 @@ import TSCUtility import Utilities @main -public struct PackageCollectionGenerate: ParsableCommand { +public struct PackageCollectionGenerate: AsyncParsableCommand { public static let configuration = CommandConfiguration( abstract: "Generate a package collection from the given list of packages." ) @@ -65,7 +65,7 @@ public struct PackageCollectionGenerate: ParsableCommand { public init() {} - public func run() throws { + public func run() async throws { Backtrace.install() // Parse auth tokens @@ -91,20 +91,21 @@ public struct PackageCollectionGenerate: ParsableCommand { let githubPackageMetadataProvider = GitHubPackageMetadataProvider(authTokens: authTokens) // Generate metadata for each package - let packages: [Model.Collection.Package] = input.packages.compactMap { package in + var packages: [Model.Collection.Package] = [] + for package in input.packages { do { - let packageMetadata = try self.generateMetadata(for: package, metadataProvider: githubPackageMetadataProvider, jsonDecoder: jsonDecoder) + let packageMetadata = try await self.generateMetadata(for: package, metadataProvider: githubPackageMetadataProvider, jsonDecoder: jsonDecoder) print("\(packageMetadata)", verbose: self.verbose) guard !packageMetadata.versions.isEmpty else { printError("Skipping package \(package.url) because it does not have any valid versions.") - return nil + continue } - return packageMetadata + packages.append(packageMetadata) } catch { printError("Failed to generate metadata for package \(package.url): \(error)") - return nil + continue } } @@ -132,7 +133,7 @@ public struct PackageCollectionGenerate: ParsableCommand { } catch { outputAbsolutePath = try AbsolutePath( validating: self.outputPath, - relativeTo: try AbsolutePath(validating: FileManager.default.currentDirectoryPath) + relativeTo: AbsolutePath(validating: FileManager.default.currentDirectoryPath) ) } let outputDirectory = outputAbsolutePath.parentDirectory @@ -147,7 +148,7 @@ public struct PackageCollectionGenerate: ParsableCommand { private func generateMetadata(for package: PackageCollectionGeneratorInput.Package, metadataProvider: PackageMetadataProvider, - jsonDecoder: JSONDecoder) throws -> Model.Collection.Package { + jsonDecoder: JSONDecoder) async throws -> Model.Collection.Package { print("Processing Package(\(package.url))", inColor: .cyan, verbose: self.verbose) // Try to locate the directory where the repository might have been cloned to previously @@ -158,7 +159,7 @@ public struct PackageCollectionGenerate: ParsableCommand { } catch { workingDirectoryAbsolutePath = try AbsolutePath( validating: workingDirectoryPath, - relativeTo: try AbsolutePath(validating: FileManager.default.currentDirectoryPath) + relativeTo: AbsolutePath(validating: FileManager.default.currentDirectoryPath) ) } @@ -178,7 +179,7 @@ public struct PackageCollectionGenerate: ParsableCommand { try GitUtilities.clone(repositoryURL, to: gitDirectoryPath) } - return try self.generateMetadata( + return try await self.generateMetadata( for: package, gitDirectoryPath: gitDirectoryPath, metadataProvider: metadataProvider, @@ -188,11 +189,11 @@ public struct PackageCollectionGenerate: ParsableCommand { } // Fallback to tmp directory if we cannot use the working directory for some reason or it's unspecified - return try withTemporaryDirectory(removeTreeOnDeinit: true) { tmpDir in + return try await withTemporaryDirectory(removeTreeOnDeinit: true) { tmpDir in // Clone the package repository try GitUtilities.clone(package.url.absoluteString, to: tmpDir) - return try self.generateMetadata( + return try await self.generateMetadata( for: package, gitDirectoryPath: tmpDir, metadataProvider: metadataProvider, @@ -204,10 +205,10 @@ public struct PackageCollectionGenerate: ParsableCommand { private func generateMetadata(for package: PackageCollectionGeneratorInput.Package, gitDirectoryPath: AbsolutePath, metadataProvider: PackageMetadataProvider, - jsonDecoder: JSONDecoder) throws -> Model.Collection.Package { + jsonDecoder: JSONDecoder) async throws -> Model.Collection.Package { var additionalMetadata: PackageBasicMetadata? do { - additionalMetadata = try temp_await { callback in metadataProvider.get(package.url, callback: callback) } + additionalMetadata = try await metadataProvider.get(package.url) } catch { printError("Failed to fetch additional metadata: \(error)") } @@ -268,8 +269,7 @@ public struct PackageCollectionGenerate: ParsableCommand { excludedTargets: Set, signer: PackageCollectionModel.V1.Signer?, gitDirectoryPath: AbsolutePath, - jsonDecoder: JSONDecoder) throws -> Model.Collection.Package.Version - { + jsonDecoder: JSONDecoder) throws -> Model.Collection.Package.Version { // Check out the git tag print("Checking out version \(version)", inColor: .yellow, verbose: self.verbose) try ShellUtilities.run(Git.tool, "-C", gitDirectoryPath.pathString, "checkout", version) @@ -301,8 +301,7 @@ public struct PackageCollectionGenerate: ParsableCommand { private func defaultManifest(excludedProducts: Set, excludedTargets: Set, gitDirectoryPath: AbsolutePath, - jsonDecoder: JSONDecoder) throws -> Model.Collection.Package.Version.Manifest - { + jsonDecoder: JSONDecoder) throws -> Model.Collection.Package.Version.Manifest { // Run `swift package describe --type json` to generate JSON package description let packageDescriptionJSON = try ShellUtilities.run(ShellUtilities.shell, "-c", "cd \(gitDirectoryPath) && swift package describe --type json") let packageDescription = try jsonDecoder.decode(PackageDescription.self, from: packageDescriptionJSON.data(using: .utf8) ?? Data()) diff --git a/Sources/PackageCollectionGenerator/PackageMetadataProviders/GitHubPackageMetadataProvider.swift b/Sources/PackageCollectionGenerator/PackageMetadataProviders/GitHubPackageMetadataProvider.swift index 287eeeb..b6740d9 100644 --- a/Sources/PackageCollectionGenerator/PackageMetadataProviders/GitHubPackageMetadataProvider.swift +++ b/Sources/PackageCollectionGenerator/PackageMetadataProviders/GitHubPackageMetadataProvider.swift @@ -32,92 +32,95 @@ struct GitHubPackageMetadataProvider: PackageMetadataProvider { self.decoder = JSONDecoder.makeWithDefaults() } - func get(_ packageURL: URL, callback: @escaping (Result) -> Void) { - guard let baseURL = self.apiURL(packageURL.absoluteString) else { - return callback(.failure(Errors.invalidGitURL(packageURL))) - } + func get(_ packageURL: URL) async throws -> PackageBasicMetadata { + try await withCheckedThrowingContinuation { continuation in + + guard let baseURL = self.apiURL(packageURL.absoluteString) else { + return continuation.resume(throwing: Errors.invalidGitURL(packageURL)) + } - let metadataURL = baseURL - let readmeURL = baseURL.appendingPathComponent("readme") - let licenseURL = baseURL.appendingPathComponent("license") - - let sync = DispatchGroup() - let results = ThreadSafeKeyValueStore>() - - // get the main data - sync.enter() - var metadataHeaders = HTTPClientHeaders() - metadataHeaders.add(name: "Accept", value: "application/vnd.github.mercy-preview+json") - let metadataOptions = self.makeRequestOptions(validResponseCodes: [200, 401, 403, 404]) - let hasAuthorization = metadataOptions.authorizationProvider?(metadataURL) != nil - self.httpClient.get(metadataURL, headers: metadataHeaders, options: metadataOptions) { result in - defer { sync.leave() } - results[metadataURL] = result - if case .success(let response) = result { - let apiLimit = response.headers.get("X-RateLimit-Limit").first.flatMap(Int.init) ?? -1 - let apiRemaining = response.headers.get("X-RateLimit-Remaining").first.flatMap(Int.init) ?? -1 - switch (response.statusCode, hasAuthorization, apiRemaining) { - case (_, _, 0): - results[metadataURL] = .failure(Errors.apiLimitsExceeded(metadataURL, apiLimit, apiRemaining)) - case (401, true, _): - results[metadataURL] = .failure(Errors.invalidAuthToken(metadataURL)) - case (401, false, _): - results[metadataURL] = .failure(Errors.permissionDenied(metadataURL)) - case (403, _, _): - results[metadataURL] = .failure(Errors.permissionDenied(metadataURL)) - case (404, _, _): - results[metadataURL] = .failure(Errors.notFound(metadataURL)) - case (200, _, _): - // if successful, fan out multiple API calls - [readmeURL, licenseURL].forEach { url in - sync.enter() - var headers = HTTPClientHeaders() - headers.add(name: "Accept", value: "application/vnd.github.v3+json") - let options = self.makeRequestOptions(validResponseCodes: [200]) - self.httpClient.get(url, headers: headers, options: options) { result in - defer { sync.leave() } - results[url] = result + let metadataURL = baseURL + let readmeURL = baseURL.appendingPathComponent("readme") + let licenseURL = baseURL.appendingPathComponent("license") + + let sync = DispatchGroup() + let results = ThreadSafeKeyValueStore>() + + // get the main data + sync.enter() + var metadataHeaders = HTTPClientHeaders() + metadataHeaders.add(name: "Accept", value: "application/vnd.github.mercy-preview+json") + let metadataOptions = self.makeRequestOptions(validResponseCodes: [200, 401, 403, 404]) + let hasAuthorization = metadataOptions.authorizationProvider?(metadataURL) != nil + self.httpClient.get(metadataURL, headers: metadataHeaders, options: metadataOptions) { result in + defer { sync.leave() } + results[metadataURL] = result + if case .success(let response) = result { + let apiLimit = response.headers.get("X-RateLimit-Limit").first.flatMap(Int.init) ?? -1 + let apiRemaining = response.headers.get("X-RateLimit-Remaining").first.flatMap(Int.init) ?? -1 + switch (response.statusCode, hasAuthorization, apiRemaining) { + case (_, _, 0): + results[metadataURL] = .failure(Errors.apiLimitsExceeded(metadataURL, apiLimit, apiRemaining)) + case (401, true, _): + results[metadataURL] = .failure(Errors.invalidAuthToken(metadataURL)) + case (401, false, _): + results[metadataURL] = .failure(Errors.permissionDenied(metadataURL)) + case (403, _, _): + results[metadataURL] = .failure(Errors.permissionDenied(metadataURL)) + case (404, _, _): + results[metadataURL] = .failure(Errors.notFound(metadataURL)) + case (200, _, _): + // if successful, fan out multiple API calls + for url in [readmeURL, licenseURL] { + sync.enter() + var headers = HTTPClientHeaders() + headers.add(name: "Accept", value: "application/vnd.github.v3+json") + let options = self.makeRequestOptions(validResponseCodes: [200]) + self.httpClient.get(url, headers: headers, options: options) { result in + defer { sync.leave() } + results[url] = result + } } + default: + results[metadataURL] = .failure(Errors.invalidResponse(metadataURL, "Invalid status code: \(response.statusCode)")) } - default: - results[metadataURL] = .failure(Errors.invalidResponse(metadataURL, "Invalid status code: \(response.statusCode)")) } } - } - // process results - sync.notify(queue: self.httpClient.configuration.callbackQueue) { - do { - // check for main request error state - switch results[metadataURL] { - case .none: - throw Errors.invalidResponse(metadataURL, "Response missing") - case .some(.failure(let error)): - throw error - case .some(.success(let metadataResponse)): - guard let metadata = try metadataResponse.decodeBody(GetRepositoryResponse.self, using: self.decoder) else { - throw Errors.invalidResponse(metadataURL, "Empty body") - } + // process results + sync.notify(queue: self.httpClient.configuration.callbackQueue) { + do { + // check for main request error state + switch results[metadataURL] { + case .none: + throw Errors.invalidResponse(metadataURL, "Response missing") + case .some(.failure(let error)): + throw error + case .some(.success(let metadataResponse)): + guard let metadata = try metadataResponse.decodeBody(GetRepositoryResponse.self, using: self.decoder) else { + throw Errors.invalidResponse(metadataURL, "Empty body") + } - let readme = try results[readmeURL]?.success?.decodeBody(Readme.self, using: self.decoder) - let license = try results[licenseURL]?.success?.decodeBody(License.self, using: self.decoder) + let readme = try results[readmeURL]?.success?.decodeBody(Readme.self, using: self.decoder) + let license = try results[licenseURL]?.success?.decodeBody(License.self, using: self.decoder) - let model = PackageBasicMetadata( - summary: metadata.description, - keywords: metadata.topics, - readmeURL: readme?.downloadURL, - license: license.flatMap { .init(name: $0.license.spdxID, url: $0.downloadURL) } - ) + let model = PackageBasicMetadata( + summary: metadata.description, + keywords: metadata.topics, + readmeURL: readme?.downloadURL, + license: license.flatMap { .init(name: $0.license.spdxID, url: $0.downloadURL) } + ) - callback(.success(model)) + continuation.resume(returning: model) + } + } catch { + continuation.resume(throwing: error) } - } catch { - return callback(.failure(error)) } } } - internal func apiURL(_ url: String) -> Foundation.URL? { + func apiURL(_ url: String) -> Foundation.URL? { if let gitURL = GitURL.from(url) { return URL(string: "https://\(Self.apiHostPrefix)\(gitURL.host)/repos/\(gitURL.owner)/\(gitURL.repository)") } diff --git a/Sources/PackageCollectionGenerator/PackageMetadataProviders/PackageMetadataProvider.swift b/Sources/PackageCollectionGenerator/PackageMetadataProviders/PackageMetadataProvider.swift index f7184a1..8b5bcee 100644 --- a/Sources/PackageCollectionGenerator/PackageMetadataProviders/PackageMetadataProvider.swift +++ b/Sources/PackageCollectionGenerator/PackageMetadataProviders/PackageMetadataProvider.swift @@ -17,7 +17,7 @@ import Foundation import PackageCollectionsModel protocol PackageMetadataProvider { - func get(_ packageURL: URL, callback: @escaping (Result) -> Void) + func get(_ packageURL: URL) async throws -> PackageBasicMetadata } enum AuthTokenType: Hashable, CustomStringConvertible { diff --git a/Sources/PackageCollectionSigner/PackageCollectionSign.swift b/Sources/PackageCollectionSigner/PackageCollectionSign.swift index 4b8b973..afa53ba 100644 --- a/Sources/PackageCollectionSigner/PackageCollectionSign.swift +++ b/Sources/PackageCollectionSigner/PackageCollectionSign.swift @@ -53,7 +53,7 @@ public struct PackageCollectionSign: AsyncParsableCommand { try await self._run(signer: nil) } - internal func _run(signer: PackageCollectionSigner?) async throws { + func _run(signer: PackageCollectionSigner?) async throws { Backtrace.install() guard !self.certChainPaths.isEmpty else { diff --git a/Tests/PackageCollectionGeneratorTests/GitHubPackageMetadataProviderTests.swift b/Tests/PackageCollectionGeneratorTests/GitHubPackageMetadataProviderTests.swift index 81317ed..2b7ffa1 100644 --- a/Tests/PackageCollectionGeneratorTests/GitHubPackageMetadataProviderTests.swift +++ b/Tests/PackageCollectionGeneratorTests/GitHubPackageMetadataProviderTests.swift @@ -42,7 +42,7 @@ final class GitHubPackageMetadataProviderTests: XCTestCase { XCTAssertNil(provider.apiURL("bad/Hello-World.git")) } - func testGood() throws { + func testGood() async throws { let repoURL = URL(string: "https://github.com/octocat/Hello-World.git")! let apiURL = URL(string: "https://api.github.com/repos/octocat/Hello-World")! let authTokens = [AuthTokenType.github("github.com"): "foo"] @@ -78,7 +78,7 @@ final class GitHubPackageMetadataProviderTests: XCTestCase { httpClient.configuration.retryStrategy = .none let provider = GitHubPackageMetadataProvider(authTokens: authTokens, httpClient: httpClient) - let metadata = try temp_await { callback in provider.get(repoURL, callback: callback) } + let metadata = try await provider.get(repoURL) XCTAssertEqual("This your first repo!", metadata.summary) XCTAssertEqual(["octocat", "atom", "electron", "api"], metadata.keywords) @@ -87,7 +87,7 @@ final class GitHubPackageMetadataProviderTests: XCTestCase { XCTAssertEqual(URL(string: "https://raw.githubusercontent.com/benbalter/gman/master/LICENSE?lab=true"), metadata.license?.url) } - func testInvalidAuthToken() throws { + func testInvalidAuthToken() async throws { let repoURL = URL(string: "https://github.com/octocat/Hello-World.git")! let apiURL = URL(string: "https://api.github.com/repos/octocat/Hello-World")! let authTokens = [AuthTokenType.github("github.com"): "foo"] @@ -106,12 +106,15 @@ final class GitHubPackageMetadataProviderTests: XCTestCase { httpClient.configuration.retryStrategy = .none let provider = GitHubPackageMetadataProvider(authTokens: authTokens, httpClient: httpClient) - XCTAssertThrowsError(try temp_await { callback in provider.get(repoURL, callback: callback) }, "should throw error") { error in + do { + _ = try await provider.get(repoURL) + XCTFail("should throw error") + } catch { XCTAssertEqual(error as? GitHubPackageMetadataProvider.Errors, .invalidAuthToken(apiURL)) } } - func testRepoNotFound() throws { + func testRepoNotFound() async throws { let repoURL = URL(string: "https://github.com/octocat/Hello-World.git")! let apiURL = URL(string: "https://api.github.com/repos/octocat/Hello-World")! let authTokens = [AuthTokenType.github("github.com"): "foo"] @@ -125,12 +128,15 @@ final class GitHubPackageMetadataProviderTests: XCTestCase { httpClient.configuration.retryStrategy = .none let provider = GitHubPackageMetadataProvider(authTokens: authTokens, httpClient: httpClient) - XCTAssertThrowsError(try temp_await { callback in provider.get(repoURL, callback: callback) }, "should throw error") { error in + do { + _ = try await provider.get(repoURL) + XCTFail("should throw error") + } catch { XCTAssertEqual(error as? GitHubPackageMetadataProvider.Errors, .notFound(apiURL)) } } - func testOthersNotFound() throws { + func testOthersNotFound() async throws { let repoURL = URL(string: "https://github.com/octocat/Hello-World.git")! let apiURL = URL(string: "https://api.github.com/repos/octocat/Hello-World")! let authTokens = [AuthTokenType.github("github.com"): "foo"] @@ -156,7 +162,7 @@ final class GitHubPackageMetadataProviderTests: XCTestCase { httpClient.configuration.retryStrategy = .none let provider = GitHubPackageMetadataProvider(authTokens: authTokens, httpClient: httpClient) - let metadata = try temp_await { callback in provider.get(repoURL, callback: callback) } + let metadata = try await provider.get(repoURL) XCTAssertEqual("This your first repo!", metadata.summary) XCTAssertEqual(["octocat", "atom", "electron", "api"], metadata.keywords) @@ -164,7 +170,7 @@ final class GitHubPackageMetadataProviderTests: XCTestCase { XCTAssertNil(metadata.license) } - func testPermissionDenied() throws { + func testPermissionDenied() async throws { let repoURL = URL(string: "https://github.com/octocat/Hello-World.git")! let apiURL = URL(string: "https://api.github.com/repos/octocat/Hello-World")! @@ -177,20 +183,26 @@ final class GitHubPackageMetadataProviderTests: XCTestCase { httpClient.configuration.retryStrategy = .none let provider = GitHubPackageMetadataProvider(httpClient: httpClient) - XCTAssertThrowsError(try temp_await { callback in provider.get(repoURL, callback: callback) }, "should throw error") { error in + do { + _ = try await provider.get(repoURL) + XCTFail("should throw error") + } catch { XCTAssertEqual(error as? GitHubPackageMetadataProvider.Errors, .permissionDenied(apiURL)) } } - func testInvalidURL() throws { + func testInvalidURL() async throws { let repoURL = URL(string: "/")! let provider = GitHubPackageMetadataProvider() - XCTAssertThrowsError(try temp_await { callback in provider.get(repoURL, callback: callback) }, "should throw error") { error in + do { + _ = try await provider.get(repoURL) + XCTFail("should throw error") + } catch { XCTAssertEqual(error as? GitHubPackageMetadataProvider.Errors, .invalidGitURL(repoURL)) } } - func testForRealz() throws { + func testForRealz() async throws { #if ENABLE_GITHUB_NETWORK_TEST #else try XCTSkipIf(true) @@ -211,7 +223,7 @@ final class GitHubPackageMetadataProviderTests: XCTestCase { let provider = GitHubPackageMetadataProvider(authTokens: authTokens, httpClient: httpClient) for _ in 0 ... 60 { - let metadata = try temp_await { callback in provider.get(repoURL, callback: callback) } + let metadata = try await provider.get(repoURL) XCTAssertNotNil(metadata) XCTAssert(metadata.keywords!.count > 0) XCTAssertNotNil(metadata.readmeURL) diff --git a/Tests/PackageCollectionGeneratorTests/PackageCollectionGenerateTests.swift b/Tests/PackageCollectionGeneratorTests/PackageCollectionGenerateTests.swift index 21a53b2..fee8d63 100644 --- a/Tests/PackageCollectionGeneratorTests/PackageCollectionGenerateTests.swift +++ b/Tests/PackageCollectionGeneratorTests/PackageCollectionGenerateTests.swift @@ -31,8 +31,8 @@ final class PackageCollectionGenerateTests: XCTestCase { XCTAssert(help.contains(expectedUsageDescription), "Help did not contain expected usage description, instead it had:\n\(help)") } - func test_endToEnd() throws { - try withTemporaryDirectory(prefix: "PackageCollectionToolTests", removeTreeOnDeinit: true) { tmpDir in + func test_endToEnd() async throws { + try await withTemporaryDirectory(prefix: "PackageCollectionToolTests", removeTreeOnDeinit: true) { tmpDir in // TestRepoOne has tags [0.1.0] let repoOneArchivePath = try AbsolutePath(validating: #file).parentDirectory.appending(components: "Inputs", "TestRepoOne.tgz") try systemQuietly(["tar", "-x", "-v", "-C", tmpDir.pathString, "-f", repoOneArchivePath.pathString]) @@ -210,7 +210,7 @@ final class PackageCollectionGenerateTests: XCTestCase { workingDirectoryPath.pathString, ].compactMap { $0 } let cmd = try PackageCollectionGenerate.parse(flags) - try cmd.run() + try await cmd.run() let jsonDecoder = JSONDecoder.makeWithDefaults() @@ -228,8 +228,8 @@ final class PackageCollectionGenerateTests: XCTestCase { } } - func test_excludedVersions() throws { - try withTemporaryDirectory(prefix: "PackageCollectionToolTests", removeTreeOnDeinit: true) { tmpDir in + func test_excludedVersions() async throws { + try await withTemporaryDirectory(prefix: "PackageCollectionToolTests", removeTreeOnDeinit: true) { tmpDir in // TestRepoOne has tags [0.1.0] let repoOneArchivePath = try AbsolutePath(validating: #file).parentDirectory.appending(components: "Inputs", "TestRepoOne.tgz") try systemQuietly(["tar", "-x", "-v", "-C", tmpDir.pathString, "-f", repoOneArchivePath.pathString]) @@ -273,7 +273,7 @@ final class PackageCollectionGenerateTests: XCTestCase { "--working-directory-path", workingDirectoryPath.pathString, ]) - try cmd.run() + try await cmd.run() let expectedPackages = [ Model.Collection.Package( diff --git a/Tests/PackageCollectionGeneratorTests/PackageCollectionGeneratorInputTests.swift b/Tests/PackageCollectionGeneratorTests/PackageCollectionGeneratorInputTests.swift index 3aa6382..061544d 100644 --- a/Tests/PackageCollectionGeneratorTests/PackageCollectionGeneratorInputTests.swift +++ b/Tests/PackageCollectionGeneratorTests/PackageCollectionGeneratorInputTests.swift @@ -48,7 +48,7 @@ class PackageCollectionGeneratorInputTests: XCTestCase { let inputFilePath = try AbsolutePath(validating: #file).parentDirectory.appending(components: "Inputs", "test-input.json") let input = try JSONDecoder().decode( PackageCollectionGeneratorInput.self, - from: Data(try localFileSystem.readFileContents(inputFilePath).contents) + from: Data(localFileSystem.readFileContents(inputFilePath).contents) ) XCTAssertEqual(expectedInput, input)