From 2d5751c9dad6ed9a52766bec89854b69a6261fa2 Mon Sep 17 00:00:00 2001 From: Roddy Munro Date: Wed, 12 Jun 2024 20:28:36 -0300 Subject: [PATCH] v1.0.0 merge (#18) * Add ArgumentParser (#16) * Add ArgumentParser dependency * Rename SwiftPolyglotCore struct * Add RuntimeError * Rename SwiftPolyglot struct, add ParsableCommand adoption and add RuntimeError usage * Parse arguments and flag using ArgumentParser and adjust SwiftPolyglotCore initializer's parameters * Update README * Rename property * Format * Fix command name in help's message * Add Concurrency (#17) * Set minimum macOS version to version 10.15 (Catalina) * Add MissingTranslation struct * Add concurrency to core functionality * Adopt AsyncParsableCommand protocol to provide an asynchronous entry point * Add XCTest extension for testing async throwing expressions * Add concurrency to tests * Fix SwiftFormat issues (#19) * Remove trailing commas * Fix indentation * Move inline try to start of expression * Remove trailing white spaces * Use opaque generic parameters instead of generic parameters with constraints * Replace consecutive blank lines with a single blank line * Disable hoistAwait (#20) * Fix swiftformat (#21) * Improve assertions on async throwing expressions (#22) * Add Equatable adoption for custom error * Improve custom method for asserting on asynchronous expressions which should run successfully without throwing an error * Improve custom method for asserting on asynchronous expressions which should throw an error * Reenable hoistAwait SwiftFormat rule --------- Co-authored-by: Pere Bohigas --- .swiftformat | 3 +- Package.resolved | 14 ++ Package.swift | 11 +- README.md | 8 +- Sources/SwiftPolyglot/RuntimeError.swift | 15 ++ Sources/SwiftPolyglot/SwiftPolyglot.swift | 36 +++ Sources/SwiftPolyglot/main.swift | 22 -- .../MissingTranslation.swift | 30 +++ Sources/SwiftPolyglotCore/SwiftPolyglot.swift | 192 ---------------- .../SwiftPolyglotCore/SwiftPolyglotCore.swift | 213 ++++++++++++++++++ .../SwiftPolyglotError.swift | 5 +- .../SwiftPolyglotCoreTests.swift | 44 ++-- .../XCTest+AsyncThrowingExpression.swift | 62 +++++ 13 files changed, 412 insertions(+), 243 deletions(-) create mode 100644 Package.resolved create mode 100644 Sources/SwiftPolyglot/RuntimeError.swift create mode 100755 Sources/SwiftPolyglot/SwiftPolyglot.swift delete mode 100755 Sources/SwiftPolyglot/main.swift create mode 100644 Sources/SwiftPolyglotCore/MissingTranslation.swift delete mode 100644 Sources/SwiftPolyglotCore/SwiftPolyglot.swift create mode 100644 Sources/SwiftPolyglotCore/SwiftPolyglotCore.swift create mode 100644 Tests/SwiftPolyglotCoreTests/XCTest+AsyncThrowingExpression.swift diff --git a/.swiftformat b/.swiftformat index 10b24f6..b2c3655 100644 --- a/.swiftformat +++ b/.swiftformat @@ -11,4 +11,5 @@ # rules ---disable preferForLoop \ No newline at end of file +--disable preferForLoop + diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..d126c77 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,14 @@ +{ + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "46989693916f56d1186bd59ac15124caef896560", + "version" : "1.3.1" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift index 8178487..16708a3 100644 --- a/Package.swift +++ b/Package.swift @@ -5,13 +5,22 @@ import PackageDescription let package = Package( name: "SwiftPolyglot", + platforms: [ + .macOS(.v10_15), + ], products: [ .executable(name: "swiftpolyglot", targets: ["SwiftPolyglot"]), ], + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMajor(from: "1.3.1")), + ], targets: [ .executableTarget( name: "SwiftPolyglot", - dependencies: ["SwiftPolyglotCore"] + dependencies: [ + "SwiftPolyglotCore", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] ), .target(name: "SwiftPolyglotCore"), .testTarget( diff --git a/README.md b/README.md index c423e9d..191d9c8 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ $ swift build -c release ``` $ cd ../path/to/your/project -$ swift run --package-path ../path/to/SwiftPolyglot swiftpolyglot "en,es,de" +$ swift run --package-path ../path/to/SwiftPolyglot swiftpolyglot en es de ``` ## Arguments -You must specify at least one language code, they must be within quotation marks, and they must be separated by commas. If you are not providing a translation for your language of origin, you do not need to specify that language. Otherwise, you will get errors due to missing translations. +You must specify at least one language code, and they must be separated by spaces. If you are not providing a translation for your language of origin, you do not need to specify that language. Otherwise, you will get errors due to missing translations. -By default, SwiftPolyglot will not throw an error at the end of the script if there are translations missing. However, you can enable error throwing by adding the argument `--errorOnMissing` +By default, SwiftPolyglot will not throw an error at the end of the script if there are translations missing. However, you can enable error throwing by adding the flag `--error-on-missing` ## Integrating with GitHub Actions @@ -60,6 +60,6 @@ jobs: - name: validate translations run: | swift build --package-path ../SwiftPolyglot --configuration release - swift run --package-path ../SwiftPolyglot swiftpolyglot "es,fr,de,it" --errorOnMissing + swift run --package-path ../SwiftPolyglot swiftpolyglot es fr de it --error-on-missing ``` diff --git a/Sources/SwiftPolyglot/RuntimeError.swift b/Sources/SwiftPolyglot/RuntimeError.swift new file mode 100644 index 0000000..c90d531 --- /dev/null +++ b/Sources/SwiftPolyglot/RuntimeError.swift @@ -0,0 +1,15 @@ +enum RuntimeError: Error { + case coreError(description: String) + case fileListingNotPossible +} + +extension RuntimeError: CustomStringConvertible { + var description: String { + switch self { + case let .coreError(description): + return description + case .fileListingNotPossible: + return "It was not possible to list all files to be checked" + } + } +} diff --git a/Sources/SwiftPolyglot/SwiftPolyglot.swift b/Sources/SwiftPolyglot/SwiftPolyglot.swift new file mode 100755 index 0000000..eafcd7d --- /dev/null +++ b/Sources/SwiftPolyglot/SwiftPolyglot.swift @@ -0,0 +1,36 @@ +import ArgumentParser +import Foundation +import SwiftPolyglotCore + +@main +struct SwiftPolyglot: AsyncParsableCommand { + static let configuration: CommandConfiguration = .init(commandName: "swiftpolyglot") + + @Flag(help: "Log errors instead of warnings for missing translations.") + private var errorOnMissing = false + + @Argument(help: "Specify the language(s) to be checked.") + private var languages: [String] + + func run() async throws { + guard + let enumerator = FileManager.default.enumerator(atPath: FileManager.default.currentDirectoryPath), + let filePaths = enumerator.allObjects as? [String] + else { + throw RuntimeError.fileListingNotPossible + } + + let swiftPolyglotCore: SwiftPolyglotCore = .init( + filePaths: filePaths, + languageCodes: languages, + logsErrorOnMissingTranslation: errorOnMissing, + isRunningInAGitHubAction: ProcessInfo.processInfo.environment["GITHUB_ACTIONS"] == "true" + ) + + do { + try await swiftPolyglotCore.run() + } catch { + throw RuntimeError.coreError(description: error.localizedDescription) + } + } +} diff --git a/Sources/SwiftPolyglot/main.swift b/Sources/SwiftPolyglot/main.swift deleted file mode 100755 index 7bc4e28..0000000 --- a/Sources/SwiftPolyglot/main.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import SwiftPolyglotCore - -guard - let enumerator = FileManager.default.enumerator(atPath: FileManager.default.currentDirectoryPath), - let filePaths = enumerator.allObjects as? [String] -else { - exit(EXIT_FAILURE) -} - -do { - let swiftPolyglot: SwiftPolyglot = try .init( - arguments: Array(CommandLine.arguments.dropFirst()), - filePaths: filePaths, - runningOnAGitHubAction: ProcessInfo.processInfo.environment["GITHUB_ACTIONS"] == "true" - ) - - try swiftPolyglot.run() -} catch { - print(error.localizedDescription) - exit(EXIT_FAILURE) -} diff --git a/Sources/SwiftPolyglotCore/MissingTranslation.swift b/Sources/SwiftPolyglotCore/MissingTranslation.swift new file mode 100644 index 0000000..886e80f --- /dev/null +++ b/Sources/SwiftPolyglotCore/MissingTranslation.swift @@ -0,0 +1,30 @@ +struct MissingTranslation { + enum Category { + case deviceMissingOrNotTranslated(forDevice: String, inLanguage: String) + case missingOrNotTranslated(inLanguage: String) + case missingTranslation(forLanguage: String) + case missingTranslationForAllLanguages + case pluralMissingOrNotTranslated(forPluralForm: String, inLanguage: String) + } + + let category: Category + let filePath: String + let originalString: String +} + +extension MissingTranslation { + var message: String { + switch category { + case let .deviceMissingOrNotTranslated(device, language): + return "'\(originalString)' device '\(device)' is missing or not translated in '\(language)' in file: \(filePath)" + case let .missingOrNotTranslated(language): + return "'\(originalString)' is missing or not translated in '\(language)' in file: \(filePath)" + case let .missingTranslation(language): + return "'\(originalString)' is missing translations for language '\(language)' in file: \(filePath)" + case .missingTranslationForAllLanguages: + return "'\(originalString)' is not translated in any language in file: \(filePath)" + case let .pluralMissingOrNotTranslated(pluralForm, language): + return "'\(originalString)' plural form '\(pluralForm)' is missing or not translated in '\(language)' in file: \(filePath)" + } + } +} diff --git a/Sources/SwiftPolyglotCore/SwiftPolyglot.swift b/Sources/SwiftPolyglotCore/SwiftPolyglot.swift deleted file mode 100644 index dd2e9d4..0000000 --- a/Sources/SwiftPolyglotCore/SwiftPolyglot.swift +++ /dev/null @@ -1,192 +0,0 @@ -import Foundation - -public struct SwiftPolyglot { - private static let errorOnMissingArgument = "--errorOnMissing" - - private let arguments: [String] - private let filePaths: [String] - private let languageCodes: [String] - private let runningOnAGitHubAction: Bool - - private var logErrorOnMissing: Bool { - arguments.contains(Self.errorOnMissingArgument) - } - - public init(arguments: [String], filePaths: [String], runningOnAGitHubAction: Bool) throws { - let languageCodes = arguments[0].split(separator: ",").map(String.init) - - guard - !languageCodes.contains(Self.errorOnMissingArgument), - !languageCodes.isEmpty - else { - throw SwiftPolyglotError.noLanguageCodes - } - - self.arguments = arguments - self.filePaths = filePaths - self.languageCodes = languageCodes - self.runningOnAGitHubAction = runningOnAGitHubAction - } - - public func run() throws { - var missingTranslations = false - - try searchDirectory(for: languageCodes, missingTranslations: &missingTranslations) - - if missingTranslations, logErrorOnMissing { - throw SwiftPolyglotError.missingTranslations - } else if missingTranslations { - print("Completed with missing translations.") - } else { - print("All translations are present.") - } - } - - private func checkDeviceVariations( - devices: [String: [String: Any]], - originalString: String, - lang: String, - fileURL: URL, - missingTranslations: inout Bool - ) { - for (device, value) in devices { - guard let stringUnit = value["stringUnit"] as? [String: Any], - let state = stringUnit["state"] as? String, state == "translated" - else { - logWarning( - file: fileURL.path, - message: "'\(originalString)' device '\(device)' is missing or not translated in \(lang) in file: \(fileURL.path)" - ) - missingTranslations = true - continue - } - } - } - - private func checkPluralizations( - pluralizations: [String: [String: Any]], - originalString: String, - lang: String, - fileURL: URL, - missingTranslations: inout Bool - ) { - for (pluralForm, value) in pluralizations { - guard let stringUnit = value["stringUnit"] as? [String: Any], - let state = stringUnit["state"] as? String, state == "translated" - else { - logWarning( - file: fileURL.path, - message: "'\(originalString)' plural form '\(pluralForm)' is missing or not translated in \(lang) in file: \(fileURL.path)" - ) - missingTranslations = true - continue - } - } - } - - private func checkTranslations(in fileURL: URL, for languages: [String], missingTranslations: inout Bool) throws { - guard let data = try? Data(contentsOf: fileURL), - let jsonObject = try? JSONSerialization.jsonObject(with: data), - let jsonDict = jsonObject as? [String: Any], - let strings = jsonDict["strings"] as? [String: [String: Any]] - else { - if runningOnAGitHubAction { - print("::warning file=\(fileURL.path)::Could not process file at path: \(fileURL.path)") - } else { - print("Could not process file at path: \(fileURL.path)") - } - return - } - - for (originalString, translations) in strings { - guard let localizations = translations["localizations"] as? [String: [String: Any]] else { - logWarning( - file: fileURL.path, - message: "'\(originalString)' is not translated in any language in file: \(fileURL.path)" - ) - missingTranslations = true - continue - } - - for lang in languages { - guard let languageDict = localizations[lang] else { - logWarning( - file: fileURL.path, - message: "'\(originalString)' is missing translations for language: \(lang) in file: \(fileURL.path)" - ) - missingTranslations = true - continue - } - - if let variations = languageDict["variations"] as? [String: [String: [String: Any]]] { - try checkVariations( - variations: variations, - originalString: originalString, - lang: lang, - fileURL: fileURL, - missingTranslations: &missingTranslations - ) - } else if let stringUnit = languageDict["stringUnit"] as? [String: Any], - let state = stringUnit["state"] as? String, state != "translated" - { - logWarning( - file: fileURL.path, - message: "'\(originalString)' is missing or not translated in \(lang) in file: \(fileURL.path)" - ) - missingTranslations = true - } - } - } - } - - private func checkVariations( - variations: [String: [String: [String: Any]]], - originalString: String, - lang: String, - fileURL: URL, - missingTranslations: inout Bool - ) throws { - for (variationKey, variationDict) in variations { - if variationKey == "plural" { - checkPluralizations( - pluralizations: variationDict, - originalString: originalString, - lang: lang, - fileURL: fileURL, - missingTranslations: &missingTranslations - ) - } else if variationKey == "device" { - checkDeviceVariations( - devices: variationDict, - originalString: originalString, - lang: lang, - fileURL: fileURL, - missingTranslations: &missingTranslations - ) - } else { - throw SwiftPolyglotError.unsupportedVariation(variation: variationKey) - } - } - } - - private func logWarning(file: String, message: String) { - if runningOnAGitHubAction { - if logErrorOnMissing { - print("::error file=\(file)::\(message)") - } else { - print("::warning file=\(file)::\(message)") - } - } else { - print(message) - } - } - - private func searchDirectory(for languages: [String], missingTranslations: inout Bool) throws { - for filePath in filePaths { - if filePath.hasSuffix(".xcstrings") { - let fileURL = URL(fileURLWithPath: filePath) - try checkTranslations(in: fileURL, for: languages, missingTranslations: &missingTranslations) - } - } - } -} diff --git a/Sources/SwiftPolyglotCore/SwiftPolyglotCore.swift b/Sources/SwiftPolyglotCore/SwiftPolyglotCore.swift new file mode 100644 index 0000000..2ee8450 --- /dev/null +++ b/Sources/SwiftPolyglotCore/SwiftPolyglotCore.swift @@ -0,0 +1,213 @@ +import Foundation + +public struct SwiftPolyglotCore { + private let filePaths: [String] + private let languageCodes: [String] + private let logsErrorOnMissingTranslation: Bool + private let isRunningInAGitHubAction: Bool + + public init( + filePaths: [String], + languageCodes: [String], + logsErrorOnMissingTranslation: Bool, + isRunningInAGitHubAction: Bool + ) { + self.filePaths = filePaths + self.languageCodes = languageCodes + self.logsErrorOnMissingTranslation = logsErrorOnMissingTranslation + self.isRunningInAGitHubAction = isRunningInAGitHubAction + } + + public func run() async throws { + let stringCatalogFileURLs: [URL] = getStringCatalogURLs(from: filePaths) + + let missingTranslations: [MissingTranslation] = try await withThrowingTaskGroup(of: [MissingTranslation].self) { taskGroup in + for fileURL in stringCatalogFileURLs { + taskGroup.addTask { + let strings: [String: [String: Any]] = extractStrings( + from: fileURL, + isRunningInAGitHubAction: isRunningInAGitHubAction + ) + + let missingTranslations: [MissingTranslation] = try await getMissingTranslations(from: strings, in: fileURL.path) + + let missingTranslationsLogs: [String] = missingTranslations.map { missingTranslation in + if isRunningInAGitHubAction { + return logForGitHubAction( + missingTranslation: missingTranslation, + logWithError: logsErrorOnMissingTranslation + ) + } else { + return missingTranslation.message + } + } + + missingTranslationsLogs.forEach { print($0) } + + return missingTranslations + } + } + + return try await taskGroup.reduce(into: [MissingTranslation]()) { partialResult, missingTranslations in + partialResult.append(contentsOf: missingTranslations) + } + } + + if !missingTranslations.isEmpty, logsErrorOnMissingTranslation { + throw SwiftPolyglotError.missingTranslations + } else if !missingTranslations.isEmpty { + print("Completed with missing translations.") + } else { + print("All translations are present.") + } + } + + private func extractStrings(from fileURL: URL, isRunningInAGitHubAction: Bool) -> [String: [String: Any]] { + guard + let data = try? Data(contentsOf: fileURL), + let jsonObject = try? JSONSerialization.jsonObject(with: data), + let jsonDict = jsonObject as? [String: Any], + let strings = jsonDict["strings"] as? [String: [String: Any]] + else { + if isRunningInAGitHubAction { + print("::warning file=\(fileURL.path)::Could not process file at path: \(fileURL.path)") + } else { + print("Could not process file at path: \(fileURL.path)") + } + + return [:] + } + + return strings + } + + private func getMissingTranslations( + from strings: [String: [String: Any]], + in filePath: String + ) async throws -> [MissingTranslation] { + var missingTranslations: [MissingTranslation] = [] + + for (originalString, translations) in strings { + guard let localizations = translations["localizations"] as? [String: [String: Any]] else { + missingTranslations.append( + MissingTranslation( + category: .missingTranslationForAllLanguages, + filePath: filePath, + originalString: originalString + ) + ) + + continue + } + + for lang in languageCodes { + guard let languageDict = localizations[lang] else { + missingTranslations.append( + MissingTranslation( + category: .missingTranslation(forLanguage: lang), + filePath: filePath, + originalString: originalString + ) + ) + + continue + } + + if let variations = languageDict["variations"] as? [String: [String: [String: Any]]] { + try missingTranslations.append( + contentsOf: + getMissingTranslationsFromVariations( + variations, + originalString: originalString, + lang: lang, + filePath: filePath + ) + ) + } else if + let stringUnit = languageDict["stringUnit"] as? [String: Any], + let state = stringUnit["state"] as? String, + state != "translated" + { + missingTranslations.append( + MissingTranslation( + category: .missingOrNotTranslated(inLanguage: lang), + filePath: filePath, + originalString: originalString + ) + ) + } + } + } + + return missingTranslations + } + + private func getMissingTranslationsFromVariations( + _ variations: [String: [String: [String: Any]]], + originalString: String, + lang: String, + filePath: String + ) throws -> [MissingTranslation] { + var missingTranslations: [MissingTranslation] = [] + + for (variationKey, variationDict) in variations { + if variationKey == "plural" { + for (pluralForm, value) in variationDict { + guard + let stringUnit = value["stringUnit"] as? [String: Any], + let state = stringUnit["state"] as? String, + state == "translated" + else { + missingTranslations.append( + MissingTranslation( + category: .pluralMissingOrNotTranslated(forPluralForm: pluralForm, inLanguage: lang), + filePath: filePath, + originalString: originalString + ) + ) + + continue + } + } + } else if variationKey == "device" { + for (device, value) in variationDict { + guard + let stringUnit = value["stringUnit"] as? [String: Any], + let state = stringUnit["state"] as? String, + state == "translated" + else { + missingTranslations.append( + MissingTranslation( + category: .deviceMissingOrNotTranslated(forDevice: device, inLanguage: lang), + filePath: filePath, + originalString: originalString + ) + ) + + continue + } + } + } else { + throw SwiftPolyglotError.unsupportedVariation(variation: variationKey) + } + } + + return missingTranslations + } + + private func getStringCatalogURLs(from filePaths: [String]) -> [URL] { + filePaths.compactMap { filePath in + guard filePath.hasSuffix(".xcstrings") else { return nil } + + return URL(fileURLWithPath: filePath) + } + } + + private func logForGitHubAction(missingTranslation: MissingTranslation, logWithError: Bool) -> String { + if logWithError { + return "::error file=\(missingTranslation.filePath)::\(missingTranslation.message)" + } else { + return "::warning file=\(missingTranslation.filePath)::\(missingTranslation.message)" + } + } +} diff --git a/Sources/SwiftPolyglotCore/SwiftPolyglotError.swift b/Sources/SwiftPolyglotCore/SwiftPolyglotError.swift index c4113ac..9e95462 100644 --- a/Sources/SwiftPolyglotCore/SwiftPolyglotError.swift +++ b/Sources/SwiftPolyglotCore/SwiftPolyglotError.swift @@ -2,17 +2,16 @@ import Foundation enum SwiftPolyglotError: Error { case missingTranslations - case noLanguageCodes case unsupportedVariation(variation: String) } +extension SwiftPolyglotError: Equatable {} + extension SwiftPolyglotError: LocalizedError { public var errorDescription: String? { switch self { case .missingTranslations: return "Error: One or more translations are missing." - case .noLanguageCodes: - return "Usage: swiftpolyglot [--errorOnMissing]" case let .unsupportedVariation(variation): return "Variation type '\(variation)' is not supported. Please create an issue in GitHub" } diff --git a/Tests/SwiftPolyglotCoreTests/SwiftPolyglotCoreTests.swift b/Tests/SwiftPolyglotCoreTests/SwiftPolyglotCoreTests.swift index 5f7f21c..448ab9c 100644 --- a/Tests/SwiftPolyglotCoreTests/SwiftPolyglotCoreTests.swift +++ b/Tests/SwiftPolyglotCoreTests/SwiftPolyglotCoreTests.swift @@ -2,7 +2,7 @@ import XCTest final class SwiftPolyglotCoreTests: XCTestCase { - func testStringCatalogFullyTranslated() throws { + func testStringCatalogFullyTranslated() async throws { guard let stringCatalogFilePath = Bundle.module.path( forResource: "FullyTranslated", @@ -14,16 +14,17 @@ final class SwiftPolyglotCoreTests: XCTestCase { return } - let swiftPolyglot: SwiftPolyglot = try .init( - arguments: ["ca,de,en,es"], + let swiftPolyglotCore: SwiftPolyglotCore = .init( filePaths: [stringCatalogFilePath], - runningOnAGitHubAction: false + languageCodes: ["ca", "de", "en", "es"], + logsErrorOnMissingTranslation: false, + isRunningInAGitHubAction: false ) - XCTAssertNoThrow(try swiftPolyglot.run()) + await XCTAssertNoThrowAsync(swiftPolyglotCore.run) } - func testStringCatalogVariationsFullyTranslated() throws { + func testStringCatalogVariationsFullyTranslated() async throws { guard let stringCatalogFilePath = Bundle.module.path( forResource: "VariationsFullyTranslated", @@ -35,16 +36,17 @@ final class SwiftPolyglotCoreTests: XCTestCase { return } - let swiftPolyglot: SwiftPolyglot = try .init( - arguments: ["ca,de,en,es"], + let swiftPolyglotCore: SwiftPolyglotCore = .init( filePaths: [stringCatalogFilePath], - runningOnAGitHubAction: false + languageCodes: ["ca", "de", "en", "es"], + logsErrorOnMissingTranslation: false, + isRunningInAGitHubAction: false ) - XCTAssertNoThrow(try swiftPolyglot.run()) + await XCTAssertNoThrowAsync(swiftPolyglotCore.run) } - func testStringCatalogWithMissingTranslations() throws { + func testStringCatalogWithMissingTranslations() async throws { guard let stringCatalogFilePath = Bundle.module.path( forResource: "WithMissingTranslations", @@ -56,16 +58,17 @@ final class SwiftPolyglotCoreTests: XCTestCase { return } - let swiftPolyglot: SwiftPolyglot = try .init( - arguments: ["ca,de,en,es", "--errorOnMissing"], + let swiftPolyglotCore: SwiftPolyglotCore = .init( filePaths: [stringCatalogFilePath], - runningOnAGitHubAction: false + languageCodes: ["ca", "de", "en", "es"], + logsErrorOnMissingTranslation: true, + isRunningInAGitHubAction: false ) - XCTAssertThrowsError(try swiftPolyglot.run()) + await XCTAssertThrowsErrorAsync(swiftPolyglotCore.run, SwiftPolyglotError.missingTranslations) } - func testStringCatalogWithMissingVariations() throws { + func testStringCatalogWithMissingVariations() async throws { guard let stringCatalogFilePath = Bundle.module.path( forResource: "VariationsWithMissingTranslations", @@ -77,12 +80,13 @@ final class SwiftPolyglotCoreTests: XCTestCase { return } - let swiftPolyglot: SwiftPolyglot = try .init( - arguments: ["de,en", "--errorOnMissing"], + let swiftPolyglotCore: SwiftPolyglotCore = .init( filePaths: [stringCatalogFilePath], - runningOnAGitHubAction: false + languageCodes: ["de, en"], + logsErrorOnMissingTranslation: true, + isRunningInAGitHubAction: false ) - XCTAssertThrowsError(try swiftPolyglot.run()) + await XCTAssertThrowsErrorAsync(swiftPolyglotCore.run, SwiftPolyglotError.missingTranslations) } } diff --git a/Tests/SwiftPolyglotCoreTests/XCTest+AsyncThrowingExpression.swift b/Tests/SwiftPolyglotCoreTests/XCTest+AsyncThrowingExpression.swift new file mode 100644 index 0000000..24b0e15 --- /dev/null +++ b/Tests/SwiftPolyglotCoreTests/XCTest+AsyncThrowingExpression.swift @@ -0,0 +1,62 @@ +import XCTest + +/// Asserts that an asynchronous expression do not throw an error. +/// (Intended to function as a drop-in asynchronous version of `XCTAssertNoThrow`.) +/// +/// Example usage: +/// +/// await assertNoThrowAsync(sut.function) +/// +/// - Parameters: +/// - expression: An asynchronous expression that can throw an error. +/// - failureMessage: An optional description of a failure. +/// - file: The file where the failure occurs. The default is the file path of the test case where this function is being called. +/// - line: The line number where the failure occurs. The default is the line number where this function is being called. +public func XCTAssertNoThrowAsync( + _ expression: () async throws -> some Any, + failureMessage: String = "Asynchronous call did throw an error.", + file: StaticString = #filePath, + line: UInt = #line +) async { + do { + _ = try await expression() + } catch { + XCTFail(failureMessage, file: file, line: line) + } +} + +/// Asserts that an asynchronous expression throws an error. +/// (Intended to function as a drop-in asynchronous version of `XCTAssertThrowsError`.) +/// +/// Example usage: +/// +/// await assertThrowsAsyncError(sut.function, MyError.specificError) +/// +/// - Parameters: +/// - expression: An asynchronous expression that can throw an error. +/// - errorThrown: The error type that should be thrown. +/// - failureMessage: An optional description of a failure. +/// - file: The file where the failure occurs. The default is the file path of the test case where this function is being called. +/// - line: The line number where the failure occurs. The default is the line number where this function is being called. +/// +/// from: https://arturgruchala.com/testing-async-await-exceptions/ +func XCTAssertThrowsErrorAsync( + _ expression: () async throws -> some Any, + _ errorThrown: E, + failureMessage: String = "Asynchronous call did not throw an error.", + file: StaticString = #filePath, + line: UInt = #line +) async where E: Equatable, E: Error { + do { + _ = try await expression() + XCTFail(failureMessage, file: file, line: line) + } catch { + XCTAssertEqual( + error as? E, + errorThrown, + "Asynchronous call did not throw the given error \"\(errorThrown)\".", + file: file, + line: line + ) + } +}