From 333c7fab0c54a93107f1babfb1255be6ea217109 Mon Sep 17 00:00:00 2001 From: "LamTrinh.Dev" Date: Tue, 24 Jun 2025 00:15:19 +0700 Subject: [PATCH 1/9] Update the link for Proposoals 0022-writing-direction-attribute.md (#1255) --- Proposals/0022-writing-direction-attribute.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Proposals/0022-writing-direction-attribute.md b/Proposals/0022-writing-direction-attribute.md index 56c024b4d..c29313dbd 100644 --- a/Proposals/0022-writing-direction-attribute.md +++ b/Proposals/0022-writing-direction-attribute.md @@ -1,6 +1,6 @@ # Writing Direction Attribute -* Proposal: [SF-0022](NNNN-writing-direction-attribute.md) +* Proposal: [SF-0022](0022-writing-direction-attribute.md) * Authors: [Max Obermeier](https://github.com/themomax) * Review Manager: [Tina L](https://github.com/itingliu) * Status: **Approved and Implemented** From 3ee8bc0d7eb3ac998466a49429882a181aff9926 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Mon, 23 Jun 2025 16:29:16 -0700 Subject: [PATCH 2/9] Convert JSON/PropertyList tests to swift-testing (#1364) * Convert JSONEncoder tests * Convert PropertyListEncoder tests * Gather source location from callers to AssertEqualPaths * Mark JSONEncoderTests.depthTraversal() as @MainActor --- .../JSONEncoderTests.swift | 1232 +++++++++-------- .../PropertyListEncoderTests.swift | 872 ++++++------ 2 files changed, 1083 insertions(+), 1021 deletions(-) diff --git a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift index 6697df493..6fc131ba6 100644 --- a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift +++ b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift @@ -9,138 +9,145 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// -// -// RUN: %target-run-simple-swift -// REQUIRES: executable_test -// REQUIRES: objc_interop -// REQUIRES: rdar49634697 -// REQUIRES: rdar55727144 -#if canImport(TestSupport) -import TestSupport -#endif // canImport(TestSupport) +import Testing + +#if canImport(Darwin) +import Darwin +#elseif canImport(Bionic) +@preconcurrency import Bionic +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(CRT) +import CRT +#elseif os(WASI) +@preconcurrency import WASILibc +#endif #if canImport(FoundationEssentials) @_spi(SwiftCorelibsFoundation) -@testable import FoundationEssentials +import FoundationEssentials #endif #if FOUNDATION_FRAMEWORK -@testable import Foundation +import Foundation #endif // MARK: - Test Suite -final class JSONEncoderTests : XCTestCase { +@Suite("JSONEncoder") +private struct JSONEncoderTests { // MARK: - Encoding Top-Level Empty Types - func testEncodingTopLevelEmptyStruct() { + @Test func encodingTopLevelEmptyStruct() { let empty = EmptyStruct() _testRoundTrip(of: empty, expectedJSON: _jsonEmptyDictionary) } - func testEncodingTopLevelEmptyClass() { + @Test func encodingTopLevelEmptyClass() { let empty = EmptyClass() _testRoundTrip(of: empty, expectedJSON: _jsonEmptyDictionary) } // MARK: - Encoding Top-Level Single-Value Types - func testEncodingTopLevelSingleValueEnum() { + @Test func encodingTopLevelSingleValueEnum() { _testRoundTrip(of: Switch.off) _testRoundTrip(of: Switch.on) } - func testEncodingTopLevelSingleValueStruct() { + @Test func encodingTopLevelSingleValueStruct() { _testRoundTrip(of: Timestamp(3141592653)) } - func testEncodingTopLevelSingleValueClass() { + @Test func encodingTopLevelSingleValueClass() { _testRoundTrip(of: Counter()) } // MARK: - Encoding Top-Level Structured Types - func testEncodingTopLevelStructuredStruct() { + @Test func encodingTopLevelStructuredStruct() { // Address is a struct type with multiple fields. let address = Address.testValue _testRoundTrip(of: address) } - func testEncodingTopLevelStructuredSingleStruct() { + @Test func encodingTopLevelStructuredSingleStruct() { // Numbers is a struct which encodes as an array through a single value container. let numbers = Numbers.testValue _testRoundTrip(of: numbers) } - func testEncodingTopLevelStructuredSingleClass() { + @Test func encodingTopLevelStructuredSingleClass() { // Mapping is a class which encodes as a dictionary through a single value container. let mapping = Mapping.testValue _testRoundTrip(of: mapping) } - func testEncodingTopLevelDeepStructuredType() { + @Test func encodingTopLevelDeepStructuredType() { // Company is a type with fields which are Codable themselves. let company = Company.testValue _testRoundTrip(of: company) } - func testEncodingClassWhichSharesEncoderWithSuper() { + @Test func encodingClassWhichSharesEncoderWithSuper() { // Employee is a type which shares its encoder & decoder with its superclass, Person. let employee = Employee.testValue _testRoundTrip(of: employee) } - func testEncodingTopLevelNullableType() { + @Test func encodingTopLevelNullableType() { // EnhancedBool is a type which encodes either as a Bool or as nil. - _testRoundTrip(of: EnhancedBool.true, expectedJSON: "true".data(using: String._Encoding.utf8)!) - _testRoundTrip(of: EnhancedBool.false, expectedJSON: "false".data(using: String._Encoding.utf8)!) - _testRoundTrip(of: EnhancedBool.fileNotFound, expectedJSON: "null".data(using: String._Encoding.utf8)!) + _testRoundTrip(of: EnhancedBool.true, expectedJSON: "true".data(using: .utf8)!) + _testRoundTrip(of: EnhancedBool.false, expectedJSON: "false".data(using: .utf8)!) + _testRoundTrip(of: EnhancedBool.fileNotFound, expectedJSON: "null".data(using: .utf8)!) } - func testEncodingTopLevelArrayOfInt() { + @Test func encodingTopLevelArrayOfInt() throws { let a = [1,2,3] - let result1 = String(data: try! JSONEncoder().encode(a), encoding: String._Encoding.utf8) - XCTAssertEqual(result1, "[1,2,3]") + let result1 = String(data: try JSONEncoder().encode(a), encoding: .utf8) + #expect(result1 == "[1,2,3]") let b : [Int] = [] - let result2 = String(data: try! JSONEncoder().encode(b), encoding: String._Encoding.utf8) - XCTAssertEqual(result2, "[]") + let result2 = String(data: try JSONEncoder().encode(b), encoding: .utf8) + #expect(result2 == "[]") } - func testEncodingTopLevelWithConfiguration() throws { + @Test func encodingTopLevelWithConfiguration() throws { // CodableTypeWithConfiguration is a struct that conforms to CodableWithConfiguration let value = CodableTypeWithConfiguration.testValue let encoder = JSONEncoder() let decoder = JSONDecoder() var decoded = try decoder.decode(CodableTypeWithConfiguration.self, from: try encoder.encode(value, configuration: .init(1)), configuration: .init(1)) - XCTAssertEqual(decoded, value) + #expect(decoded == value) decoded = try decoder.decode(CodableTypeWithConfiguration.self, from: try encoder.encode(value, configuration: CodableTypeWithConfiguration.ConfigProviding.self), configuration: CodableTypeWithConfiguration.ConfigProviding.self) - XCTAssertEqual(decoded, value) + #expect(decoded == value) } -#if false // FIXME: XCTest doesn't support crash tests yet rdar://20195010&22387653 - func testEncodingConflictedTypeNestedContainersWithTheSameTopLevelKey() { + #if FOUNDATION_EXIT_TESTS + @Test func encodingConflictedTypeNestedContainersWithTheSameTopLevelKey() async { struct Model : Encodable, Equatable { let first: String - + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: TopLevelCodingKeys.self) - + var firstNestedContainer = container.nestedContainer(keyedBy: FirstNestedCodingKeys.self, forKey: .top) try firstNestedContainer.encode(self.first, forKey: .first) - + // The following line would fail as it attempts to re-encode into already encoded container is invalid. This will always fail var secondNestedContainer = container.nestedUnkeyedContainer(forKey: .top) try secondNestedContainer.encode("second") } - + init(first: String) { self.first = first } - + static var testValue: Model { return Model(first: "Johnny Appleseed") } - + enum TopLevelCodingKeys : String, CodingKey { case top } @@ -148,20 +155,21 @@ final class JSONEncoderTests : XCTestCase { case first } } - - let model = Model.testValue - // This following test would fail as it attempts to re-encode into already encoded container is invalid. This will always fail - expectCrashLater() - _testEncodeFailure(of: model) + + await #expect(processExitsWith: .failure) { + let model = Model.testValue + // This following test would fail as it attempts to re-encode into already encoded container is invalid. This will always fail + _ = try JSONEncoder().encode(model) + } } -#endif + #endif // MARK: - Date Strategy Tests - func testEncodingDateSecondsSince1970() { + @Test func encodingDateSecondsSince1970() { // Cannot encode an arbitrary number of seconds since we've lost precision since 1970. let seconds = 1000.0 - let expectedJSON = "1000".data(using: String._Encoding.utf8)! + let expectedJSON = "1000".data(using: .utf8)! _testRoundTrip(of: Date(timeIntervalSince1970: seconds), expectedJSON: expectedJSON, @@ -175,10 +183,10 @@ final class JSONEncoderTests : XCTestCase { dateDecodingStrategy: .secondsSince1970) } - func testEncodingDateMillisecondsSince1970() { + @Test func encodingDateMillisecondsSince1970() { // Cannot encode an arbitrary number of seconds since we've lost precision since 1970. let seconds = 1000.0 - let expectedJSON = "1000000".data(using: String._Encoding.utf8)! + let expectedJSON = "1000000".data(using: .utf8)! _testRoundTrip(of: Date(timeIntervalSince1970: seconds), expectedJSON: expectedJSON, @@ -215,7 +223,7 @@ final class JSONEncoderTests : XCTestCase { } } - func test_encodingDateCustom() { + @Test func encodingDateCustom() { let timestamp = Date() // We'll encode a number instead of a date. @@ -225,7 +233,7 @@ final class JSONEncoderTests : XCTestCase { } let decode = { @Sendable (_: Decoder) throws -> Date in return timestamp } - let expectedJSON = "42".data(using: String._Encoding.utf8)! + let expectedJSON = "42".data(using: .utf8)! _testRoundTrip(of: timestamp, expectedJSON: expectedJSON, dateEncodingStrategy: .custom(encode), @@ -238,21 +246,21 @@ final class JSONEncoderTests : XCTestCase { dateDecodingStrategy: .custom(decode)) // So should wrapped dates. - let expectedJSON_array = "[42]".data(using: String._Encoding.utf8)! + let expectedJSON_array = "[42]".data(using: .utf8)! _testRoundTrip(of: TopLevelArrayWrapper(timestamp), expectedJSON: expectedJSON_array, dateEncodingStrategy: .custom(encode), dateDecodingStrategy: .custom(decode)) } - func testEncodingDateCustomEmpty() { + @Test func encodingDateCustomEmpty() { let timestamp = Date() // Encoding nothing should encode an empty keyed container ({}). let encode = { @Sendable (_: Date, _: Encoder) throws -> Void in } let decode = { @Sendable (_: Decoder) throws -> Date in return timestamp } - let expectedJSON = "{}".data(using: String._Encoding.utf8)! + let expectedJSON = "{}".data(using: .utf8)! _testRoundTrip(of: timestamp, expectedJSON: expectedJSON, dateEncodingStrategy: .custom(encode), @@ -266,10 +274,10 @@ final class JSONEncoderTests : XCTestCase { } // MARK: - Data Strategy Tests - func testEncodingData() { + @Test func encodingData() { let data = Data([0xDE, 0xAD, 0xBE, 0xEF]) - let expectedJSON = "[222,173,190,239]".data(using: String._Encoding.utf8)! + let expectedJSON = "[222,173,190,239]".data(using: .utf8)! _testRoundTrip(of: data, expectedJSON: expectedJSON, dataEncodingStrategy: .deferredToData, @@ -282,7 +290,7 @@ final class JSONEncoderTests : XCTestCase { dataDecodingStrategy: .deferredToData) } - func testEncodingDataCustom() { + @Test func encodingDataCustom() { // We'll encode a number instead of data. let encode = { @Sendable (_ data: Data, _ encoder: Encoder) throws -> Void in var container = encoder.singleValueContainer() @@ -290,7 +298,7 @@ final class JSONEncoderTests : XCTestCase { } let decode = { @Sendable (_: Decoder) throws -> Data in return Data() } - let expectedJSON = "42".data(using: String._Encoding.utf8)! + let expectedJSON = "42".data(using: .utf8)! _testRoundTrip(of: Data(), expectedJSON: expectedJSON, dataEncodingStrategy: .custom(encode), @@ -303,12 +311,12 @@ final class JSONEncoderTests : XCTestCase { dataDecodingStrategy: .custom(decode)) } - func testEncodingDataCustomEmpty() { + @Test func encodingDataCustomEmpty() { // Encoding nothing should encode an empty keyed container ({}). let encode = { @Sendable (_: Data, _: Encoder) throws -> Void in } let decode = { @Sendable (_: Decoder) throws -> Data in return Data() } - let expectedJSON = "{}".data(using: String._Encoding.utf8)! + let expectedJSON = "{}".data(using: .utf8)! _testRoundTrip(of: Data(), expectedJSON: expectedJSON, dataEncodingStrategy: .custom(encode), @@ -322,7 +330,7 @@ final class JSONEncoderTests : XCTestCase { } // MARK: - Non-Conforming Floating Point Strategy Tests - func testEncodingNonConformingFloats() { + @Test func encodingNonConformingFloats() { _testEncodeFailure(of: Float.infinity) _testEncodeFailure(of: Float.infinity) _testEncodeFailure(of: -Float.infinity) @@ -342,62 +350,62 @@ final class JSONEncoderTests : XCTestCase { _testEncodeFailure(of: Double.nan) } - func testEncodingNonConformingFloatStrings() { + @Test func encodingNonConformingFloatStrings() { let encodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy = .convertToString(positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN") let decodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN") _testRoundTrip(of: Float.infinity, - expectedJSON: "\"INF\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"INF\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) _testRoundTrip(of: -Float.infinity, - expectedJSON: "\"-INF\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"-INF\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) // Since Float.nan != Float.nan, we have to use a placeholder that'll encode NaN but actually round-trip. _testRoundTrip(of: FloatNaNPlaceholder(), - expectedJSON: "\"NaN\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"NaN\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) _testRoundTrip(of: Double.infinity, - expectedJSON: "\"INF\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"INF\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) _testRoundTrip(of: -Double.infinity, - expectedJSON: "\"-INF\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"-INF\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) // Since Double.nan != Double.nan, we have to use a placeholder that'll encode NaN but actually round-trip. _testRoundTrip(of: DoubleNaNPlaceholder(), - expectedJSON: "\"NaN\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"NaN\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) // Optional Floats and Doubles should encode the same way. _testRoundTrip(of: Optional(Float.infinity), - expectedJSON: "\"INF\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"INF\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) _testRoundTrip(of: Optional(-Float.infinity), - expectedJSON: "\"-INF\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"-INF\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) _testRoundTrip(of: Optional(Double.infinity), - expectedJSON: "\"INF\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"INF\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) _testRoundTrip(of: Optional(-Double.infinity), - expectedJSON: "\"-INF\"".data(using: String._Encoding.utf8)!, + expectedJSON: "\"-INF\"".data(using: .utf8)!, nonConformingFloatEncodingStrategy: encodingStrategy, nonConformingFloatDecodingStrategy: decodingStrategy) } // MARK: - Directly Encoded Array Tests - func testDirectlyEncodedArrays() { + @Test func directlyEncodedArrays() { let encodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy = .convertToString(positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN") let decodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .convertFromString(positiveInfinity: "INF", negativeInfinity: "-INF", nan: "NaN") @@ -430,7 +438,7 @@ final class JSONEncoderTests : XCTestCase { } } - func testEncodingKeyStrategyCustom() { + @Test func encodingKeyStrategyCustom() { let expected = "{\"QQQhello\":\"test\"}" let encoded = EncodeMe(keyName: "hello") @@ -441,9 +449,9 @@ final class JSONEncoderTests : XCTestCase { } encoder.keyEncodingStrategy = .custom(customKeyConversion) let resultData = try! encoder.encode(encoded) - let resultString = String(bytes: resultData, encoding: String._Encoding.utf8) + let resultString = String(bytes: resultData, encoding: .utf8) - XCTAssertEqual(expected, resultString) + #expect(expected == resultString) } private struct EncodeFailure : Encodable { @@ -462,7 +470,7 @@ final class JSONEncoderTests : XCTestCase { let outerValue: EncodeNested } - func testEncodingKeyStrategyPath() { + @Test func encodingKeyStrategyPath() throws { // Make sure a more complex path shows up the way we want // Make sure the path reflects keys in the Swift, not the resulting ones in the JSON let expected = "{\"QQQouterValue\":{\"QQQnestedValue\":{\"QQQhelloWorld\":\"test\"}}}" @@ -480,26 +488,26 @@ final class JSONEncoderTests : XCTestCase { callCount = callCount + 1 if path.count == 0 { - XCTFail("The path should always have at least one entry") + Issue.record("The path should always have at least one entry") } else if path.count == 1 { - XCTAssertEqual(["outerValue"], path.map { $0.stringValue }) + #expect(["outerValue"] == path.map { $0.stringValue }) } else if path.count == 2 { - XCTAssertEqual(["outerValue", "nestedValue"], path.map { $0.stringValue }) + #expect(["outerValue", "nestedValue"] == path.map { $0.stringValue }) } else if path.count == 3 { - XCTAssertEqual(["outerValue", "nestedValue", "helloWorld"], path.map { $0.stringValue }) + #expect(["outerValue", "nestedValue", "helloWorld"] == path.map { $0.stringValue }) } else { - XCTFail("The path mysteriously had more entries") + Issue.record("The path mysteriously had more entries") } let key = _TestKey(stringValue: "QQQ" + path.last!.stringValue)! return key } encoder.keyEncodingStrategy = .custom(customKeyConversion) - let resultData = try! encoder.encode(encoded) - let resultString = String(bytes: resultData, encoding: String._Encoding.utf8) + let resultData = try encoder.encode(encoded) + let resultString = String(bytes: resultData, encoding: .utf8) - XCTAssertEqual(expected, resultString) - XCTAssertEqual(3, callCount) + #expect(expected == resultString) + #expect(3 == callCount) } private struct DecodeMe : Decodable { @@ -516,8 +524,8 @@ final class JSONEncoderTests : XCTestCase { private struct DecodeMe2 : Decodable { var hello: String } - func testDecodingKeyStrategyCustom() { - let input = "{\"----hello\":\"test\"}".data(using: String._Encoding.utf8)! + @Test func decodingKeyStrategyCustom() throws { + let input = "{\"----hello\":\"test\"}".data(using: .utf8)! let decoder = JSONDecoder() let customKeyConversion = { @Sendable (_ path: [CodingKey]) -> CodingKey in // This converter removes the first 4 characters from the start of all string keys, if it has more than 4 characters @@ -527,31 +535,31 @@ final class JSONEncoderTests : XCTestCase { return _TestKey(stringValue: newString)! } decoder.keyDecodingStrategy = .custom(customKeyConversion) - let result = try! decoder.decode(DecodeMe2.self, from: input) + let result = try decoder.decode(DecodeMe2.self, from: input) - XCTAssertEqual("test", result.hello) + #expect("test" == result.hello) } - func testDecodingDictionaryStringKeyConversionUntouched() { - let input = "{\"leave_me_alone\":\"test\"}".data(using: String._Encoding.utf8)! + @Test func decodingDictionaryStringKeyConversionUntouched() throws { + let input = "{\"leave_me_alone\":\"test\"}".data(using: .utf8)! let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - let result = try! decoder.decode([String: String].self, from: input) + let result = try decoder.decode([String: String].self, from: input) - XCTAssertEqual(["leave_me_alone": "test"], result) + #expect(["leave_me_alone": "test"] == result) } - func testDecodingDictionaryFailureKeyPath() { - let input = "{\"leave_me_alone\":\"test\"}".data(using: String._Encoding.utf8)! + @Test func decodingDictionaryFailureKeyPath() { + let input = "{\"leave_me_alone\":\"test\"}".data(using: .utf8)! let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - do { - _ = try decoder.decode([String: Int].self, from: input) - } catch DecodingError.typeMismatch(_, let context) { - XCTAssertEqual(1, context.codingPath.count) - XCTAssertEqual("leave_me_alone", context.codingPath[0].stringValue) - } catch { - XCTFail("Unexpected error: \(String(describing: error))") + #expect { + try decoder.decode([String: Int].self, from: input) + } throws: { + guard case DecodingError.typeMismatch(_, let context) = $0 else { + return false + } + return (1 == context.codingPath.count) && ("leave_me_alone" == context.codingPath[0].stringValue) } } @@ -567,7 +575,7 @@ final class JSONEncoderTests : XCTestCase { var thisIsCamelCase : String } - func testKeyStrategyDuplicateKeys() { + @Test func keyStrategyDuplicateKeys() throws { // This test is mostly to make sure we don't assert on duplicate keys struct DecodeMe5 : Codable { var oneTwo : String @@ -603,48 +611,44 @@ final class JSONEncoderTests : XCTestCase { // Decoding // This input has a dictionary with two keys, but only one will end up in the container - let input = "{\"unused key 1\":\"test1\",\"unused key 2\":\"test2\"}".data(using: String._Encoding.utf8)! + let input = "{\"unused key 1\":\"test1\",\"unused key 2\":\"test2\"}".data(using: .utf8)! let decoder = JSONDecoder() decoder.keyDecodingStrategy = .custom(customKeyConversion) - let decodingResult = try! decoder.decode(DecodeMe5.self, from: input) + let decodingResult = try decoder.decode(DecodeMe5.self, from: input) // There will be only one result for oneTwo. - XCTAssertEqual(1, decodingResult.numberOfKeys) + #expect(1 == decodingResult.numberOfKeys) // While the order in which these values should be taken is NOT defined by the JSON spec in any way, the historical behavior has been to select the *first* value for a given key. - XCTAssertEqual(decodingResult.oneTwo, "test1") + #expect(decodingResult.oneTwo == "test1") // Encoding let encoded = DecodeMe5() let encoder = JSONEncoder() encoder.keyEncodingStrategy = .custom(customKeyConversion) - let decodingResultData = try! encoder.encode(encoded) - let decodingResultString = String(bytes: decodingResultData, encoding: String._Encoding.utf8) + let decodingResultData = try encoder.encode(encoded) + let decodingResultString = String(bytes: decodingResultData, encoding: .utf8) // There will be only one value in the result (the second one encoded) - XCTAssertEqual("{\"oneTwo\":\"test2\"}", decodingResultString) + #expect("{\"oneTwo\":\"test2\"}" == decodingResultString) } // MARK: - Encoder Features - func testNestedContainerCodingPaths() { + @Test func nestedContainerCodingPaths() { let encoder = JSONEncoder() - do { - let _ = try encoder.encode(NestedContainersTestType()) - } catch let error as NSError { - XCTFail("Caught error during encoding nested container types: \(error)") + #expect(throws: Never.self) { + try encoder.encode(NestedContainersTestType()) } } - func testSuperEncoderCodingPaths() { + @Test func superEncoderCodingPaths() { let encoder = JSONEncoder() - do { - let _ = try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) - } catch let error as NSError { - XCTFail("Caught error during encoding nested container types: \(error)") + #expect(throws: Never.self) { + try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) } } // MARK: - Type coercion - func testTypeCoercion() { + @Test func typeCoercion() { _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int8].self) _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int16].self) @@ -675,25 +679,19 @@ final class JSONEncoderTests : XCTestCase { _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self) } - func testDecodingConcreteTypeParameter() { + @Test func decodingConcreteTypeParameter() throws { let encoder = JSONEncoder() - guard let json = try? encoder.encode(Employee.testValue) else { - XCTFail("Unable to encode Employee.") - return - } + let json = try encoder.encode(Employee.testValue) let decoder = JSONDecoder() - guard let decoded = try? decoder.decode(Employee.self as Person.Type, from: json) else { - XCTFail("Failed to decode Employee as Person from JSON.") - return - } + let decoded = try decoder.decode(Employee.self as Person.Type, from: json) - expectEqual(type(of: decoded), Employee.self, "Expected decoded value to be of type Employee; got \(type(of: decoded)) instead.") + #expect(type(of: decoded) == Employee.self, "Expected decoded value to be of type Employee; got \(type(of: decoded)) instead.") } // MARK: - Encoder State // SR-6078 - func testEncoderStateThrowOnEncode() { + @Test func encoderStateThrowOnEncode() { struct ReferencingEncoderWrapper : Encodable { let value: T init(_ value: T) { self.value = value } @@ -722,7 +720,7 @@ final class JSONEncoderTests : XCTestCase { _ = try? JSONEncoder().encode(ReferencingEncoderWrapper([Float.infinity])) } - func testEncoderStateThrowOnEncodeCustomDate() { + @Test func encoderStateThrowOnEncodeCustomDate() { // This test is identical to testEncoderStateThrowOnEncode, except throwing via a custom Date closure. struct ReferencingEncoderWrapper : Encodable { let value: T @@ -746,7 +744,7 @@ final class JSONEncoderTests : XCTestCase { _ = try? encoder.encode(ReferencingEncoderWrapper(Date())) } - func testEncoderStateThrowOnEncodeCustomData() { + @Test func encoderStateThrowOnEncodeCustomData() { // This test is identical to testEncoderStateThrowOnEncode, except throwing via a custom Data closure. struct ReferencingEncoderWrapper : Encodable { let value: T @@ -770,7 +768,7 @@ final class JSONEncoderTests : XCTestCase { _ = try? encoder.encode(ReferencingEncoderWrapper(Data())) } - func test_106506794() throws { + @Test func issue106506794() throws { struct Level1: Codable, Equatable { let level2: Level2 @@ -802,25 +800,21 @@ final class JSONEncoderTests : XCTestCase { let value = Level1.init(level2: .init(name: "level2")) let data = try JSONEncoder().encode(value) - do { - let decodedValue = try JSONDecoder().decode(Level1.self, from: data) - XCTAssertEqual(value, decodedValue) - } catch { - XCTFail("Decode should not have failed with error: \(error))") - } + let decodedValue = try JSONDecoder().decode(Level1.self, from: data) + #expect(value == decodedValue) } // MARK: - Decoder State // SR-6048 - func testDecoderStateThrowOnDecode() { + @Test func decoderStateThrowOnDecode() throws { // The container stack here starts as [[1,2,3]]. Attempting to decode as [String] matches the outer layer (Array), and begins decoding the array. // Once Array decoding begins, 1 is pushed onto the container stack ([[1,2,3], 1]), and 1 is attempted to be decoded as String. This throws a .typeMismatch, but the container is not popped off the stack. // When attempting to decode [Int], the container stack is still ([[1,2,3], 1]), and 1 fails to decode as [Int]. - let json = "[1,2,3]".data(using: String._Encoding.utf8)! - let _ = try! JSONDecoder().decode(EitherDecodable<[String], [Int]>.self, from: json) + let json = "[1,2,3]".data(using: .utf8)! + let _ = try JSONDecoder().decode(EitherDecodable<[String], [Int]>.self, from: json) } - func testDecoderStateThrowOnDecodeCustomDate() { + @Test func decoderStateThrowOnDecodeCustomDate() throws { // This test is identical to testDecoderStateThrowOnDecode, except we're going to fail because our closure throws an error, not because we hit a type mismatch. let decoder = JSONDecoder() decoder.dateDecodingStrategy = .custom({ decoder in @@ -828,11 +822,11 @@ final class JSONEncoderTests : XCTestCase { throw CustomError.foo }) - let json = "1".data(using: String._Encoding.utf8)! - let _ = try! decoder.decode(EitherDecodable.self, from: json) + let json = "1".data(using: .utf8)! + let _ = try decoder.decode(EitherDecodable.self, from: json) } - func testDecoderStateThrowOnDecodeCustomData() { + @Test func decoderStateThrowOnDecodeCustomData() throws { // This test is identical to testDecoderStateThrowOnDecode, except we're going to fail because our closure throws an error, not because we hit a type mismatch. let decoder = JSONDecoder() decoder.dataDecodingStrategy = .custom({ decoder in @@ -840,20 +834,20 @@ final class JSONEncoderTests : XCTestCase { throw CustomError.foo }) - let json = "1".data(using: String._Encoding.utf8)! - let _ = try! decoder.decode(EitherDecodable.self, from: json) + let json = "1".data(using: .utf8)! + let _ = try decoder.decode(EitherDecodable.self, from: json) } - func testDecodingFailure() { + @Test func decodingFailure() { struct DecodeFailure : Decodable { var invalid: String } let toDecode = "{\"invalid\": json}"; - _testDecodeFailure(of: DecodeFailure.self, data: toDecode.data(using: String._Encoding.utf8)!) + _testDecodeFailure(of: DecodeFailure.self, data: toDecode.data(using: .utf8)!) } - func testDecodingFailureThrowInInitKeyedContainer() { + @Test func decodingFailureThrowInInitKeyedContainer() { struct DecodeFailure : Decodable { private enum CodingKeys: String, CodingKey { case checkedString @@ -875,10 +869,10 @@ final class JSONEncoderTests : XCTestCase { } let toDecode = "{ \"checkedString\" : \"baz\" }" - _testDecodeFailure(of: DecodeFailure.self, data: toDecode.data(using: String._Encoding.utf8)!) + _testDecodeFailure(of: DecodeFailure.self, data: toDecode.data(using: .utf8)!) } - func testDecodingFailureThrowInInitSingleContainer() { + @Test func decodingFailureThrowInInitSingleContainer() { struct DecodeFailure : Decodable { private enum Error: Swift.Error { case expectedError @@ -896,18 +890,18 @@ final class JSONEncoderTests : XCTestCase { } let toDecode = "{ \"checkedString\" : \"baz\" }" - _testDecodeFailure(of: DecodeFailure.self, data: toDecode.data(using: String._Encoding.utf8)!) + _testDecodeFailure(of: DecodeFailure.self, data: toDecode.data(using: .utf8)!) } - func testInvalidFragment() { + @Test func invalidFragment() { struct DecodeFailure: Decodable { var foo: String } let toDecode = "\"foo" - _testDecodeFailure(of: DecodeFailure.self, data: toDecode.data(using: String._Encoding.utf8)!) + _testDecodeFailure(of: DecodeFailure.self, data: toDecode.data(using: .utf8)!) } - func testRepeatedFailedNilChecks() { + @Test func repeatedFailedNilChecks() { struct RepeatNilCheckDecodable : Decodable { enum Failure : Error { case badNil @@ -951,11 +945,13 @@ final class JSONEncoderTests : XCTestCase { } } } - let json = "[1, 2, 3]".data(using: String._Encoding.utf8)! - XCTAssertNoThrow(try JSONDecoder().decode(RepeatNilCheckDecodable.self, from: json)) + let json = "[1, 2, 3]".data(using: .utf8)! + #expect(throws: Never.self) { + try JSONDecoder().decode(RepeatNilCheckDecodable.self, from: json) + } } - func testDelayedDecoding() throws { + @Test func delayedDecoding() throws { // One variation is deferring the use of a container. struct DelayedDecodable_ContainerVersion : Codable { @@ -989,7 +985,9 @@ final class JSONEncoderTests : XCTestCase { let data = try JSONEncoder().encode(before) let decoded = try JSONDecoder().decode(DelayedDecodable_ContainerVersion.self, from: data) - XCTAssertNoThrow(try decoded.i) + #expect(throws: Never.self) { + try decoded.i + } // The other variant is deferring the use of the *top-level* decoder. This does NOT work for non-top level decoders. struct DelayedDecodable_DecoderVersion : Codable { @@ -1020,29 +1018,25 @@ final class JSONEncoderTests : XCTestCase { } // Reuse the same data. let decoded2 = try JSONDecoder().decode(DelayedDecodable_DecoderVersion.self, from: data) - XCTAssertNoThrow(try decoded2.i) + #expect(throws: Never.self) { + try decoded2.i + } } // MARK: - Helper Functions private var _jsonEmptyDictionary: Data { - return "{}".data(using: String._Encoding.utf8)! + return "{}".data(using: .utf8)! } - private func _testEncodeFailure(of value: T) { - do { - let _ = try JSONEncoder().encode(value) - XCTFail("Encode of top-level \(T.self) was expected to fail.") - } catch { - XCTAssertNotNil(error); + private func _testEncodeFailure(of value: T, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(throws: (any Error).self, "Encode of top-level \(T.self) was expected to fail.", sourceLocation: sourceLocation) { + try JSONEncoder().encode(value) } } - private func _testDecodeFailure(of value: T.Type, data: Data) { - do { - let _ = try JSONDecoder().decode(value, from: data) - XCTFail("Decode of top-level \(value) was expected to fail.") - } catch { - XCTAssertNotNil(error); + private func _testDecodeFailure(of value: T.Type, data: Data, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(throws: (any Error).self, "Decode of top-level \(value) was expected to fail.", sourceLocation: sourceLocation) { + try JSONDecoder().decode(value, from: data) } } @@ -1056,8 +1050,9 @@ final class JSONEncoderTests : XCTestCase { keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy = .useDefaultKeys, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy = .throw, - nonConformingFloatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .throw) where T : Codable, T : Equatable { - var payload: Data! = nil + nonConformingFloatDecodingStrategy: JSONDecoder.NonConformingFloatDecodingStrategy = .throw, + sourceLocation: SourceLocation = #_sourceLocation) where T : Codable, T : Equatable { + var payload: Data do { let encoder = JSONEncoder() encoder.outputFormatting = outputFormatting @@ -1067,13 +1062,14 @@ final class JSONEncoderTests : XCTestCase { encoder.keyEncodingStrategy = keyEncodingStrategy payload = try encoder.encode(value) } catch { - XCTFail("Failed to encode \(T.self) to JSON: \(error)") + Issue.record("Failed to encode \(T.self) to JSON: \(error)", sourceLocation: sourceLocation) + return } if let expectedJSON = json { let expected = String(data: expectedJSON, encoding: .utf8)! let actual = String(data: payload, encoding: .utf8)! - XCTAssertEqual(expected, actual, "Produced JSON not identical to expected JSON.") + #expect(expected == actual, "Produced JSON not identical to expected JSON.", sourceLocation: sourceLocation) } do { @@ -1083,27 +1079,21 @@ final class JSONEncoderTests : XCTestCase { decoder.nonConformingFloatDecodingStrategy = nonConformingFloatDecodingStrategy decoder.keyDecodingStrategy = keyDecodingStrategy let decoded = try decoder.decode(T.self, from: payload) - XCTAssertEqual(decoded, value, "\(T.self) did not round-trip to an equal value.") + #expect(decoded == value, "\(T.self) did not round-trip to an equal value.", sourceLocation: sourceLocation) } catch { - XCTFail("Failed to decode \(T.self) from JSON: \(error)") + Issue.record("Failed to decode \(T.self) from JSON: \(error)", sourceLocation: sourceLocation) } } - private func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type) where T : Codable, U : Codable { - do { + private func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type, sourceLocation: SourceLocation = #_sourceLocation) where T : Codable, U : Codable { + #expect(throws: (any Error).self, "Coercion from \(T.self) to \(U.self) was expected to fail.", sourceLocation: sourceLocation) { let data = try JSONEncoder().encode(value) let _ = try JSONDecoder().decode(U.self, from: data) - XCTFail("Coercion from \(T.self) to \(U.self) was expected to fail.") - } catch {} + } } - private func _test(JSONString: String, to object: T) { -#if FOUNDATION_FRAMEWORK - let encs : [String._Encoding] = [.utf8, .utf16BigEndian, .utf16LittleEndian, .utf32BigEndian, .utf32LittleEndian] -#else - // TODO: Reenable other encoding once string.data(using:) is fully implemented. - let encs : [String._Encoding] = [.utf8, .utf16BigEndian, .utf16LittleEndian] -#endif + private func _test(JSONString: String, to object: T, sourceLocation: SourceLocation = #_sourceLocation) { + let encs : [String.Encoding] = [.utf8, .utf16BigEndian, .utf16LittleEndian, .utf32BigEndian, .utf32LittleEndian] let decoder = JSONDecoder() for enc in encs { let data = JSONString.data(using: enc)! @@ -1111,26 +1101,26 @@ final class JSONEncoderTests : XCTestCase { do { parsed = try decoder.decode(T.self, from: data) } catch { - XCTFail("Failed to decode \(JSONString) with encoding \(enc): Error: \(error)") + Issue.record("Failed to decode \(JSONString) with encoding \(enc): Error: \(error)", sourceLocation: sourceLocation) continue } - XCTAssertEqual(object, parsed) + #expect(object == parsed, sourceLocation: sourceLocation) } } - func test_JSONEscapedSlashes() { + @Test func jsonEscapedSlashes() { _test(JSONString: "\"\\/test\\/path\"", to: "/test/path") _test(JSONString: "\"\\\\/test\\\\/path\"", to: "\\/test\\/path") } - func test_JSONEscapedForwardSlashes() { + @Test func jsonEscapedForwardSlashes() { _testRoundTrip(of: ["/":1], expectedJSON: """ {"\\/":1} -""".data(using: String._Encoding.utf8)!) +""".data(using: .utf8)!) } - func test_JSONUnicodeCharacters() { + @Test func jsonUnicodeCharacters() { // UTF8: // E9 96 86 E5 B4 AC EB B0 BA EB 80 AB E9 A2 92 // 閆崬밺뀫颒 @@ -1138,7 +1128,7 @@ final class JSONEncoderTests : XCTestCase { _test(JSONString: "[\"本日\"]", to: ["本日"]) } - func test_JSONUnicodeEscapes() throws { + @Test func jsonUnicodeEscapes() throws { let testCases = [ // e-acute and greater-than-or-equal-to "\"\\u00e9\\u2265\"" : "é≥", @@ -1157,7 +1147,7 @@ final class JSONEncoderTests : XCTestCase { } } - func test_encodingJSONHexUnicodeEscapes() throws { + @Test func encodingJSONHexUnicodeEscapes() throws { let testCases = [ "\u{0001}\u{0002}\u{0003}": "\"\\u0001\\u0002\\u0003\"", "\u{0010}\u{0018}\u{001f}": "\"\\u0010\\u0018\\u001f\"", @@ -1167,58 +1157,58 @@ final class JSONEncoderTests : XCTestCase { } } - func test_JSONBadUnicodeEscapes() { - let badCases = ["\\uD834", "\\uD834hello", "hello\\uD834", "\\uD834\\u1221", "\\uD8", "\\uD834x\\uDD1E"] - for str in badCases { - let data = str.data(using: String._Encoding.utf8)! - XCTAssertThrowsError(try JSONDecoder().decode(String.self, from: data)) + @Test(arguments: [ + "\\uD834", "\\uD834hello", "hello\\uD834", "\\uD834\\u1221", "\\uD8", "\\uD834x\\uDD1E" + ]) + func jsonBadUnicodeEscapes(str: String) { + let data = str.data(using: .utf8)! + #expect(throws: (any Error).self) { + try JSONDecoder().decode(String.self, from: data) } } - func test_nullByte() throws { + @Test func nullByte() throws { let string = "abc\u{0000}def" let encoder = JSONEncoder() let decoder = JSONDecoder() let data = try encoder.encode([string]) let decoded = try decoder.decode([String].self, from: data) - XCTAssertEqual([string], decoded) + #expect([string] == decoded) let data2 = try encoder.encode([string:string]) let decoded2 = try decoder.decode([String:String].self, from: data2) - XCTAssertEqual([string:string], decoded2) + #expect([string:string] == decoded2) struct Container: Codable { let s: String } let data3 = try encoder.encode(Container(s: string)) let decoded3 = try decoder.decode(Container.self, from: data3) - XCTAssertEqual(decoded3.s, string) + #expect(decoded3.s == string) } - func test_superfluouslyEscapedCharacters() { + @Test func superfluouslyEscapedCharacters() { let json = "[\"\\h\\e\\l\\l\\o\"]" - XCTAssertThrowsError(try JSONDecoder().decode([String].self, from: json.data(using: String._Encoding.utf8)!)) + #expect(throws: (any Error).self) { + try JSONDecoder().decode([String].self, from: json.data(using: .utf8)!) + } } - func test_equivalentUTF8Sequences() { + @Test func equivalentUTF8Sequences() throws { let json = """ { "caf\\u00e9" : true, "cafe\\u0301" : false } -""".data(using: String._Encoding.utf8)! +""".data(using: .utf8)! - do { - let dict = try JSONDecoder().decode([String:Bool].self, from: json) - XCTAssertEqual(dict.count, 1) - } catch { - XCTFail("Unexpected error: \(error)") - } + let dict = try JSONDecoder().decode([String:Bool].self, from: json) + #expect(dict.count == 1) } - func test_JSONControlCharacters() { + @Test func jsonControlCharacters() { let array = [ "\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004", "\\u0005", "\\u0006", "\\u0007", "\\b", "\\t", @@ -1235,7 +1225,7 @@ final class JSONEncoderTests : XCTestCase { } } - func test_JSONNumberFragments() { + @Test func jsonNumberFragments() { let array = ["0 ", "1.0 ", "0.1 ", "1e3 ", "-2.01e-3 ", "0", "1.0", "1e3", "-2.01e-3", "0e-10"] let expected = [0, 1.0, 0.1, 1000, -0.00201, 0, 1.0, 1000, -0.00201, 0] for (json, expected) in zip(array, expected) { @@ -1243,55 +1233,59 @@ final class JSONEncoderTests : XCTestCase { } } - func test_invalidJSONNumbersFailAsExpected() { + @Test func invalidJSONNumbersFailAsExpected() { let array = ["0.", "1e ", "-2.01e- ", "+", "2.01e-1234", "+2.0q", "2s", "NaN", "nan", "Infinity", "inf", "-", "0x42", "1.e2"] for json in array { - let data = json.data(using: String._Encoding.utf8)! - XCTAssertThrowsError(try JSONDecoder().decode(Float.self, from: data), "Expected error for input \"\(json)\"") + let data = json.data(using: .utf8)! + #expect(throws: (any Error).self, "Expected error for input \"\(json)\"") { + _ = try JSONDecoder().decode(Float.self, from: data) + } } } - func _checkExpectedThrownDataCorruptionUnderlyingError(contains substring: String, closure: () throws -> Void) { + func _checkExpectedThrownDataCorruptionUnderlyingError(contains substring: String, sourceLocation: SourceLocation = #_sourceLocation, closure: () throws -> Void) { do { try closure() - XCTFail("Expected failure containing string: \"\(substring)\"") + Issue.record("Expected failure containing string: \"\(substring)\"", sourceLocation: sourceLocation) } catch let error as DecodingError { guard case let .dataCorrupted(context) = error else { - XCTFail("Unexpected DecodingError type: \(error)") + Issue.record("Unexpected DecodingError type: \(error)", sourceLocation: sourceLocation) return } #if FOUNDATION_FRAMEWORK let nsError = context.underlyingError! as NSError - XCTAssertTrue(nsError.debugDescription.contains(substring), "Description \"\(nsError.debugDescription)\" doesn't contain substring \"\(substring)\"") + #expect(nsError.debugDescription.contains(substring), "Description \"\(nsError.debugDescription)\" doesn't contain substring \"\(substring)\"", sourceLocation: sourceLocation) #endif } catch { - XCTFail("Unexpected error type: \(error)") + Issue.record("Unexpected error type: \(error)", sourceLocation: sourceLocation) } } - func test_topLevelFragmentsWithGarbage() { + @Test func topLevelFragmentsWithGarbage() { _checkExpectedThrownDataCorruptionUnderlyingError(contains: "Unexpected character") { - let _ = try JSONDecoder().decode(Bool.self, from: "tru_".data(using: String._Encoding.utf8)!) - let _ = try json5Decoder.decode(Bool.self, from: "tru_".data(using: String._Encoding.utf8)!) + let _ = try JSONDecoder().decode(Bool.self, from: "tru_".data(using: .utf8)!) + let _ = try json5Decoder.decode(Bool.self, from: "tru_".data(using: .utf8)!) } _checkExpectedThrownDataCorruptionUnderlyingError(contains: "Unexpected character") { - let _ = try JSONDecoder().decode(Bool.self, from: "fals_".data(using: String._Encoding.utf8)!) - let _ = try json5Decoder.decode(Bool.self, from: "fals_".data(using: String._Encoding.utf8)!) + let _ = try JSONDecoder().decode(Bool.self, from: "fals_".data(using: .utf8)!) + let _ = try json5Decoder.decode(Bool.self, from: "fals_".data(using: .utf8)!) } _checkExpectedThrownDataCorruptionUnderlyingError(contains: "Unexpected character") { - let _ = try JSONDecoder().decode(Bool?.self, from: "nul_".data(using: String._Encoding.utf8)!) - let _ = try json5Decoder.decode(Bool?.self, from: "nul_".data(using: String._Encoding.utf8)!) + let _ = try JSONDecoder().decode(Bool?.self, from: "nul_".data(using: .utf8)!) + let _ = try json5Decoder.decode(Bool?.self, from: "nul_".data(using: .utf8)!) } } - func test_topLevelNumberFragmentsWithJunkDigitCharacters() { - let fullData = "3.141596".data(using: String._Encoding.utf8)! + @Test func topLevelNumberFragmentsWithJunkDigitCharacters() throws { + let fullData = "3.141596".data(using: .utf8)! let partialData = fullData[0..<4] - XCTAssertEqual(3.14, try JSONDecoder().decode(Double.self, from: partialData)) + #expect(try 3.14 == JSONDecoder().decode(Double.self, from: partialData)) } - func test_depthTraversal() { + @Test + @MainActor // Deeply recursive tests which requires running on the main thread which has a higher stack size limit + func depthTraversal() { struct SuperNestedArray : Decodable { init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() @@ -1305,44 +1299,50 @@ final class JSONEncoderTests : XCTestCase { let jsonGood = String(repeating: "[", count: MAX_DEPTH / 2) + String(repeating: "]", count: MAX_DEPTH / 2) let jsonBad = String(repeating: "[", count: MAX_DEPTH + 1) + String(repeating: "]", count: MAX_DEPTH + 1) - XCTAssertNoThrow(try JSONDecoder().decode(SuperNestedArray.self, from: jsonGood.data(using: String._Encoding.utf8)!)) - XCTAssertThrowsError(try JSONDecoder().decode(SuperNestedArray.self, from: jsonBad.data(using: String._Encoding.utf8)!)) + #expect(throws: Never.self) { + try JSONDecoder().decode(SuperNestedArray.self, from: jsonGood.data(using: .utf8)!) + } + #expect(throws: (any Error).self) { + try JSONDecoder().decode(SuperNestedArray.self, from: jsonBad.data(using: .utf8)!) + } } - func test_JSONPermitsTrailingCommas() { + @Test func jsonPermitsTrailingCommas() throws { // Trailing commas aren't valid JSON and should never be emitted, but are syntactically unambiguous and are allowed by // most parsers for ease of use. let json = "{\"key\" : [ true, ],}" - let data = json.data(using: String._Encoding.utf8)! + let data = json.data(using: .utf8)! - let result = try! JSONDecoder().decode([String:[Bool]].self, from: data) + let result = try JSONDecoder().decode([String:[Bool]].self, from: data) let expected = ["key" : [true]] - XCTAssertEqual(result, expected) + #expect(result == expected) } - func test_whitespaceOnlyData() { - let data = " ".data(using: String._Encoding.utf8)! - XCTAssertThrowsError(try JSONDecoder().decode(Int.self, from: data)) + @Test func whitespaceOnlyData() { + let data = " ".data(using: .utf8)! + #expect(throws: (any Error).self) { + try JSONDecoder().decode(Int.self, from: data) + } } - func test_smallFloatNumber() { + @Test func smallFloatNumber() { _testRoundTrip(of: [["magic_number" : 7.45673334164903e-115]]) } - func test_largeIntegerNumber() { + @Test func largeIntegerNumber() throws { let num : UInt64 = 6032314514195021674 let json = "{\"a\":\(num)}" - let data = json.data(using: String._Encoding.utf8)! + let data = json.data(using: .utf8)! - let result = try! JSONDecoder().decode([String:UInt64].self, from: data) - let number = result["a"]! - XCTAssertEqual(number, num) + let result = try JSONDecoder().decode([String:UInt64].self, from: data) + let number = try #require(result["a"]) + #expect(number == num) } - func test_largeIntegerNumberIsNotRoundedToNearestDoubleWhenDecodingAsAnInteger() { - XCTAssertEqual(Double(sign: .plus, exponent: 63, significand: 1).ulp, 2048) - XCTAssertEqual(Double(sign: .plus, exponent: 64, significand: 1).ulp, 4096) + @Test func largeIntegerNumberIsNotRoundedToNearestDoubleWhenDecodingAsAnInteger() { + #expect(Double(sign: .plus, exponent: 63, significand: 1).ulp == 2048) + #expect(Double(sign: .plus, exponent: 64, significand: 1).ulp == 4096) let int64s: [(String, Int64?)] = [ ("-9223372036854776833", nil), // -2^63 - 1025 (Double: -2^63 - 2048) @@ -1373,18 +1373,18 @@ final class JSONEncoderTests : XCTestCase { decoder.allowsJSON5 = json5 for (json, value) in int64s { - let result = try? decoder.decode(Int64.self, from: json.data(using: String._Encoding.utf8)!) - XCTAssertEqual(result, value, "Unexpected \(decoder) result for input \"\(json)\"") + let result = try? decoder.decode(Int64.self, from: json.data(using: .utf8)!) + #expect(result == value, "Unexpected \(decoder) result for input \"\(json)\"") } for (json, value) in uint64s { - let result = try? decoder.decode(UInt64.self, from: json.data(using: String._Encoding.utf8)!) - XCTAssertEqual(result, value, "Unexpected \(decoder) result for input \"\(json)\"") + let result = try? decoder.decode(UInt64.self, from: json.data(using: .utf8)!) + #expect(result == value, "Unexpected \(decoder) result for input \"\(json)\"") } } } - func test_roundTrippingExtremeValues() { + @Test func roundTrippingExtremeValues() { struct Numbers : Codable, Equatable { let floats : [Float] let doubles : [Double] @@ -1393,32 +1393,30 @@ final class JSONEncoderTests : XCTestCase { _testRoundTrip(of: testValue) } - func test_roundTrippingInt128() { - let values = [ - Int128.min, - Int128.min + 1, - -0x1_0000_0000_0000_0000, - 0x0_8000_0000_0000_0000, - -1, - 0, - 0x7fff_ffff_ffff_ffff, - 0x8000_0000_0000_0000, - 0xffff_ffff_ffff_ffff, - 0x1_0000_0000_0000_0000, - .max - ] - for i128 in values { - _testRoundTrip(of: i128) - } + @Test(arguments: [ + Int128.min, + Int128.min + 1, + -0x1_0000_0000_0000_0000, + 0x0_8000_0000_0000_0000, + -1, + 0, + 0x7fff_ffff_ffff_ffff, + 0x8000_0000_0000_0000, + 0xffff_ffff_ffff_ffff, + 0x1_0000_0000_0000_0000, + .max + ]) + func roundTrippingInt128(i128: Int128) { + _testRoundTrip(of: i128) } - func test_Int128SlowPath() { + @Test func int128SlowPath() throws { let decoder = JSONDecoder() let work: [Int128] = [18446744073709551615, -18446744073709551615] for value in work { // force the slow-path by appending ".0" - let json = "\(value).0".data(using: String._Encoding.utf8)! - XCTAssertEqual(value, try? decoder.decode(Int128.self, from: json)) + let json = "\(value).0".data(using: .utf8)! + #expect(try value == decoder.decode(Int128.self, from: json)) } // These should work, but making them do so probably requires // rewriting the slow path to use a dedicated parser. For now, @@ -1429,35 +1427,35 @@ final class JSONEncoderTests : XCTestCase { ] for value in shouldWorkButDontYet { // force the slow-path by appending ".0" - let json = "\(value).0".data(using: String._Encoding.utf8)! - XCTAssertThrowsError(try decoder.decode(Int128.self, from: json)) + let json = "\(value).0".data(using: .utf8)! + #expect(throws: (any Error).self) { + try decoder.decode(Int128.self, from: json) + } } } - func test_roundTrippingUInt128() { - let values = [ - UInt128.zero, - 1, - 0x0000_0000_0000_0000_7fff_ffff_ffff_ffff, - 0x0000_0000_0000_0000_8000_0000_0000_0000, - 0x0000_0000_0000_0000_ffff_ffff_ffff_ffff, - 0x0000_0000_0000_0001_0000_0000_0000_0000, - 0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_ffff, - 0x8000_0000_0000_0000_0000_0000_0000_0000, - .max - ] - for u128 in values { - _testRoundTrip(of: u128) - } + @Test(arguments: [ + UInt128.zero, + 1, + 0x0000_0000_0000_0000_7fff_ffff_ffff_ffff, + 0x0000_0000_0000_0000_8000_0000_0000_0000, + 0x0000_0000_0000_0000_ffff_ffff_ffff_ffff, + 0x0000_0000_0000_0001_0000_0000_0000_0000, + 0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_ffff, + 0x8000_0000_0000_0000_0000_0000_0000_0000, + .max + ]) + func roundTrippingUInt128(u128: UInt128) { + _testRoundTrip(of: u128) } - func test_UInt128SlowPath() { + @Test func uint128SlowPath() throws { let decoder = JSONDecoder() let work: [UInt128] = [18446744073709551615] for value in work { // force the slow-path by appending ".0" - let json = "\(value).0".data(using: String._Encoding.utf8)! - XCTAssertEqual(value, try? decoder.decode(UInt128.self, from: json)) + let json = "\(value).0".data(using: .utf8)! + #expect(try value == decoder.decode(UInt128.self, from: json)) } // These should work, but making them do so probably requires // rewriting the slow path to use a dedicated parser. For now, @@ -1468,12 +1466,14 @@ final class JSONEncoderTests : XCTestCase { ] for value in shouldWorkButDontYet { // force the slow-path by appending ".0" - let json = "\(value).0".data(using: String._Encoding.utf8)! - XCTAssertThrowsError(try decoder.decode(UInt128.self, from: json)) + let json = "\(value).0".data(using: .utf8)! + #expect(throws: (any Error).self) { + try decoder.decode(UInt128.self, from: json) + } } } - func test_roundTrippingDoubleValues() { + @Test func roundTrippingDoubleValues() { struct Numbers : Codable, Equatable { let doubles : [String:Double] let decimals : [String:Decimal] @@ -1508,12 +1508,14 @@ final class JSONEncoderTests : XCTestCase { _testRoundTrip(of: testValue) } - func test_decodeLargeDoubleAsInteger() { - let data = try! JSONEncoder().encode(Double.greatestFiniteMagnitude) - XCTAssertThrowsError(try JSONDecoder().decode(UInt64.self, from: data)) + @Test func decodeLargeDoubleAsInteger() throws { + let data = try JSONEncoder().encode(Double.greatestFiniteMagnitude) + #expect(throws: (any Error).self) { + try JSONDecoder().decode(UInt64.self, from: data) + } } - func test_localeDecimalPolicyIndependence() { + @Test func localeDecimalPolicyIndependence() throws { var currentLocale: UnsafeMutablePointer? = nil if let localePtr = setlocale(LC_ALL, nil) { currentLocale = strdup(localePtr) @@ -1528,114 +1530,110 @@ final class JSONEncoderTests : XCTestCase { let orig = ["decimalValue" : 1.1] - do { - setlocale(LC_ALL, "fr_FR") - let data = try JSONEncoder().encode(orig) + setlocale(LC_ALL, "fr_FR") + let data = try JSONEncoder().encode(orig) #if os(Windows) - setlocale(LC_ALL, "en_US") + setlocale(LC_ALL, "en_US") #else - setlocale(LC_ALL, "en_US_POSIX") + setlocale(LC_ALL, "en_US_POSIX") #endif - let decoded = try JSONDecoder().decode(type(of: orig).self, from: data) + let decoded = try JSONDecoder().decode(type(of: orig).self, from: data) - XCTAssertEqual(orig, decoded) - } catch { - XCTFail("Error: \(error)") - } + #expect(orig == decoded) } - func test_whitespace() { + @Test func whitespace() { let tests : [(json: String, expected: [String:Bool])] = [ ("{\"v\"\n : true}", ["v":true]), ("{\"v\"\r\n : true}", ["v":true]), ("{\"v\"\r : true}", ["v":true]) ] for test in tests { - let data = test.json.data(using: String._Encoding.utf8)! + let data = test.json.data(using: .utf8)! let decoded = try! JSONDecoder().decode([String:Bool].self, from: data) - XCTAssertEqual(test.expected, decoded) + #expect(test.expected == decoded) } } - func test_assumesTopLevelDictionary() { + @Test func assumesTopLevelDictionary() throws { let decoder = JSONDecoder() decoder.assumesTopLevelDictionary = true let json = "\"x\" : 42" - do { - let result = try decoder.decode([String:Int].self, from: json.data(using: String._Encoding.utf8)!) - XCTAssertEqual(result, ["x" : 42]) - } catch { - XCTFail("Error thrown while decoding assumed top-level dictionary: \(error)") - } + var result = try decoder.decode([String:Int].self, from: json.data(using: .utf8)!) + #expect(result == ["x" : 42]) let jsonWithBraces = "{\"x\" : 42}" - do { - let result = try decoder.decode([String:Int].self, from: jsonWithBraces.data(using: String._Encoding.utf8)!) - XCTAssertEqual(result, ["x" : 42]) - } catch { - XCTFail("Error thrown while decoding assumed top-level dictionary: \(error)") - } + result = try decoder.decode([String:Int].self, from: jsonWithBraces.data(using: .utf8)!) + #expect(result == ["x" : 42]) - do { - let result = try decoder.decode([String:Int].self, from: Data()) - XCTAssertEqual(result, [:]) - } catch { - XCTFail("Error thrown while decoding empty assumed top-level dictionary: \(error)") - } + result = try decoder.decode([String:Int].self, from: Data()) + #expect(result == [:]) let jsonWithEndBraceOnly = "\"x\" : 42}" - XCTAssertThrowsError(try decoder.decode([String:Int].self, from: jsonWithEndBraceOnly.data(using: String._Encoding.utf8)!)) + #expect(throws: (any Error).self) { + try decoder.decode([String:Int].self, from: jsonWithEndBraceOnly.data(using: .utf8)!) + } let jsonWithStartBraceOnly = "{\"x\" : 42" - XCTAssertThrowsError(try decoder.decode([String:Int].self, from: jsonWithStartBraceOnly.data(using: String._Encoding.utf8)!)) + #expect(throws: (any Error).self) { + try decoder.decode([String:Int].self, from: jsonWithStartBraceOnly.data(using: .utf8)!) + } } - func test_BOMPrefixes() { + @Test func bomPrefixes() throws { let json = "\"👍🏻\"" let decoder = JSONDecoder() // UTF-8 BOM let utf8_BOM = Data([0xEF, 0xBB, 0xBF]) - XCTAssertEqual("👍🏻", try decoder.decode(String.self, from: utf8_BOM + json.data(using: String._Encoding.utf8)!)) + #expect(try "👍🏻" == decoder.decode(String.self, from: utf8_BOM + json.data(using: .utf8)!)) // UTF-16 BE let utf16_BE_BOM = Data([0xFE, 0xFF]) - XCTAssertEqual("👍🏻", try decoder.decode(String.self, from: utf16_BE_BOM + json.data(using: String._Encoding.utf16BigEndian)!)) + #expect(try "👍🏻" == decoder.decode(String.self, from: utf16_BE_BOM + json.data(using: .utf16BigEndian)!)) // UTF-16 LE let utf16_LE_BOM = Data([0xFF, 0xFE]) - XCTAssertEqual("👍🏻", try decoder.decode(String.self, from: utf16_LE_BOM + json.data(using: String._Encoding.utf16LittleEndian)!)) + #expect(try "👍🏻" == decoder.decode(String.self, from: utf16_LE_BOM + json.data(using: .utf16LittleEndian)!)) // UTF-32 BE let utf32_BE_BOM = Data([0x0, 0x0, 0xFE, 0xFF]) - XCTAssertEqual("👍🏻", try decoder.decode(String.self, from: utf32_BE_BOM + json.data(using: String._Encoding.utf32BigEndian)!)) + #expect(try "👍🏻" == decoder.decode(String.self, from: utf32_BE_BOM + json.data(using: .utf32BigEndian)!)) // UTF-32 LE let utf32_LE_BOM = Data([0xFE, 0xFF, 0, 0]) - XCTAssertEqual("👍🏻", try decoder.decode(String.self, from: utf32_LE_BOM + json.data(using: String._Encoding.utf32LittleEndian)!)) + #expect(try "👍🏻" == decoder.decode(String.self, from: utf32_LE_BOM + json.data(using: .utf32LittleEndian)!)) // Try some mismatched BOMs - XCTAssertThrowsError(try decoder.decode(String.self, from: utf32_LE_BOM + json.data(using: String._Encoding.utf32BigEndian)!)) + #expect(throws: (any Error).self) { + try decoder.decode(String.self, from: utf32_LE_BOM + json.data(using: .utf32BigEndian)!) + } - XCTAssertThrowsError(try decoder.decode(String.self, from: utf16_BE_BOM + json.data(using: String._Encoding.utf32LittleEndian)!)) + #expect(throws: (any Error).self) { + try decoder.decode(String.self, from: utf16_BE_BOM + json.data(using: .utf32LittleEndian)!) + } - XCTAssertThrowsError(try decoder.decode(String.self, from: utf8_BOM + json.data(using: String._Encoding.utf16BigEndian)!)) + #expect(throws: (any Error).self) { + try decoder.decode(String.self, from: utf8_BOM + json.data(using: .utf16BigEndian)!) + } } - func test_invalidKeyUTF8() { + @Test func invalidKeyUTF8() { // {"key[255]":"value"} // The invalid UTF-8 byte sequence in the key should trigger a thrown error, not a crash. let data = Data([123, 34, 107, 101, 121, 255, 34, 58, 34, 118, 97, 108, 117, 101, 34, 125]) struct Example: Decodable { let key: String } - XCTAssertThrowsError(try JSONDecoder().decode(Example.self, from: data)) + #expect(throws: (any Error).self) { + try JSONDecoder().decode(Example.self, from: data) + } } - func test_valueNotFoundError() { + @Test func valueNotFoundError() { struct ValueNotFound : Decodable { let a: Bool let nope: String? @@ -1657,28 +1655,36 @@ final class JSONEncoderTests : XCTestCase { } } } - let json = "{\"a\":true}".data(using: String._Encoding.utf8)! + let json = "{\"a\":true}".data(using: .utf8)! // The expected valueNotFound error is swalled by the init(from:) implementation. - XCTAssertNoThrow(try JSONDecoder().decode(ValueNotFound.self, from: json)) + #expect(throws: Never.self) { + try JSONDecoder().decode(ValueNotFound.self, from: json) + } } - func test_infiniteDate() { + @Test func infiniteDate() { let date = Date(timeIntervalSince1970: .infinity) let encoder = JSONEncoder() encoder.dateEncodingStrategy = .deferredToDate - XCTAssertThrowsError(try encoder.encode([date])) + #expect(throws: (any Error).self) { + try encoder.encode([date]) + } encoder.dateEncodingStrategy = .secondsSince1970 - XCTAssertThrowsError(try encoder.encode([date])) + #expect(throws: (any Error).self) { + try encoder.encode([date]) + } encoder.dateEncodingStrategy = .millisecondsSince1970 - XCTAssertThrowsError(try encoder.encode([date])) + #expect(throws: (any Error).self) { + try encoder.encode([date]) + } } - func test_typeEncodesNothing() { + @Test func typeEncodesNothing() { struct EncodesNothing : Encodable { func encode(to encoder: Encoder) throws { // Intentionally nothing. @@ -1686,18 +1692,20 @@ final class JSONEncoderTests : XCTestCase { } let enc = JSONEncoder() - XCTAssertThrowsError(try enc.encode(EncodesNothing())) + #expect(throws: (any Error).self) { + try enc.encode(EncodesNothing()) + } // Unknown if the following behavior is strictly correct, but it's what the prior implementation does, so this test exists to make sure we maintain compatibility. let arrayData = try! enc.encode([EncodesNothing()]) - XCTAssertEqual("[{}]", String(data: arrayData, encoding: .utf8)) + #expect("[{}]" == String(data: arrayData, encoding: .utf8)) let objectData = try! enc.encode(["test" : EncodesNothing()]) - XCTAssertEqual("{\"test\":{}}", String(data: objectData, encoding: .utf8)) + #expect("{\"test\":{}}" == String(data: objectData, encoding: .utf8)) } - func test_superEncoders() { + @Test func superEncoders() throws { struct SuperEncoding : Encodable { enum CodingKeys: String, CodingKey { case firstSuper @@ -1731,16 +1739,16 @@ final class JSONEncoderTests : XCTestCase { // NOTE!!! At present, the order in which the values in the unkeyed container's superEncoders above get inserted into the resulting array depends on the order in which the superEncoders are deinit'd!! This can result in some very unexpected results, and this pattern is not recommended. This test exists just to verify compatibility. } } - let data = try! JSONEncoder().encode(SuperEncoding()) + let data = try JSONEncoder().encode(SuperEncoding()) let string = String(data: data, encoding: .utf8)! - XCTAssertTrue(string.contains("\"firstSuper\":\"First\"")) - XCTAssertTrue(string.contains("\"secondSuper\":\"Second\"")) - XCTAssertTrue(string.contains("[0,\"First\",\"Second\",42]")) - XCTAssertTrue(string.contains("{\"direct\":\"super\"}")) + #expect(string.contains("\"firstSuper\":\"First\"")) + #expect(string.contains("\"secondSuper\":\"Second\"")) + #expect(string.contains("[0,\"First\",\"Second\",42]")) + #expect(string.contains("{\"direct\":\"super\"}")) } - func testRedundantKeys() { + @Test func redundantKeys() throws { // Last encoded key wins. struct RedundantEncoding : Encodable { @@ -1773,26 +1781,26 @@ final class JSONEncoderTests : XCTestCase { } } } - var data = try! JSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: false)) - XCTAssertEqual(String(data: data, encoding: .utf8), ("{\"key\":42}")) + var data = try JSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: false)) + #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try! JSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: true)) - XCTAssertEqual(String(data: data, encoding: .utf8), ("{\"key\":42}")) + data = try JSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: true)) + #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try! JSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: false)) - XCTAssertEqual(String(data: data, encoding: .utf8), ("{\"key\":42}")) + data = try JSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: false)) + #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try! JSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: true)) - XCTAssertEqual(String(data: data, encoding: .utf8), ("{\"key\":42}")) + data = try JSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: true)) + #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try! JSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: false)) - XCTAssertEqual(String(data: data, encoding: .utf8), ("{\"key\":42}")) + data = try JSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: false)) + #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try! JSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: true)) - XCTAssertEqual(String(data: data, encoding: .utf8), ("{\"key\":42}")) + data = try JSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: true)) + #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) } - func test_SR17581_codingEmptyDictionaryWithNonstringKeyDoesRoundtrip() throws { + @Test func SR17581_codingEmptyDictionaryWithNonstringKeyDoesRoundtrip() throws { struct Something: Codable { struct Key: Codable, Hashable { var x: String @@ -1822,11 +1830,11 @@ final class JSONEncoderTests : XCTestCase { let toEncode = Something(dict: [:]) let data = try JSONEncoder().encode(toEncode) let result = try JSONDecoder().decode(Something.self, from: data) - XCTAssertEqual(result.dict.count, 0) + #expect(result.dict.count == 0) } - // None of these tests can be run in our automatic test suites right now, because they are expected to hit a preconditionFailure. They can only be verified manually. - func disabled_testPreconditionFailuresForContainerReplacement() { + #if FOUNDATION_EXIT_TESTS + @Test func preconditionFailuresForContainerReplacement() async { struct RedundantEncoding : Encodable { enum Subcase { case replaceValueWithKeyedContainer @@ -1860,36 +1868,45 @@ final class JSONEncoderTests : XCTestCase { } } } - let _ = try! JSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithKeyedContainer)) -// let _ = try! JSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithUnkeyedContainer)) -// let _ = try! JSONEncoder().encode(RedundantEncoding(subcase: .replaceKeyedContainerWithUnkeyed)) -// let _ = try! JSONEncoder().encode(RedundantEncoding(subcase: .replaceUnkeyedContainerWithKeyed)) + await #expect(processExitsWith: .failure) { + let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithKeyedContainer)) + } + await #expect(processExitsWith: .failure) { + let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithUnkeyedContainer)) + } + await #expect(processExitsWith: .failure) { + let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceKeyedContainerWithUnkeyed)) + } + await #expect(processExitsWith: .failure) { + let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceUnkeyedContainerWithKeyed)) + } } + #endif - func test_decodeIfPresent() throws { + @Test func decodeIfPresent() throws { let emptyDictJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testEmptyDict = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: emptyDictJSON) - XCTAssertEqual(testEmptyDict, .allNils) + #expect(testEmptyDict == .allNils) let allNullDictJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testAllNullDict = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allNullDictJSON) - XCTAssertEqual(testAllNullDict, .allNils) + #expect(testAllNullDict == .allNils) let allOnesDictJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) let testAllOnesDict = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allOnesDictJSON) - XCTAssertEqual(testAllOnesDict, .allOnes) + #expect(testAllOnesDict == .allOnes) let emptyArrayJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testEmptyArray = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: emptyArrayJSON) - XCTAssertEqual(testEmptyArray, .allNils) + #expect(testEmptyArray == .allNils) let allNullArrayJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testAllNullArray = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allNullArrayJSON) - XCTAssertEqual(testAllNullArray, .allNils) + #expect(testAllNullArray == .allNils) let allOnesArrayJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) let testAllOnesArray = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allOnesArrayJSON) - XCTAssertEqual(testAllOnesArray, .allOnes) + #expect(testAllOnesArray == .allOnes) } } @@ -1901,7 +1918,7 @@ extension JSONEncoderTests { return decoder } - func test_json5Numbers() { + @Test func json5Numbers() { let decoder = json5Decoder let successfulIntegers: [(String,Int)] = [ @@ -1929,11 +1946,9 @@ extension JSONEncoderTests { ("1E+02", 100), ] for (json, expected) in successfulIntegers { - do { - let val = try decoder.decode(Int.self, from: json.data(using: String._Encoding.utf8)!) - XCTAssertEqual(val, expected, "Wrong value parsed from input \"\(json)\"") - } catch { - XCTFail("Error when parsing input \"\(json)\": \(error)") + #expect(throws: Never.self, "Error when parsing input \"\(json)\"") { + let val = try decoder.decode(Int.self, from: json.data(using: .utf8)!) + #expect(val == expected, "Wrong value parsed from input \"\(json)\"") } } @@ -1973,15 +1988,13 @@ extension JSONEncoderTests { ("+0X1f", Double(+0x1f)), ] for (json, expected) in successfulDoubles { - do { - let val = try decoder.decode(Double.self, from: json.data(using: String._Encoding.utf8)!) + #expect(throws: Never.self, "Error when parsing input \"\(json)\"") { + let val = try decoder.decode(Double.self, from: json.data(using: .utf8)!) if expected.isNaN { - XCTAssertTrue(val.isNaN, "Wrong value \(val) parsed from input \"\(json)\"") + #expect(val.isNaN, "Wrong value \(val) parsed from input \"\(json)\"") } else { - XCTAssertEqual(val, expected, "Wrong value parsed from input \"\(json)\"") + #expect(val == expected, "Wrong value parsed from input \"\(json)\"") } - } catch { - XCTFail("Error when parsing input \"\(json)\": \(error)") } } @@ -2007,10 +2020,9 @@ extension JSONEncoderTests { "-1E ", ] for json in unsuccessfulIntegers { - do { - let _ = try decoder.decode(Int.self, from: json.data(using: String._Encoding.utf8)!) - XCTFail("Expected failure for input \"\(json)\"") - } catch { } + #expect(throws: (any Error).self, "Expected failure for input \"\(json)\"") { + try decoder.decode(Int.self, from: json.data(using: .utf8)!) + } } let unsuccessfulDoubles = [ @@ -2038,14 +2050,13 @@ extension JSONEncoderTests { "0xFFFFFFFFFFFFFFFFFFFFFF", ]; for json in unsuccessfulDoubles { - do { - let _ = try decoder.decode(Double.self, from: json.data(using: String._Encoding.utf8)!) - XCTFail("Expected failure for input \"\(json)\"") - } catch { } + #expect(throws: (any Error).self, "Expected failure for input \"\(json)\"") { + try decoder.decode(Double.self, from: json.data(using: .utf8)!) + } } } - func test_json5Null() { + @Test func json5Null() { let validJSON = "null" let invalidJSON = [ "Null", @@ -2056,14 +2067,18 @@ extension JSONEncoderTests { "nu " ] - XCTAssertNoThrow(try json5Decoder.decode(NullReader.self, from: validJSON.data(using: String._Encoding.utf8)!)) + #expect(throws: Never.self) { + try json5Decoder.decode(NullReader.self, from: validJSON.data(using: .utf8)!) + } for json in invalidJSON { - XCTAssertThrowsError(try json5Decoder.decode(NullReader.self, from: json.data(using: String._Encoding.utf8)!), "Expected failure while decoding input \"\(json)\"") + #expect(throws: (any Error).self, "Expected failure while decoding input \"\(json)\"") { + try json5Decoder.decode(NullReader.self, from: json.data(using: .utf8)!) + } } } - func test_json5EsotericErrors() { + @Test func json5EsotericErrors() { // All of the following should fail let arrayStrings = [ "[", @@ -2092,17 +2107,23 @@ extension JSONEncoderTests { [.init(ascii: "{"), 0xf0, 0x80, 0x80], // Invalid UTF-8: Initial byte of 3-byte sequence with only one continuation ] for json in arrayStrings { - XCTAssertThrowsError(try json5Decoder.decode([String].self, from: json.data(using: String._Encoding.utf8)!), "Expected error for input \"\(json)\"") + #expect(throws: (any Error).self, "Expected error for input \"\(json)\"") { + try json5Decoder.decode([String].self, from: json.data(using: .utf8)!) + } } for json in objectStrings { - XCTAssertThrowsError(try json5Decoder.decode([String:Bool].self, from: json.data(using: String._Encoding.utf8)!), "Expected error for input \(json)") + #expect(throws: (any Error).self, "Expected error for input \(json)") { + try json5Decoder.decode([String:Bool].self, from: json.data(using: .utf8)!) + } } for json in objectCharacterArrays { - XCTAssertThrowsError(try json5Decoder.decode([String:Bool].self, from: Data(json)), "Expected error for input \(json)") + #expect(throws: (any Error).self, "Expected error for input \(json)") { + try json5Decoder.decode([String:Bool].self, from: Data(json)) + } } } - func test_json5Strings() { + @Test func json5Strings() { let stringsToTrues = [ "{v\n : true}", "{v \n : true}", @@ -2143,21 +2164,23 @@ extension JSONEncoderTests { ] for json in stringsToTrues { - XCTAssertNoThrow(try json5Decoder.decode([String:Bool].self, from: json.data(using: String._Encoding.utf8)!), "Failed to parse \"\(json)\"") + #expect(throws: Never.self, "Failed to parse \"\(json)\"") { + try json5Decoder.decode([String:Bool].self, from: json.data(using: .utf8)!) + } } for (json, expected) in stringsToStrings { do { - let decoded = try json5Decoder.decode([String:String].self, from: json.data(using: String._Encoding.utf8)!) - XCTAssertEqual(expected, decoded["v"]) + let decoded = try json5Decoder.decode([String:String].self, from: json.data(using: .utf8)!) + #expect(expected == decoded["v"]) } catch { if let expected { - XCTFail("Expected \(expected) for input \"\(json)\", but failed with \(error)") + Issue.record("Expected \(expected) for input \"\(json)\", but failed with \(error)") } } } } - func test_json5AssumedDictionary() { + @Test func json5AssumedDictionary() { let decoder = json5Decoder decoder.assumesTopLevelDictionary = true @@ -2186,11 +2209,11 @@ extension JSONEncoderTests { ] for (json, expected) in stringsToString { do { - let decoded = try decoder.decode([String:String].self, from: json.data(using: String._Encoding.utf8)!) - XCTAssertEqual(expected, decoded) + let decoded = try decoder.decode([String:String].self, from: json.data(using: .utf8)!) + #expect(expected == decoded) } catch { if let expected { - XCTFail("Expected \(expected) for input \"\(json)\", but failed with \(error)") + Issue.record("Expected \(expected) for input \"\(json)\", but failed with \(error)") } } } @@ -2208,22 +2231,26 @@ extension JSONEncoderTests { "hello: \"world\", goodbye: {\"hi\":\"there\",},", // more than one value, nested dictionary, trailing comma 2 ] for json in stringsToNestedDictionary { - do { - let decoded = try decoder.decode(HelloGoodbye.self, from: json.data(using: String._Encoding.utf8)!) - XCTAssertEqual(helloGoodbyeExpectedValue, decoded) - } catch { - XCTFail("Expected \(helloGoodbyeExpectedValue) for input \"\(json)\", but failed with \(error)") + #expect(throws: Never.self, "Unexpected error for input \"\(json)\"") { + let decoded = try decoder.decode(HelloGoodbye.self, from: json.data(using: .utf8)!) + #expect(helloGoodbyeExpectedValue == decoded) } } - let arrayJSON = "[1,2,3]".data(using: String._Encoding.utf8)! // Assumed dictionary can't be an array - XCTAssertThrowsError(try decoder.decode([Int].self, from: arrayJSON)) + let arrayJSON = "[1,2,3]".data(using: .utf8)! // Assumed dictionary can't be an array + #expect(throws: (any Error).self) { + try decoder.decode([Int].self, from: arrayJSON) + } - let strFragmentJSON = "fragment".data(using: String._Encoding.utf8)! // Assumed dictionary can't be a fragment - XCTAssertThrowsError(try decoder.decode(String.self, from: strFragmentJSON)) + let strFragmentJSON = "fragment".data(using: .utf8)! // Assumed dictionary can't be a fragment + #expect(throws: (any Error).self) { + try decoder.decode(String.self, from: strFragmentJSON) + } - let numFragmentJSON = "42".data(using: String._Encoding.utf8)! // Assumed dictionary can't be a fragment - XCTAssertThrowsError(try decoder.decode(Int.self, from: numFragmentJSON)) + let numFragmentJSON = "42".data(using: .utf8)! // Assumed dictionary can't be a fragment + #expect(throws: (any Error).self) { + try decoder.decode(Int.self, from: numFragmentJSON) + } } enum JSON5SpecTestType { @@ -2247,7 +2274,7 @@ extension JSONEncoderTests { // MARK: - SnakeCase Tests extension JSONEncoderTests { - func testDecodingKeyStrategyCamel() { + @Test func decodingKeyStrategyCamel() throws { let fromSnakeCaseTests = [ ("", ""), // don't die on empty string ("a", "a"), // single character @@ -2286,30 +2313,30 @@ extension JSONEncoderTests { for test in fromSnakeCaseTests { // This JSON contains the camel case key that the test object should decode with, then it uses the snake case key (test.0) as the actual key for the boolean value. - let input = "{\"camelCaseKey\":\"\(test.1)\",\"\(test.0)\":true}".data(using: String._Encoding.utf8)! + let input = "{\"camelCaseKey\":\"\(test.1)\",\"\(test.0)\":true}".data(using: .utf8)! let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - let result = try! decoder.decode(DecodeMe.self, from: input) + let result = try decoder.decode(DecodeMe.self, from: input) - XCTAssertTrue(result.found) + #expect(result.found) } } - func testEncodingDictionaryStringKeyConversionUntouched() { + @Test func encodingDictionaryStringKeyConversionUntouched() throws { let expected = "{\"leaveMeAlone\":\"test\"}" let toEncode: [String: String] = ["leaveMeAlone": "test"] let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - let resultData = try! encoder.encode(toEncode) - let resultString = String(bytes: resultData, encoding: String._Encoding.utf8) + let resultData = try encoder.encode(toEncode) + let resultString = String(bytes: resultData, encoding: .utf8) - XCTAssertEqual(expected, resultString) + #expect(expected == resultString) } - func testKeyStrategySnakeGeneratedAndCustom() { + @Test func keyStrategySnakeGeneratedAndCustom() throws { // Test that this works with a struct that has automatically generated keys struct DecodeMe4 : Codable { var thisIsCamelCase : String @@ -2321,72 +2348,74 @@ extension JSONEncoderTests { } // Decoding - let input = "{\"foo_bar\":\"test\",\"this_is_camel_case_too\":\"test2\"}".data(using: String._Encoding.utf8)! + let input = "{\"foo_bar\":\"test\",\"this_is_camel_case_too\":\"test2\"}".data(using: .utf8)! let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - let decodingResult = try! decoder.decode(DecodeMe4.self, from: input) + let decodingResult = try decoder.decode(DecodeMe4.self, from: input) - XCTAssertEqual("test", decodingResult.thisIsCamelCase) - XCTAssertEqual("test2", decodingResult.thisIsCamelCaseToo) + #expect("test" == decodingResult.thisIsCamelCase) + #expect("test2" == decodingResult.thisIsCamelCaseToo) // Encoding let encoded = DecodeMe4(thisIsCamelCase: "test", thisIsCamelCaseToo: "test2") let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - let encodingResultData = try! encoder.encode(encoded) - let encodingResultString = String(bytes: encodingResultData, encoding: String._Encoding.utf8) - XCTAssertTrue(encodingResultString!.contains("foo_bar")) - XCTAssertTrue(encodingResultString!.contains("this_is_camel_case_too")) + let encodingResultData = try encoder.encode(encoded) + let encodingResultString = try #require(String(bytes: encodingResultData, encoding: .utf8)) + #expect(encodingResultString.contains("foo_bar")) + #expect(encodingResultString.contains("this_is_camel_case_too")) } - func testDecodingDictionaryFailureKeyPathNested() { - let input = "{\"top_level\": {\"sub_level\": {\"nested_value\": {\"int_value\": \"not_an_int\"}}}}".data(using: String._Encoding.utf8)! + @Test func decodingDictionaryFailureKeyPathNested() { + let input = "{\"top_level\": {\"sub_level\": {\"nested_value\": {\"int_value\": \"not_an_int\"}}}}".data(using: .utf8)! let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase do { _ = try decoder.decode([String: [String : DecodeFailureNested]].self, from: input) } catch DecodingError.typeMismatch(_, let context) { - XCTAssertEqual(4, context.codingPath.count) - XCTAssertEqual("top_level", context.codingPath[0].stringValue) - XCTAssertEqual("sub_level", context.codingPath[1].stringValue) - XCTAssertEqual("nestedValue", context.codingPath[2].stringValue) - XCTAssertEqual("intValue", context.codingPath[3].stringValue) + #expect(4 == context.codingPath.count) + #expect("top_level" == context.codingPath[0].stringValue) + #expect("sub_level" == context.codingPath[1].stringValue) + #expect("nestedValue" == context.codingPath[2].stringValue) + #expect("intValue" == context.codingPath[3].stringValue) } catch { - XCTFail("Unexpected error: \(String(describing: error))") + Issue.record("Unexpected error: \(String(describing: error))") } } - func testDecodingKeyStrategyCamelGenerated() { + @Test func decodingKeyStrategyCamelGenerated() throws { let encoded = DecodeMe3(thisIsCamelCase: "test") let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - let resultData = try! encoder.encode(encoded) - let resultString = String(bytes: resultData, encoding: String._Encoding.utf8) - XCTAssertEqual("{\"this_is_camel_case\":\"test\"}", resultString) + let resultData = try encoder.encode(encoded) + let resultString = String(bytes: resultData, encoding: .utf8) + #expect("{\"this_is_camel_case\":\"test\"}" == resultString) } - func testDecodingStringExpectedType() { - let input = #"{"thisIsCamelCase": null}"#.data(using: String._Encoding.utf8)! - do { + @Test func decodingStringExpectedType() { + let input = #"{"thisIsCamelCase": null}"#.data(using: .utf8)! + #expect { _ = try JSONDecoder().decode(DecodeMe3.self, from: input) - } catch DecodingError.valueNotFound(let expected, _) { - XCTAssertTrue(expected == String.self) - } catch { - XCTFail("Unexpected error: \(String(describing: error))") + } throws: { + guard let decodingError = $0 as? DecodingError, + case let DecodingError.valueNotFound(expected, _) = decodingError else { + return false + } + return expected == String.self } } - func testEncodingKeyStrategySnakeGenerated() { + @Test func encodingKeyStrategySnakeGenerated() throws { // Test that this works with a struct that has automatically generated keys - let input = "{\"this_is_camel_case\":\"test\"}".data(using: String._Encoding.utf8)! + let input = "{\"this_is_camel_case\":\"test\"}".data(using: .utf8)! let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - let result = try! decoder.decode(DecodeMe3.self, from: input) + let result = try decoder.decode(DecodeMe3.self, from: input) - XCTAssertEqual("test", result.thisIsCamelCase) + #expect("test" == result.thisIsCamelCase) } - func testEncodingDictionaryFailureKeyPath() { + @Test func encodingDictionaryFailureKeyPath() { let toEncode: [String: EncodeFailure] = ["key": EncodeFailure(someValue: Double.nan)] let encoder = JSONEncoder() @@ -2394,15 +2423,15 @@ extension JSONEncoderTests { do { _ = try encoder.encode(toEncode) } catch EncodingError.invalidValue(_, let context) { - XCTAssertEqual(2, context.codingPath.count) - XCTAssertEqual("key", context.codingPath[0].stringValue) - XCTAssertEqual("someValue", context.codingPath[1].stringValue) + #expect(2 == context.codingPath.count) + #expect("key" == context.codingPath[0].stringValue) + #expect("someValue" == context.codingPath[1].stringValue) } catch { - XCTFail("Unexpected error: \(String(describing: error))") + Issue.record("Unexpected error: \(String(describing: error))") } } - func testEncodingDictionaryFailureKeyPathNested() { + @Test func encodingDictionaryFailureKeyPathNested() { let toEncode: [String: [String: EncodeFailureNested]] = ["key": ["sub_key": EncodeFailureNested(nestedValue: EncodeFailure(someValue: Double.nan))]] let encoder = JSONEncoder() @@ -2410,17 +2439,17 @@ extension JSONEncoderTests { do { _ = try encoder.encode(toEncode) } catch EncodingError.invalidValue(_, let context) { - XCTAssertEqual(4, context.codingPath.count) - XCTAssertEqual("key", context.codingPath[0].stringValue) - XCTAssertEqual("sub_key", context.codingPath[1].stringValue) - XCTAssertEqual("nestedValue", context.codingPath[2].stringValue) - XCTAssertEqual("someValue", context.codingPath[3].stringValue) + #expect(4 == context.codingPath.count) + #expect("key" == context.codingPath[0].stringValue) + #expect("sub_key" == context.codingPath[1].stringValue) + #expect("nestedValue" == context.codingPath[2].stringValue) + #expect("someValue" == context.codingPath[3].stringValue) } catch { - XCTFail("Unexpected error: \(String(describing: error))") + Issue.record("Unexpected error: \(String(describing: error))") } } - func testEncodingKeyStrategySnake() { + @Test func encodingKeyStrategySnake() throws { let toSnakeCaseTests = [ ("simpleOneTwo", "simple_one_two"), ("myURL", "my_url"), @@ -2459,22 +2488,22 @@ extension JSONEncoderTests { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase - let resultData = try! encoder.encode(encoded) - let resultString = String(bytes: resultData, encoding: String._Encoding.utf8) + let resultData = try encoder.encode(encoded) + let resultString = String(bytes: resultData, encoding: .utf8) - XCTAssertEqual(expected, resultString) + #expect(expected == resultString) } } - func test_twoByteUTF16Inputs() { + @Test func twoByteUTF16Inputs() throws { let json = "7" let decoder = JSONDecoder() - XCTAssertEqual(7, try decoder.decode(Int.self, from: json.data(using: .utf16BigEndian)!)) - XCTAssertEqual(7, try decoder.decode(Int.self, from: json.data(using: .utf16LittleEndian)!)) + #expect(try 7 == decoder.decode(Int.self, from: json.data(using: .utf16BigEndian)!)) + #expect(try 7 == decoder.decode(Int.self, from: json.data(using: .utf16LittleEndian)!)) } - private func _run_passTest(name: String, json5: Bool = false, type: T.Type) { + private func _run_passTest(name: String, json5: Bool = false, type: T.Type, sourceLocation: SourceLocation = #_sourceLocation) { let jsonData = testData(forResource: name, withExtension: json5 ? "json5" : "json" , subdirectory: json5 ? "JSON5/pass" : "JSON/pass")! let plistData = testData(forResource: name, withExtension: "plist", subdirectory: "JSON/pass") @@ -2484,7 +2513,7 @@ extension JSONEncoderTests { do { decoded = try decoder.decode(T.self, from: jsonData) } catch { - XCTFail("Pass test \"\(name)\" failed with error: \(error)") + Issue.record("Pass test \"\(name)\" failed with error: \(error)", sourceLocation: sourceLocation) return } @@ -2492,18 +2521,22 @@ extension JSONEncoderTests { prettyPrintEncoder.outputFormatting = .prettyPrinted for encoder in [JSONEncoder(), prettyPrintEncoder] { - let reencodedData = try! encoder.encode(decoded) - let redecodedObjects = try! decoder.decode(T.self, from: reencodedData) - XCTAssertEqual(decoded, redecodedObjects) + #expect(throws: Never.self, sourceLocation: sourceLocation) { + let reencodedData = try encoder.encode(decoded) + let redecodedObjects = try decoder.decode(T.self, from: reencodedData) + #expect(decoded == redecodedObjects) + } if let plistData { - let decodedPlistObjects = try! PropertyListDecoder().decode(T.self, from: plistData) - XCTAssertEqual(decoded, decodedPlistObjects) + #expect(throws: Never.self, sourceLocation: sourceLocation) { + let decodedPlistObjects = try PropertyListDecoder().decode(T.self, from: plistData) + #expect(decoded == decodedPlistObjects) + } } } } - func test_JSONPassTests() { + @Test func jsonPassTests() { _run_passTest(name: "pass1-utf8", type: JSONPass.Test1.self) _run_passTest(name: "pass1-utf16be", type: JSONPass.Test1.self) _run_passTest(name: "pass1-utf16le", type: JSONPass.Test1.self) @@ -2525,7 +2558,7 @@ extension JSONEncoderTests { _run_passTest(name: "pass15", type: JSONPass.Test15.self) } - func test_json5PassJSONFiles() { + @Test func json5PassJSONFiles() { _run_passTest(name: "example", json5: true, type: JSON5Pass.Example.self) _run_passTest(name: "hex", json5: true, type: JSON5Pass.Hex.self) _run_passTest(name: "numbers", json5: true, type: JSON5Pass.Numbers.self) @@ -2533,20 +2566,17 @@ extension JSONEncoderTests { _run_passTest(name: "whitespace", json5: true, type: JSON5Pass.Whitespace.self) } - private func _run_failTest(name: String, type: T.Type) { + private func _run_failTest(name: String, type: T.Type, sourceLocation: SourceLocation = #_sourceLocation) { let jsonData = testData(forResource: name, withExtension: "json", subdirectory: "JSON/fail")! let decoder = JSONDecoder() decoder.assumesTopLevelDictionary = true - do { - let _ = try decoder.decode(T.self, from: jsonData) - XCTFail("Decoding should have failed for invalid JSON data (test name: \(name))") - } catch { - print(error as NSError) + #expect(throws: (any Error).self, "Decoding should have failed for invalid JSON data (test name: \(name))", sourceLocation: sourceLocation) { + try decoder.decode(T.self, from: jsonData) } } - func test_JSONFailTests() { + @Test func jsonFailTests() { _run_failTest(name: "fail1", type: JSONFail.Test1.self) _run_failTest(name: "fail2", type: JSONFail.Test2.self) _run_failTest(name: "fail3", type: JSONFail.Test3.self) @@ -2590,7 +2620,7 @@ extension JSONEncoderTests { } - func _run_json5SpecTest(_ category: String, _ name: String, testType: JSON5SpecTestType, type: T.Type) { + func _run_json5SpecTest(_ category: String, _ name: String, testType: JSON5SpecTestType, type: T.Type, sourceLocation: SourceLocation = #_sourceLocation) { let subdirectory = "/JSON5/spec/\(category)" let ext = testType.fileExtension let jsonData = testData(forResource: name, withExtension: ext, subdirectory: subdirectory)! @@ -2601,47 +2631,53 @@ extension JSONEncoderTests { switch testType { case .json, .json5_foundationPermissiveJSON: // Valid JSON should remain valid JSON5 - XCTAssertNoThrow(try json5.decode(type, from: jsonData)) + #expect(throws: Never.self, sourceLocation: sourceLocation) { + _ = try json5.decode(type, from: jsonData) + } // Repeat with non-JSON5-compliant decoder. - XCTAssertNoThrow(try json.decode(type, from: jsonData)) + #expect(throws: Never.self, sourceLocation: sourceLocation) { + _ = try json.decode(type, from: jsonData) + } case .json5: - XCTAssertNoThrow(try json5.decode(type, from: jsonData)) + #expect(throws: Never.self, sourceLocation: sourceLocation) { + _ = try json5.decode(type, from: jsonData) + } // Regular JSON decoder should throw. do { let val = try json.decode(type, from: jsonData) - XCTFail("Expected decode failure (original JSON)for test \(name).\(ext), but got: \(val)") + Issue.record("Expected decode failure (original JSON)for test \(name).\(ext), but got: \(val)", sourceLocation: sourceLocation) } catch { } case .js: // Valid ES5 that's explicitly disallowed by JSON5 is also invalid JSON. do { let val = try json5.decode(type, from: jsonData) - XCTFail("Expected decode failure (JSON5) for test \(name).\(ext), but got: \(val)") + Issue.record("Expected decode failure (JSON5) for test \(name).\(ext), but got: \(val)", sourceLocation: sourceLocation) } catch { } // Regular JSON decoder should also throw. do { let val = try json.decode(type, from: jsonData) - XCTFail("Expected decode failure (original JSON) for test \(name).\(ext), but got: \(val)") + Issue.record("Expected decode failure (original JSON) for test \(name).\(ext), but got: \(val)", sourceLocation: sourceLocation) } catch { } case .malformed: // Invalid ES5 should remain invalid JSON5 do { let val = try json5.decode(type, from: jsonData) - XCTFail("Expected decode failure (JSON5) for test \(name).\(ext), but got: \(val)") + Issue.record("Expected decode failure (JSON5) for test \(name).\(ext), but got: \(val)", sourceLocation: sourceLocation) } catch { } // Regular JSON decoder should also throw. do { let val = try json.decode(type, from: jsonData) - XCTFail("Expected decode failure (original JSON) for test \(name).\(ext), but got: \(val)") + Issue.record("Expected decode failure (original JSON) for test \(name).\(ext), but got: \(val)", sourceLocation: sourceLocation) } catch { } } } // Also tests non-JSON5 decoder against the non-JSON5 tests in this test suite. - func test_json5Spec() { + @Test func json5Spec() { // Expected successes: _run_json5SpecTest("arrays", "empty-array", testType: .json, type: [Bool].self) _run_json5SpecTest("arrays", "regular-array", testType: .json, type: [Bool?].self) @@ -2768,9 +2804,9 @@ extension JSONEncoderTests { } - func testEncodingDateISO8601() { + @Test func encodingDateISO8601() { let timestamp = Date(timeIntervalSince1970: 1000) - let expectedJSON = "\"\(timestamp.formatted(.iso8601))\"".data(using: String._Encoding.utf8)! + let expectedJSON = "\"\(timestamp.formatted(.iso8601))\"".data(using: .utf8)! _testRoundTrip(of: timestamp, expectedJSON: expectedJSON, @@ -2785,10 +2821,10 @@ extension JSONEncoderTests { dateDecodingStrategy: .iso8601) } - func testEncodingDataBase64() { + @Test func encodingDataBase64() { let data = Data([0xDE, 0xAD, 0xBE, 0xEF]) - let expectedJSON = "\"3q2+7w==\"".data(using: String._Encoding.utf8)! + let expectedJSON = "\"3q2+7w==\"".data(using: .utf8)! _testRoundTrip(of: data, expectedJSON: expectedJSON) // Optional data should encode the same way. @@ -2798,8 +2834,8 @@ extension JSONEncoderTests { // MARK: - Decimal Tests extension JSONEncoderTests { - func testInterceptDecimal() { - let expectedJSON = "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".data(using: String._Encoding.utf8)! + @Test func interceptDecimal() { + let expectedJSON = "10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000".data(using: .utf8)! // Want to make sure we write out a JSON number, not the keyed encoding here. // 1e127 is too big to fit natively in a Double, too, so want to make sure it's encoded as a Decimal. @@ -2810,16 +2846,16 @@ extension JSONEncoderTests { _testRoundTrip(of: Optional(decimal), expectedJSON: expectedJSON) } - func test_hugeNumbers() { + @Test func hugeNumbers() throws { let json = "23456789012000000000000000000000000000000000000000000000000000000000000000000 " - let data = json.data(using: String._Encoding.utf8)! + let data = json.data(using: .utf8)! - let decimal = try! JSONDecoder().decode(Decimal.self, from: data) + let decimal = try JSONDecoder().decode(Decimal.self, from: data) let expected = Decimal(string: json) - XCTAssertEqual(decimal, expected) + #expect(decimal == expected) } - func testInterceptLargeDecimal() { + @Test func interceptLargeDecimal() { struct TestBigDecimal: Codable, Equatable { var uint64Max: Decimal = Decimal(UInt64.max) var unit64MaxPlus1: Decimal = Decimal( @@ -2845,9 +2881,11 @@ extension JSONEncoderTests { _testRoundTrip(of: testBigDecimal) } - func testOverlargeDecimal() { + @Test func overlargeDecimal() { // Check value too large fails to decode. - XCTAssertThrowsError(try JSONDecoder().decode(Decimal.self, from: "100e200".data(using: .utf8)!)) + #expect(throws: (any Error).self) { + try JSONDecoder().decode(Decimal.self, from: "100e200".data(using: .utf8)!) + } } } @@ -2856,13 +2894,13 @@ extension JSONEncoderTests { #if FOUNDATION_FRAMEWORK extension JSONEncoderTests { // This will remain a framework-only test due to dependence on `DateFormatter`. - func testEncodingDateFormatted() { + @Test func encodingDateFormatted() { let formatter = DateFormatter() formatter.dateStyle = .full formatter.timeStyle = .full let timestamp = Date(timeIntervalSince1970: 1000) - let expectedJSON = "\"\(formatter.string(from: timestamp))\"".data(using: String._Encoding.utf8)! + let expectedJSON = "\"\(formatter.string(from: timestamp))\"".data(using: .utf8)! _testRoundTrip(of: timestamp, expectedJSON: expectedJSON, @@ -2876,7 +2914,7 @@ extension JSONEncoderTests { dateDecodingStrategy: .formatted(formatter)) // So should wrapped dates. - let expectedJSON_array = "[\"\(formatter.string(from: timestamp))\"]".data(using: String._Encoding.utf8)! + let expectedJSON_array = "[\"\(formatter.string(from: timestamp))\"]".data(using: .utf8)! _testRoundTrip(of: TopLevelArrayWrapper(timestamp), expectedJSON: expectedJSON_array, dateEncodingStrategy: .formatted(formatter), @@ -2887,26 +2925,26 @@ extension JSONEncoderTests { // MARK: - .sortedKeys Tests extension JSONEncoderTests { - func testEncodingTopLevelStructuredClass() { + @Test func encodingTopLevelStructuredClass() { // Person is a class with multiple fields. - let expectedJSON = "{\"email\":\"appleseed@apple.com\",\"name\":\"Johnny Appleseed\"}".data(using: String._Encoding.utf8)! + let expectedJSON = "{\"email\":\"appleseed@apple.com\",\"name\":\"Johnny Appleseed\"}".data(using: .utf8)! let person = Person.testValue _testRoundTrip(of: person, expectedJSON: expectedJSON, outputFormatting: [.sortedKeys]) } - func testEncodingOutputFormattingSortedKeys() { - let expectedJSON = "{\"email\":\"appleseed@apple.com\",\"name\":\"Johnny Appleseed\"}".data(using: String._Encoding.utf8)! + @Test func encodingOutputFormattingSortedKeys() { + let expectedJSON = "{\"email\":\"appleseed@apple.com\",\"name\":\"Johnny Appleseed\"}".data(using: .utf8)! let person = Person.testValue _testRoundTrip(of: person, expectedJSON: expectedJSON, outputFormatting: [.sortedKeys]) } - func testEncodingOutputFormattingPrettyPrintedSortedKeys() { - let expectedJSON = "{\n \"email\" : \"appleseed@apple.com\",\n \"name\" : \"Johnny Appleseed\"\n}".data(using: String._Encoding.utf8)! + @Test func encodingOutputFormattingPrettyPrintedSortedKeys() { + let expectedJSON = "{\n \"email\" : \"appleseed@apple.com\",\n \"name\" : \"Johnny Appleseed\"\n}".data(using: .utf8)! let person = Person.testValue _testRoundTrip(of: person, expectedJSON: expectedJSON, outputFormatting: [.prettyPrinted, .sortedKeys]) } - func testEncodingSortedKeys() { + @Test func encodingSortedKeys() { // When requesting sorted keys, dictionary keys are sorted prior to being written out. // This sort should be stable, numeric, and follow human-readable sorting rules as defined by the system locale. let dict = [ @@ -2928,14 +2966,14 @@ extension JSONEncoderTests { "bar" : 10 ] - _testRoundTrip(of: dict, expectedJSON: #"{"FOO":2,"Foo":1,"Foo11":8,"Foo2":5,"bar":10,"foo":3,"foo1":4,"foo12":7,"foo3":6,"føo":9}"#.data(using: String._Encoding.utf8)!, outputFormatting: [.sortedKeys]) + _testRoundTrip(of: dict, expectedJSON: #"{"FOO":2,"Foo":1,"Foo11":8,"Foo2":5,"bar":10,"foo":3,"foo1":4,"foo12":7,"foo3":6,"føo":9}"#.data(using: .utf8)!, outputFormatting: [.sortedKeys]) } - func testEncodingSortedKeysStableOrdering() { + @Test func encodingSortedKeysStableOrdering() { // We want to make sure that keys of different length (but with identical prefixes) always sort in a stable way, regardless of their hash ordering. var dict = ["AAA" : 1, "AAAAAAB" : 2] var expectedJSONString = "{\"AAA\":1,\"AAAAAAB\":2}" - _testRoundTrip(of: dict, expectedJSON: expectedJSONString.data(using: String._Encoding.utf8)!, outputFormatting: [.sortedKeys]) + _testRoundTrip(of: dict, expectedJSON: expectedJSONString.data(using: .utf8)!, outputFormatting: [.sortedKeys]) // We don't want this test to rely on the hashing of Strings or how Dictionary uses that hash. // We'll insert a large number of keys into this dictionary and guarantee that the ordering of the above keys has indeed not changed. @@ -2960,10 +2998,10 @@ extension JSONEncoderTests { expectedJSONString.insert(contentsOf: insertedKeyJSON, at: expectedJSONString.index(before: expectedJSONString.endIndex)) } - _testRoundTrip(of: dict, expectedJSON: expectedJSONString.data(using: String._Encoding.utf8)!, outputFormatting: [.sortedKeys]) + _testRoundTrip(of: dict, expectedJSON: expectedJSONString.data(using: .utf8)!, outputFormatting: [.sortedKeys]) } - func testEncodingMultipleNestedContainersWithTheSameTopLevelKey() { + @Test func encodingMultipleNestedContainersWithTheSameTopLevelKey() { struct Model : Codable, Equatable { let first: String let second: String @@ -3011,11 +3049,11 @@ extension JSONEncoderTests { } let model = Model.testValue - let expectedJSON = "{\"top\":{\"first\":\"Johnny Appleseed\",\"second\":\"appleseed@apple.com\"}}".data(using: String._Encoding.utf8)! + let expectedJSON = "{\"top\":{\"first\":\"Johnny Appleseed\",\"second\":\"appleseed@apple.com\"}}".data(using: .utf8)! _testRoundTrip(of: model, expectedJSON: expectedJSON, outputFormatting: [.sortedKeys]) } - func test_redundantKeyedContainer() { + @Test func redundantKeyedContainer() throws { struct EncodesTwice: Encodable { enum CodingKeys: String, CodingKey { case container @@ -3041,13 +3079,13 @@ extension JSONEncoderTests { let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys - let data = try! encoder.encode(EncodesTwice()) + let data = try encoder.encode(EncodesTwice()) let string = String(data: data, encoding: .utf8)! - XCTAssertEqual(string, "{\"container\":{\"foo\":\"Test\",\"somethingElse\":\"SecondAgain\"},\"somethingElse\":\"Foo\"}") + #expect(string == "{\"container\":{\"foo\":\"Test\",\"somethingElse\":\"SecondAgain\"},\"somethingElse\":\"Foo\"}") } - func test_singleValueDictionaryAmendedByContainer() { + @Test func singleValueDictionaryAmendedByContainer() throws { struct Test: Encodable { enum CodingKeys: String, CodingKey { case a @@ -3063,18 +3101,18 @@ extension JSONEncoderTests { } let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys - let data = try! encoder.encode(Test()) + let data = try encoder.encode(Test()) let string = String(data: data, encoding: .utf8)! - XCTAssertEqual(string, "{\"a\":\"c\",\"other\":\"foo\"}") + #expect(string == "{\"a\":\"c\",\"other\":\"foo\"}") } } // MARK: - URL Tests extension JSONEncoderTests { - func testInterceptURL() { + @Test func interceptURL() { // Want to make sure JSONEncoder writes out single-value URLs, not the keyed encoding. - let expectedJSON = "\"http:\\/\\/swift.org\"".data(using: String._Encoding.utf8)! + let expectedJSON = "\"http:\\/\\/swift.org\"".data(using: .utf8)! let url = URL(string: "http://swift.org")! _testRoundTrip(of: url, expectedJSON: expectedJSON) @@ -3082,9 +3120,9 @@ extension JSONEncoderTests { _testRoundTrip(of: Optional(url), expectedJSON: expectedJSON) } - func testInterceptURLWithoutEscapingOption() { + @Test func interceptURLWithoutEscapingOption() { // Want to make sure JSONEncoder writes out single-value URLs, not the keyed encoding. - let expectedJSON = "\"http://swift.org\"".data(using: String._Encoding.utf8)! + let expectedJSON = "\"http://swift.org\"".data(using: .utf8)! let url = URL(string: "http://swift.org")! _testRoundTrip(of: url, expectedJSON: expectedJSON, outputFormatting: [.withoutEscapingSlashes]) @@ -3094,30 +3132,30 @@ extension JSONEncoderTests { } // MARK: - Helper Global Functions -func expectEqualPaths(_ lhs: [CodingKey], _ rhs: [CodingKey], _ prefix: String) { - if lhs.count != rhs.count { - XCTFail("\(prefix) [CodingKey].count mismatch: \(lhs.count) != \(rhs.count)") - return - } +func expectEqualPaths(_ lhs: [CodingKey], _ rhs: [CodingKey], _ prefix: String, sourceLocation: SourceLocation = #_sourceLocation) { + if lhs.count != rhs.count { + Issue.record("\(prefix) [CodingKey].count mismatch: \(lhs.count) != \(rhs.count)", sourceLocation: sourceLocation) + return + } - for (key1, key2) in zip(lhs, rhs) { - switch (key1.intValue, key2.intValue) { - case (.none, .none): break - case (.some(let i1), .none): - XCTFail("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != nil") - return - case (.none, .some(let i2)): - XCTFail("\(prefix) CodingKey.intValue mismatch: nil != \(type(of: key2))(\(i2))") - return - case (.some(let i1), .some(let i2)): - guard i1 == i2 else { - XCTFail("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != \(type(of: key2))(\(i2))") + for (key1, key2) in zip(lhs, rhs) { + switch (key1.intValue, key2.intValue) { + case (.none, .none): break + case (.some(let i1), .none): + Issue.record("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != nil", sourceLocation: sourceLocation) return - } + case (.none, .some(let i2)): + Issue.record("\(prefix) CodingKey.intValue mismatch: nil != \(type(of: key2))(\(i2))", sourceLocation: sourceLocation) + return + case (.some(let i1), .some(let i2)): + guard i1 == i2 else { + Issue.record("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != \(type(of: key2))(\(i2))", sourceLocation: sourceLocation) + return + } } - XCTAssertEqual(key1.stringValue, key2.stringValue, "\(prefix) CodingKey.stringValue mismatch: \(type(of: key1))('\(key1.stringValue)') != \(type(of: key2))('\(key2.stringValue)')") - } + #expect(key1.stringValue == key2.stringValue, "\(prefix) CodingKey.stringValue mismatch: \(type(of: key1))('\(key1.stringValue)') != \(type(of: key2))('\(key2.stringValue)')", sourceLocation: sourceLocation) + } } // MARK: - Test Types diff --git a/Tests/FoundationEssentialsTests/PropertyListEncoderTests.swift b/Tests/FoundationEssentialsTests/PropertyListEncoderTests.swift index cd8336bd0..9ac492da9 100644 --- a/Tests/FoundationEssentialsTests/PropertyListEncoderTests.swift +++ b/Tests/FoundationEssentialsTests/PropertyListEncoderTests.swift @@ -7,9 +7,7 @@ //===----------------------------------------------------------------------===// // -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if FOUNDATION_FRAMEWORK @testable import Foundation @@ -19,24 +17,23 @@ import TestSupport // MARK: - Test Suite -class TestPropertyListEncoder : XCTestCase { +@Suite("PropertyListEncoder") +private struct PropertyListEncoderTests { // MARK: - Encoding Top-Level Empty Types -#if FIXED_64141381 - func testEncodingTopLevelEmptyStruct() { + @Test func encodingTopLevelEmptyStruct() { let empty = EmptyStruct() _testRoundTrip(of: empty, in: .binary, expectedPlist: _plistEmptyDictionaryBinary) _testRoundTrip(of: empty, in: .xml, expectedPlist: _plistEmptyDictionaryXML) } - func testEncodingTopLevelEmptyClass() { + @Test func encodingTopLevelEmptyClass() { let empty = EmptyClass() _testRoundTrip(of: empty, in: .binary, expectedPlist: _plistEmptyDictionaryBinary) _testRoundTrip(of: empty, in: .xml, expectedPlist: _plistEmptyDictionaryXML) } -#endif // MARK: - Encoding Top-Level Single-Value Types - func testEncodingTopLevelSingleValueEnum() { + @Test func encodingTopLevelSingleValueEnum() { let s1 = Switch.off _testEncodeFailure(of: s1, in: .binary) _testEncodeFailure(of: s1, in: .xml) @@ -50,7 +47,7 @@ class TestPropertyListEncoder : XCTestCase { _testRoundTrip(of: TopLevelWrapper(s2), in: .xml) } - func testEncodingTopLevelSingleValueStruct() { + @Test func encodingTopLevelSingleValueStruct() { let t = Timestamp(3141592653) _testEncodeFailure(of: t, in: .binary) _testEncodeFailure(of: t, in: .xml) @@ -58,7 +55,7 @@ class TestPropertyListEncoder : XCTestCase { _testRoundTrip(of: TopLevelWrapper(t), in: .xml) } - func testEncodingTopLevelSingleValueClass() { + @Test func encodingTopLevelSingleValueClass() { let c = Counter() _testEncodeFailure(of: c, in: .binary) _testEncodeFailure(of: c, in: .xml) @@ -67,49 +64,49 @@ class TestPropertyListEncoder : XCTestCase { } // MARK: - Encoding Top-Level Structured Types - func testEncodingTopLevelStructuredStruct() { + @Test func encodingTopLevelStructuredStruct() { // Address is a struct type with multiple fields. let address = Address.testValue _testRoundTrip(of: address, in: .binary) _testRoundTrip(of: address, in: .xml) } - func testEncodingTopLevelStructuredClass() { + @Test func encodingTopLevelStructuredClass() { // Person is a class with multiple fields. let person = Person.testValue _testRoundTrip(of: person, in: .binary) _testRoundTrip(of: person, in: .xml) } - func testEncodingTopLevelStructuredSingleStruct() { + @Test func encodingTopLevelStructuredSingleStruct() { // Numbers is a struct which encodes as an array through a single value container. let numbers = Numbers.testValue _testRoundTrip(of: numbers, in: .binary) _testRoundTrip(of: numbers, in: .xml) } - func testEncodingTopLevelStructuredSingleClass() { + @Test func encodingTopLevelStructuredSingleClass() { // Mapping is a class which encodes as a dictionary through a single value container. let mapping = Mapping.testValue _testRoundTrip(of: mapping, in: .binary) _testRoundTrip(of: mapping, in: .xml) } - func testEncodingTopLevelDeepStructuredType() { + @Test func encodingTopLevelDeepStructuredType() { // Company is a type with fields which are Codable themselves. let company = Company.testValue _testRoundTrip(of: company, in: .binary) _testRoundTrip(of: company, in: .xml) } - func testEncodingClassWhichSharesEncoderWithSuper() { + @Test func encodingClassWhichSharesEncoderWithSuper() { // Employee is a type which shares its encoder & decoder with its superclass, Person. let employee = Employee.testValue _testRoundTrip(of: employee, in: .binary) _testRoundTrip(of: employee, in: .xml) } - func testEncodingTopLevelNullableType() { + @Test func encodingTopLevelNullableType() { // EnhancedBool is a type which encodes either as a Bool or as nil. _testEncodeFailure(of: EnhancedBool.true, in: .binary) _testEncodeFailure(of: EnhancedBool.true, in: .xml) @@ -126,20 +123,19 @@ class TestPropertyListEncoder : XCTestCase { _testRoundTrip(of: TopLevelWrapper(EnhancedBool.fileNotFound), in: .xml) } - func testEncodingTopLevelWithConfiguration() throws { + @Test func encodingTopLevelWithConfiguration() throws { // CodableTypeWithConfiguration is a struct that conforms to CodableWithConfiguration let value = CodableTypeWithConfiguration.testValue let encoder = PropertyListEncoder() let decoder = PropertyListDecoder() var decoded = try decoder.decode(CodableTypeWithConfiguration.self, from: try encoder.encode(value, configuration: .init(1)), configuration: .init(1)) - XCTAssertEqual(decoded, value) + #expect(decoded == value) decoded = try decoder.decode(CodableTypeWithConfiguration.self, from: try encoder.encode(value, configuration: CodableTypeWithConfiguration.ConfigProviding.self), configuration: CodableTypeWithConfiguration.ConfigProviding.self) - XCTAssertEqual(decoded, value) + #expect(decoded == value) } -#if FIXED_64141381 - func testEncodingMultipleNestedContainersWithTheSameTopLevelKey() { + @Test func encodingMultipleNestedContainersWithTheSameTopLevelKey() { struct Model : Codable, Equatable { let first: String let second: String @@ -186,13 +182,12 @@ class TestPropertyListEncoder : XCTestCase { } let model = Model.testValue - let expectedXML = "\n\n\n\n\ttop\n\t\n\t\tfirst\n\t\tJohnny Appleseed\n\t\tsecond\n\t\tappleseed@apple.com\n\t\n\n\n".data(using: String._Encoding.utf8)! + let expectedXML = "\n\n\n\n\ttop\n\t\n\t\tfirst\n\t\tJohnny Appleseed\n\t\tsecond\n\t\tappleseed@apple.com\n\t\n\n\n".data(using: .utf8)! _testRoundTrip(of: model, in: .xml, expectedPlist: expectedXML) } -#endif -#if false // FIXME: XCTest doesn't support crash tests yet rdar://20195010&22387653 - func testEncodingConflictedTypeNestedContainersWithTheSameTopLevelKey() { +#if FOUNDATION_EXIT_TESTS + @Test func encodingConflictedTypeNestedContainersWithTheSameTopLevelKey() async { struct Model : Encodable, Equatable { let first: String @@ -223,125 +218,128 @@ class TestPropertyListEncoder : XCTestCase { } } - let model = Model.testValue - // This following test would fail as it attempts to re-encode into already encoded container is invalid. This will always fail - expectCrashLater() - _testEncodeFailure(of: model, in: .xml) + await #expect(processExitsWith: .failure) { + let model = Model.testValue + // This following test would fail as it attempts to re-encode into already encoded container is invalid. This will always fail + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + let _ = try encoder.encode(model) + } } #endif // MARK: - Encoder Features - func testNestedContainerCodingPaths() { + @Test func nestedContainerCodingPaths() { let encoder = PropertyListEncoder() - do { - let _ = try encoder.encode(NestedContainersTestType()) - } catch let error as NSError { - XCTFail("Caught error during encoding nested container types: \(error)") + #expect(throws: Never.self) { + try encoder.encode(NestedContainersTestType()) } } - func testSuperEncoderCodingPaths() { + @Test func superEncoderCodingPaths() { let encoder = PropertyListEncoder() - do { - let _ = try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) - } catch let error as NSError { - XCTFail("Caught error during encoding nested container types: \(error)") + #expect(throws: Never.self) { + try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) } } #if FOUNDATION_FRAMEWORK // requires PropertyListSerialization, JSONSerialization - func testEncodingTopLevelData() { - let data = try! JSONSerialization.data(withJSONObject: [String](), options: []) - _testRoundTrip(of: data, in: .binary, expectedPlist: try! PropertyListSerialization.data(fromPropertyList: data, format: .binary, options: 0)) - _testRoundTrip(of: data, in: .xml, expectedPlist: try! PropertyListSerialization.data(fromPropertyList: data, format: .xml, options: 0)) + @Test func encodingTopLevelData() throws { + let data = try JSONSerialization.data(withJSONObject: [String](), options: []) + _testRoundTrip(of: data, in: .binary, expectedPlist: try PropertyListSerialization.data(fromPropertyList: data, format: .binary, options: 0)) + _testRoundTrip(of: data, in: .xml, expectedPlist: try PropertyListSerialization.data(fromPropertyList: data, format: .xml, options: 0)) } - func testInterceptData() { - let data = try! JSONSerialization.data(withJSONObject: [String](), options: []) + @Test func interceptData() throws { + let data = try JSONSerialization.data(withJSONObject: [String](), options: []) let topLevel = TopLevelWrapper(data) let plist = ["value": data] - _testRoundTrip(of: topLevel, in: .binary, expectedPlist: try! PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0)) - _testRoundTrip(of: topLevel, in: .xml, expectedPlist: try! PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)) + _testRoundTrip(of: topLevel, in: .binary, expectedPlist: try PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0)) + _testRoundTrip(of: topLevel, in: .xml, expectedPlist: try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)) } - func testInterceptDate() { + @Test func interceptDate() throws { let date = Date(timeIntervalSinceReferenceDate: 0) let topLevel = TopLevelWrapper(date) let plist = ["value": date] - _testRoundTrip(of: topLevel, in: .binary, expectedPlist: try! PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0)) - _testRoundTrip(of: topLevel, in: .xml, expectedPlist: try! PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)) + _testRoundTrip(of: topLevel, in: .binary, expectedPlist: try PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0)) + _testRoundTrip(of: topLevel, in: .xml, expectedPlist: try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)) } -#endif // FOUNDATION_FRaMEWORK +#endif // FOUNDATION_FRAMEWORK // MARK: - Type coercion - func testTypeCoercion() { - func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type) where T : Codable, U : Codable { + @Test func typeCoercion() throws { + func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type, sourceLocation: SourceLocation = #_sourceLocation) throws where T : Codable, U : Codable { let encoder = PropertyListEncoder() encoder.outputFormat = .xml - let xmlData = try! encoder.encode(value) - XCTAssertThrowsError(try PropertyListDecoder().decode(U.self, from: xmlData), "Coercion from \(T.self) to \(U.self) was expected to fail.") + let xmlData = try encoder.encode(value) + #expect(throws: (any Error).self, "Coercion from \(T.self) to \(U.self) was expected to fail.", sourceLocation: sourceLocation) { + try PropertyListDecoder().decode(U.self, from: xmlData) + } encoder.outputFormat = .binary - let binaryData = try! encoder.encode(value) - XCTAssertThrowsError(try PropertyListDecoder().decode(U.self, from: binaryData), "Coercion from \(T.self) to \(U.self) was expected to fail.") - } - - _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int8].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int16].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int32].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int64].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt8].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt16].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt32].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt64].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [Float].self) - _testRoundTripTypeCoercionFailure(of: [false, true], as: [Double].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int8], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int16], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int32], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int64], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt8], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt16], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt32], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt64], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Float], as: [Bool].self) - _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self) + let binaryData = try encoder.encode(value) + #expect(throws: (any Error).self, "Coercion from \(T.self) to \(U.self) was expected to fail.", sourceLocation: sourceLocation) { + try PropertyListDecoder().decode(U.self, from: binaryData) + } + } + + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int8].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int16].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int32].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [Int64].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt8].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt16].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt32].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [UInt64].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [Float].self) + try _testRoundTripTypeCoercionFailure(of: [false, true], as: [Double].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int8], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int16], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int32], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [Int64], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt8], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt16], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt32], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0, 1] as [UInt64], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Float], as: [Bool].self) + try _testRoundTripTypeCoercionFailure(of: [0.0, 1.0] as [Double], as: [Bool].self) // Real -> Integer coercions that are impossible. - _testRoundTripTypeCoercionFailure(of: [256] as [Double], as: [UInt8].self) - _testRoundTripTypeCoercionFailure(of: [-129] as [Double], as: [Int8].self) - _testRoundTripTypeCoercionFailure(of: [-1.0] as [Double], as: [UInt64].self) - _testRoundTripTypeCoercionFailure(of: [3.14159] as [Double], as: [UInt64].self) - _testRoundTripTypeCoercionFailure(of: [.infinity] as [Double], as: [UInt64].self) - _testRoundTripTypeCoercionFailure(of: [.nan] as [Double], as: [UInt64].self) + try _testRoundTripTypeCoercionFailure(of: [256] as [Double], as: [UInt8].self) + try _testRoundTripTypeCoercionFailure(of: [-129] as [Double], as: [Int8].self) + try _testRoundTripTypeCoercionFailure(of: [-1.0] as [Double], as: [UInt64].self) + try _testRoundTripTypeCoercionFailure(of: [3.14159] as [Double], as: [UInt64].self) + try _testRoundTripTypeCoercionFailure(of: [.infinity] as [Double], as: [UInt64].self) + try _testRoundTripTypeCoercionFailure(of: [.nan] as [Double], as: [UInt64].self) // Especially for binary plist, ensure we maintain different encoded representations of special values like Int64(-1) and UInt64.max, which have the same 8 byte representation. - _testRoundTripTypeCoercionFailure(of: [Int64(-1)], as: [UInt64].self) - _testRoundTripTypeCoercionFailure(of: [UInt64.max], as: [Int64].self) + try _testRoundTripTypeCoercionFailure(of: [Int64(-1)], as: [UInt64].self) + try _testRoundTripTypeCoercionFailure(of: [UInt64.max], as: [Int64].self) } - func testIntegerRealCoercion() throws { - func _testRoundTripTypeCoercion(of value: T, expectedCoercedValue: U) throws { + @Test func integerRealCoercion() throws { + func _testRoundTripTypeCoercion(of value: T, expectedCoercedValue: U, sourceLocation: SourceLocation = #_sourceLocation) throws { let encoder = PropertyListEncoder() encoder.outputFormat = .xml let xmlData = try encoder.encode([value]) var decoded = try PropertyListDecoder().decode([U].self, from: xmlData) - XCTAssertEqual(decoded.first!, expectedCoercedValue) + #expect(decoded.first == expectedCoercedValue, sourceLocation: sourceLocation) encoder.outputFormat = .binary let binaryData = try encoder.encode([value]) decoded = try PropertyListDecoder().decode([U].self, from: binaryData) - XCTAssertEqual(decoded.first!, expectedCoercedValue) + #expect(decoded.first == expectedCoercedValue, sourceLocation: sourceLocation) } try _testRoundTripTypeCoercion(of: 1 as UInt64, expectedCoercedValue: 1.0 as Double) @@ -358,25 +356,19 @@ class TestPropertyListEncoder : XCTestCase { try _testRoundTripTypeCoercion(of: 2.99792458e8 as Double, expectedCoercedValue: 299792458) } - func testDecodingConcreteTypeParameter() { + @Test func decodingConcreteTypeParameter() throws { let encoder = PropertyListEncoder() - guard let plist = try? encoder.encode(Employee.testValue) else { - XCTFail("Unable to encode Employee.") - return - } + let plist = try encoder.encode(Employee.testValue) let decoder = PropertyListDecoder() - guard let decoded = try? decoder.decode(Employee.self as Person.Type, from: plist) else { - XCTFail("Failed to decode Employee as Person from plist.") - return - } + let decoded = try decoder.decode(Employee.self as Person.Type, from: plist) - expectEqual(type(of: decoded), Employee.self, "Expected decoded value to be of type Employee; got \(type(of: decoded)) instead.") + #expect(type(of: decoded) == Employee.self, "Expected decoded value to be of type Employee; got \(type(of: decoded)) instead.") } // MARK: - Encoder State // SR-6078 - func testEncoderStateThrowOnEncode() { + @Test func encoderStateThrowOnEncode() { struct Wrapper : Encodable { let value: T init(_ value: T) { self.value = value } @@ -420,14 +412,16 @@ class TestPropertyListEncoder : XCTestCase { // MARK: - Decoder State // SR-6048 - func testDecoderStateThrowOnDecode() { - let plist = try! PropertyListEncoder().encode([1,2,3]) - let _ = try! PropertyListDecoder().decode(EitherDecodable<[String], [Int]>.self, from: plist) + @Test func decoderStateThrowOnDecode() { + #expect(throws: Never.self) { + let plist = try PropertyListEncoder().encode([1,2,3]) + let _ = try PropertyListDecoder().decode(EitherDecodable<[String], [Int]>.self, from: plist) + } } #if FOUNDATION_FRAMEWORK // MARK: - NSKeyedArchiver / NSKeyedUnarchiver integration - func testArchiving() { + @Test func archiving() throws { struct CodableType: Codable, Equatable { let willBeNil: String? let arrayOfOptionals: [String?] @@ -442,18 +436,14 @@ class TestPropertyListEncoder : XCTestCase { arrayOfOptionals: ["a", "b", nil, "c"], dictionaryOfArrays: [ "data" : [Data([0xfe, 0xed, 0xfa, 0xce]), Data([0xba, 0xaa, 0xaa, 0xad])]]) - do { - try keyedArchiver.encodeEncodable(value, forKey: "strings") - keyedArchiver.finishEncoding() - let data = keyedArchiver.encodedData - - let keyedUnarchiver = try NSKeyedUnarchiver(forReadingFrom: data) - let unarchived = try keyedUnarchiver.decodeTopLevelDecodable(CodableType.self, forKey: "strings") - - XCTAssertEqual(unarchived, value) - } catch { - XCTFail("Unexpected error: \(error)") - } + try keyedArchiver.encodeEncodable(value, forKey: "strings") + keyedArchiver.finishEncoding() + let data = keyedArchiver.encodedData + + let keyedUnarchiver = try NSKeyedUnarchiver(forReadingFrom: data) + let unarchived = try keyedUnarchiver.decodeTopLevelDecodable(CodableType.self, forKey: "strings") + + #expect(unarchived == value) } #endif @@ -463,41 +453,40 @@ class TestPropertyListEncoder : XCTestCase { } private var _plistEmptyDictionaryXML: Data { - return "\n\n\n\n\n".data(using: String._Encoding.utf8)! + return "\n\n\n\n\n".data(using: .utf8)! } - private func _testEncodeFailure(of value: T, in format: PropertyListDecoder.PropertyListFormat) { - do { + private func _testEncodeFailure(of value: T, in format: PropertyListDecoder.PropertyListFormat, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(throws: (any Error).self, "Encode of top-level \(T.self) was expected to fail.", sourceLocation: sourceLocation) { let encoder = PropertyListEncoder() encoder.outputFormat = format let _ = try encoder.encode(value) - XCTFail("Encode of top-level \(T.self) was expected to fail.") - } catch {} + } } @discardableResult - private func _testRoundTrip(of value: T, in format: PropertyListDecoder.PropertyListFormat, expectedPlist plist: Data? = nil) -> T? where T : Codable, T : Equatable { + private func _testRoundTrip(of value: T, in format: PropertyListDecoder.PropertyListFormat, expectedPlist plist: Data? = nil, sourceLocation: SourceLocation = #_sourceLocation) -> T? where T : Codable, T : Equatable { var payload: Data! = nil do { let encoder = PropertyListEncoder() encoder.outputFormat = format payload = try encoder.encode(value) } catch { - XCTFail("Failed to encode \(T.self) to plist: \(error)") + Issue.record("Failed to encode \(T.self) to plist: \(error)") } if let expectedPlist = plist { - XCTAssertEqual(expectedPlist, payload, "Produced plist not identical to expected plist.") + #expect(expectedPlist == payload, "Produced plist not identical to expected plist.") } do { var decodedFormat: PropertyListDecoder.PropertyListFormat = format let decoded = try PropertyListDecoder().decode(T.self, from: payload, format: &decodedFormat) - XCTAssertEqual(format, decodedFormat, "Encountered plist format differed from requested format.") - XCTAssertEqual(decoded, value, "\(T.self) did not round-trip to an equal value.") + #expect(format == decodedFormat, "Encountered plist format differed from requested format.") + #expect(decoded == value, "\(T.self) did not round-trip to an equal value.") return decoded } catch { - XCTFail("Failed to decode \(T.self) from plist: \(error)") + Issue.record("Failed to decode \(T.self) from plist: \(error)") return nil } } @@ -508,7 +497,7 @@ class TestPropertyListEncoder : XCTestCase { } // MARK: - Other tests - func testUnkeyedContainerContainingNulls() throws { + @Test func unkeyedContainerContainingNulls() throws { struct UnkeyedContainerContainingNullTestType : Codable, Equatable { var array = [String?]() @@ -543,32 +532,40 @@ class TestPropertyListEncoder : XCTestCase { _testRoundTrip(of: UnkeyedContainerContainingNullTestType(array: array), in: .binary) } - func test_invalidNSDataKey_82142612() { + @Test func invalidNSDataKey_82142612() { let data = testData(forResource: "Test_82142612", withExtension: "bad")! let decoder = PropertyListDecoder() - XCTAssertThrowsError(try decoder.decode([String:String].self, from: data)) + #expect(throws: (any Error).self) { + try decoder.decode([String:String].self, from: data) + } // Repeat something similar with XML. - let xmlData = "abcdxyz".data(using: String._Encoding.utf8)! - XCTAssertThrowsError(try decoder.decode([String:String].self, from: xmlData)) + let xmlData = "abcdxyz".data(using: .utf8)! + #expect(throws: (any Error).self) { + try decoder.decode([String:String].self, from: xmlData) + } } #if FOUNDATION_FRAMEWORK // TODO: Depends on data's range(of:) implementation - func test_nonStringDictionaryKey() { + @Test func nonStringDictionaryKey() throws { let decoder = PropertyListDecoder() let encoder = PropertyListEncoder() encoder.outputFormat = .binary - var data = try! encoder.encode(["abcd":"xyz"]) + var data = try encoder.encode(["abcd":"xyz"]) // Replace the tag for the ASCII string (0101) that is length 4 ("abcd" => length: 0100) with a boolean "true" tag (0000_1001) let range = data.range(of: Data([0b0101_0100]))! data.replaceSubrange(range, with: Data([0b000_1001])) - XCTAssertThrowsError(try decoder.decode([String:String].self, from: data)) + #expect(throws: (any Error).self) { + try decoder.decode([String:String].self, from: data) + } - let xmlData = "abcdxyz".data(using: String._Encoding.utf8)! - XCTAssertThrowsError(try decoder.decode([String:String].self, from: xmlData)) + let xmlData = "abcdxyz".data(using: .utf8)! + #expect(throws: (any Error).self) { + try decoder.decode([String:String].self, from: xmlData) + } } #endif @@ -605,42 +602,46 @@ class TestPropertyListEncoder : XCTestCase { } } - func test_5616259() throws { + @Test func issue5616259() throws { let plistData = testData(forResource: "Test_5616259", withExtension: "bad")! - XCTAssertThrowsError(try PropertyListDecoder().decode([String].self, from: plistData)) + #expect(throws: (any Error).self) { + try PropertyListDecoder().decode([String].self, from: plistData) + } } - func test_genericProperties_XML() throws { + @Test func genericProperties_XML() throws { let data = testData(forResource: "Generic_XML_Properties", withExtension: "plist")! let props = try PropertyListDecoder().decode(GenericProperties.self, from: data) - XCTAssertNil(props.assertionFailure) + #expect(props.assertionFailure == nil) } - func test_genericProperties_binary() throws { + @Test func genericProperties_binary() throws { let data = testData(forResource: "Generic_XML_Properties_Binary", withExtension: "plist")! let props = try PropertyListDecoder().decode(GenericProperties.self, from: data) - XCTAssertNil(props.assertionFailure) + #expect(props.assertionFailure == nil) } // Binary plist parser should parse any version 'bplist0?' - func test_5877417() throws { + @Test func issue5877417() throws { var data = testData(forResource: "Generic_XML_Properties_Binary", withExtension: "plist")! // Modify the data so the header starts with bplist0x data[7] = UInt8(ascii: "x") let props = try PropertyListDecoder().decode(GenericProperties.self, from: data) - XCTAssertNil(props.assertionFailure) + #expect(props.assertionFailure == nil) } - func test_xmlErrors() { + @Test func xmlErrors() { let data = testData(forResource: "Generic_XML_Properties", withExtension: "plist")! let originalXML = String(data: data, encoding: .utf8)! // Try an empty plist - XCTAssertThrowsError(try PropertyListDecoder().decode(GenericProperties.self, from: Data())) + #expect(throws: (any Error).self) { + try PropertyListDecoder().decode(GenericProperties.self, from: Data()) + } // We'll modify this string in all kinds of nasty ways to introduce errors // --- /* @@ -669,44 +670,46 @@ class TestPropertyListEncoder : XCTestCase { var errorPlists = [String : String]() errorPlists["Deleted leading <"] = String(originalXML[originalXML.index(after: originalXML.startIndex)...]) - errorPlists["Unterminated comment"] = originalXML.replacingOccurrences(of: "", with: "<-- unending comment\n") - errorPlists["Mess with DOCTYPE"] = originalXML.replacingOccurrences(of: "DOCTYPE", with: "foobar") + errorPlists["Unterminated comment"] = originalXML.replacing("", with: "<-- unending comment\n") + errorPlists["Mess with DOCTYPE"] = originalXML.replacing("DOCTYPE", with: "foobar") - let range = originalXML.range(of: "//EN")! + let range = originalXML.firstRange(of: "//EN")! errorPlists["Early EOF"] = String(originalXML[originalXML.startIndex ..< range.lowerBound]) - errorPlists["MalformedDTD"] = originalXML.replacingOccurrences(of: "", with: "") - errorPlists["Bad open tag"] = originalXML.replacingOccurrences(of: "", with: "") - errorPlists["Extra plist object"] = originalXML.replacingOccurrences(of: "", with: "hello\n") - errorPlists["Non-key inside dict"] = originalXML.replacingOccurrences(of: "array1", with: "hello\narray1") - errorPlists["Missing value for key"] = originalXML.replacingOccurrences(of: "value1", with: "") - errorPlists["Malformed real tag"] = originalXML.replacingOccurrences(of: "42", with: "abc123") - errorPlists["Empty int tag"] = originalXML.replacingOccurrences(of: "42", with: "") - errorPlists["Strange int tag"] = originalXML.replacingOccurrences(of: "42", with: "42q") - errorPlists["Hex digit in non-hex int"] = originalXML.replacingOccurrences(of: "42", with: "42A") - errorPlists["Enormous int"] = originalXML.replacingOccurrences(of: "42", with: "99999999999999999999999999999999999999999") + errorPlists["MalformedDTD"] = originalXML.replacing("", with: "") + errorPlists["Bad open tag"] = originalXML.replacing("", with: "") + errorPlists["Extra plist object"] = originalXML.replacing("", with: "hello\n") + errorPlists["Non-key inside dict"] = originalXML.replacing("array1", with: "hello\narray1") + errorPlists["Missing value for key"] = originalXML.replacing("value1", with: "") + errorPlists["Malformed real tag"] = originalXML.replacing("42", with: "abc123") + errorPlists["Empty int tag"] = originalXML.replacing("42", with: "") + errorPlists["Strange int tag"] = originalXML.replacing("42", with: "42q") + errorPlists["Hex digit in non-hex int"] = originalXML.replacing("42", with: "42A") + errorPlists["Enormous int"] = originalXML.replacing("42", with: "99999999999999999999999999999999999999999") errorPlists["Empty plist"] = "" - errorPlists["Empty date"] = originalXML.replacingOccurrences(of: "1976-04-01T12:00:00Z", with: "") - errorPlists["Empty real"] = originalXML.replacingOccurrences(of: "42", with: "") - errorPlists["Fake inline DTD"] = originalXML.replacingOccurrences(of: "PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"", with: "[]") + errorPlists["Empty date"] = originalXML.replacing("1976-04-01T12:00:00Z", with: "") + errorPlists["Empty real"] = originalXML.replacing("42", with: "") + errorPlists["Fake inline DTD"] = originalXML.replacing("PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"", with: "[]") for (name, badPlist) in errorPlists { - let data = badPlist.data(using: String._Encoding.utf8)! - XCTAssertThrowsError(try PropertyListDecoder().decode(GenericProperties.self, from: data), "Case \(name) did not fail as expected") + let data = badPlist.data(using: .utf8)! + #expect(throws: (any Error).self, "Case \(name) did not fail as expected") { + try PropertyListDecoder().decode(GenericProperties.self, from: data) + } } } - func test_6164184() throws { + @Test func issue6164184() throws { let xml = "0x721B0x1111-0xFFFF" - let array = try PropertyListDecoder().decode([Int].self, from: xml.data(using: String._Encoding.utf8)!) - XCTAssertEqual([0x721B, 0x1111, -0xFFFF], array) + let array = try PropertyListDecoder().decode([Int].self, from: xml.data(using: .utf8)!) + #expect([0x721B, 0x1111, -0xFFFF] == array) } - func test_xmlIntegerEdgeCases() throws { - func checkValidEdgeCase(_ xml: String, type: T.Type, expected: T) throws { - let value = try PropertyListDecoder().decode(type, from: xml.data(using: String._Encoding.utf8)!) - XCTAssertEqual(value, expected) + @Test func xmlIntegerEdgeCases() throws { + func checkValidEdgeCase(_ xml: String, type: T.Type, expected: T, sourceLocation: SourceLocation = #_sourceLocation) throws { + let value = try PropertyListDecoder().decode(type, from: xml.data(using: .utf8)!) + #expect(value == expected, sourceLocation: sourceLocation) } try checkValidEdgeCase("127", type: Int8.self, expected: .max) @@ -732,8 +735,10 @@ class TestPropertyListEncoder : XCTestCase { try checkValidEdgeCase("4294967295", type: UInt32.self, expected: .max) try checkValidEdgeCase("18446744073709551615", type: UInt64.self, expected: .max) - func checkInvalidEdgeCase(_ xml: String, type: T.Type) { - XCTAssertThrowsError(try PropertyListDecoder().decode(type, from: xml.data(using: String._Encoding.utf8)!)) + func checkInvalidEdgeCase(_ xml: String, type: T.Type, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(throws: (any Error).self, sourceLocation: sourceLocation) { + try PropertyListDecoder().decode(type, from: xml.data(using: .utf8)!) + } } checkInvalidEdgeCase("128", type: Int8.self) @@ -760,14 +765,14 @@ class TestPropertyListEncoder : XCTestCase { checkInvalidEdgeCase("18446744073709551616", type: UInt64.self) } - func test_xmlIntegerWhitespace() throws { + @Test func xmlIntegerWhitespace() throws { let xml = " +\t42\t- 99 -\t0xFACE" - let value = try PropertyListDecoder().decode([Int].self, from: xml.data(using: String._Encoding.utf8)!) - XCTAssertEqual(value, [42, -99, -0xFACE]) + let value = try PropertyListDecoder().decode([Int].self, from: xml.data(using: .utf8)!) + #expect(value == [42, -99, -0xFACE]) } - func test_binaryNumberEdgeCases() throws { + @Test func binaryNumberEdgeCases() throws { _testRoundTrip(of: [Int8.max], in: .binary) _testRoundTrip(of: [Int8.min], in: .binary) _testRoundTrip(of: [Int16.max], in: .binary) @@ -795,8 +800,8 @@ class TestPropertyListEncoder : XCTestCase { _testRoundTrip(of: [-Double.infinity], in: .binary) } - func test_binaryReals() throws { - func encode(_: T.Type) -> (data: Data, expected: [T]) { + @Test func binaryReals() throws { + func encode(_: T.Type) throws -> (data: Data, expected: [T]) { let expected: [T] = [ 1.5, 2, @@ -808,27 +813,23 @@ class TestPropertyListEncoder : XCTestCase { ] let encoder = PropertyListEncoder() encoder.outputFormat = .binary - let data = try! encoder.encode(expected) + let data = try encoder.encode(expected) return (data, expected) } - func test(_ type: T.Type) { - let (data, expected) = encode(type) - do { - let result = try PropertyListDecoder().decode([T].self, from: data) - XCTAssertEqual(result, expected, "Type: \(type)") - } catch { - XCTFail("Expected error \(error) for type: \(type)") - } + func test(_ type: T.Type) throws { + let (data, expected) = try encode(type) + let result = try PropertyListDecoder().decode([T].self, from: data) + #expect(result == expected, "Type: \(type)") } - test(Float.self) - test(Double.self) + try test(Float.self) + try test(Double.self) } - func test_XMLReals() throws { + @Test func xmlReals() throws { let xml = "1.52 -3.141.00000000000000000000000131415.9e-4-iNfinfInItY" - let array = try PropertyListDecoder().decode([Float].self, from: xml.data(using: String._Encoding.utf8)!) + let array = try PropertyListDecoder().decode([Float].self, from: xml.data(using: .utf8)!) let expected: [Float] = [ 1.5, 2, @@ -838,76 +839,78 @@ class TestPropertyListEncoder : XCTestCase { -.infinity, .infinity ] - XCTAssertEqual(array, expected) + #expect(array == expected) // nan doesn't work with equality. let xmlNAN = "nAnNANnan" - let arrayNAN = try PropertyListDecoder().decode([Float].self, from: xmlNAN.data(using: String._Encoding.utf8)!) + let arrayNAN = try PropertyListDecoder().decode([Float].self, from: xmlNAN.data(using: .utf8)!) for val in arrayNAN { - XCTAssertTrue(val.isNaN) + #expect(val.isNaN) } } - func test_bad_XMLReals() { - let badRealXMLs = [ - "0x10", - "notanumber", - "infinite", - "1.2.3", - "1.e", - "1.5 ", // Trailing whitespace is rejected, unlike leading whitespace. - "", - ] - for xml in badRealXMLs { - XCTAssertThrowsError(try PropertyListDecoder().decode(Float.self, from: xml.data(using: String._Encoding.utf8)!), "Input: \(xml)") + @Test(arguments: [ + "0x10", + "notanumber", + "infinite", + "1.2.3", + "1.e", + "1.5 ", // Trailing whitespace is rejected, unlike leading whitespace. + "", + ]) + func bad_XMLReals(xml: String) { + #expect(throws: (any Error).self) { + try PropertyListDecoder().decode(Float.self, from: xml.data(using: .utf8)!) } } - func test_oldStylePlist_invalid() { - let data = "goodbye cruel world".data(using: String._Encoding.utf16)! - XCTAssertThrowsError(try PropertyListDecoder().decode(String.self, from: data)) + @Test func oldStylePlist_invalid() { + let data = "goodbye cruel world".data(using: .utf16)! + #expect(throws: (any Error).self) { + try PropertyListDecoder().decode(String.self, from: data) + } } // Microsoft: Microsoft vso 1857102 : High Sierra regression that caused data loss : CFBundleCopyLocalizedString returns incorrect string // Escaped octal chars can be shorter than 3 chars long; i.e. \5 ≡ \05 ≡ \005. - func test_oldStylePlist_getSlashedChars_octal() { + @Test func oldStylePlist_getSlashedChars_octal() throws { // ('\0', '\00', '\000', '\1', '\01', '\001', ..., '\777') let data = testData(forResource: "test_oldStylePlist_getSlashedChars_octal", withExtension: "plist")! - let actualStrings = try! PropertyListDecoder().decode([String].self, from: data) + let actualStrings = try PropertyListDecoder().decode([String].self, from: data) let expectedData = testData(forResource: "test_oldStylePlist_getSlashedChars_octal_expected", withExtension: "plist")! - let expectedStrings = try! PropertyListDecoder().decode([String].self, from: expectedData) + let expectedStrings = try PropertyListDecoder().decode([String].self, from: expectedData) - XCTAssertEqual(actualStrings, expectedStrings) + #expect(actualStrings == expectedStrings) } // Old-style plists support Unicode literals via \U syntax. They can be 1–4 characters wide. - func test_oldStylePlist_getSlashedChars_unicode() { + @Test func oldStylePlist_getSlashedChars_unicode() throws { // ('\U0', '\U00', '\U000', '\U0000', '\U1', ..., '\UFFFF') let data = testData(forResource: "test_oldStylePlist_getSlashedChars_unicode", withExtension: "plist")! - let actualStrings = try! PropertyListDecoder().decode([String].self, from: data) + let actualStrings = try PropertyListDecoder().decode([String].self, from: data) let expectedData = testData(forResource: "test_oldStylePlist_getSlashedChars_unicode_expected", withExtension: "plist")! - let expectedStrings = try! PropertyListDecoder().decode([String].self, from: expectedData) + let expectedStrings = try PropertyListDecoder().decode([String].self, from: expectedData) - XCTAssertEqual(actualStrings, expectedStrings) + #expect(actualStrings == expectedStrings) } - func test_oldStylePlist_getSlashedChars_literals() { + @Test func oldStylePlist_getSlashedChars_literals() throws { let literals = ["\u{7}", "\u{8}", "\u{12}", "\n", "\r", "\t", "\u{11}", "\"", "\\n"] - let data = "('\\a', '\\b', '\\f', '\\n', '\\r', '\\t', '\\v', '\\\"', '\\\\n')".data(using: String._Encoding.utf8)! + let data = "('\\a', '\\b', '\\f', '\\n', '\\r', '\\t', '\\v', '\\\"', '\\\\n')".data(using: .utf8)! - let strings = try! PropertyListDecoder().decode([String].self, from: data) - XCTAssertEqual(strings, literals) + let strings = try PropertyListDecoder().decode([String].self, from: data) + #expect(strings == literals) } - func test_oldStylePlist_dictionary() { + @Test func oldStylePlist_dictionary() { let data = """ { "test key" = value; testData = ; "nested array" = (a, b, c); } -""".data(using: String._Encoding.utf16)! +""".data(using: .utf16)! struct Values: Decodable { let testKey: String @@ -922,20 +925,20 @@ class TestPropertyListEncoder : XCTestCase { } do { let decoded = try PropertyListDecoder().decode(Values.self, from: data) - XCTAssertEqual(decoded.testKey, "value") - XCTAssertEqual(decoded.testData, Data([0xfe, 0xed, 0xfa, 0xce])) - XCTAssertEqual(decoded.nestedArray, ["a", "b", "c"]) + #expect(decoded.testKey == "value") + #expect(decoded.testData == Data([0xfe, 0xed, 0xfa, 0xce])) + #expect(decoded.nestedArray == ["a", "b", "c"]) } catch { - XCTFail("Unexpected error: \(error)") + Issue.record("Unexpected error: \(error)") } } - func test_oldStylePlist_stringsFileFormat() { + @Test func oldStylePlist_stringsFileFormat() { let data = """ string1 = "Good morning"; string2 = "Good afternoon"; string3 = "Good evening"; -""".data(using: String._Encoding.utf16)! +""".data(using: .utf16)! do { let decoded = try PropertyListDecoder().decode([String:String].self, from: data) @@ -944,19 +947,19 @@ string3 = "Good evening"; "string2": "Good afternoon", "string3": "Good evening" ] - XCTAssertEqual(decoded, expected) + #expect(decoded == expected) } catch { - XCTFail("Unexpected error: \(error)") + Issue.record("Unexpected error: \(error)") } } - func test_oldStylePlist_comments() { + @Test func oldStylePlist_comments() { let data = """ // Initial comment */ string1 = /*Test*/ "Good morning"; // Test string2 = "Good afternoon" /*Test// */; string3 = "Good evening"; // Test -""".data(using: String._Encoding.utf16)! +""".data(using: .utf16)! do { let decoded = try PropertyListDecoder().decode([String:String].self, from: data) @@ -965,30 +968,30 @@ string3 = "Good evening"; // Test "string2": "Good afternoon", "string3": "Good evening" ] - XCTAssertEqual(decoded, expected) + #expect(decoded == expected) } catch { - XCTFail("Unexpected error: \(error)") + Issue.record("Unexpected error: \(error)") } } #if FOUNDATION_FRAMEWORK // Requires __PlistDictionaryDecoder - func test_oldStylePlist_data() { + @Test func oldStylePlist_data() { let data = """ data1 = <7465 73 74 696E67 31 323334>; -""".data(using: String._Encoding.utf16)! +""".data(using: .utf16)! do { let decoded = try PropertyListDecoder().decode([String:Data].self, from: data) - let expected = ["data1" : "testing1234".data(using: String._Encoding.utf8)!] - XCTAssertEqual(decoded, expected) + let expected = ["data1" : "testing1234".data(using: .utf8)!] + #expect(decoded == expected) } catch { - XCTFail("Unexpected error: \(error)") + Issue.record("Unexpected error: \(error)") } } #endif @@ -996,40 +999,38 @@ data1 = <7465 #if FOUNDATION_FRAMEWORK // Requires PropertyListSerialization - func test_BPlistCollectionReferences() { + @Test func bplistCollectionReferences() throws { // Use NSArray/NSDictionary and PropertyListSerialization so that we get a bplist with internal references. let c: NSArray = [ "a", "a", "a" ] let b: NSArray = [ c, c, c ] let a: NSArray = [ b, b, b ] let d: NSDictionary = ["a" : a, "b" : b, "c" : c] - let data = try! PropertyListSerialization.data(fromPropertyList: d, format: .binary, options: 0) + let data = try PropertyListSerialization.data(fromPropertyList: d, format: .binary, options: 0) - do { - struct DecodedReferences: Decodable { - let a: [[[String]]] - let b: [[String]] - let c: [String] - } - - let decoded = try PropertyListDecoder().decode(DecodedReferences.self, from: data) - XCTAssertEqual(decoded.a, a as! [[[String]]]) - XCTAssertEqual(decoded.b, b as! [[String]]) - XCTAssertEqual(decoded.c, c as! [String]) - } catch { - XCTFail("Unexpected error: \(error)") + struct DecodedReferences: Decodable { + let a: [[[String]]] + let b: [[String]] + let c: [String] } + + let decoded = try PropertyListDecoder().decode(DecodedReferences.self, from: data) + #expect(decoded.a == a as? [[[String]]]) + #expect(decoded.b == b as? [[String]]) + #expect(decoded.c == c as? [String]) } #endif - func test_reallyOldDates_5842198() throws { + @Test func reallyOldDates_5842198() throws { let plist = "\n\n\n0009-09-15T23:16:13Z\n" - let data = plist.data(using: String._Encoding.utf8)! + let data = plist.data(using: .utf8)! - XCTAssertNoThrow(try PropertyListDecoder().decode(Date.self, from: data)) + #expect(throws: Never.self) { + try PropertyListDecoder().decode(Date.self, from: data) + } } - func test_badDates() throws { + @Test func badDates() throws { let timeInterval = TimeInterval(-63145612800) // This is the equivalent of an all-zero gregorian date. let date = Date(timeIntervalSinceReferenceDate: timeInterval) @@ -1037,26 +1038,26 @@ data1 = <7465 _testRoundTrip(of: [date], in: .binary) } - func test_badDate_encode() throws { + @Test func badDate_encode() throws { let date = Date(timeIntervalSinceReferenceDate: -63145612800) // 0000-01-02 AD let encoder = PropertyListEncoder() encoder.outputFormat = .xml let data = try encoder.encode([date]) let str = String(data: data, encoding: String.Encoding.utf8) - XCTAssertEqual(str, "\n\n\n\n\t0000-01-02T00:00:00Z\n\n\n") + #expect(str == "\n\n\n\n\t0000-01-02T00:00:00Z\n\n\n") } - func test_badDate_decode() throws { + @Test func badDate_decode() throws { // Test that we can correctly decode a distant date in the past let plist = "\n\n\n0000-01-02T00:00:00Z\n" - let data = plist.data(using: String._Encoding.utf8)! + let data = plist.data(using: .utf8)! let d = try PropertyListDecoder().decode(Date.self, from: data) - XCTAssertEqual(d.timeIntervalSinceReferenceDate, -63145612800) + #expect(d.timeIntervalSinceReferenceDate == -63145612800) } - func test_realEncodeRemoveZeroSuffix() throws { + @Test func realEncodeRemoveZeroSuffix() throws { // Tests that we encode "whole-value reals" (such as `2.0`, `-5.0`, etc) // **without** the `.0` for backwards compactability let encoder = PropertyListEncoder() @@ -1065,166 +1066,171 @@ data1 = <7465 let wholeFloat: Float = 2.0 var data = try encoder.encode([wholeFloat]) - var str = try XCTUnwrap(String(data: data, encoding: String.Encoding.utf8)) - var expected = template.replacingOccurrences( - of: "<%EXPECTED%>", with: "2") - XCTAssertEqual(str, expected) + var str = try #require(String(data: data, encoding: String.Encoding.utf8)) + var expected = template.replacing( + "<%EXPECTED%>", with: "2") + #expect(str == expected) let wholeDouble: Double = -5.0 data = try encoder.encode([wholeDouble]) - str = try XCTUnwrap(String(data: data, encoding: String.Encoding.utf8)) - expected = template.replacingOccurrences( - of: "<%EXPECTED%>", with: "-5") - XCTAssertEqual(str, expected) + str = try #require(String(data: data, encoding: String.Encoding.utf8)) + expected = template.replacing( + "<%EXPECTED%>", with: "-5") + #expect(str == expected) // Make sure other reals are not affacted let notWholeDouble = 0.5 data = try encoder.encode([notWholeDouble]) - str = try XCTUnwrap(String(data: data, encoding: String.Encoding.utf8)) - expected = template.replacingOccurrences( - of: "<%EXPECTED%>", with: "0.5") - XCTAssertEqual(str, expected) + str = try #require(String(data: data, encoding: String.Encoding.utf8)) + expected = template.replacing( + "<%EXPECTED%>", with: "0.5") + #expect(str == expected) } - func test_farFutureDates() throws { + @Test func farFutureDates() throws { let date = Date(timeIntervalSince1970: 999999999999.0) _testRoundTrip(of: [date], in: .xml) } - func test_122065123_encode() throws { + @Test func encode_122065123() throws { let date = Date(timeIntervalSinceReferenceDate: 728512994) // 2024-02-01 20:43:14 UTC let encoder = PropertyListEncoder() encoder.outputFormat = .xml let data = try encoder.encode([date]) let str = String(data: data, encoding: String.Encoding.utf8) - XCTAssertEqual(str, "\n\n\n\n\t2024-02-01T20:43:14Z\n\n\n") // Previously encoded as "2024-01-32T20:43:14Z" + #expect(str == "\n\n\n\n\t2024-02-01T20:43:14Z\n\n\n") // Previously encoded as "2024-01-32T20:43:14Z" } - func test_122065123_decodingCompatibility() throws { + @Test func decodingCompatibility_122065123() throws { // Test that we can correctly decode an invalid date let plist = "\n\n\n2024-01-32T20:43:14Z\n" - let data = plist.data(using: String._Encoding.utf8)! + let data = plist.data(using: .utf8)! let d = try PropertyListDecoder().decode(Date.self, from: data) - XCTAssertEqual(d.timeIntervalSinceReferenceDate, 728512994) // 2024-02-01T20:43:14Z + #expect(d.timeIntervalSinceReferenceDate == 728512994) // 2024-02-01T20:43:14Z } - func test_multibyteCharacters_escaped_noencoding() throws { - let plistData = "These are copyright signs © © blah blah blah.".data(using: String._Encoding.utf8)! + @Test func multibyteCharacters_escaped_noencoding() throws { + let plistData = "These are copyright signs © © blah blah blah.".data(using: .utf8)! let result = try PropertyListDecoder().decode(String.self, from: plistData) - XCTAssertEqual("These are copyright signs © © blah blah blah.", result) + #expect("These are copyright signs © © blah blah blah." == result) } - func test_escapedCharacters() throws { - let plistData = "&'<>"".data(using: String._Encoding.utf8)! + @Test func escapedCharacters() throws { + let plistData = "&'<>"".data(using: .utf8)! let result = try PropertyListDecoder().decode(String.self, from: plistData) - XCTAssertEqual("&'<>\"", result) + #expect("&'<>\"" == result) } - func test_dataWithBOM_utf8() throws { + @Test func dataWithBOM_utf8() throws { let bom = Data([0xef, 0xbb, 0xbf]) - let plist = bom + "\n\n\nhello\n".data(using: String._Encoding.utf8)! + let plist = bom + "\n\n\nhello\n".data(using: .utf8)! let result = try PropertyListDecoder().decode(String.self, from: plist) - XCTAssertEqual(result, "hello") + #expect(result == "hello") } - -#if FOUNDATION_FRAMEWORK - // TODO: Depends on UTF32 encoding on non-Darwin platforms - func test_dataWithBOM_utf32be() throws { + @Test func dataWithBOM_utf32be() throws { let bom = Data([0x00, 0x00, 0xfe, 0xff]) - let plist = bom + "\n\n\nhello\n".data(using: String._Encoding.utf32BigEndian)! + let plist = bom + "\n\n\nhello\n".data(using: .utf32BigEndian)! let result = try PropertyListDecoder().decode(String.self, from: plist) - XCTAssertEqual(result, "hello") + #expect(result == "hello") } - func test_dataWithBOM_utf32le() throws { + @Test func dataWithBOM_utf32le() throws { let bom = Data([0xff, 0xfe]) - let plist = bom + "\n\n\nhello\n".data(using: String._Encoding.utf16LittleEndian)! + let plist = bom + "\n\n\nhello\n".data(using: .utf16LittleEndian)! let result = try PropertyListDecoder().decode(String.self, from: plist) - XCTAssertEqual(result, "hello") + #expect(result == "hello") } -#endif - func test_plistWithBadUTF8() throws { + @Test func plistWithBadUTF8() throws { let data = testData(forResource: "bad_plist", withExtension: "bad")! - XCTAssertThrowsError(try PropertyListDecoder().decode([String].self, from: data)) - } + #expect(throws: (any Error).self) { + try PropertyListDecoder().decode([String].self, from: data) +} } - func test_plistWithEscapedCharacters() throws { - let plist = "com.apple.security.temporary-exception.sbpl(allow mach-lookup (global-name-regex #"^[0-9]+$"))".data(using: String._Encoding.utf8)! + @Test func plistWithEscapedCharacters() throws { + let plist = "com.apple.security.temporary-exception.sbpl(allow mach-lookup (global-name-regex #"^[0-9]+$"))".data(using: .utf8)! let result = try PropertyListDecoder().decode([String:String].self, from: plist) - XCTAssertEqual(result, ["com.apple.security.temporary-exception.sbpl" : "(allow mach-lookup (global-name-regex #\"^[0-9]+$\"))"]) + #expect(result == ["com.apple.security.temporary-exception.sbpl" : "(allow mach-lookup (global-name-regex #\"^[0-9]+$\"))"]) } #if FOUNDATION_FRAMEWORK // OpenStep format is not supported in Essentials - func test_returnRightFormatFromParse() throws { - let plist = "{ CFBundleDevelopmentRegion = en; }".data(using: String._Encoding.utf8)! + @Test func returnRightFormatFromParse() throws { + let plist = "{ CFBundleDevelopmentRegion = en; }".data(using: .utf8)! var format : PropertyListDecoder.PropertyListFormat = .binary let _ = try PropertyListDecoder().decode([String:String].self, from: plist, format: &format) - XCTAssertEqual(format, .openStep) + #expect(format == .openStep) } #endif - func test_decodingEmoji() throws { - let plist = "emoji🚘".data(using: String._Encoding.utf8)! + @Test func decodingEmoji() throws { + let plist = "emoji🚘".data(using: .utf8)! let result = try PropertyListDecoder().decode([String:String].self, from: plist) let expected = "\u{0001F698}" - XCTAssertEqual(expected, result["emoji"]) + #expect(expected == result["emoji"]) } - func test_decodingTooManyCharactersError() throws { + @Test func decodingTooManyCharactersError() throws { // Try a plist with too many characters to be a unicode escape sequence - let plist = "emoji".data(using: String._Encoding.utf8)! - - XCTAssertThrowsError(try PropertyListDecoder().decode([String:String].self, from: plist)) + let plist = "emoji".data(using: .utf8)! + #expect(throws: (any Error).self) { + try PropertyListDecoder().decode([String:String].self, from: plist) + } // Try a plist with an invalid unicode escape sequence - let plist2 = "emoji".data(using: String._Encoding.utf8)! + let plist2 = "emoji".data(using: .utf8)! - XCTAssertThrowsError(try PropertyListDecoder().decode([String:String].self, from: plist2)) + #expect(throws: (any Error).self) { + try PropertyListDecoder().decode([String:String].self, from: plist2) + } } - func test_roundTripEmoji() throws { + @Test func roundTripEmoji() throws { let strings = ["🚘", "👩🏻‍❤️‍👨🏿", "🏋🏽‍♂️🕺🏼🥌"] _testRoundTrip(of: strings, in: .xml) _testRoundTrip(of: strings, in: .binary) } - func test_roundTripEscapedStrings() { + @Test func roundTripEscapedStrings() { let strings = ["&", "<", ">"] _testRoundTrip(of: strings, in: .xml) } - func test_unterminatedComment() { - let plist = "".data(using: String._Encoding.utf8)! - XCTAssertThrowsError(try PropertyListDecoder().decode([String].self, from: plist)) - } + @Test func unterminatedComment() { + let plist = "".data(using: .utf8)! + #expect(throws: (any Error).self) { + try PropertyListDecoder().decode([String].self, from: plist) +} } - func test_incompleteOpenTag() { - let plist = ".allNils) let testEmptyDict = try PropertyListDecoder().decode(DecodeIfPresentAllTypes.self, from: emptyDictEncoding) - XCTAssertEqual(testEmptyDict, .allNils) + #expect(testEmptyDict == .allNils) let allNullDictEncoding = try encoder.encode(DecodeIfPresentAllTypes.allNils) let testAllNullDict = try PropertyListDecoder().decode(DecodeIfPresentAllTypes.self, from: allNullDictEncoding) - XCTAssertEqual(testAllNullDict, .allNils) + #expect(testAllNullDict == .allNils) let allOnesDictEncoding = try encoder.encode(DecodeIfPresentAllTypes.allOnes) let testAllOnesDict = try PropertyListDecoder().decode(DecodeIfPresentAllTypes.self, from: allOnesDictEncoding) - XCTAssertEqual(testAllOnesDict, .allOnes) + #expect(testAllOnesDict == .allOnes) let emptyArrayEncoding = try encoder.encode(DecodeIfPresentAllTypes.allNils) let testEmptyArray = try PropertyListDecoder().decode(DecodeIfPresentAllTypes.self, from: emptyArrayEncoding) - XCTAssertEqual(testEmptyArray, .allNils) + #expect(testEmptyArray == .allNils) let allNullArrayEncoding = try encoder.encode(DecodeIfPresentAllTypes.allNils) let testAllNullArray = try PropertyListDecoder().decode(DecodeIfPresentAllTypes.self, from: allNullArrayEncoding) - XCTAssertEqual(testAllNullArray, .allNils) + #expect(testAllNullArray == .allNils) let allOnesArrayEncoding = try encoder.encode(DecodeIfPresentAllTypes.allOnes) let testAllOnesArray = try PropertyListDecoder().decode(DecodeIfPresentAllTypes.self, from: allOnesArrayEncoding) - XCTAssertEqual(testAllOnesArray, .allOnes) + #expect(testAllOnesArray == .allOnes) } } - func test_garbageCharactersAfterXMLTagName() throws { - let garbage = "barfoo".data(using: String._Encoding.utf8)! - - XCTAssertThrowsError(try PropertyListDecoder().decode([String:String].self, from: garbage)) + @Test func garbageCharactersAfterXMLTagName() throws { + let garbage = "barfoo".data(using: .utf8)! + #expect(throws: (any Error).self) { + try PropertyListDecoder().decode([String:String].self, from: garbage) + } // Historical behavior allows for whitespace to immediately follow tag names - let acceptable = "barfoo".data(using: String._Encoding.utf8)! + let acceptable = "barfoo".data(using: .utf8)! - XCTAssertEqual(try PropertyListDecoder().decode([String:String].self, from: acceptable), ["bar":"foo"]) + #expect(try PropertyListDecoder().decode([String:String].self, from: acceptable) == ["bar":"foo"]) } } // MARK: - Helper Global Functions -func XCTAssertEqualPaths(_ lhs: [CodingKey], _ rhs: [CodingKey], _ prefix: String) { +func AssertEqualPaths(_ lhs: [CodingKey], _ rhs: [CodingKey], _ prefix: String, sourceLocation: SourceLocation = #_sourceLocation) { if lhs.count != rhs.count { - XCTFail("\(prefix) [CodingKey].count mismatch: \(lhs.count) != \(rhs.count)") + Issue.record("\(prefix) [CodingKey].count mismatch: \(lhs.count) != \(rhs.count)", sourceLocation: sourceLocation) return } @@ -1570,21 +1594,21 @@ func XCTAssertEqualPaths(_ lhs: [CodingKey], _ rhs: [CodingKey], _ prefix: Strin switch (key1.intValue, key2.intValue) { case (.none, .none): break case (.some(let i1), .none): - XCTFail("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != nil") + Issue.record("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != nil", sourceLocation: sourceLocation) return case (.none, .some(let i2)): - XCTFail("\(prefix) CodingKey.intValue mismatch: nil != \(type(of: key2))(\(i2))") + Issue.record("\(prefix) CodingKey.intValue mismatch: nil != \(type(of: key2))(\(i2))", sourceLocation: sourceLocation) return case (.some(let i1), .some(let i2)): guard i1 == i2 else { - XCTFail("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != \(type(of: key2))(\(i2))") + Issue.record("\(prefix) CodingKey.intValue mismatch: \(type(of: key1))(\(i1)) != \(type(of: key2))(\(i2))", sourceLocation: sourceLocation) return } break } - XCTAssertEqual(key1.stringValue, key2.stringValue, "\(prefix) CodingKey.stringValue mismatch: \(type(of: key1))('\(key1.stringValue)') != \(type(of: key2))('\(key2.stringValue)')") + #expect(key1.stringValue == key2.stringValue, "\(prefix) CodingKey.stringValue mismatch: \(type(of: key1))('\(key1.stringValue)') != \(type(of: key2))('\(key2.stringValue)')", sourceLocation: sourceLocation) } } @@ -1927,13 +1951,13 @@ private struct NestedContainersTestType : Encodable { func encode(to encoder: Encoder) throws { if self.testSuperEncoder { var topLevelContainer = encoder.container(keyedBy: TopLevelCodingKeys.self) - XCTAssertEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.") - XCTAssertEqualPaths(topLevelContainer.codingPath, [], "New first-level keyed container has non-empty codingPath.") + AssertEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.") + AssertEqualPaths(topLevelContainer.codingPath, [], "New first-level keyed container has non-empty codingPath.") let superEncoder = topLevelContainer.superEncoder(forKey: .a) - XCTAssertEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.") - XCTAssertEqualPaths(topLevelContainer.codingPath, [], "First-level keyed container's codingPath changed.") - XCTAssertEqualPaths(superEncoder.codingPath, [TopLevelCodingKeys.a], "New superEncoder had unexpected codingPath.") + AssertEqualPaths(encoder.codingPath, [], "Top-level Encoder's codingPath changed.") + AssertEqualPaths(topLevelContainer.codingPath, [], "First-level keyed container's codingPath changed.") + AssertEqualPaths(superEncoder.codingPath, [TopLevelCodingKeys.a], "New superEncoder had unexpected codingPath.") _testNestedContainers(in: superEncoder, baseCodingPath: [TopLevelCodingKeys.a]) } else { _testNestedContainers(in: encoder, baseCodingPath: []) @@ -1941,57 +1965,57 @@ private struct NestedContainersTestType : Encodable { } func _testNestedContainers(in encoder: Encoder, baseCodingPath: [CodingKey]) { - XCTAssertEqualPaths(encoder.codingPath, baseCodingPath, "New encoder has non-empty codingPath.") + AssertEqualPaths(encoder.codingPath, baseCodingPath, "New encoder has non-empty codingPath.") // codingPath should not change upon fetching a non-nested container. var firstLevelContainer = encoder.container(keyedBy: TopLevelCodingKeys.self) - XCTAssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - XCTAssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "New first-level keyed container has non-empty codingPath.") + AssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") + AssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "New first-level keyed container has non-empty codingPath.") // Nested Keyed Container do { // Nested container for key should have a new key pushed on. var secondLevelContainer = firstLevelContainer.nestedContainer(keyedBy: IntermediateCodingKeys.self, forKey: .a) - XCTAssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - XCTAssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") - XCTAssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], "New second-level keyed container had unexpected codingPath.") + AssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") + AssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") + AssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], "New second-level keyed container had unexpected codingPath.") // Inserting a keyed container should not change existing coding paths. let thirdLevelContainerKeyed = secondLevelContainer.nestedContainer(keyedBy: IntermediateCodingKeys.self, forKey: .one) - XCTAssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - XCTAssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") - XCTAssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], "Second-level keyed container's codingPath changed.") - XCTAssertEqualPaths(thirdLevelContainerKeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.one], "New third-level keyed container had unexpected codingPath.") + AssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") + AssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") + AssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], "Second-level keyed container's codingPath changed.") + AssertEqualPaths(thirdLevelContainerKeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.one], "New third-level keyed container had unexpected codingPath.") // Inserting an unkeyed container should not change existing coding paths. let thirdLevelContainerUnkeyed = secondLevelContainer.nestedUnkeyedContainer(forKey: .two) - XCTAssertEqualPaths(encoder.codingPath, baseCodingPath + [], "Top-level Encoder's codingPath changed.") - XCTAssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath + [], "First-level keyed container's codingPath changed.") - XCTAssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], "Second-level keyed container's codingPath changed.") - XCTAssertEqualPaths(thirdLevelContainerUnkeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.two], "New third-level unkeyed container had unexpected codingPath.") + AssertEqualPaths(encoder.codingPath, baseCodingPath + [], "Top-level Encoder's codingPath changed.") + AssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath + [], "First-level keyed container's codingPath changed.") + AssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.a], "Second-level keyed container's codingPath changed.") + AssertEqualPaths(thirdLevelContainerUnkeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.a, IntermediateCodingKeys.two], "New third-level unkeyed container had unexpected codingPath.") } // Nested Unkeyed Container do { // Nested container for key should have a new key pushed on. var secondLevelContainer = firstLevelContainer.nestedUnkeyedContainer(forKey: .b) - XCTAssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - XCTAssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") - XCTAssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], "New second-level keyed container had unexpected codingPath.") + AssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") + AssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") + AssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], "New second-level keyed container had unexpected codingPath.") // Appending a keyed container should not change existing coding paths. let thirdLevelContainerKeyed = secondLevelContainer.nestedContainer(keyedBy: IntermediateCodingKeys.self) - XCTAssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - XCTAssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") - XCTAssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], "Second-level unkeyed container's codingPath changed.") - XCTAssertEqualPaths(thirdLevelContainerKeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 0)], "New third-level keyed container had unexpected codingPath.") + AssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") + AssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") + AssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], "Second-level unkeyed container's codingPath changed.") + AssertEqualPaths(thirdLevelContainerKeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 0)], "New third-level keyed container had unexpected codingPath.") // Appending an unkeyed container should not change existing coding paths. let thirdLevelContainerUnkeyed = secondLevelContainer.nestedUnkeyedContainer() - XCTAssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") - XCTAssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") - XCTAssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], "Second-level unkeyed container's codingPath changed.") - XCTAssertEqualPaths(thirdLevelContainerUnkeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 1)], "New third-level unkeyed container had unexpected codingPath.") + AssertEqualPaths(encoder.codingPath, baseCodingPath, "Top-level Encoder's codingPath changed.") + AssertEqualPaths(firstLevelContainer.codingPath, baseCodingPath, "First-level keyed container's codingPath changed.") + AssertEqualPaths(secondLevelContainer.codingPath, baseCodingPath + [TopLevelCodingKeys.b], "Second-level unkeyed container's codingPath changed.") + AssertEqualPaths(thirdLevelContainerUnkeyed.codingPath, baseCodingPath + [TopLevelCodingKeys.b, _TestKey(index: 1)], "New third-level unkeyed container had unexpected codingPath.") } } } From 90ef7f1bccf295ff6e551e0c103dcb245f7b235a Mon Sep 17 00:00:00 2001 From: Tina L <49205802+itingliu@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:48:13 -0700 Subject: [PATCH 3/9] Implement Locale.Region category filtering methods (#1253) --- .../Locale/Locale+Components_ICU.swift | 178 +++++++++++++++++- .../LocaleRegionTests.swift | 56 ++++++ 2 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 Tests/FoundationInternationalizationTests/LocaleRegionTests.swift diff --git a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift index 6aaf8f5e9..b257b65a8 100644 --- a/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift +++ b/Sources/FoundationInternationalization/Locale/Locale+Components_ICU.swift @@ -263,7 +263,7 @@ extension Locale.Region { internal static let _isoRegionCodes: [String] = { var status = U_ZERO_ERROR - let types = [URGN_WORLD, URGN_CONTINENT, URGN_SUBCONTINENT, URGN_TERRITORY] + let types = [URGN_WORLD, URGN_CONTINENT, URGN_SUBCONTINENT, URGN_TERRITORY, URGN_GROUPING] var codes: [String] = [] for t in types { status = U_ZERO_ERROR @@ -275,6 +275,182 @@ extension Locale.Region { } return codes }() + + /// Categories of a region. See https://www.unicode.org/reports/tr35/tr35-35/tr35-info.html#Territory_Data + @available(FoundationPreview 6.2, *) + public struct Category: Codable, Sendable, Hashable, CustomDebugStringConvertible { + public var debugDescription: String { + switch inner { + case .world: + return "world" + case .continent: + return "continent" + case .subcontinent: + return "subcontinent" + case .territory: + return "territory" + case .grouping: + return "grouping" + } + } + + enum Inner { + case world + case continent + case subcontinent + case territory + case grouping + } + + var inner: Inner + fileprivate init(_ inner: Inner) { + self.inner = inner + } + + var uregionType: URegionType { + switch inner { + case .world: + return URGN_WORLD + case .continent: + return URGN_CONTINENT + case .subcontinent: + return URGN_SUBCONTINENT + case .territory: + return URGN_TERRITORY + case .grouping: + return URGN_GROUPING + } + } + + fileprivate init?(uregionType: URegionType) { + switch uregionType { + case URGN_CONTINENT: + self = .init(.continent) + case URGN_WORLD: + self = .init(.world) + case URGN_SUBCONTINENT: + self = .init(.subcontinent) + case URGN_TERRITORY: + self = .init(.territory) + case URGN_GROUPING: + self = .init(.grouping) + default: + return nil + } + } + + /// Category representing the whold world. + public static let world: Category = Category(.world) + + /// Category representing a continent, regions contained directly by world. + public static let continent: Category = Category(.continent) + + /// Category representing a sub-continent, regions contained directly by a continent. + public static let subcontinent: Category = Category(.subcontinent) + + /// Category representing a territory. + public static let territory: Category = Category(.territory) + + /// Category representing a grouping, regions that has a well defined membership. + public static let grouping: Category = Category(.grouping) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let inner: Inner + switch try container.decode(Int.self) { + case 0: + inner = .world + case 1: + inner = .continent + case 2: + inner = .subcontinent + case 3: + inner = .territory + case 4: + inner = .grouping + default: + throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Unknown Category")) + } + self = .init(inner) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch inner { + case .world: + try container.encode(0) + case .continent: + try container.encode(1) + case .subcontinent: + try container.encode(2) + case .territory: + try container.encode(3) + case .grouping: + try container.encode(4) + + } + } + } + + /// An array of regions matching the specified categories. + @available(FoundationPreview 6.2, *) + public static func isoRegions(ofCategory category: Category) -> [Locale.Region] { + var status = U_ZERO_ERROR + let values = uregion_getAvailable(category.uregionType, &status) + guard let values, status.isSuccess else { + return [] + } + return ICU.Enumerator(enumerator: values).elements.map { Locale.Region($0) } + } + + /// The category of the region. + @available(FoundationPreview 6.2, *) + public var category: Category? { + var status = U_ZERO_ERROR + let icuRegion = uregion_getRegionFromCode(identifier, &status) + guard status.isSuccess, let icuRegion else { + return nil + } + let type = uregion_getType(icuRegion) + return Category(uregionType: type) + } + + /// An array of the sub-regions, matching the specified category of the region. + @available(FoundationPreview 6.2, *) + public func subRegions(ofCategoy category: Category) -> [Locale.Region] { + var status = U_ZERO_ERROR + let icuRegion = uregion_getRegionFromCode(identifier, &status) + guard let icuRegion, status.isSuccess else { + return [] + } + + status = U_ZERO_ERROR + let enumerator = uregion_getContainedRegionsOfType(icuRegion, category.uregionType, &status) + guard let enumerator, status.isSuccess else { + return [] + } + return ICU.Enumerator(enumerator: enumerator).elements.map { Locale.Region($0) } + } + + /// The subcontinent that contains this region, if any. + @available(FoundationPreview 6.2, *) + public var subcontinent: Locale.Region? { + var status = U_ZERO_ERROR + let icuRegion = uregion_getRegionFromCode(identifier, &status) + guard let icuRegion, status.isSuccess else { + return nil + } + + guard let containing = uregion_getContainingRegionOfType(icuRegion, URGN_SUBCONTINENT) else { + return nil + } + + guard let code = String(validatingCString: uregion_getRegionCode(containing)) else { + return nil + } + + return Locale.Region(code) + } } @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) diff --git a/Tests/FoundationInternationalizationTests/LocaleRegionTests.swift b/Tests/FoundationInternationalizationTests/LocaleRegionTests.swift new file mode 100644 index 000000000..db475c916 --- /dev/null +++ b/Tests/FoundationInternationalizationTests/LocaleRegionTests.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing + +#if FOUNDATION_FRAMEWORK +import Foundation +#else +import FoundationEssentials +import FoundationInternationalization +#endif + +@Suite("Locale.Region Tests") +struct LocaleRegionTests { + @Test func regionCategory() async throws { + #expect(Locale.Region.unknown.category == nil) + #expect(Locale.Region.world.category == .world) + #expect(Locale.Region.unitedStates.category == .territory) + #expect(Locale.Region("EU").category == .grouping) + #expect(Locale.Region("not a region").category == nil) + + let africa = Locale.Region("002") + #expect(africa.category == .continent) + + let continentOfSpain = try #require(Locale.Region.spain.continent) + #expect(continentOfSpain.category == .continent) + } + + @Test func subcontinent() async throws { + #expect(Locale.Region.unknown.subcontinent == nil) + #expect(Locale.Region.world.subcontinent == nil) + #expect(Locale.Region("not a region").subcontinent == nil) + #expect(Locale.Region.argentina.subcontinent == Locale.Region("005")) + } + + @Test func subRegionOfCategory() async throws { + #expect(Locale.Region.unknown.subRegions(ofCategoy: .world) == []) + #expect(Locale.Region.unknown.subRegions(ofCategoy: .territory) == []) + + #expect(Set(Locale.Region.world.subRegions(ofCategoy: .continent)) == Set(Locale.Region.isoRegions(ofCategory: .continent))) + + #expect(Locale.Region.argentina.subRegions(ofCategoy: .continent) == []) + #expect(Locale.Region.argentina.subRegions(ofCategoy: .territory) == Locale.Region.argentina.subRegions) + + #expect(Locale.Region("not a region").subRegions(ofCategoy: .territory) == []) + } +} From 1c53883abb6d90d3dd04b0b58ff9132b5591373c Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Tue, 24 Jun 2025 11:00:08 -0700 Subject: [PATCH 4/9] Avoid passing (U)Int128 as test args (#1376) * Avoid passing (U)Int128 as test args * Fix build issue --- .../JSONEncoderTests.swift | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift index 6fc131ba6..466dcceff 100644 --- a/Tests/FoundationEssentialsTests/JSONEncoderTests.swift +++ b/Tests/FoundationEssentialsTests/JSONEncoderTests.swift @@ -1393,21 +1393,20 @@ private struct JSONEncoderTests { _testRoundTrip(of: testValue) } - @Test(arguments: [ - Int128.min, - Int128.min + 1, - -0x1_0000_0000_0000_0000, - 0x0_8000_0000_0000_0000, - -1, - 0, - 0x7fff_ffff_ffff_ffff, - 0x8000_0000_0000_0000, - 0xffff_ffff_ffff_ffff, - 0x1_0000_0000_0000_0000, - .max - ]) - func roundTrippingInt128(i128: Int128) { - _testRoundTrip(of: i128) + @Test func roundTrippingInt128() { + for i128 in [Int128.min, + Int128.min + 1, + -0x1_0000_0000_0000_0000, + 0x0_8000_0000_0000_0000, + -1, + 0, + 0x7fff_ffff_ffff_ffff, + 0x8000_0000_0000_0000, + 0xffff_ffff_ffff_ffff, + 0x1_0000_0000_0000_0000, + .max] { + _testRoundTrip(of: i128) + } } @Test func int128SlowPath() throws { @@ -1434,19 +1433,18 @@ private struct JSONEncoderTests { } } - @Test(arguments: [ - UInt128.zero, - 1, - 0x0000_0000_0000_0000_7fff_ffff_ffff_ffff, - 0x0000_0000_0000_0000_8000_0000_0000_0000, - 0x0000_0000_0000_0000_ffff_ffff_ffff_ffff, - 0x0000_0000_0000_0001_0000_0000_0000_0000, - 0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_ffff, - 0x8000_0000_0000_0000_0000_0000_0000_0000, - .max - ]) - func roundTrippingUInt128(u128: UInt128) { - _testRoundTrip(of: u128) + @Test func roundTrippingUInt128() { + for u128 in [UInt128.zero, + 1, + 0x0000_0000_0000_0000_7fff_ffff_ffff_ffff, + 0x0000_0000_0000_0000_8000_0000_0000_0000, + 0x0000_0000_0000_0000_ffff_ffff_ffff_ffff, + 0x0000_0000_0000_0001_0000_0000_0000_0000, + 0x7fff_ffff_ffff_ffff_ffff_ffff_ffff_ffff, + 0x8000_0000_0000_0000_0000_0000_0000_0000, + .max] { + _testRoundTrip(of: u128) + } } @Test func uint128SlowPath() throws { From 0c0db2069e7d720d91a8ed52f4bc53be96493138 Mon Sep 17 00:00:00 2001 From: Jonathan Flat <50605158+jrflat@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:36:15 -0600 Subject: [PATCH 5/9] (153668328) File URLs created using a base URL which contains either ? or # return truncated URL.path (#1378) --- .../FoundationEssentials/URL/URL_Swift.swift | 18 +++--- .../FoundationEssentialsTests/URLTests.swift | 60 +++++++++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/Sources/FoundationEssentials/URL/URL_Swift.swift b/Sources/FoundationEssentials/URL/URL_Swift.swift index e694e7c0a..3b0559187 100644 --- a/Sources/FoundationEssentials/URL/URL_Swift.swift +++ b/Sources/FoundationEssentials/URL/URL_Swift.swift @@ -299,16 +299,18 @@ internal final class _SwiftURL: Sendable, Hashable, Equatable { return builder.string } let baseParseInfo = baseURL._swiftURL?._parseInfo - let baseEncodedComponents = baseParseInfo?.encodedComponents ?? [] - if let baseUser = baseURL.user(percentEncoded: !baseEncodedComponents.contains(.user)) { + // If we aren't in the special case where we need the original + // string, always leave the base components encoded. + let baseComponentsToDecode = !original ? [] : baseParseInfo?.encodedComponents ?? [] + if let baseUser = baseURL.user(percentEncoded: !baseComponentsToDecode.contains(.user)) { builder.user = baseUser } - if let basePassword = baseURL.password(percentEncoded: !baseEncodedComponents.contains(.password)) { + if let basePassword = baseURL.password(percentEncoded: !baseComponentsToDecode.contains(.password)) { builder.password = basePassword } if let baseHost = baseParseInfo?.host { - builder.host = baseEncodedComponents.contains(.host) && baseParseInfo!.didPercentEncodeHost ? Parser.percentDecode(baseHost) : String(baseHost) - } else if let baseHost = baseURL.host(percentEncoded: !baseEncodedComponents.contains(.host)) { + builder.host = baseComponentsToDecode.contains(.host) && baseParseInfo!.didPercentEncodeHost ? Parser.percentDecode(baseHost) : String(baseHost) + } else if let baseHost = baseURL.host(percentEncoded: !baseComponentsToDecode.contains(.host)) { builder.host = baseHost } if let basePort = baseParseInfo?.portString { @@ -317,8 +319,8 @@ internal final class _SwiftURL: Sendable, Hashable, Equatable { builder.portString = String(basePort) } if builder.path.isEmpty { - builder.path = baseURL.path(percentEncoded: !baseEncodedComponents.contains(.path)) - if builder.query == nil, let baseQuery = baseURL.query(percentEncoded: !baseEncodedComponents.contains(.query)) { + builder.path = baseURL.path(percentEncoded: !baseComponentsToDecode.contains(.path)) + if builder.query == nil, let baseQuery = baseURL.query(percentEncoded: !baseComponentsToDecode.contains(.query)) { builder.query = baseQuery } } else { @@ -327,7 +329,7 @@ internal final class _SwiftURL: Sendable, Hashable, Equatable { } else if baseURL.hasAuthority && baseURL.path().isEmpty { "/" + builder.path } else { - baseURL.path(percentEncoded: !baseEncodedComponents.contains(.path)).merging(relativePath: builder.path) + baseURL.path(percentEncoded: !baseComponentsToDecode.contains(.path)).merging(relativePath: builder.path) } builder.path = newPath.removingDotSegments } diff --git a/Tests/FoundationEssentialsTests/URLTests.swift b/Tests/FoundationEssentialsTests/URLTests.swift index 8c49e3bcf..cb8a92be1 100644 --- a/Tests/FoundationEssentialsTests/URLTests.swift +++ b/Tests/FoundationEssentialsTests/URLTests.swift @@ -727,6 +727,66 @@ private struct URLTests { #expect(schemeRelative.relativePath == "") } + @Test func deletingLastPathComponentWithBase() throws { + let basePath = "/Users/foo-bar/Test1 Test2? Test3/Test4" + let baseURL = URL(filePath: basePath, directoryHint: .isDirectory) + let fileURL = URL(filePath: "../Test5.txt", directoryHint: .notDirectory, relativeTo: baseURL) + #expect(fileURL.path == "/Users/foo-bar/Test1 Test2? Test3/Test5.txt") + #expect(fileURL.deletingLastPathComponent().path == "/Users/foo-bar/Test1 Test2? Test3") + #expect(baseURL.deletingLastPathComponent().path == "/Users/foo-bar/Test1 Test2? Test3") + } + + @Test func encodedAbsoluteString() throws { + let base = URL(string: "http://user name:pass word@😂😂😂.com/pa th/p?qu ery#frag ment") + #expect(base?.absoluteString == "http://user%20name:pass%20word@xn--g28haa.com/pa%20th/p?qu%20ery#frag%20ment") + var url = URL(string: "relative", relativeTo: base) + #expect(url?.absoluteString == "http://user%20name:pass%20word@xn--g28haa.com/pa%20th/relative") + url = URL(string: "rela tive", relativeTo: base) + #expect(url?.absoluteString == "http://user%20name:pass%20word@xn--g28haa.com/pa%20th/rela%20tive") + url = URL(string: "relative?qu", relativeTo: base) + #expect(url?.absoluteString == "http://user%20name:pass%20word@xn--g28haa.com/pa%20th/relative?qu") + url = URL(string: "rela tive?q u", relativeTo: base) + #expect(url?.absoluteString == "http://user%20name:pass%20word@xn--g28haa.com/pa%20th/rela%20tive?q%20u") + + let fileBase = URL(filePath: "/Users/foo bar/more spaces/") + #expect(fileBase.absoluteString == "file:///Users/foo%20bar/more%20spaces/") + + url = URL(string: "relative", relativeTo: fileBase) + #expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative") + #expect(url?.path == "/Users/foo bar/more spaces/relative") + + url = URL(string: "rela tive", relativeTo: fileBase) + #expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive") + #expect(url?.path == "/Users/foo bar/more spaces/rela tive") + + // URL(string:) should count ? as the query delimiter + url = URL(string: "relative?query", relativeTo: fileBase) + #expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative?query") + #expect(url?.path == "/Users/foo bar/more spaces/relative") + + url = URL(string: "rela tive?qu ery", relativeTo: fileBase) + #expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive?qu%20ery") + #expect(url?.path == "/Users/foo bar/more spaces/rela tive") + + // URL(filePath:) should encode ? as part of the path + url = URL(filePath: "relative?query", relativeTo: fileBase) + #expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative%3Fquery") + #expect(url?.path == "/Users/foo bar/more spaces/relative?query") + + url = URL(filePath: "rela tive?qu ery", relativeTo: fileBase) + #expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive%3Fqu%20ery") + #expect(url?.path == "/Users/foo bar/more spaces/rela tive?qu ery") + + // URL(filePath:) should encode %3F as part of the path + url = URL(filePath: "relative%3Fquery", relativeTo: fileBase) + #expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/relative%253Fquery") + #expect(url?.path == "/Users/foo bar/more spaces/relative%3Fquery") + + url = URL(filePath: "rela tive%3Fqu ery", relativeTo: fileBase) + #expect(url?.absoluteString == "file:///Users/foo%20bar/more%20spaces/rela%20tive%253Fqu%20ery") + #expect(url?.path == "/Users/foo bar/more spaces/rela tive%3Fqu ery") + } + @Test func filePathDropsTrailingSlashes() throws { var url = URL(filePath: "/path/slashes///") #expect(url.path() == "/path/slashes///") From a6d57b749e7a65406cdfa37837997190cbb83f9a Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Wed, 25 Jun 2025 12:44:21 -0700 Subject: [PATCH 6/9] Convert FoundationEssentials formatting tests to swift-testing (#1359) --- .../TimeZone/TimeZone.swift | 9 - .../TimeZone/TimeZone_ObjC.swift | 2 +- .../BinaryInteger+FormatStyleTests.swift | 80 +++-- .../HTTPFormatStyleFormattingTests.swift | 178 ++++++----- .../ISO8601FormatStyleFormattingTests.swift | 152 +++++----- .../ISO8601FormatStyleParsingTests.swift | 281 +++++++++--------- .../CalendarTests.swift | 2 +- ...tyleInternationalizationParsingTests.swift | 32 ++ 8 files changed, 371 insertions(+), 365 deletions(-) create mode 100644 Tests/FoundationInternationalizationTests/Formatting/ISO8601FormatStyleInternationalizationParsingTests.swift diff --git a/Sources/FoundationEssentials/TimeZone/TimeZone.swift b/Sources/FoundationEssentials/TimeZone/TimeZone.swift index ef03f4c5c..c28f7abcd 100644 --- a/Sources/FoundationEssentials/TimeZone/TimeZone.swift +++ b/Sources/FoundationEssentials/TimeZone/TimeZone.swift @@ -71,15 +71,6 @@ public struct TimeZone : Hashable, Equatable, Sendable { } } - internal init?(name: String) { - // Try the cache first - if let cached = TimeZoneCache.cache.fixed(name) { - _tz = cached - } else { - return nil - } - } - /// Returns a time zone identified by a given abbreviation. /// /// In general, you are discouraged from using abbreviations except for unique instances such as "GMT". Time Zone abbreviations are not standardized and so a given abbreviation may have multiple meanings--for example, "EST" refers to Eastern Time in both the United States and Australia diff --git a/Sources/FoundationInternationalization/TimeZone/TimeZone_ObjC.swift b/Sources/FoundationInternationalization/TimeZone/TimeZone_ObjC.swift index 67d24c598..a4138812b 100644 --- a/Sources/FoundationInternationalization/TimeZone/TimeZone_ObjC.swift +++ b/Sources/FoundationInternationalization/TimeZone/TimeZone_ObjC.swift @@ -23,7 +23,7 @@ extension NSTimeZone { static func _timeZoneWith(name: String, data: Data?) -> _NSSwiftTimeZone? { if let data { // We don't cache data-based TimeZones - guard let tz = TimeZone(name: name) else { + guard let tz = TimeZone(identifier: name) else { return nil } return _NSSwiftTimeZone(timeZone: tz, data: data) diff --git a/Tests/FoundationEssentialsTests/Formatting/BinaryInteger+FormatStyleTests.swift b/Tests/FoundationEssentialsTests/Formatting/BinaryInteger+FormatStyleTests.swift index 76cfd50e5..20982c43b 100644 --- a/Tests/FoundationEssentialsTests/Formatting/BinaryInteger+FormatStyleTests.swift +++ b/Tests/FoundationEssentialsTests/Formatting/BinaryInteger+FormatStyleTests.swift @@ -5,17 +5,12 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// -// -// RUN: %target-run-simple-swift -// REQUIRES: executable_test -import XCTest +import Testing #if canImport(FoundationEssentials) @testable import FoundationEssentials -#endif - -#if FOUNDATION_FRAMEWORK +#else @testable import Foundation #endif @@ -27,16 +22,17 @@ import Numberick import BigInt #endif -final class BinaryIntegerFormatStyleTests: XCTestCase { +@Suite("BinaryIntegerFormatStyle") +private struct BinaryIntegerFormatStyleTests { // NSR == numericStringRepresentation - func checkNSR(value: some BinaryInteger, expected: String) { - XCTAssertEqual(String(decoding: value.numericStringRepresentation.utf8, as: Unicode.ASCII.self), expected) + func checkNSR(value: some BinaryInteger, expected: String, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(String(decoding: value.numericStringRepresentation.utf8, as: Unicode.ASCII.self) == expected) } - func testNumericStringRepresentation_builtinIntegersLimits() throws { - func check(type: I.Type = I.self, min: String, max: String) { - checkNSR(value: I.min, expected: min) - checkNSR(value: I.max, expected: max) + @Test func numericStringRepresentation_builtinIntegersLimits() throws { + func check(type: I.Type = I.self, min: String, max: String, sourceLocation: SourceLocation = #_sourceLocation) { + checkNSR(value: I.min, expected: min, sourceLocation: sourceLocation) + checkNSR(value: I.max, expected: max, sourceLocation: sourceLocation) } check(type: Int8.self, min: "-128", max: "127") @@ -52,13 +48,13 @@ final class BinaryIntegerFormatStyleTests: XCTestCase { check(type: UInt128.self, min: "0", max: "340282366920938463463374607431768211455") } - func testNumericStringRepresentation_builtinIntegersAroundDecimalMagnitude() throws { - func check(type: I.Type = I.self, magnitude: String, oneLess: String, oneMore: String) { + @Test func numericStringRepresentation_builtinIntegersAroundDecimalMagnitude() throws { + func check(type: I.Type = I.self, magnitude: String, oneLess: String, oneMore: String, sourceLocation: SourceLocation = #_sourceLocation) { var mag = I(1); while !mag.multipliedReportingOverflow(by: 10).overflow { mag *= 10 } - checkNSR(value: mag, expected: magnitude) - checkNSR(value: mag - 1, expected: oneLess) - checkNSR(value: mag + 1, expected: oneMore) + checkNSR(value: mag, expected: magnitude, sourceLocation: sourceLocation) + checkNSR(value: mag - 1, expected: oneLess, sourceLocation: sourceLocation) + checkNSR(value: mag + 1, expected: oneMore, sourceLocation: sourceLocation) } check(type: Int8.self, magnitude: "100", oneLess: "99", oneMore: "101") @@ -105,14 +101,14 @@ final class BinaryIntegerFormatStyleTests: XCTestCase { String(repeating: "1234567890", count: 10), String(repeating: "1234567890", count: 100)] { if let value = initialiser(valueAsString) { // The test cases cover a wide range of values, that don't all fit into every type tested (i.e. the fixed-width types from Numberick). - XCTAssertEqual(value.description, valueAsString) // Sanity check that it initialised from the string correctly. + #expect(value.description == valueAsString) // Sanity check that it initialised from the string correctly. checkNSR(value: value, expected: valueAsString) if I.isSigned { let negativeValueAsString = "-" + valueAsString let negativeValue = initialiser(negativeValueAsString)! - XCTAssertEqual(negativeValue.description, negativeValueAsString) // Sanity check that it initialised from the string correctly. + #expect(negativeValue.description == negativeValueAsString) // Sanity check that it initialised from the string correctly. checkNSR(value: negativeValue, expected: negativeValueAsString) } } @@ -120,7 +116,7 @@ final class BinaryIntegerFormatStyleTests: XCTestCase { } #if canImport(Numberick) - func testNumericStringRepresentation_largeIntegers() throws { + @Test func numericStringRepresentation_largeIntegers() throws { check(type: Int128.self, initialiser: { Int128($0) }) check(type: UInt128.self, initialiser: { UInt128($0) }) @@ -130,7 +126,7 @@ final class BinaryIntegerFormatStyleTests: XCTestCase { #endif #if canImport(BigInt) - func testNumericStringRepresentation_arbitraryPrecisionIntegers() throws { + @Test func numericStringRepresentation_arbitraryPrecisionIntegers() throws { check(type: BigInt.self, initialiser: { BigInt($0)! }) check(type: BigUInt.self, initialiser: { BigUInt($0)! }) } @@ -138,11 +134,11 @@ final class BinaryIntegerFormatStyleTests: XCTestCase { #endif // canImport(Numberick) || canImport(BigInt) } -final class BinaryIntegerFormatStyleTestsUsingBinaryIntegerWords: XCTestCase { +struct BinaryIntegerFormatStyleTestsUsingBinaryIntegerWords { // MARK: Tests - func testInt32() { + @Test func int32() { check( Int32(truncatingIfNeeded: 0x00000000 as UInt32), expectation: "0") check( Int32(truncatingIfNeeded: 0x03020100 as UInt32), expectation: "50462976") check( Int32(truncatingIfNeeded: 0x7fffffff as UInt32), expectation: "2147483647") // Int32.max @@ -152,7 +148,7 @@ final class BinaryIntegerFormatStyleTestsUsingBinaryIntegerWords: XCTestCase { check( Int32(truncatingIfNeeded: 0xffffffff as UInt32), expectation: "-1") } - func testUInt32() { + @Test func uint32() { check(UInt32(truncatingIfNeeded: 0x00000000 as UInt32), expectation: "0") // UInt32.min check(UInt32(truncatingIfNeeded: 0x03020100 as UInt32), expectation: "50462976") check(UInt32(truncatingIfNeeded: 0x7fffffff as UInt32), expectation: "2147483647") @@ -162,7 +158,7 @@ final class BinaryIntegerFormatStyleTestsUsingBinaryIntegerWords: XCTestCase { check(UInt32(truncatingIfNeeded: 0xffffffff as UInt32), expectation: "4294967295") // UInt32.max } - func testInt64() { + @Test func int64() { check( Int64(truncatingIfNeeded: 0x0000000000000000 as UInt64), expectation: "0") check( Int64(truncatingIfNeeded: 0x0706050403020100 as UInt64), expectation: "506097522914230528") check( Int64(truncatingIfNeeded: 0x7fffffffffffffff as UInt64), expectation: "9223372036854775807") // Int64.max @@ -172,7 +168,7 @@ final class BinaryIntegerFormatStyleTestsUsingBinaryIntegerWords: XCTestCase { check( Int64(truncatingIfNeeded: 0xffffffffffffffff as UInt64), expectation: "-1") } - func testUInt64() { + @Test func uint64() { check(UInt64(truncatingIfNeeded: 0x0000000000000000 as UInt64), expectation: "0") // UInt64.min check(UInt64(truncatingIfNeeded: 0x0706050403020100 as UInt64), expectation: "506097522914230528") check(UInt64(truncatingIfNeeded: 0x7fffffffffffffff as UInt64), expectation: "9223372036854775807") @@ -184,7 +180,7 @@ final class BinaryIntegerFormatStyleTestsUsingBinaryIntegerWords: XCTestCase { // MARK: Tests + Big Integer - func testInt128() { + @Test func int128() { check(x64:[0x0000000000000000, 0x0000000000000000] as [UInt64], isSigned: true, expectation: "0") check(x64:[0x0706050403020100, 0x0f0e0d0c0b0a0908] as [UInt64], isSigned: true, expectation: "20011376718272490338853433276725592320") check(x64:[0xffffffffffffffff, 0x7fffffffffffffff] as [UInt64], isSigned: true, expectation: "170141183460469231731687303715884105727") // Int128.max @@ -193,7 +189,7 @@ final class BinaryIntegerFormatStyleTestsUsingBinaryIntegerWords: XCTestCase { check(x64:[0xffffffffffffffff, 0xffffffffffffffff] as [UInt64], isSigned: true, expectation: "-1") } - func testUInt128() { + @Test func uint128() { check(x64:[0x0000000000000000, 0x0000000000000000] as [UInt64], isSigned: false, expectation: "0") // UInt128.min check(x64:[0x0706050403020100, 0x0f0e0d0c0b0a0908] as [UInt64], isSigned: false, expectation: "20011376718272490338853433276725592320") check(x64:[0x0000000000000000, 0x8000000000000000] as [UInt64], isSigned: false, expectation: "170141183460469231731687303715884105728") @@ -204,12 +200,12 @@ final class BinaryIntegerFormatStyleTestsUsingBinaryIntegerWords: XCTestCase { // MARK: Tests + Big Integer + Miscellaneous - func testWordsIsEmptyResultsInZero() { + @Test func wordsIsEmptyResultsInZero() { check(words:[ ] as [UInt], isSigned: true, expectation: "0") check(words:[ ] as [UInt], isSigned: false, expectation: "0") } - func testSignExtendingDoesNotChangeTheResult() { + @Test func signExtendingDoesNotChangeTheResult() { check(words:[ 0 ] as [UInt], isSigned: true, expectation: "0") check(words:[ 0, 0 ] as [UInt], isSigned: true, expectation: "0") check(words:[ 0, 0, 0 ] as [UInt], isSigned: true, expectation: "0") @@ -228,22 +224,22 @@ final class BinaryIntegerFormatStyleTestsUsingBinaryIntegerWords: XCTestCase { // MARK: Assertions - func check(_ integer: some BinaryInteger, expectation: String, file: StaticString = #filePath, line: UInt = #line) { - XCTAssertEqual(integer.description, expectation, "integer description does not match expectation", file: file, line: line) - check(ascii: integer.numericStringRepresentation.utf8, expectation: expectation, file: file, line: line) - check(words: Array(integer.words), isSigned: type(of: integer).isSigned, expectation: expectation, file: file, line: line) + func check(_ integer: some BinaryInteger, expectation: String, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(integer.description == expectation, "integer description does not match expectation", sourceLocation: sourceLocation) + check(ascii: integer.numericStringRepresentation.utf8, expectation: expectation, sourceLocation: sourceLocation) + check(words: Array(integer.words), isSigned: type(of: integer).isSigned, expectation: expectation, sourceLocation: sourceLocation) } - func check(x64: [UInt64], isSigned: Bool, expectation: String, file: StaticString = #filePath, line: UInt = #line) { - check(words: x64.flatMap(\.words), isSigned: isSigned, expectation: expectation, file: file, line: line) + func check(x64: [UInt64], isSigned: Bool, expectation: String, sourceLocation: SourceLocation = #_sourceLocation) { + check(words: x64.flatMap(\.words), isSigned: isSigned, expectation: expectation, sourceLocation: sourceLocation) } - func check(words: [UInt], isSigned: Bool, expectation: String, file: StaticString = #filePath, line: UInt = #line) { + func check(words: [UInt], isSigned: Bool, expectation: String, sourceLocation: SourceLocation = #_sourceLocation) { let ascii = numericStringRepresentationForBinaryInteger(words: words, isSigned: isSigned).utf8 - check(ascii: ascii, expectation: expectation, file: file, line: line) + check(ascii: ascii, expectation: expectation, sourceLocation: sourceLocation) } - func check(ascii: some Collection, expectation: String, file: StaticString = #filePath, line: UInt = #line) { - XCTAssertEqual(String(decoding: ascii, as: Unicode.ASCII.self), expectation, file: file, line: line) + func check(ascii: some Collection, expectation: String, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(String(decoding: ascii, as: Unicode.ASCII.self) == expectation, sourceLocation: sourceLocation) } } diff --git a/Tests/FoundationEssentialsTests/Formatting/HTTPFormatStyleFormattingTests.swift b/Tests/FoundationEssentialsTests/Formatting/HTTPFormatStyleFormattingTests.swift index 5d4b23a30..6db8bf07c 100644 --- a/Tests/FoundationEssentialsTests/Formatting/HTTPFormatStyleFormattingTests.swift +++ b/Tests/FoundationEssentialsTests/Formatting/HTTPFormatStyleFormattingTests.swift @@ -10,129 +10,125 @@ // //===----------------------------------------------------------------------===// -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if canImport(FoundationEssentials) -@testable import FoundationEssentials -#endif - -#if FOUNDATION_FRAMEWORK -@testable import Foundation +import FoundationEssentials +#else +import Foundation #endif -final class HTTPFormatStyleFormattingTests: XCTestCase { +@Suite("HTTPFormatStyle Formatting") +private struct HTTPFormatStyleFormattingTests { - func test_HTTPFormat() throws { + @Test func basics() throws { let date = Date.now let formatted = date.formatted(.http) // e.g. "Fri, 17 Jan 2025 19:03:05 GMT" - let parsed = try? Date(formatted, strategy: .http) + let parsed = try Date(formatted, strategy: .http) - let result = try XCTUnwrap(parsed) - XCTAssertEqual(date.timeIntervalSinceReferenceDate, result.timeIntervalSinceReferenceDate, accuracy: 1.0) + #expect(abs(date.timeIntervalSinceReferenceDate - parsed.timeIntervalSinceReferenceDate) <= 1.0) } - func test_HTTPFormat_components() throws { + @Test func components() throws { let date = Date.now let formatted = date.formatted(.http) // e.g. "Fri, 17 Jan 2025 19:03:05 GMT" - let parsed = try? DateComponents(formatted, strategy: .http) - - let result = try XCTUnwrap(parsed) + let parsed = try DateComponents(formatted, strategy: .http) - let resultDate = Calendar(identifier: .gregorian).date(from: result) - let resultDateUnwrapped = try XCTUnwrap(resultDate) + let resultDate = Calendar(identifier: .gregorian).date(from: parsed) + let resultDateUnwrapped = try #require(resultDate) - XCTAssertEqual(date.timeIntervalSinceReferenceDate, resultDateUnwrapped.timeIntervalSinceReferenceDate, accuracy: 1.0) + #expect(abs(date.timeIntervalSinceReferenceDate - resultDateUnwrapped.timeIntervalSinceReferenceDate) <= 1.0) } - func test_HTTPFormat_variousInputs() throws { - let tests = [ - "Mon, 20 Jan 2025 01:02:03 GMT", - "Tue, 20 Jan 2025 10:02:03 GMT", - "Wed, 20 Jan 2025 23:02:03 GMT", - "Thu, 20 Jan 2025 01:10:03 GMT", - "Fri, 20 Jan 2025 01:50:59 GMT", - "Sat, 20 Jan 2025 01:50:60 GMT", // 60 is valid, treated as 0 - "Sun, 20 Jan 2025 01:03:03 GMT", - "20 Jan 2025 01:02:03 GMT", // Missing weekdays is ok - "20 Jan 2025 10:02:03 GMT", - "20 Jan 2025 23:02:03 GMT", - "20 Jan 2025 01:10:03 GMT", - "20 Jan 2025 01:50:59 GMT", - "20 Jan 2025 01:50:60 GMT", - "20 Jan 2025 01:03:03 GMT", - "Mon, 20 Jan 2025 01:03:03 GMT", - "Mon, 03 Feb 2025 01:03:03 GMT", - "Mon, 03 Mar 2025 01:03:03 GMT", - "Mon, 14 Apr 2025 01:03:03 GMT", - "Mon, 05 May 2025 01:03:03 GMT", - "Mon, 21 Jul 2025 01:03:03 GMT", - "Mon, 04 Aug 2025 01:03:03 GMT", - "Mon, 22 Sep 2025 01:03:03 GMT", - "Mon, 30 Oct 2025 01:03:03 GMT", - "Mon, 24 Nov 2025 01:03:03 GMT", - "Mon, 22 Dec 2025 01:03:03 GMT", - "Tue, 29 Feb 2028 01:03:03 GMT", // leap day - ] - - for good in tests { - XCTAssertNotNil(try? Date(good, strategy: .http), "Input \(good) was nil") - XCTAssertNotNil(try? DateComponents(good, strategy: .http), "Input \(good) was nil") + @Test(arguments: [ + "Mon, 20 Jan 2025 01:02:03 GMT", + "Tue, 20 Jan 2025 10:02:03 GMT", + "Wed, 20 Jan 2025 23:02:03 GMT", + "Thu, 20 Jan 2025 01:10:03 GMT", + "Fri, 20 Jan 2025 01:50:59 GMT", + "Sat, 20 Jan 2025 01:50:60 GMT", // 60 is valid, treated as 0 + "Sun, 20 Jan 2025 01:03:03 GMT", + "20 Jan 2025 01:02:03 GMT", // Missing weekdays is ok + "20 Jan 2025 10:02:03 GMT", + "20 Jan 2025 23:02:03 GMT", + "20 Jan 2025 01:10:03 GMT", + "20 Jan 2025 01:50:59 GMT", + "20 Jan 2025 01:50:60 GMT", + "20 Jan 2025 01:03:03 GMT", + "Mon, 20 Jan 2025 01:03:03 GMT", + "Mon, 03 Feb 2025 01:03:03 GMT", + "Mon, 03 Mar 2025 01:03:03 GMT", + "Mon, 14 Apr 2025 01:03:03 GMT", + "Mon, 05 May 2025 01:03:03 GMT", + "Mon, 21 Jul 2025 01:03:03 GMT", + "Mon, 04 Aug 2025 01:03:03 GMT", + "Mon, 22 Sep 2025 01:03:03 GMT", + "Mon, 30 Oct 2025 01:03:03 GMT", + "Mon, 24 Nov 2025 01:03:03 GMT", + "Mon, 22 Dec 2025 01:03:03 GMT", + "Tue, 29 Feb 2028 01:03:03 GMT", // leap day + ]) + func variousInputs(good: String) { + #expect(throws: Never.self) { + try Date(good, strategy: .http) + } + #expect(throws: Never.self) { + try DateComponents(good, strategy: .http) } } - func test_HTTPFormat_badInputs() throws { - let tests = [ - "Xri, 17 Jan 2025 19:03:05 GMT", - "Fri, 17 Janu 2025 19:03:05 GMT", - "Fri, 17Jan 2025 19:03:05 GMT", - "Fri, 17 Xrz 2025 19:03:05 GMT", - "Fri, 17 Jan 2025 19:03:05", // missing GMT - "Fri, 1 Jan 2025 19:03:05 GMT", - "Fri, 17 Jan 2025 1:03:05 GMT", - "Fri, 17 Jan 2025 19:3:05 GMT", - "Fri, 17 Jan 2025 19:03:5 GMT", - "Fri, 17 Jan 2025 19:03:05 GmT", - "Fri, 17 Jan 20252 19:03:05 GMT", - "Fri, 17 Jan 252 19:03:05 GMT", - "fri, 17 Jan 2025 19:03:05 GMT", // miscapitalized - "Fri, 17 jan 2025 19:03:05 GMT", - "Fri, 16 Jan 2025 25:03:05 GMT", // nonsense date - "Fri, 30 Feb 2025 25:03:05 GMT", // nonsense date - ] - - for bad in tests { - XCTAssertNil(try? Date(bad, strategy: .http), "Input \(bad) was not nil") - XCTAssertNil(try? DateComponents(bad, strategy: .http), "Input \(bad) was not nil") + @Test(arguments: [ + "Xri, 17 Jan 2025 19:03:05 GMT", + "Fri, 17 Janu 2025 19:03:05 GMT", + "Fri, 17Jan 2025 19:03:05 GMT", + "Fri, 17 Xrz 2025 19:03:05 GMT", + "Fri, 17 Jan 2025 19:03:05", // missing GMT + "Fri, 1 Jan 2025 19:03:05 GMT", + "Fri, 17 Jan 2025 1:03:05 GMT", + "Fri, 17 Jan 2025 19:3:05 GMT", + "Fri, 17 Jan 2025 19:03:5 GMT", + "Fri, 17 Jan 2025 19:03:05 GmT", + "Fri, 17 Jan 20252 19:03:05 GMT", + "Fri, 17 Jan 252 19:03:05 GMT", + "fri, 17 Jan 2025 19:03:05 GMT", // miscapitalized + "Fri, 17 jan 2025 19:03:05 GMT", + "Fri, 16 Jan 2025 25:03:05 GMT", // nonsense date + "Fri, 30 Feb 2025 25:03:05 GMT", // nonsense date + ]) + func badInputs(bad: String) { + #expect(throws: (any Error).self) { + try Date(bad, strategy: .http) + } + #expect(throws: (any Error).self) { + try DateComponents(bad, strategy: .http) } } - func test_HTTPComponentsFormat() throws { + @Test func componentsFormat() throws { let input = "Fri, 17 Jan 2025 19:03:05 GMT" - let parsed = try? DateComponents(input, strategy: .http) + let parsed = try DateComponents(input, strategy: .http) - XCTAssertEqual(parsed?.weekday, 6) - XCTAssertEqual(parsed?.day, 17) - XCTAssertEqual(parsed?.month, 1) - XCTAssertEqual(parsed?.year, 2025) - XCTAssertEqual(parsed?.hour, 19) - XCTAssertEqual(parsed?.minute, 3) - XCTAssertEqual(parsed?.second, 5) - XCTAssertEqual(parsed?.timeZone, TimeZone.gmt) + #expect(parsed.weekday == 6) + #expect(parsed.day == 17) + #expect(parsed.month == 1) + #expect(parsed.year == 2025) + #expect(parsed.hour == 19) + #expect(parsed.minute == 3) + #expect(parsed.second == 5) + #expect(parsed.timeZone == TimeZone.gmt) } - func test_validatingResultOfParseVsString() throws { + @Test func validatingResultOfParseVsString() throws { // This date will parse correctly, but of course the value of 99 does not correspond to the actual day. let strangeDate = "Mon, 99 Jan 2025 19:03:05 GMT" - let date = try XCTUnwrap(Date(strangeDate, strategy: .http)) - let components = try XCTUnwrap(DateComponents(strangeDate, strategy: .http)) + let date = try Date(strangeDate, strategy: .http) + let components = try DateComponents(strangeDate, strategy: .http) let actualDay = Calendar(identifier: .gregorian).component(.day, from: date) - let componentDay = try XCTUnwrap(components.day) - XCTAssertNotEqual(actualDay, componentDay) + let componentDay = try #require(components.day) + #expect(actualDay != componentDay) } } diff --git a/Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleFormattingTests.swift b/Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleFormattingTests.swift index c0ceccb0b..d3b3bb725 100644 --- a/Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleFormattingTests.swift +++ b/Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleFormattingTests.swift @@ -10,9 +10,7 @@ // //===----------------------------------------------------------------------===// -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if canImport(FoundationEssentials) @testable import FoundationEssentials @@ -22,41 +20,42 @@ import TestSupport @testable import Foundation #endif -final class ISO8601FormatStyleFormattingTests: XCTestCase { +@Suite("ISO8601FormatStyle Formatting") +private struct ISO8601FormatStyleFormattingTests { - func test_ISO8601Format() throws { + @Test func iso8601Format() throws { let date = Date(timeIntervalSinceReferenceDate: 665076946.0) let fractionalSecondsDate = Date(timeIntervalSinceReferenceDate: 665076946.011) let iso8601 = Date.ISO8601FormatStyle() // Date is: "2022-01-28 15:35:46" - XCTAssertEqual(iso8601.format(date), "2022-01-28T15:35:46Z") + #expect(iso8601.format(date) == "2022-01-28T15:35:46Z") - XCTAssertEqual(iso8601.time(includingFractionalSeconds: true).format(fractionalSecondsDate), "15:35:46.011") + #expect(iso8601.time(includingFractionalSeconds: true).format(fractionalSecondsDate) == "15:35:46.011") - XCTAssertEqual(iso8601.year().month().day().time(includingFractionalSeconds: true).format(fractionalSecondsDate), "2022-01-28T15:35:46.011") + #expect(iso8601.year().month().day().time(includingFractionalSeconds: true).format(fractionalSecondsDate) == "2022-01-28T15:35:46.011") // Day-only results: the default time is midnight for parsed date when the time piece is missing // Date is: "2022-01-28 00:00:00" - XCTAssertEqual(iso8601.year().month().day().dateSeparator(.dash).format(date), "2022-01-28") + #expect(iso8601.year().month().day().dateSeparator(.dash).format(date) == "2022-01-28") // Date is: "2022-01-28 00:00:00" - XCTAssertEqual(iso8601.year().month().day().dateSeparator(.omitted).format(date), "20220128") + #expect(iso8601.year().month().day().dateSeparator(.omitted).format(date) == "20220128") // Time-only results: we use the default date of the format style, 1970-01-01, to supplement the parsed date without year, month or day // Date is: "1970-01-23 00:00:00" - XCTAssertEqual(iso8601.weekOfYear().day().dateSeparator(.dash).format(date), "W04-05") + #expect(iso8601.weekOfYear().day().dateSeparator(.dash).format(date) == "W04-05") // Date is: "1970-01-28 15:35:46" - XCTAssertEqual(iso8601.day().time(includingFractionalSeconds: false).timeSeparator(.colon).format(date), "028T15:35:46") + #expect(iso8601.day().time(includingFractionalSeconds: false).timeSeparator(.colon).format(date) == "028T15:35:46") // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(iso8601.time(includingFractionalSeconds: false).timeSeparator(.colon).format(date), "15:35:46") + #expect(iso8601.time(includingFractionalSeconds: false).timeSeparator(.colon).format(date) == "15:35:46") // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted).format(date), "15:35:46Z") + #expect(iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted).format(date) == "15:35:46Z") // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(iso8601.time(includingFractionalSeconds: false).timeZone(separator: .colon).format(date), "15:35:46Z") + #expect(iso8601.time(includingFractionalSeconds: false).timeZone(separator: .colon).format(date) == "15:35:46Z") // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(iso8601.timeZone(separator: .colon).time(includingFractionalSeconds: false).timeSeparator(.colon).format(date), "15:35:46Z") + #expect(iso8601.timeZone(separator: .colon).time(includingFractionalSeconds: false).timeSeparator(.colon).format(date) == "15:35:46Z") // Time zones @@ -67,32 +66,32 @@ final class ISO8601FormatStyleFormattingTests: XCTestCase { var iso8601PacificIsh = iso8601 iso8601PacificIsh.timeZone = TimeZone(secondsFromGMT: -3600 * 8 - 30)! - XCTAssertEqual(iso8601Pacific.timeSeparator(.omitted).format(date), "2022-01-28T073546-0800") - XCTAssertEqual(iso8601Pacific.timeSeparator(.omitted).timeZoneSeparator(.colon).format(date), "2022-01-28T073546-08:00") + #expect(iso8601Pacific.timeSeparator(.omitted).format(date) == "2022-01-28T073546-0800") + #expect(iso8601Pacific.timeSeparator(.omitted).timeZoneSeparator(.colon).format(date) == "2022-01-28T073546-08:00") - XCTAssertEqual(iso8601PacificIsh.timeSeparator(.omitted).format(date), "2022-01-28T073516-080030") - XCTAssertEqual(iso8601PacificIsh.timeSeparator(.omitted).timeZoneSeparator(.colon).format(date), "2022-01-28T073516-08:00:30") + #expect(iso8601PacificIsh.timeSeparator(.omitted).format(date) == "2022-01-28T073516-080030") + #expect(iso8601PacificIsh.timeSeparator(.omitted).timeZoneSeparator(.colon).format(date) == "2022-01-28T073516-08:00:30") var iso8601gmtP1 = iso8601 iso8601gmtP1.timeZone = TimeZone(secondsFromGMT: 3600)! - XCTAssertEqual(iso8601gmtP1.timeSeparator(.omitted).format(date), "2022-01-28T163546+0100") - XCTAssertEqual(iso8601gmtP1.timeSeparator(.omitted).timeZoneSeparator(.colon).format(date), "2022-01-28T163546+01:00") + #expect(iso8601gmtP1.timeSeparator(.omitted).format(date) == "2022-01-28T163546+0100") + #expect(iso8601gmtP1.timeSeparator(.omitted).timeZoneSeparator(.colon).format(date) == "2022-01-28T163546+01:00") } - func test_ISO8601ComponentsFormatMissingPieces() throws { + @Test func iso8601ComponentsFormatMissingPieces() throws { // Example code from the proposal let components = DateComponents(year: 1999, month: 12, day: 31) let formatted = components.formatted(.iso8601) - XCTAssertEqual(formatted, "1999-12-31T00:00:00Z") + #expect(formatted == "1999-12-31T00:00:00Z") let emptyComponents = DateComponents() let emptyFormatted = emptyComponents.formatted(.iso8601) - XCTAssertEqual(emptyFormatted, "1970-01-01T00:00:00Z") + #expect(emptyFormatted == "1970-01-01T00:00:00Z") } - func test_ISO8601ComponentsFormat() throws { + @Test func iso8601ComponentsFormat() throws { let date = Date(timeIntervalSinceReferenceDate: 665076946.0) // Be sure to use the ISO8601 calendar here to decompose to the right week of year components (the starting day is not the same as gregorian) let componentsInGMT = Calendar(identifier: .iso8601).dateComponents(in: .gmt, from: date) @@ -101,33 +100,33 @@ final class ISO8601FormatStyleFormattingTests: XCTestCase { let iso8601 = DateComponents.ISO8601FormatStyle() // Date is: "2022-01-28 15:35:46" - XCTAssertEqual(iso8601.format(componentsInGMT), "2022-01-28T15:35:46Z") + #expect(iso8601.format(componentsInGMT) == "2022-01-28T15:35:46Z") - XCTAssertEqual(iso8601.time(includingFractionalSeconds: true).format(fractionalSecondsComponents), "15:35:46.011") + #expect(iso8601.time(includingFractionalSeconds: true).format(fractionalSecondsComponents) == "15:35:46.011") - XCTAssertEqual(iso8601.year().month().day().time(includingFractionalSeconds: true).format(fractionalSecondsComponents), "2022-01-28T15:35:46.011") + #expect(iso8601.year().month().day().time(includingFractionalSeconds: true).format(fractionalSecondsComponents) == "2022-01-28T15:35:46.011") // Day-only results: the default time is midnight for parsed date when the time piece is missing // Date is: "2022-01-28 00:00:00" - XCTAssertEqual(iso8601.year().month().day().dateSeparator(.dash).format(componentsInGMT), "2022-01-28") + #expect(iso8601.year().month().day().dateSeparator(.dash).format(componentsInGMT) == "2022-01-28") // Date is: "2022-01-28 00:00:00" - XCTAssertEqual(iso8601.year().month().day().dateSeparator(.omitted).format(componentsInGMT), "20220128") + #expect(iso8601.year().month().day().dateSeparator(.omitted).format(componentsInGMT) == "20220128") // Time-only results: we use the default date of the format style, 1970-01-01, to supplement the parsed date without year, month or day // Date is: "1970-01-23 00:00:00" - XCTAssertEqual(iso8601.weekOfYear().day().dateSeparator(.dash).format(componentsInGMT), "W04-05") + #expect(iso8601.weekOfYear().day().dateSeparator(.dash).format(componentsInGMT) == "W04-05") // Date is: "1970-01-28 15:35:46" - XCTAssertEqual(iso8601.day().time(includingFractionalSeconds: false).timeSeparator(.colon).format(componentsInGMT), "028T15:35:46") + #expect(iso8601.day().time(includingFractionalSeconds: false).timeSeparator(.colon).format(componentsInGMT) == "028T15:35:46") // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(iso8601.time(includingFractionalSeconds: false).timeSeparator(.colon).format(componentsInGMT), "15:35:46") + #expect(iso8601.time(includingFractionalSeconds: false).timeSeparator(.colon).format(componentsInGMT) == "15:35:46") // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted).format(componentsInGMT), "15:35:46Z") + #expect(iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted).format(componentsInGMT) == "15:35:46Z") // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(iso8601.time(includingFractionalSeconds: false).timeZone(separator: .colon).format(componentsInGMT), "15:35:46Z") + #expect(iso8601.time(includingFractionalSeconds: false).timeZone(separator: .colon).format(componentsInGMT) == "15:35:46Z") // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(iso8601.timeZone(separator: .colon).time(includingFractionalSeconds: false).timeSeparator(.colon).format(componentsInGMT), "15:35:46Z") + #expect(iso8601.timeZone(separator: .colon).time(includingFractionalSeconds: false).timeSeparator(.colon).format(componentsInGMT) == "15:35:46Z") // Time zones @@ -136,8 +135,8 @@ final class ISO8601FormatStyleFormattingTests: XCTestCase { iso8601Pacific.timeZone = pacificTimeZone let componentsInPacific = Calendar(identifier: .iso8601).dateComponents(in: pacificTimeZone, from: date) - XCTAssertEqual(iso8601Pacific.timeSeparator(.omitted).format(componentsInPacific), "2022-01-28T073546-0800") - XCTAssertEqual(iso8601Pacific.timeSeparator(.omitted).timeZoneSeparator(.colon).format(componentsInPacific), "2022-01-28T073546-08:00") + #expect(iso8601Pacific.timeSeparator(.omitted).format(componentsInPacific) == "2022-01-28T073546-0800") + #expect(iso8601Pacific.timeSeparator(.omitted).timeZoneSeparator(.colon).format(componentsInPacific) == "2022-01-28T073546-08:00") // Has a seconds component (-28830) var iso8601PacificIsh = iso8601 @@ -145,71 +144,72 @@ final class ISO8601FormatStyleFormattingTests: XCTestCase { iso8601PacificIsh.timeZone = pacificIshTimeZone let componentsInPacificIsh = Calendar(identifier: .iso8601).dateComponents(in: pacificIshTimeZone, from: date) - XCTAssertEqual(iso8601PacificIsh.timeSeparator(.omitted).format(componentsInPacificIsh), "2022-01-28T073516-080030") - XCTAssertEqual(iso8601PacificIsh.timeSeparator(.omitted).timeZoneSeparator(.colon).format(componentsInPacificIsh), "2022-01-28T073516-08:00:30") + #expect(iso8601PacificIsh.timeSeparator(.omitted).format(componentsInPacificIsh) == "2022-01-28T073516-080030") + #expect(iso8601PacificIsh.timeSeparator(.omitted).timeZoneSeparator(.colon).format(componentsInPacificIsh) == "2022-01-28T073516-08:00:30") var iso8601gmtP1 = iso8601 let gmtP1TimeZone = TimeZone(secondsFromGMT: 3600)! iso8601gmtP1.timeZone = gmtP1TimeZone let componentsInGMTP1 = Calendar(identifier: .iso8601).dateComponents(in: gmtP1TimeZone, from: date) - XCTAssertEqual(iso8601gmtP1.timeSeparator(.omitted).format(componentsInGMTP1), "2022-01-28T163546+0100") - XCTAssertEqual(iso8601gmtP1.timeSeparator(.omitted).timeZoneSeparator(.colon).format(componentsInGMTP1), "2022-01-28T163546+01:00") + #expect(iso8601gmtP1.timeSeparator(.omitted).format(componentsInGMTP1) == "2022-01-28T163546+0100") + #expect(iso8601gmtP1.timeSeparator(.omitted).timeZoneSeparator(.colon).format(componentsInGMTP1) == "2022-01-28T163546+01:00") } - func test_codable() { + @Test func codable() throws { let iso8601Style = Date.ISO8601FormatStyle().year().month().day() let encoder = JSONEncoder() - let encodedStyle = try! encoder.encode(iso8601Style) + let encodedStyle = try encoder.encode(iso8601Style) let decoder = JSONDecoder() - let decodedStyle = try? decoder.decode(Date.ISO8601FormatStyle.self, from: encodedStyle) - XCTAssertNotNil(decodedStyle) + #expect(throws: Never.self) { + _ = try decoder.decode(Date.ISO8601FormatStyle.self, from: encodedStyle) + } } - func testLeadingDotSyntax() { + @Test func leadingDotSyntax() { let _ = Date().formatted(.iso8601) } - func test_ISO8601FormatWithDate() throws { + @Test func iso8601FormatWithDate() throws { // dateFormatter.date(from: "2021-07-01 15:56:32")! let date = Date(timeIntervalSinceReferenceDate: 646847792.0) // Thursday - XCTAssertEqual(date.formatted(.iso8601), "2021-07-01T15:56:32Z") - XCTAssertEqual(date.formatted(.iso8601.dateSeparator(.omitted)), "20210701T15:56:32Z") - XCTAssertEqual(date.formatted(.iso8601.dateTimeSeparator(.space)), "2021-07-01 15:56:32Z") - XCTAssertEqual(date.formatted(.iso8601.timeSeparator(.omitted)), "2021-07-01T155632Z") - XCTAssertEqual(date.formatted(.iso8601.dateSeparator(.omitted).timeSeparator(.omitted)), "20210701T155632Z") - XCTAssertEqual(date.formatted(.iso8601.year().month().day().time(includingFractionalSeconds: false).timeZone(separator: .omitted)), "2021-07-01T15:56:32Z") - XCTAssertEqual(date.formatted(.iso8601.year().month().day().time(includingFractionalSeconds: true).timeZone(separator: .omitted).dateSeparator(.dash).dateTimeSeparator(.standard).timeSeparator(.colon)), "2021-07-01T15:56:32.000Z") - - XCTAssertEqual(date.formatted(.iso8601.year()), "2021") - XCTAssertEqual(date.formatted(.iso8601.year().month()), "2021-07") - XCTAssertEqual(date.formatted(.iso8601.year().month().day()), "2021-07-01") - XCTAssertEqual(date.formatted(.iso8601.year().month().day().dateSeparator(.omitted)), "20210701") - - XCTAssertEqual(date.formatted(.iso8601.year().weekOfYear()), "2021-W26") - XCTAssertEqual(date.formatted(.iso8601.year().weekOfYear().day()), "2021-W26-04") // day() is the weekday number - XCTAssertEqual(date.formatted(.iso8601.year().day()), "2021-182") // day() is the ordinal day - - XCTAssertEqual(date.formatted(.iso8601.time(includingFractionalSeconds: false)), "15:56:32") - XCTAssertEqual(date.formatted(.iso8601.time(includingFractionalSeconds: true)), "15:56:32.000") - XCTAssertEqual(date.formatted(.iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted)), "15:56:32Z") + #expect(date.formatted(.iso8601) == "2021-07-01T15:56:32Z") + #expect(date.formatted(.iso8601.dateSeparator(.omitted)) == "20210701T15:56:32Z") + #expect(date.formatted(.iso8601.dateTimeSeparator(.space)) == "2021-07-01 15:56:32Z") + #expect(date.formatted(.iso8601.timeSeparator(.omitted)) == "2021-07-01T155632Z") + #expect(date.formatted(.iso8601.dateSeparator(.omitted).timeSeparator(.omitted)) == "20210701T155632Z") + #expect(date.formatted(.iso8601.year().month().day().time(includingFractionalSeconds: false).timeZone(separator: .omitted)) == "2021-07-01T15:56:32Z") + #expect(date.formatted(.iso8601.year().month().day().time(includingFractionalSeconds: true).timeZone(separator: .omitted).dateSeparator(.dash).dateTimeSeparator(.standard).timeSeparator(.colon)) == "2021-07-01T15:56:32.000Z") + + #expect(date.formatted(.iso8601.year()) == "2021") + #expect(date.formatted(.iso8601.year().month()) == "2021-07") + #expect(date.formatted(.iso8601.year().month().day()) == "2021-07-01") + #expect(date.formatted(.iso8601.year().month().day().dateSeparator(.omitted)) == "20210701") + + #expect(date.formatted(.iso8601.year().weekOfYear()) == "2021-W26") + #expect(date.formatted(.iso8601.year().weekOfYear().day()) == "2021-W26-04") // day() is the weekday number + #expect(date.formatted(.iso8601.year().day()) == "2021-182") // day() is the ordinal day + + #expect(date.formatted(.iso8601.time(includingFractionalSeconds: false)) == "15:56:32") + #expect(date.formatted(.iso8601.time(includingFractionalSeconds: true)) == "15:56:32.000") + #expect(date.formatted(.iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted)) == "15:56:32Z") } - func test_remoteDate() throws { + @Test func remoteDate() throws { let date = Date(timeIntervalSince1970: 999999999999.0) // Remote date - XCTAssertEqual(date.formatted(.iso8601), "33658-09-27T01:46:39Z") - XCTAssertEqual(date.formatted(.iso8601.year().weekOfYear().day()), "33658-W39-05") // day() is the weekday number + #expect(date.formatted(.iso8601) == "33658-09-27T01:46:39Z") + #expect(date.formatted(.iso8601.year().weekOfYear().day()) == "33658-W39-05") // day() is the weekday number } - func test_internal_formatDateComponents() throws { + @Test func internal_formatDateComponents() throws { let dc = DateComponents(year: -2025, month: 1, day: 20, hour: 0, minute: 0, second: 0) let str = Date.ISO8601FormatStyle().format(dc, appendingTimeZoneOffset: 0) - XCTAssertEqual(str, "-2025-01-20T00:00:00Z") + #expect(str == "-2025-01-20T00:00:00Z") } - func test_rounding() { + @Test func rounding() { // Date is: "1970-01-01 15:35:45.9999" let date = Date(timeIntervalSinceReferenceDate: -978251054.0 - 0.0001) let str = Date.ISO8601FormatStyle().timeZone(separator: .colon).time(includingFractionalSeconds: true).timeSeparator(.colon).format(date) - XCTAssertEqual(str, "15:35:45.999Z") + #expect(str == "15:35:45.999Z") } } diff --git a/Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleParsingTests.swift b/Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleParsingTests.swift index 2c728093a..e96304734 100644 --- a/Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleParsingTests.swift +++ b/Tests/FoundationEssentialsTests/Formatting/ISO8601FormatStyleParsingTests.swift @@ -6,157 +6,162 @@ // //===----------------------------------------------------------------------===// -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if canImport(FoundationEssentials) -@testable import FoundationEssentials +import FoundationEssentials #endif #if FOUNDATION_FRAMEWORK -@testable import Foundation +import Foundation #endif -final class ISO8601FormatStyleParsingTests: XCTestCase { +@Suite("ISO8601FormatStyle Parsing") +private struct ISO8601FormatStyleParsingTests { /// See also the format-only tests in DateISO8601FormatStyleEssentialsTests - func test_ISO8601Parse() throws { + @Test func iso8601Parse() throws { let iso8601 = Date.ISO8601FormatStyle() // Date is: "2022-01-28 15:35:46" - XCTAssertEqual(try? iso8601.parse("2022-01-28T15:35:46Z"), Date(timeIntervalSinceReferenceDate: 665076946.0)) + #expect(try iso8601.parse("2022-01-28T15:35:46Z") == Date(timeIntervalSinceReferenceDate: 665076946.0)) var iso8601Pacific = iso8601 iso8601Pacific.timeZone = TimeZone(secondsFromGMT: -3600 * 8)! - XCTAssertEqual(try? iso8601Pacific.timeSeparator(.omitted).parse("2022-01-28T073546-0800"), Date(timeIntervalSinceReferenceDate: 665076946.0)) + #expect(try iso8601Pacific.timeSeparator(.omitted).parse("2022-01-28T073546-0800") == Date(timeIntervalSinceReferenceDate: 665076946.0)) // Day-only results: the default time is midnight for parsed date when the time piece is missing // Date is: "2022-01-28 00:00:00" - XCTAssertEqual(try? iso8601.year().month().day().dateSeparator(.dash).parse("2022-01-28"), Date(timeIntervalSinceReferenceDate: 665020800.0)) + #expect(try iso8601.year().month().day().dateSeparator(.dash).parse("2022-01-28") == Date(timeIntervalSinceReferenceDate: 665020800.0)) // Date is: "2022-01-28 00:00:00" - XCTAssertEqual(try? iso8601.year().month().day().dateSeparator(.omitted).parse("20220128"), Date(timeIntervalSinceReferenceDate: 665020800.0)) + #expect(try iso8601.year().month().day().dateSeparator(.omitted).parse("20220128") == Date(timeIntervalSinceReferenceDate: 665020800.0)) // Time-only results: we use the default date of the format style, 1970-01-01, to supplement the parsed date without year, month or day // Date is: "1970-01-23 00:00:00" - XCTAssertEqual(try? iso8601.weekOfYear().day().dateSeparator(.dash).parse("W04-05"), Date(timeIntervalSinceReferenceDate: -976406400.0)) + #expect(try iso8601.weekOfYear().day().dateSeparator(.dash).parse("W04-05") == Date(timeIntervalSinceReferenceDate: -976406400.0)) // Date is: "1970-01-28 15:35:46" - XCTAssertEqual(try? iso8601.day().time(includingFractionalSeconds: false).timeSeparator(.colon).parse("028T15:35:46"), Date(timeIntervalSinceReferenceDate: -975918254.0)) + #expect(try iso8601.day().time(includingFractionalSeconds: false).timeSeparator(.colon).parse("028T15:35:46") == Date(timeIntervalSinceReferenceDate: -975918254.0)) // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(try? iso8601.time(includingFractionalSeconds: false).timeSeparator(.colon).parse("15:35:46"), Date(timeIntervalSinceReferenceDate: -978251054.0)) + #expect(try iso8601.time(includingFractionalSeconds: false).timeSeparator(.colon).parse("15:35:46") == Date(timeIntervalSinceReferenceDate: -978251054.0)) // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(try? iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted).parse("15:35:46Z"), Date(timeIntervalSinceReferenceDate: -978251054.0)) + #expect(try iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted).parse("15:35:46Z") == Date(timeIntervalSinceReferenceDate: -978251054.0)) // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(try? iso8601.time(includingFractionalSeconds: false).timeZone(separator: .colon).parse("15:35:46Z"), Date(timeIntervalSinceReferenceDate: -978251054.0)) + #expect(try iso8601.time(includingFractionalSeconds: false).timeZone(separator: .colon).parse("15:35:46Z") == Date(timeIntervalSinceReferenceDate: -978251054.0)) // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(try? iso8601.timeZone(separator: .colon).time(includingFractionalSeconds: false).timeSeparator(.colon).parse("15:35:46Z"), Date(timeIntervalSinceReferenceDate: -978251054.0)) + #expect(try iso8601.timeZone(separator: .colon).time(includingFractionalSeconds: false).timeSeparator(.colon).parse("15:35:46Z") == Date(timeIntervalSinceReferenceDate: -978251054.0)) } - func test_ISO8601ParseComponents_fromString() throws { + @Test func iso8601ParseComponents_fromString() throws { let components = try DateComponents.ISO8601FormatStyle().parse("2022-01-28T15:35:46Z") - XCTAssertEqual(components, DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, year: 2022, month: 1, day: 28, hour: 15, minute: 35, second: 46)) - XCTAssertNotNil(components.date) + #expect(components == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, year: 2022, month: 1, day: 28, hour: 15, minute: 35, second: 46)) + #expect(components.date != nil) } - func test_ISO8601ParseComponents_missingComponents() throws { + @Test func iso8601ParseComponents_missingComponents() throws { // Default style requires time - XCTAssertThrowsError(try DateComponents.ISO8601FormatStyle().parse("2022-01-28")) + #expect(throws: (any Error).self) { + try DateComponents.ISO8601FormatStyle().parse("2022-01-28") + } } /// See also the format-only tests in DateISO8601FormatStyleEssentialsTests - func test_ISO8601ParseComponents() throws { + @Test func iso8601ParseComponents() throws { let iso8601 = DateComponents.ISO8601FormatStyle() // Date is: "2022-01-28 15:35:46" - XCTAssertEqual(try? iso8601.parse("2022-01-28T15:35:46Z"), DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, year: 2022, month: 1, day: 28, hour: 15, minute: 35, second: 46)) + #expect(try iso8601.parse("2022-01-28T15:35:46Z") == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, year: 2022, month: 1, day: 28, hour: 15, minute: 35, second: 46)) var iso8601Pacific = iso8601 let tz = TimeZone(secondsFromGMT: -3600 * 8)! iso8601Pacific.timeZone = tz - XCTAssertEqual(try? iso8601Pacific.timeSeparator(.omitted).parse("2022-01-28T073546-0800"), DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: tz, year: 2022, month: 1, day: 28, hour: 7, minute: 35, second: 46)) + #expect(try iso8601Pacific.timeSeparator(.omitted).parse("2022-01-28T073546-0800") == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: tz, year: 2022, month: 1, day: 28, hour: 7, minute: 35, second: 46)) // Day-only results: the default time is midnight for parsed date when the time piece is missing // Date is: "2022-01-28 00:00:00" - XCTAssertEqual(try? iso8601.year().month().day().dateSeparator(.dash).parse("2022-01-28"), DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, year: 2022, month: 1, day: 28)) + #expect(try iso8601.year().month().day().dateSeparator(.dash).parse("2022-01-28") == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, year: 2022, month: 1, day: 28)) // Date is: "2022-01-28 00:00:00" - XCTAssertEqual(try? iso8601.year().month().day().dateSeparator(.omitted).parse("20220128"), DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, year: 2022, month: 1, day: 28)) + #expect(try iso8601.year().month().day().dateSeparator(.omitted).parse("20220128") == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, year: 2022, month: 1, day: 28)) // Time-only results: we use the default date of the format style, 1970-01-01, to supplement the parsed date without year, month or day // Date is: "1970-01-23 00:00:00" // note: weekday as understood by Calendar is not the same integer value as the one in the ISO8601 format - XCTAssertEqual(try? iso8601.weekOfYear().day().dateSeparator(.dash).parse("W04-05"), DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, weekday: 6, weekOfYear: 4)) + #expect(try iso8601.weekOfYear().day().dateSeparator(.dash).parse("W04-05") == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, weekday: 6, weekOfYear: 4)) // Date is: "1970-01-28 15:35:46" var expectedWithDayOfYear = DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, hour: 15, minute: 35, second: 46) expectedWithDayOfYear.dayOfYear = 28 - XCTAssertEqual(try? iso8601.day().time(includingFractionalSeconds: false).timeSeparator(.colon).parse("028T15:35:46"), expectedWithDayOfYear) + #expect(try iso8601.day().time(includingFractionalSeconds: false).timeSeparator(.colon).parse("028T15:35:46") == expectedWithDayOfYear) // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(try? iso8601.time(includingFractionalSeconds: false).timeSeparator(.colon).parse("15:35:46"), DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, hour: 15, minute: 35, second: 46)) + #expect(try iso8601.time(includingFractionalSeconds: false).timeSeparator(.colon).parse("15:35:46") == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, hour: 15, minute: 35, second: 46)) // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(try? iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted).parse("15:35:46Z"), DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, hour: 15, minute: 35, second: 46)) + #expect(try iso8601.time(includingFractionalSeconds: false).timeZone(separator: .omitted).parse("15:35:46Z") == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, hour: 15, minute: 35, second: 46)) // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(try? iso8601.time(includingFractionalSeconds: false).timeZone(separator: .colon).parse("15:35:46Z"), DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, hour: 15, minute: 35, second: 46)) + #expect(try iso8601.time(includingFractionalSeconds: false).timeZone(separator: .colon).parse("15:35:46Z") == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, hour: 15, minute: 35, second: 46)) // Date is: "1970-01-01 15:35:46" - XCTAssertEqual(try? iso8601.timeZone(separator: .colon).time(includingFractionalSeconds: false).timeSeparator(.colon).parse("15:35:46Z"), DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, hour: 15, minute: 35, second: 46)) + #expect(try iso8601.timeZone(separator: .colon).time(includingFractionalSeconds: false).timeSeparator(.colon).parse("15:35:46Z") == DateComponents(calendar: Calendar(identifier: .iso8601), timeZone: .gmt, hour: 15, minute: 35, second: 46)) } - func test_ISO8601FractionalSecondsAreOptional() { + @Test func iso8601FractionalSecondsAreOptional() { let iso8601 = Date.ISO8601FormatStyle() let iso8601WithFraction = Date.ISO8601FormatStyle(includingFractionalSeconds: true) let str = "2022-01-28T15:35:46Z" let strWithFraction = "2022-01-28T15:35:46.123Z" - XCTAssertNotNil(try? iso8601.parse(str)) - XCTAssertNotNil(try? iso8601.parse(strWithFraction)) + #expect(throws: Never.self) { + try iso8601.parse(str) + } + #expect(throws: Never.self) { + try iso8601.parse(strWithFraction) + } - XCTAssertNotNil(try? iso8601WithFraction.parse(str)) - XCTAssertNotNil(try? iso8601WithFraction.parse(strWithFraction)) + #expect(throws: Never.self) { + try iso8601WithFraction.parse(str) + } + #expect(throws: Never.self) { + try iso8601WithFraction.parse(strWithFraction) + } } - func test_weekOfYear() throws { + @Test(arguments: [ + ("2019-W52-07", "2019-12-29"), + ("2020-W01-01", "2019-12-30"), + ("2020-W01-02", "2019-12-31"), + ("2020-W01-03", "2020-01-01"), + ("2026-W53-01", "2026-12-28"), + ("2026-W53-02", "2026-12-29"), + ("2026-W53-03", "2026-12-30"), + ("2026-W53-04", "2026-12-31"), + ("2026-W53-05", "2027-01-01"), + ("2026-W53-06", "2027-01-02"), + ("2026-W53-07", "2027-01-03"), + ("2027-W01-01", "2027-01-04"), + ("2027-W01-02", "2027-01-05") + ]) + func weekOfYear(date: (String, String)) throws { let iso8601 = Date.ISO8601FormatStyle() - // Test some dates around the 2019 - 2020 end of year, and 2026 which has W53 - let dates = [ - ("2019-W52-07", "2019-12-29"), - ("2020-W01-01", "2019-12-30"), - ("2020-W01-02", "2019-12-31"), - ("2020-W01-03", "2020-01-01"), - ("2026-W53-01", "2026-12-28"), - ("2026-W53-02", "2026-12-29"), - ("2026-W53-03", "2026-12-30"), - ("2026-W53-04", "2026-12-31"), - ("2026-W53-05", "2027-01-01"), - ("2026-W53-06", "2027-01-02"), - ("2026-W53-07", "2027-01-03"), - ("2027-W01-01", "2027-01-04"), - ("2027-W01-02", "2027-01-05") - ] - - for d in dates { - let parsedWoY = try iso8601.year().weekOfYear().day().parse(d.0) - let parsedY = try iso8601.year().month().day().parse(d.1) - XCTAssertEqual(parsedWoY, parsedY) - } + let parsedWoY = try iso8601.year().weekOfYear().day().parse(date.0) + let parsedY = try iso8601.year().month().day().parse(date.1) + #expect(parsedWoY == parsedY) } - func test_zeroLeadingDigits() { + @Test func zeroLeadingDigits() throws { // The parser allows for an arbitrary number of 0 pads in digits, including none. let iso8601 = Date.ISO8601FormatStyle() // Date is: "2022-01-28 15:35:46" - XCTAssertEqual(try? iso8601.parse("2022-01-28T15:35:46Z"), Date(timeIntervalSinceReferenceDate: 665076946.0)) - XCTAssertEqual(try? iso8601.parse("002022-01-28T15:35:46Z"), Date(timeIntervalSinceReferenceDate: 665076946.0)) - XCTAssertEqual(try? iso8601.parse("2022-0001-28T15:35:46Z"), Date(timeIntervalSinceReferenceDate: 665076946.0)) - XCTAssertEqual(try? iso8601.parse("2022-01-0028T15:35:46Z"), Date(timeIntervalSinceReferenceDate: 665076946.0)) - XCTAssertEqual(try? iso8601.parse("2022-1-28T15:35:46Z"), Date(timeIntervalSinceReferenceDate: 665076946.0)) - XCTAssertEqual(try? iso8601.parse("2022-01-28T15:35:06Z"), Date(timeIntervalSinceReferenceDate: 665076906.0)) - XCTAssertEqual(try? iso8601.parse("2022-01-28T15:35:6Z"), Date(timeIntervalSinceReferenceDate: 665076906.0)) - XCTAssertEqual(try? iso8601.parse("2022-01-28T15:05:46Z"), Date(timeIntervalSinceReferenceDate: 665075146.0)) - XCTAssertEqual(try? iso8601.parse("2022-01-28T15:5:46Z"), Date(timeIntervalSinceReferenceDate: 665075146.0)) + #expect(try iso8601.parse("2022-01-28T15:35:46Z") == Date(timeIntervalSinceReferenceDate: 665076946.0)) + #expect(try iso8601.parse("002022-01-28T15:35:46Z") == Date(timeIntervalSinceReferenceDate: 665076946.0)) + #expect(try iso8601.parse("2022-0001-28T15:35:46Z") == Date(timeIntervalSinceReferenceDate: 665076946.0)) + #expect(try iso8601.parse("2022-01-0028T15:35:46Z") == Date(timeIntervalSinceReferenceDate: 665076946.0)) + #expect(try iso8601.parse("2022-1-28T15:35:46Z") == Date(timeIntervalSinceReferenceDate: 665076946.0)) + #expect(try iso8601.parse("2022-01-28T15:35:06Z") == Date(timeIntervalSinceReferenceDate: 665076906.0)) + #expect(try iso8601.parse("2022-01-28T15:35:6Z") == Date(timeIntervalSinceReferenceDate: 665076906.0)) + #expect(try iso8601.parse("2022-01-28T15:05:46Z") == Date(timeIntervalSinceReferenceDate: 665075146.0)) + #expect(try iso8601.parse("2022-01-28T15:5:46Z") == Date(timeIntervalSinceReferenceDate: 665075146.0)) } - func test_timeZones() { + @Test func timeZones() throws { let iso8601 = Date.ISO8601FormatStyle() let date = Date(timeIntervalSinceReferenceDate: 665076946.0) @@ -167,49 +172,49 @@ final class ISO8601FormatStyleParsingTests: XCTestCase { var iso8601PacificIsh = iso8601 iso8601PacificIsh.timeZone = TimeZone(secondsFromGMT: -3600 * 8 - 30)! - XCTAssertEqual(try? iso8601Pacific.timeSeparator(.omitted).parse("2022-01-28T073546-0800"), date) - XCTAssertEqual(try? iso8601Pacific.timeSeparator(.omitted).timeZoneSeparator(.colon).parse("2022-01-28T073546-08:00"), date) + #expect(try iso8601Pacific.timeSeparator(.omitted).parse("2022-01-28T073546-0800") == date) + #expect(try iso8601Pacific.timeSeparator(.omitted).timeZoneSeparator(.colon).parse("2022-01-28T073546-08:00") == date) - XCTAssertEqual(try? iso8601PacificIsh.timeSeparator(.omitted).parse("2022-01-28T073516-080030"), date) - XCTAssertEqual(try? iso8601PacificIsh.timeSeparator(.omitted).timeZoneSeparator(.colon).parse("2022-01-28T073516-08:00:30"), date) + #expect(try iso8601PacificIsh.timeSeparator(.omitted).parse("2022-01-28T073516-080030") == date) + #expect(try iso8601PacificIsh.timeSeparator(.omitted).timeZoneSeparator(.colon).parse("2022-01-28T073516-08:00:30") == date) var iso8601gmtP1 = iso8601 iso8601gmtP1.timeZone = TimeZone(secondsFromGMT: 3600)! - XCTAssertEqual(try? iso8601gmtP1.timeSeparator(.omitted).parse("2022-01-28T163546+0100"), date) - XCTAssertEqual(try? iso8601gmtP1.timeSeparator(.omitted).parse("2022-01-28T163546+010000"), date) - XCTAssertEqual(try? iso8601gmtP1.timeSeparator(.omitted).timeZoneSeparator(.colon).parse("2022-01-28T163546+01:00"), date) - XCTAssertEqual(try? iso8601gmtP1.timeSeparator(.omitted).timeZoneSeparator(.colon).parse("2022-01-28T163546+01:00:00"), date) + #expect(try iso8601gmtP1.timeSeparator(.omitted).parse("2022-01-28T163546+0100") == date) + #expect(try iso8601gmtP1.timeSeparator(.omitted).parse("2022-01-28T163546+010000") == date) + #expect(try iso8601gmtP1.timeSeparator(.omitted).timeZoneSeparator(.colon).parse("2022-01-28T163546+01:00") == date) + #expect(try iso8601gmtP1.timeSeparator(.omitted).timeZoneSeparator(.colon).parse("2022-01-28T163546+01:00:00") == date) // Due to a quirk of the original implementation, colons are allowed to be present in the time zone even if the time zone separator is omitted - XCTAssertEqual(try? iso8601gmtP1.timeSeparator(.omitted).parse("2022-01-28T163546+01:00"), date) - XCTAssertEqual(try? iso8601gmtP1.timeSeparator(.omitted).parse("2022-01-28T163546+01:00:00"), date) + #expect(try iso8601gmtP1.timeSeparator(.omitted).parse("2022-01-28T163546+01:00") == date) + #expect(try iso8601gmtP1.timeSeparator(.omitted).parse("2022-01-28T163546+01:00:00") == date) } - func test_fractionalSeconds() throws { + @Test func fractionalSeconds() throws { let expectedDate = Date(timeIntervalSinceReferenceDate: 646876592.34567) var iso8601 = Date.ISO8601FormatStyle().year().month().day().time(includingFractionalSeconds: true) iso8601.timeZone = .gmt - let parsedWithFraction = try XCTUnwrap(try iso8601.parse("2021-07-01T23:56:32.34567")) - let parsedWithoutFraction = try XCTUnwrap(try iso8601.parse("2021-07-01T23:56:32")) + let parsedWithFraction = try iso8601.parse("2021-07-01T23:56:32.34567") + let parsedWithoutFraction = try iso8601.parse("2021-07-01T23:56:32") - let parsedWithFraction1 = try XCTUnwrap(try iso8601.parse("2021-07-01T23:56:32.3")) - let parsedWithFraction2 = try XCTUnwrap(try iso8601.parse("2021-07-01T23:56:32.34")) - let parsedWithFraction3 = try XCTUnwrap(try iso8601.parse("2021-07-01T23:56:32.345")) - let parsedWithFraction4 = try XCTUnwrap(try iso8601.parse("2021-07-01T23:56:32.3456")) + let parsedWithFraction1 = try iso8601.parse("2021-07-01T23:56:32.3") + let parsedWithFraction2 = try iso8601.parse("2021-07-01T23:56:32.34") + let parsedWithFraction3 = try iso8601.parse("2021-07-01T23:56:32.345") + let parsedWithFraction4 = try iso8601.parse("2021-07-01T23:56:32.3456") - XCTAssertEqual(parsedWithoutFraction.timeIntervalSinceReferenceDate, expectedDate.timeIntervalSinceReferenceDate, accuracy: 1.0) + #expect(abs(parsedWithoutFraction.timeIntervalSinceReferenceDate - expectedDate.timeIntervalSinceReferenceDate) <= 1.0) // More accurate due to inclusion of fraction - XCTAssertEqual(parsedWithFraction.timeIntervalSinceReferenceDate, expectedDate.timeIntervalSinceReferenceDate, accuracy: 0.01) - XCTAssertEqual(parsedWithFraction1.timeIntervalSinceReferenceDate, expectedDate.timeIntervalSinceReferenceDate, accuracy: 0.1) - XCTAssertEqual(parsedWithFraction2.timeIntervalSinceReferenceDate, expectedDate.timeIntervalSinceReferenceDate, accuracy: 0.1) - XCTAssertEqual(parsedWithFraction3.timeIntervalSinceReferenceDate, expectedDate.timeIntervalSinceReferenceDate, accuracy: 0.1) - XCTAssertEqual(parsedWithFraction4.timeIntervalSinceReferenceDate, expectedDate.timeIntervalSinceReferenceDate, accuracy: 0.1) + #expect(abs(parsedWithFraction.timeIntervalSinceReferenceDate - expectedDate.timeIntervalSinceReferenceDate) <= 0.01) + #expect(abs(parsedWithFraction1.timeIntervalSinceReferenceDate - expectedDate.timeIntervalSinceReferenceDate) <= 0.1) + #expect(abs(parsedWithFraction2.timeIntervalSinceReferenceDate - expectedDate.timeIntervalSinceReferenceDate) <= 0.1) + #expect(abs(parsedWithFraction3.timeIntervalSinceReferenceDate - expectedDate.timeIntervalSinceReferenceDate) <= 0.1) + #expect(abs(parsedWithFraction4.timeIntervalSinceReferenceDate - expectedDate.timeIntervalSinceReferenceDate) <= 0.1) } - func test_specialTimeZonesAndSpaces() { - let reference = try! Date("2020-03-05T12:00:00+00:00", strategy: .iso8601) + @Test func specialTimeZonesAndSpaces() throws { + let reference = try Date("2020-03-05T12:00:00+00:00", strategy: .iso8601) let tests : [(String, Date.ISO8601FormatStyle)] = [ ("2020-03-05T12:00:00+00:00", Date.ISO8601FormatStyle()), @@ -242,7 +247,7 @@ final class ISO8601FormatStyleParsingTests: XCTestCase { for (parseMe, style) in tests { let parsed = try? style.parse(parseMe) - XCTAssertEqual(parsed, reference, """ + #expect(parsed == reference, """ parsing : \(parseMe) expected: \(reference) \(reference.timeIntervalSinceReferenceDate) @@ -250,29 +255,20 @@ result : \(parsed != nil ? parsed!.debugDescription : "nil") \(parsed != nil ? """) } } - -#if canImport(FoundationInternationalization) || FOUNDATION_FRAMEWORK - func test_chileTimeZone() { - var iso8601Chile = Date.ISO8601FormatStyle().year().month().day() - iso8601Chile.timeZone = TimeZone(name: "America/Santiago")! - - let date = try? iso8601Chile.parse("2023-09-03") - XCTAssertNotNil(date) - } -#endif } -final class DateISO8601FormatStylePatternMatchingTests : XCTestCase { +@Suite("Date ISO8601FormatStyle Pattern Matching") +private struct DateISO8601FormatStylePatternMatchingTests { - func _matchFullRange(_ str: String, formatStyle: Date.ISO8601FormatStyle, expectedUpperBound: String.Index?, expectedDate: Date?, file: StaticString = #filePath, line: UInt = #line) { - _matchRange(str, formatStyle: formatStyle, range: nil, expectedUpperBound: expectedUpperBound, expectedDate: expectedDate, file: file, line: line) + func _matchFullRange(_ str: String, formatStyle: Date.ISO8601FormatStyle, expectedUpperBound: String.Index?, expectedDate: Date?, sourceLocation: SourceLocation = #_sourceLocation) { + _matchRange(str, formatStyle: formatStyle, range: nil, expectedUpperBound: expectedUpperBound, expectedDate: expectedDate, sourceLocation: sourceLocation) } - func _matchFullRange(_ str: String, formatStyle: DateComponents.ISO8601FormatStyle, expectedUpperBound: String.Index?, expectedDateComponents: DateComponents?, file: StaticString = #filePath, line: UInt = #line) { - _matchRange(str, formatStyle: formatStyle, range: nil, expectedUpperBound: expectedUpperBound, expectedDateComponents: expectedDateComponents, file: file, line: line) + func _matchFullRange(_ str: String, formatStyle: DateComponents.ISO8601FormatStyle, expectedUpperBound: String.Index?, expectedDateComponents: DateComponents?, sourceLocation: SourceLocation = #_sourceLocation) { + _matchRange(str, formatStyle: formatStyle, range: nil, expectedUpperBound: expectedUpperBound, expectedDateComponents: expectedDateComponents, sourceLocation: sourceLocation) } - func _matchRange(_ str: String, formatStyle: Date.ISO8601FormatStyle, range: Range?, expectedUpperBound: String.Index?, expectedDate: Date?, file: StaticString = #filePath, line: UInt = #line) { + func _matchRange(_ str: String, formatStyle: Date.ISO8601FormatStyle, range: Range?, expectedUpperBound: String.Index?, expectedDate: Date?, sourceLocation: SourceLocation = #_sourceLocation) { // FIXME: Need tests that starts from somewhere else let m = try? formatStyle.consuming(str, startingAt: str.startIndex, in: range ?? str.startIndex..?, expectedUpperBound: String.Index?, expectedDateComponents: DateComponents?, file: StaticString = #filePath, line: UInt = #line) { + func _matchRange(_ str: String, formatStyle: DateComponents.ISO8601FormatStyle, range: Range?, expectedUpperBound: String.Index?, expectedDateComponents: DateComponents?, sourceLocation: SourceLocation = #_sourceLocation) { // FIXME: Need tests that starts from somewhere else let m = try? formatStyle.consuming(str, startingAt: str.startIndex, in: range ?? str.startIndex.. Date: Wed, 25 Jun 2025 19:16:39 -0700 Subject: [PATCH 7/9] Remove Identifiable overloads and base Message protocol from NotificationCenter API proposal (#1380) --- .../0011-concurrency-safe-notifications.md | 115 ++++++------------ 1 file changed, 39 insertions(+), 76 deletions(-) diff --git a/Proposals/0011-concurrency-safe-notifications.md b/Proposals/0011-concurrency-safe-notifications.md index 00d7bccd3..727fa7d45 100644 --- a/Proposals/0011-concurrency-safe-notifications.md +++ b/Proposals/0011-concurrency-safe-notifications.md @@ -3,7 +3,7 @@ * Proposal: SF-0011 * Author(s): [Philippe Hausler](https://github.com/phausler), [Christopher Thielen](https://github.com/cthielen) * Review Manager: [Charles Hu](https://github.com/iCharlesHu) -* Status: **2nd Review** May. 15, 2025 ... May. 22, 2025 +* Status: **Accepted** ## Revision history @@ -11,6 +11,7 @@ * **v2** Remove `static` from `NotificationCenter.Message.isolation` to better support actor instances * **v3** Remove generic isolation pattern in favor of dedicated `MainActorMessage` and `AsyncMessage` types. Apply SE-0299-style static member lookups for `addObserver()`. Provide default value for `Message.name`. * **v4** Add `AsyncSequence` APIs for observing. Expand `Message.Subject` conformance to take either `AnyObject` or `Identifiable` where `Identifiable.ID == ObjectIdentifier`. Document `ObservationToken` automatic de-registration behavior. Drop `with` label on `post()` methods in favor of `subject` for clarity. +* **v5** Remove `Message` base protocol. Remove the `Identifiable`-based overloads. ## Introduction @@ -35,7 +36,7 @@ Well-written Swift code strongly prefers being explicit about concurrency isolat ## Proposed solution -We propose a new base protocol, `NotificationCenter.Message`, with specializations `NotificationCenter.MainActorMessage` and `NotificationCenter.AsyncMessage`, which allow the creation of strong types that can be posted and observed using `NotificationCenter`, and an optional protocol, `NotificationCenter.MessageIdentifier`, which provides a typed, ergonomic experience when registering observers. +We propose two new protocols, `NotificationCenter.MainActorMessage` and `NotificationCenter.AsyncMessage`, which allow the creation of strong types that can be posted and observed using `NotificationCenter`, and an optional protocol, `NotificationCenter.MessageIdentifier`, which provides a typed, ergonomic experience when registering observers. These protocols can be used on top of existing `Notification` declarations, enabling quick adoption. @@ -71,11 +72,11 @@ extension NotificationCenter.MessageIdentifier Messages conforming to `MainActorMessage` will bind observers to `MainActor` and deliver messages synchronously from `MainActor`-bound contexts, while messages conforming to `AsyncMessage` are `Sendable`, run observers in an asynchronous context, and are delivered asynchronously. -The optional lookup type, `NotificationCenter.MessageIdentifier`, provides an [SE-0299](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md)-style ergonomic experience for finding notification types when registering observers. The use of a separate `MessageIdentifier` type and `BaseMessageIdentifier` type ensures this lookup functionality does not impact implementations of `Message`-conforming types, and prevents `Message` types from needing to be initialized and discarded for the sole purpose of observer registration. +The optional lookup type, `NotificationCenter.MessageIdentifier`, provides an [SE-0299](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0299-extend-generic-static-member-lookup.md)-style ergonomic experience for finding notification types when registering observers. The first parameter of `addObserver(of:for:)` accepts both metatypes and instance types. Registering with a metatype enables an observer to receive all messages for the given identifier (equivalent to `object = nil` in the current `NotificationCenter`), while registering with an instance will only deliver messages related to that instance. -`NotificationCenter.Message` provides optional bi-directional interoperability with the existing `Notification` type by using the `Notification.Name` property and two optional methods, `makeMessage(:Notification)` and `makeNotification(:Self)`: +Optional bi-directional interoperability with the existing `Notification` type is available by using the `Notification.Name` property and two optional methods, `makeMessage(:Notification)` and `makeNotification(:Self)`: ```swift // Framework-side @@ -107,11 +108,11 @@ extension NSWorkspace { } ``` -Using these methods, posters and observers of both the `Notification` and `Message` type have full, bi-directional interoperability. +Using these methods, posters and observers of both the `Notification` and `Message`-style type have full, bi-directional interoperability. ## Example usage -This example adapts the existing [NSWorkspace.willLaunchApplicationNotification](https://developer.apple.com/documentation/appkit/nsworkspace/1528611-willlaunchapplicationnotificatio) `Notification` to use `NotificationCenter.Message`. It defines the optional `MessageIdentifier` to make registering observers easier, and it defines `makeMessage(:Notification)` and `makeNotification(:Self)` for bi-directional interoperability with existing NotificationCenter posters and observers. +This example adapts the existing [NSWorkspace.willLaunchApplicationNotification](https://developer.apple.com/documentation/appkit/nsworkspace/1528611-willlaunchapplicationnotificatio) `Notification` to use `NotificationCenter.MainActorMessage`. It defines the optional `MessageIdentifier` to make registering observers easier, and it defines `makeMessage(:Notification)` and `makeNotification(:Self)` for bi-directional interoperability with existing NotificationCenter posters and observers. Existing code which vends notifications do not need to alter existing `Notification` declarations, observers, or posts to adopt to this proposal. @@ -169,40 +170,45 @@ NotificationCenter.default.post( ## Detailed design -### `NotificationCenter.Message`, `NotificationCenter.MainActorMessage`, and `NotificationCenter.AsyncMessage` +### `NotificationCenter.MainActorMessage` and `NotificationCenter.AsyncMessage` -The `NotificationCenter.Message` protocol acts as a base for `NotificationCenter.MainActorMessage` and `NotificationCenter.AsyncMessage`, helping the two share functionality: +`NotificationCenter.MainActorMessage` and `NotificationCenter.AsyncMessage` contain similar functionality: ```swift @available(FoundationPreview 0.5, *) extension NotificationCenter { - public protocol Message { + public protocol MainActorMessage: SendableMetatype { associatedtype Subject + + static var name: Notification.Name { get } + + @MainActor static func makeMessage(_ notification: Notification) -> Self? + @MainActor static func makeNotification(_ message: Self) -> Notification + } + + public protocol AsyncMessage: Sendable { + associatedtype Subject + static var name: Notification.Name { get } static func makeMessage(_ notification: Notification) -> Self? static func makeNotification(_ message: Self) -> Notification } - - public protocol MainActorMessage: Message {} - public protocol AsyncMessage: Message, Sendable {} } ``` -`NotificationCenter.Message` is designed to interoperate with existing uses of `Notification` by sharing `Notification.Name` identifiers. This means an observer expecting `NotificationCenter.Message` will be called when a `Notification` is posted if the `Notification.Name` identifier matches, and vice versa. +These two protocols are designed to interoperate with existing uses of `Notification` by sharing `Notification.Name` identifiers. This means an observer expecting a type conforming to either of these protocols will be called when a `Notification` is posted if the `Notification.Name` identifier matches, and vice versa. -The protocol specifies `makeMessage(:Notification)` and `makeNotification(:Self)` to transform the payload between posters and observers of both the `NotificationCenter.Message` and `Notification` types. These methods have default implementations in cases where interoperability with `Notification` is not necessary. +The protocol specifies `makeMessage(:Notification)` and `makeNotification(:Self)` to transform the payload between posters and observers of both the `Message`-style and `Notification` types. These methods have default implementations in cases where interoperability with `Notification` is not necessary. -For `Message` types that do not need to interoperate with existing `Notification` uses, the `name` property does not need to be specified, and will default to the fully qualified name of the `Message` type, e.g. `MyModule.MyMessage`. Note that when using this default, renaming the type or relocating it to another module has a similar effect as changing ABI, as any code that was compiled separately will not be aware of the name change until recompiled. Developers can control this effect by explicitly setting the `name` property if needed. +For `Message`-style types that do not need to interoperate with existing `Notification` uses, the `name` property does not need to be specified, and will default to the fully qualified name of the `Message`-style type, e.g. `MyModule.MyMessage`. Note that when using this default, renaming the type or relocating it to another module has a similar effect as changing ABI for any code that relies on a particular spelling. Developers can control this effect by explicitly setting the `name` property if needed. -Each `Message` specifies a specific *subject* variable or metatype to observe, similar to the existing `Notification.object`, e.g. an `NSWindow` instance or the `NSWindow.self` metatype. `Message.Subject` has no conformance requirements in its protocol, but `addObserver()` and `post()` both refine `Message.Subject` to either conform to `AnyObject` or confirm to `Identifiable` where `Identifiable.ID == ObjectIdentifier`. +Each `Message`-style type specifies a specific *subject* variable or metatype to observe, similar to the existing `Notification.object`, e.g. an `NSWindow` instance or the `NSWindow.self` metatype. `Subject` has no conformance requirements in the protocols, but `addObserver()` and `post()` both refine `Subject` to conform to `AnyObject`. This allows for the posting and observing of either objects or metatypes. ### Observing messages Observing messages can be done with new overloads to `addObserver`. Clients do not need to know whether a message conforms to `MainActorMessage` or `AsyncMessage`. -Overloads are provided both for `Message.Subject: AnyObject` and `Message.Subject: Identifiable where ID == ObjectIdentifier`. This allows the observation of both reference types and value types which can provide an `ObjectIdentifier`. - For `MainActorMessage`: ```swift @@ -215,12 +221,6 @@ extension NotificationCenter { using observer: @escaping @MainActor (Message) -> Void ) -> ObservationToken where Identifier.MessageType == Message, Message.Subject: AnyObject - public func addObserver( - of subject: Message.Subject, - for identifier: Identifier, - using observer: @escaping @MainActor (Message) -> Void - ) -> ObservationToken where Identifier.MessageType == Message, Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier - // e.g. addObserver(of: NSWorkspace.self, for: .willLaunchApplication) { message in ... } public func addObserver( of subject: Message.Subject.Type, @@ -234,12 +234,6 @@ extension NotificationCenter { for messageType: Message.Type, using observer: @escaping @MainActor (Message) -> Void ) -> ObservationToken where Message.Subject: AnyObject - - public func addObserver( - of subject: Message.Subject? = nil, - for messageType: Message.Type, - using observer: @escaping @MainActor (Message) -> Void - ) -> ObservationToken where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier } ``` @@ -254,12 +248,6 @@ extension NotificationCenter { using observer: @escaping @Sendable (Message) async -> Void ) -> ObservationToken where Identifier.MessageType == Message, Message.Subject: AnyObject - public func addObserver( - of subject: Message.Subject, - for identifier: Identifier, - using observer: @escaping @Sendable (Message) async -> Void - ) -> ObservationToken where Identifier.MessageType == Message, Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier - public func addObserver( of subject: Message.Subject.Type, for identifier: Identifier, @@ -271,16 +259,10 @@ extension NotificationCenter { for messageType: Message.Type, using observer: @escaping @Sendable (Message) async -> Void ) -> ObservationToken where Message.Subject: AnyObject - - public func addObserver( - of subject: Message.Subject? = nil, - for messageType: Message.Type, - using observer: @escaping @Sendable (Message) async -> Void - ) -> ObservationToken where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier } ``` -Observer closures take a single `Message` parameter and do not receive the `subject` parameter passed to `addObserver()` nor `post()`. Not all messages use instances for their subjects, and not all subject instances are `Sendable` though their messages may be. If a `Message` author needs the `subject` to be delivered to the observer closure, they can do so by making it a property on their `Message` type. +Observer closures take a single `Message`-style parameter and do not receive the `subject` parameter passed to `addObserver()` nor `post()`. Not all messages use instances for their subjects, and not all subject instances are `Sendable` though their messages may be. If a `Message`-style type needs the `subject` to be delivered to the observer, it can be added as a property on the message type. These `addObserver()` methods return a new `ObservationToken`, which can be used with a new `removeObserver()` method for faster de-registration of observers: @@ -306,12 +288,6 @@ extension NotificationCenter { bufferSize limit: Int = 10 ) -> some AsyncSequence where Identifier.MessageType == Message, Message.Subject: AnyObject - public func messages( - of subject: Message.Subject, - for identifier: Identifier, - bufferSize limit: Int = 10 - ) -> some AsyncSequence where Identifier.MessageType == Message, Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier {} - public func messages( of subject: Message.Subject.Type, for identifier: Identifier, @@ -323,12 +299,6 @@ extension NotificationCenter { for messageType: Message.Type, bufferSize limit: Int = 10 ) -> some AsyncSequence where Message.Subject: AnyObject - - public func messages( - of subject: Message.Subject? = nil, - for messageType: Message.Type, - bufferSize limit: Int = 10 - ) -> some AsyncSequence where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier } ``` @@ -346,7 +316,7 @@ for await message in center.messages(for: AnAsyncMessage.self) { // etc. ``` -The `messages()` sequence uses a reasonably-sized buffer to reduce the likelihood of dropped messages caused by the interaction of synchronous and asynchronous code. When a `Message` is dropped, the implementation will log to aid in debugging. Message frequency in practice is typically 0-2x / second / message type and therefore unlikely to result in dropped messages. Certain UI-related messages can post in practice as often as 40 - 50x / second / message, but these are typically `MainActorMessage` and would not be subject to dropping nor available for use with `messages()`. +If the sequence buffer limit is exceeded, the implementation will log to aid in debugging. ### Posting messages @@ -362,10 +332,6 @@ extension NotificationCenter { public func post(_ message: Message, subject: Message.Subject) where Message.Subject: AnyObject - @MainActor - public func post(_ message: Message, subject: Message.Subject) - where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier - @MainActor public func post(_ message: Message, subject: Message.Subject.Type = Message.Subject.self) @@ -374,9 +340,6 @@ extension NotificationCenter { public func post(_ message: Message, subject: Message.Subject) where Message.Subject: AnyObject - public func post(_ message: Message, subject: Message.Subject) - where Message.Subject: Identifiable, Message.Subject.ID == ObjectIdentifier - public func post(_ message: Message, subject: Message.Subject.Type = Message.Subject.self) } ``` @@ -387,10 +350,10 @@ While both `post()` methods are called synchronously, only the `MainActorMessage ### Interoperability with `Notification` -Clients can migrate information to and from existing `Notification` types using `NotificationCenter.Message.makeMessage(:Notification)` and `NotificationCenter.Message.makeNotification(:Self)`. Implementing these enables the mixing of posters and observers between the `Notification` and `NotificationCenter.Message` types: +Clients can migrate information to and from existing `Notification` types using `makeMessage(:Notification)` and `makeNotification(:Self)`. Implementing these enables the mixing of posters and observers between the `Notification` and `Message`-style types: ```swift -struct EventDidOccur: NotificationCenter.Message { +struct EventDidOccur: NotificationCenter.MainActorMessage { var foo: Foo ... @@ -400,12 +363,12 @@ struct EventDidOccur: NotificationCenter.Message { } static func makeNotification(_ message: Self) -> Notification { - return Notification(name: Self.name, object: object, userInfo: ["foo": self.foo]) + return Notification(name: Self.name, userInfo: ["foo": self.foo]) } } ``` -These methods do not need to be implemented if all posters and observers are using `NotificationCenter.Message`. +These methods do not need to be implemented if all posters and observers are using `Message`-style types. See the table below for the effects of implementing `makeMessage(:Notification)` / `makeNotification(:Self)`: @@ -418,15 +381,15 @@ See the table below for the effects of implementing `makeMessage(:Notification)` Observers called via the existing, pre-Swift Concurrency `.post()` methods are either called on the same thread as the poster, or called in an explicitly passed `OperationQueue`. -However, users can still adopt `NotificationCenter.Message` with pre-Swift Concurrency `.post()` calls by providing a `NotificationCenter.Message` with the proper `Notification.Name` value and picking the correct type between `MainActorMessage` and `AsyncMessage`. +However, users can still adopt `Message`-style types with pre-Swift Concurrency `.post()` calls by providing a `Message`-style type with the proper `Notification.Name` value and picking the correct type between `MainActorMessage` and `AsyncMessage`. For example, if an Objective-C method calls the `post(name:object:userInfo:)` method on the main thread, `NotificationCenter.MainActorMessage` can be used to define a message with the same `Notification.Name`, enabling clients observing the message to access the `object` and `userInfo` parameters of the original `Notification` in a safe manner through `makeMessage(:Notification)`. ## Impact on existing code -These changes are entirely additive but could impact existing code due to the ability to interoperate between `NotificationCenter.Message` and `Notification`. +These changes are entirely additive but could impact existing code due to the ability to interoperate between `Message`-style types and `Notification`. -If an observer for `NotificationCenter.Message` receives a message posted as a `Notification` which violates the isolation contract specified in `NotificationCenter.MainActorMessage` / `NotificationCenter.AsyncMessage`, the correct fix may be to modify the existing `Notification` `.post()` call to uphold that contract. +If an observer receives a message posted as a `Notification` which violates the isolation contract specified in `NotificationCenter.MainActorMessage` / `NotificationCenter.AsyncMessage`, the correct fix may be to modify the existing `Notification` `.post()` call to uphold that contract. ## Future directions @@ -435,7 +398,7 @@ None at this time. ## Alternatives considered ### Use generic isolation to support actor instances and other global actors -A previous iteration of this proposal stored an `Actor`-conforming type on the `Message` protocol, enabling `addObserver()` and `post()` to declare `isolated` parameters conforming to the given type. This enabled a flexible form of generic isolation, enabling the use of arbitrary global actors, as well as isolating to instances of an actor: +A previous iteration of this proposal stored an `Actor`-conforming type on a `Message` protocol, enabling `addObserver()` and `post()` to declare `isolated` parameters conforming to the given type. This enabled a flexible form of generic isolation, enabling the use of arbitrary global actors, as well as isolating to instances of an actor: ```swift public func addObserver( @@ -452,7 +415,7 @@ Unfortunately, the design required careful handling to use correctly and had som * The `isolated` parameter value should really have a default value of `message.isolation` but it is not possible to cross-referencing parameter values this way in Swift today. * The use of `isolated` in the observer closure requires passing in an `Actor` type that the client likely does not need. -### Use `Message` directly for static member lookup +### Use `MainActorMessage`/`AsyncMessage` directly for static member lookup The `addObserver()` static member lookup experience requires there be a type initialized as the value of the given static member: ```swift @@ -465,12 +428,12 @@ extension NotificationCenter.MessageIdentifier } ``` -Alternatively, we could extend `Message` directly, and have the static variable return a specific `Message` type, removing the need for the `MessageIdentifier` protocol and `BaseMessageIdentifier` struct. +Alternatively, we could extend the `Message`-style protocols directly, and have the static variable return a specific `Message`-style type, removing the need for the `MessageIdentifier` protocol and `BaseMessageIdentifier` struct. -However, this puts initializer requirements on the `Message`-conforming type for the purposes of an optional lookup API, which could encourage developers to declare properties of their `Message` types as `Optional` when they shouldn't be. It also requires initializing and discarding a `Message` variable, which may or may not be large depending on future `Message` adoption, while the `MessageIdentifier` type is unlikely to grow. +However, this puts initializer requirements on the `Message`-style type for the purposes of an optional lookup API, which could encourage developers to declare properties of their `Message` types as `Optional` when they shouldn't be. It also requires initializing and discarding a `Message` variable, which may be needless. ### Deliver `subject` as a separate parameter to observers -The current proposal splits out the subject of a `Message` in the `addObserver()` overload but does not split it out in the observer closure nor the `post()` method: +The current proposal splits out the subject of a message in the `addObserver()` overload but does not split it out in the observer closure nor the `post()` method: ```swift // Subject is a separate parameter for addObserver() call, but not closure ... @@ -494,4 +457,4 @@ However, not all messages have subject instances (e.g. `addObserver(of: NSWindow Further, even messages with subjects do not necessarily need their observers to access the subject instance. -Finally, developers always have the choice of including the subject in the design of their `Message` types if they'd like. For these reasons, we've opted not to ferry `subject` through the API. +Finally, developers always have the choice of including the subject in the design of their `Message`-style types if they'd like. For these reasons, we've opted not to ferry `subject` through the API. From 04493a43074b1c254863fd1e3bd3f5cd04b51483 Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 26 Jun 2025 13:11:23 -0700 Subject: [PATCH 8/9] Revert "Adopt utimensat for setting file modification dates (#1324)" (#1379) This reverts commit 9404caa2a70fa08e4a05bf3259e24d968adc8e79. --- .../FileManager/FileManager+Files.swift | 16 ++++++++-------- .../FileManager/FileManagerTests.swift | 15 --------------- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift index 0b3b4e4f3..b2666ccbc 100644 --- a/Sources/FoundationEssentials/FileManager/FileManager+Files.swift +++ b/Sources/FoundationEssentials/FileManager/FileManager+Files.swift @@ -966,14 +966,14 @@ extension _FileManagerImpl { if let date = attributes[.modificationDate] as? Date { let (isecs, fsecs) = modf(date.timeIntervalSince1970) if let tv_sec = time_t(exactly: isecs), - let tv_nsec = Int(exactly: round(fsecs * 1000000000.0)) { - var timespecs = (timespec(), timespec()) - timespecs.0.tv_sec = tv_sec - timespecs.0.tv_nsec = tv_nsec - timespecs.1 = timespecs.0 - try withUnsafePointer(to: timespecs) { - try $0.withMemoryRebound(to: timespec.self, capacity: 2) { - if utimensat(AT_FDCWD, fileSystemRepresentation, $0, 0) != 0 { + let tv_usec = suseconds_t(exactly: round(fsecs * 1000000.0)) { + var timevals = (timeval(), timeval()) + timevals.0.tv_sec = tv_sec + timevals.0.tv_usec = tv_usec + timevals.1 = timevals.0 + try withUnsafePointer(to: timevals) { + try $0.withMemoryRebound(to: timeval.self, capacity: 2) { + if utimes(fileSystemRepresentation, $0) != 0 { throw CocoaError.errorWithFilePath(path, errno: errno, reading: false) } } diff --git a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift index 4d59b9e66..1c107c38e 100644 --- a/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift +++ b/Tests/FoundationEssentialsTests/FileManager/FileManagerTests.swift @@ -841,21 +841,6 @@ private struct FileManagerTests { } } - @Test func roundtripModificationDate() async throws { - try await FilePlayground { - "foo" - }.test { - // Precision of modification dates is dependent not only on the platform, but on the file system used - // Ensure that roundtripping supports at least millisecond-level precision, but some file systems may support more up to nanosecond precision - let date = Date(timeIntervalSince1970: 10.003) - try $0.setAttributes([.modificationDate : date], ofItemAtPath: "foo") - let readValue = try #require($0.attributesOfItem(atPath: "foo")[.modificationDate], "No value provided for file modification date") - let possibleDate = readValue as? Date - let readDate = try #require(possibleDate, "File modification date was not a date (found type \(type(of: readValue)))") - #expect(abs(readDate.timeIntervalSince1970 - date.timeIntervalSince1970) <= 0.0005, "File modification date (\(readDate.timeIntervalSince1970)) does not match expected modification date (\(date.timeIntervalSince1970))") - } - } - @Test func implicitlyConvertibleFileAttributes() async throws { try await FilePlayground { File("foo", attributes: [.posixPermissions : UInt16(0o644)]) From eb084da833b155e2e440d6ca3ae9da32b1fd4cba Mon Sep 17 00:00:00 2001 From: Jeremy Schonfeld Date: Thu, 26 Jun 2025 15:37:03 -0700 Subject: [PATCH 9/9] Begin converting FoundationInternationalizationTests to swift-testing (#1381) * Convert URL UIDNA Tests * Add infrastructure for current preference dependent tests * Convert TimeZone tests * Convert String tests * Remove unused utility file * Convert some Locale tests * Update GregorianCalendarInternationalizationTests to avoid using the default timezone * Convert Duration extension tests * Convert Decimal locale tests * Convert some Formatting tests --- ...InternationalizationPreferencesActor.swift | 44 ++ .../DecimalTests+Locale.swift | 95 ++-- .../DurationExtensionTests.swift | 17 +- .../DateIntervalFormatStyleTests.swift | 235 +++++---- .../Formatting/FormatterCacheTests.swift | 69 ++- .../Formatting/ICUPatternGeneratorTests.swift | 25 +- .../NumberFormatStyleICUSkeletonTests.swift | 149 +++--- .../ParseStrategy+RegexComponentTests.swift | 105 ++-- ...ianCalendarInternationalizationTests.swift | 6 +- .../LocaleComponentsTests.swift | 451 ++++++++---------- .../LocaleLanguageTests.swift | 60 +-- .../LocaleTestUtilities.swift | 31 -- ...ata.swift => StringICUEncodingTests.swift} | 44 +- .../StringTests+Locale.swift | 40 +- .../TimeZoneTests.swift | 339 ++++++------- .../URLTests+UIDNA.swift | 19 +- 16 files changed, 847 insertions(+), 882 deletions(-) create mode 100644 Tests/FoundationInternationalizationTests/CurrentInternationalizationPreferencesActor.swift delete mode 100644 Tests/FoundationInternationalizationTests/LocaleTestUtilities.swift rename Tests/FoundationInternationalizationTests/{StringTests+Data.swift => StringICUEncodingTests.swift} (75%) diff --git a/Tests/FoundationInternationalizationTests/CurrentInternationalizationPreferencesActor.swift b/Tests/FoundationInternationalizationTests/CurrentInternationalizationPreferencesActor.swift new file mode 100644 index 000000000..5cb85ac7d --- /dev/null +++ b/Tests/FoundationInternationalizationTests/CurrentInternationalizationPreferencesActor.swift @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationInternationalization) +@testable import FoundationEssentials +@testable import FoundationInternationalization +#else +@testable import Foundation +#endif + +// This actor is private and not exposed to tests to prevent accidentally writing tests annotated with @CurrentInternationalizationPreferencesActor which may have suspension points +// Using the global helper function below ensures that only synchronous work with no suspension points is queued +@globalActor +private actor CurrentInternationalizationPreferencesActor: GlobalActor { + static let shared = CurrentInternationalizationPreferencesActor() + + private init() {} + + @CurrentInternationalizationPreferencesActor + static func usingCurrentInternationalizationPreferences( + body: () throws -> Void // Must be synchronous to prevent suspension points within body which could introduce a change in the preferences + ) rethrows { + try body() + + // Reset everything after the test runs to ensure custom values don't persist + LocaleCache.cache.reset() + CalendarCache.cache.reset() + _ = TimeZoneCache.cache.reset() + _ = TimeZone.resetSystemTimeZone() + } +} + +internal func usingCurrentInternationalizationPreferences(_ body: sending () throws -> Void) async rethrows { + try await CurrentInternationalizationPreferencesActor.usingCurrentInternationalizationPreferences(body: body) +} diff --git a/Tests/FoundationInternationalizationTests/DecimalTests+Locale.swift b/Tests/FoundationInternationalizationTests/DecimalTests+Locale.swift index c39b7ff16..da47bcfee 100644 --- a/Tests/FoundationInternationalizationTests/DecimalTests+Locale.swift +++ b/Tests/FoundationInternationalizationTests/DecimalTests+Locale.swift @@ -10,58 +10,75 @@ // //===----------------------------------------------------------------------===// -#if canImport(TestSupport) -import TestSupport +import Testing + +#if canImport(FoundationInternationalization) +@testable import FoundationEssentials +@testable import FoundationInternationalization +#elseif FOUNDATION_FRAMEWORK +@testable import Foundation #endif -final class DecimalLocaleTests : XCTestCase { - func test_stringWithLocale() { +@Suite("Decimal (Locale)") +private struct DecimalLocaleTests { + + @Test func stringWithLocale() { + let en_US = Locale(identifier: "en_US") let fr_FR = Locale(identifier: "fr_FR") - XCTAssertEqual(Decimal(string: "1,234.56")! * 1000, Decimal(1000)) - XCTAssertEqual(Decimal(string: "1,234.56", locale: en_US)! * 1000, Decimal(1000)) - XCTAssertEqual(Decimal(string: "1,234.56", locale: fr_FR)! * 1000, Decimal(1234)) - XCTAssertEqual(Decimal(string: "1.234,56", locale: en_US)! * 1000, Decimal(1234)) - XCTAssertEqual(Decimal(string: "1.234,56", locale: fr_FR)! * 1000, Decimal(1000)) + #expect(Decimal(string: "1,234.56")! * 1000 == Decimal(1000)) + #expect(Decimal(string: "1,234.56", locale: en_US)! * 1000 == Decimal(1000)) + #expect(Decimal(string: "1,234.56", locale: fr_FR)! * 1000 == Decimal(1234)) + #expect(Decimal(string: "1.234,56", locale: en_US)! * 1000 == Decimal(1234)) + #expect(Decimal(string: "1.234,56", locale: fr_FR)! * 1000 == Decimal(1000)) - XCTAssertEqual(Decimal(string: "-1,234.56")! * 1000, Decimal(-1000)) - XCTAssertEqual(Decimal(string: "+1,234.56")! * 1000, Decimal(1000)) - XCTAssertEqual(Decimal(string: "+1234.56e3"), Decimal(1234560)) - XCTAssertEqual(Decimal(string: "+1234.56E3"), Decimal(1234560)) - XCTAssertEqual(Decimal(string: "+123456000E-3"), Decimal(123456)) + #expect(Decimal(string: "-1,234.56")! * 1000 == Decimal(-1000)) + #expect(Decimal(string: "+1,234.56")! * 1000 == Decimal(1000)) + #expect(Decimal(string: "+1234.56e3") == Decimal(1234560)) + #expect(Decimal(string: "+1234.56E3") == Decimal(1234560)) + #expect(Decimal(string: "+123456000E-3") == Decimal(123456)) - XCTAssertNil(Decimal(string: "")) - XCTAssertNil(Decimal(string: "x")) - XCTAssertEqual(Decimal(string: "-x"), Decimal.zero) - XCTAssertEqual(Decimal(string: "+x"), Decimal.zero) - XCTAssertEqual(Decimal(string: "-"), Decimal.zero) - XCTAssertEqual(Decimal(string: "+"), Decimal.zero) - XCTAssertEqual(Decimal(string: "-."), Decimal.zero) - XCTAssertEqual(Decimal(string: "+."), Decimal.zero) + #expect(Decimal(string: "") == nil) + #expect(Decimal(string: "x") == nil) + #expect(Decimal(string: "-x") == Decimal.zero) + #expect(Decimal(string: "+x") == Decimal.zero) + #expect(Decimal(string: "-") == Decimal.zero) + #expect(Decimal(string: "+") == Decimal.zero) + #expect(Decimal(string: "-.") == Decimal.zero) + #expect(Decimal(string: "+.") == Decimal.zero) - XCTAssertEqual(Decimal(string: "-0"), Decimal.zero) - XCTAssertEqual(Decimal(string: "+0"), Decimal.zero) - XCTAssertEqual(Decimal(string: "-0."), Decimal.zero) - XCTAssertEqual(Decimal(string: "+0."), Decimal.zero) - XCTAssertEqual(Decimal(string: "e1"), Decimal.zero) - XCTAssertEqual(Decimal(string: "e-5"), Decimal.zero) - XCTAssertEqual(Decimal(string: ".3e1"), Decimal(3)) + #expect(Decimal(string: "-0") == Decimal.zero) + #expect(Decimal(string: "+0") == Decimal.zero) + #expect(Decimal(string: "-0.") == Decimal.zero) + #expect(Decimal(string: "+0.") == Decimal.zero) + #expect(Decimal(string: "e1") == Decimal.zero) + #expect(Decimal(string: "e-5") == Decimal.zero) + #expect(Decimal(string: ".3e1") == Decimal(3)) - XCTAssertEqual(Decimal(string: "."), Decimal.zero) - XCTAssertEqual(Decimal(string: ".", locale: en_US), Decimal.zero) - XCTAssertNil(Decimal(string: ".", locale: fr_FR)) + #expect(Decimal(string: ".") == Decimal.zero) + #expect(Decimal(string: ".", locale: en_US) == Decimal.zero) + #expect(Decimal(string: ".", locale: fr_FR) == nil) - XCTAssertNil(Decimal(string: ",")) - XCTAssertEqual(Decimal(string: ",", locale: fr_FR), Decimal.zero) - XCTAssertNil(Decimal(string: ",", locale: en_US)) + #expect(Decimal(string: ",") == nil) + #expect(Decimal(string: ",", locale: fr_FR) == Decimal.zero) + #expect(Decimal(string: ",", locale: en_US) == nil) let s1 = "1234.5678" - XCTAssertEqual(Decimal(string: s1, locale: en_US)?.description, s1) - XCTAssertEqual(Decimal(string: s1, locale: fr_FR)?.description, "1234") + #expect(Decimal(string: s1, locale: en_US)?.description == s1) + #expect(Decimal(string: s1, locale: fr_FR)?.description == "1234") let s2 = "1234,5678" - XCTAssertEqual(Decimal(string: s2, locale: en_US)?.description, "1234") - XCTAssertEqual(Decimal(string: s2, locale: fr_FR)?.description, s1) + #expect(Decimal(string: s2, locale: en_US)?.description == "1234") + #expect(Decimal(string: s2, locale: fr_FR)?.description == s1) + } + + @Test func descriptionWithLocale() throws { + let decimal = Decimal(string: "-123456.789")! + #expect(decimal._toString(withDecimalSeparator: ".") == "-123456.789") + let en = decimal._toString(withDecimalSeparator: try #require(Locale(identifier: "en_GB").decimalSeparator)) + #expect(en == "-123456.789") + let fr = decimal._toString(withDecimalSeparator: try #require(Locale(identifier: "fr_FR").decimalSeparator)) + #expect(fr == "-123456,789") } } diff --git a/Tests/FoundationInternationalizationTests/DurationExtensionTests.swift b/Tests/FoundationInternationalizationTests/DurationExtensionTests.swift index 75235ab31..14d878ea5 100644 --- a/Tests/FoundationInternationalizationTests/DurationExtensionTests.swift +++ b/Tests/FoundationInternationalizationTests/DurationExtensionTests.swift @@ -10,31 +10,28 @@ // //===----------------------------------------------------------------------===// -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if canImport(FoundationInternationalization) @testable import FoundationEssentials @testable import FoundationInternationalization -#endif - -#if FOUNDATION_FRAMEWORK +#elseif FOUNDATION_FRAMEWORK @testable import Foundation #endif -final class DurationExtensionTests : XCTestCase { +@Suite("Duration Extension") +private struct DurationExtensionTests { - func testRoundingMode() { + @Test func roundingMode() { - func verify(_ tests: [Int64], increment: Int64, expected: [FloatingPointRoundingRule: [Int64]], file: StaticString = #filePath, line: UInt = #line) { + func verify(_ tests: [Int64], increment: Int64, expected: [FloatingPointRoundingRule: [Int64]], sourceLocation: SourceLocation = #_sourceLocation) { let modes: [FloatingPointRoundingRule] = [.down, .up, .towardZero, .awayFromZero, .toNearestOrEven, .toNearestOrAwayFromZero] for mode in modes { var actual: [Duration] = [] for test in tests { actual.append(Duration.seconds(test).rounded(increment: Duration.seconds(increment), rule: mode)) } - XCTAssertEqual(actual, expected[mode]?.map { Duration.seconds($0) }, "\(mode) does not match", file: file, line: line) + #expect(actual == expected[mode]?.map { Duration.seconds($0) }, "\(mode) does not match", sourceLocation: sourceLocation) } } diff --git a/Tests/FoundationInternationalizationTests/Formatting/DateIntervalFormatStyleTests.swift b/Tests/FoundationInternationalizationTests/Formatting/DateIntervalFormatStyleTests.swift index d00b4c57b..6ed9c6129 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/DateIntervalFormatStyleTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/DateIntervalFormatStyleTests.swift @@ -5,129 +5,124 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// -// -// RUN: %target-run-simple-swift -// REQUIRES: executable_test -// REQUIRES: objc_intero -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if canImport(FoundationInternationalization) @testable import FoundationEssentials @testable import FoundationInternationalization -#endif - -#if FOUNDATION_FRAMEWORK +#elseif FOUNDATION_FRAMEWORK @testable import Foundation #endif -final class DateIntervalFormatStyleTests: XCTestCase { - +@Suite("Date.IntervalFormatStyle") +private struct DateIntervalFormatStyleTests { + let minute: TimeInterval = 60 let hour: TimeInterval = 60 * 60 let day: TimeInterval = 60 * 60 * 24 let enUSLocale = Locale(identifier: "en_US") - let calendar = Calendar(identifier: .gregorian) + let calendar = { + var c = Calendar(identifier: .gregorian) + c.timeZone = .gmt + return c + }() let timeZone = TimeZone(abbreviation: "GMT")! - + let date = Date(timeIntervalSinceReferenceDate: 0) - - let expectedSeparator = "\u{202f}" - - func testDefaultFormatStyle() throws { + + @Test func defaultFormatStyle() throws { var style = Date.IntervalFormatStyle() style.timeZone = timeZone // Make sure the default style does produce some output - XCTAssertGreaterThan(style.format(date ..< date + hour).count, 0) + #expect(style.format(date ..< date + hour).count > 0) } - - func testBasicFormatStyle() throws { + + @Test func basicFormatStyle() throws { let style = Date.IntervalFormatStyle(locale: enUSLocale, calendar: calendar, timeZone: timeZone) - XCTAssertEqual(style.format(date.. (Date.IntervalFormatStyle)) { - + func verify(_ tests: (locale: Locale, expected: String, expectedAfternoon: String)..., sourceLocation: SourceLocation = #_sourceLocation, customStyle: (Date.IntervalFormatStyle) -> (Date.IntervalFormatStyle)) { + let style = customStyle(Date.IntervalFormatStyle(locale: enUSLocale, calendar: calendar, timeZone: timeZone)) for (i, (locale, expected, expectedAfternoon)) in tests.enumerated() { let localizedStyle = style.locale(locale) - XCTAssertEqual(localizedStyle.format(range), expected, file: file, line: line + UInt(i)) - XCTAssertEqual(localizedStyle.format(afternoon), expectedAfternoon, file: file, line: line + UInt(i)) + var loc = sourceLocation + loc.line += i + #expect(localizedStyle.format(range) == expected, sourceLocation: loc) + #expect(localizedStyle.format(afternoon) == expectedAfternoon, sourceLocation: loc) } } verify((default12, "12:00 – 1:00 AM", "1:00 – 3:00 PM"), @@ -164,36 +161,36 @@ final class DateIntervalFormatStyleTests: XCTestCase { style.hour().minute() } - verify((default24, "00:00 – 01:00", "13:00 – 15:00"), - (default24force12, "12:00 – 1:00 AM", "1:00 – 3:00 PM")) { style in + verify((default24, "00:00 – 01:00", "13:00 – 15:00"), + (default24force12, "12:00 – 1:00 AM", "1:00 – 3:00 PM")) { style in style.hour().minute() } - + #if FIXED_96909465 // ICU does not yet support two-digit hour configuration - verify((default12, "12:00 – 1:00 AM", "01:00 – 03:00 PM"), + verify((default12, "12:00 – 1:00 AM", "01:00 – 03:00 PM"), (default12force24, "00:00 – 01:00", "13:00 – 15:00"), (default24, "00:00 – 01:00", "13:00 – 15:00"), - (default24force12, "12:00 – 1:00 AM", "01:00 – 03:00 PM")) { style in + (default24force12, "12:00 – 1:00 AM", "01:00 – 03:00 PM")) { style in style.hour(.twoDigits(amPM: .abbreviated)).minute() } #endif - + verify((default12, "12:00 – 1:00", "1:00 – 3:00"), (default12force24, "00:00 – 01:00", "13:00 – 15:00")) { style in style.hour(.twoDigits(amPM: .omitted)).minute() } - + verify((default24, "00:00 – 01:00", "13:00 – 15:00")) { style in style.hour(.twoDigits(amPM: .omitted)).minute() } - + #if FIXED_97447020 verify() { style in style.hour(.twoDigits(amPM: .omitted)).minute() } #endif - + verify((default12, "Jan 1, 12:00 – 1:00 AM", "Jan 1, 1:00 – 3:00 PM"), (default12force24, "Jan 1, 00:00 – 01:00", "Jan 1, 13:00 – 15:00"), (default24, "1 Jan, 00:00 – 01:00", "1 Jan, 13:00 – 15:00"), @@ -202,28 +199,30 @@ final class DateIntervalFormatStyleTests: XCTestCase { } } #endif // FIXED_ICU_74_DAYPERIOD - - func testAutoupdatingCurrentChangesFormatResults() { - let locale = Locale.autoupdatingCurrent - let range = Date.now..<(Date.now + 3600) - - // Get a formatted result from es-ES - var prefs = LocalePreferences() - prefs.languages = ["es-ES"] - prefs.locale = "es_ES" - LocaleCache.cache.resetCurrent(to: prefs) - let formattedSpanish = range.formatted(.interval.locale(locale)) - - // Get a formatted result from en-US - prefs.languages = ["en-US"] - prefs.locale = "en_US" - LocaleCache.cache.resetCurrent(to: prefs) - let formattedEnglish = range.formatted(.interval.locale(locale)) - - // Reset to current preferences before any possibility of failing this test - LocaleCache.cache.reset() - - // No matter what 'current' was before this test was run, formattedSpanish and formattedEnglish should be different. - XCTAssertNotEqual(formattedSpanish, formattedEnglish) + + @Test func testAutoupdatingCurrentChangesFormatResults() async { + await usingCurrentInternationalizationPreferences { + let locale = Locale.autoupdatingCurrent + let range = Date.now..<(Date.now + 3600) + + // Get a formatted result from es-ES + var prefs = LocalePreferences() + prefs.languages = ["es-ES"] + prefs.locale = "es_ES" + LocaleCache.cache.resetCurrent(to: prefs) + let formattedSpanish = range.formatted(.interval.locale(locale)) + + // Get a formatted result from en-US + prefs.languages = ["en-US"] + prefs.locale = "en_US" + LocaleCache.cache.resetCurrent(to: prefs) + let formattedEnglish = range.formatted(.interval.locale(locale)) + + // Reset to current preferences before any possibility of failing this test + LocaleCache.cache.reset() + + // No matter what 'current' was before this test was run, formattedSpanish and formattedEnglish should be different. + #expect(formattedSpanish != formattedEnglish) + } } } diff --git a/Tests/FoundationInternationalizationTests/Formatting/FormatterCacheTests.swift b/Tests/FoundationInternationalizationTests/Formatting/FormatterCacheTests.swift index e5e463ad1..450267995 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/FormatterCacheTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/FormatterCacheTests.swift @@ -5,25 +5,18 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// -// -// RUN: %target-run-simple-swift -// REQUIRES: executable_test -// REQUIRES: objc_interop -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if canImport(FoundationInternationalization) @testable import FoundationEssentials @testable import FoundationInternationalization -#endif - -#if FOUNDATION_FRAMEWORK +#elseif FOUNDATION_FRAMEWORK @testable import Foundation #endif -final class FormatterCacheTests: XCTestCase { +@Suite("FormatterCache") +private struct FormatterCacheTests { final class TestCacheItem: Equatable, Sendable { static func == (lhs: FormatterCacheTests.TestCacheItem, rhs: FormatterCacheTests.TestCacheItem) -> Bool { @@ -43,7 +36,7 @@ final class FormatterCacheTests: XCTestCase { } - func testCreateItem() { + @Test func createItem() { let cache = FormatterCache() var initializerBlockInvocationCount = 0 @@ -54,20 +47,20 @@ final class FormatterCacheTests: XCTestCase { initializerBlockInvocationCount += 1 return -i } - XCTAssertEqual(item, -i) + #expect(item == -i) } // `creator` block has been called 101 times - XCTAssertEqual(initializerBlockInvocationCount, cache.countLimit + 1) + #expect(initializerBlockInvocationCount == cache.countLimit + 1) // `creator` block does not get executed when the key exists for i in 0..() - let group = DispatchGroup() - let queue = DispatchQueue(label: "formatter cache test", qos: .default, attributes: .concurrent) - - - for i in 0 ..< 5 { - queue.async(group: group) { - let cached = cache.formatter(for: i) { - return .init(value: -i, deinitBlock: { - // Test that `removeAllObjects` beneath does not trigger `deinit` of the removed objects in the locked scope. - // If it does cause the deinitialization of this instance where this block is run, we would deadlock here because the subscript getter is performed in the same locked scope as the enclosing `formatter(for:creator:)`. - _ = cache[i] - }) - } - XCTAssertEqual(cached.value, -i) - } - queue.async(group: group) { - cache.removeAllObjects() + await withDiscardingTaskGroup { group in + for i in 0 ..< 5 { + group.addTask { + let cached = cache.formatter(for: i) { + return .init(value: -i, deinitBlock: { + // Test that `removeAllObjects` beneath does not trigger `deinit` of the removed objects in the locked scope. + // If it does cause the deinitialization of this instance where this block is run, we would deadlock here because the subscript getter is performed in the same locked scope as the enclosing `formatter(for:creator:)`. + _ = cache[i] + }) + } + #expect(cached.value == -i) + } + + group.addTask { + cache.removeAllObjects() + } } } - - XCTAssertEqual(group.wait(timeout: .now().advanced(by: .seconds(3))), .success) } -#endif // FOUNDATION_FRAMEWORK } diff --git a/Tests/FoundationInternationalizationTests/Formatting/ICUPatternGeneratorTests.swift b/Tests/FoundationInternationalizationTests/Formatting/ICUPatternGeneratorTests.swift index ffebfe7ba..beb04a4f8 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/ICUPatternGeneratorTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/ICUPatternGeneratorTests.swift @@ -6,33 +6,30 @@ // //===----------------------------------------------------------------------===// -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if canImport(FoundationInternationalization) @testable import FoundationEssentials @testable import FoundationInternationalization -#endif - -#if FOUNDATION_FRAMEWORK +#elseif FOUNDATION_FRAMEWORK @testable import Foundation #endif -final class ICUPatternGeneratorTests: XCTestCase { +@Suite("ICUPatternGenerator") +private struct ICUPatternGeneratorTests { typealias DateFieldCollection = Date.FormatStyle.DateFieldCollection - func testConversationalDayPeriodsOverride() { + @Test func conversationalDayPeriodsOverride() { var locale: Locale var calendar: Calendar - func test(symbols: Date.FormatStyle.DateFieldCollection, expectedPattern: String, file: StaticString = #filePath, line: UInt = #line) { + func test(symbols: Date.FormatStyle.DateFieldCollection, expectedPattern: String, sourceLocation: SourceLocation = #_sourceLocation) { let pattern = ICUPatternGenerator.localizedPattern(symbols: symbols, locale: locale, calendar: calendar) - XCTAssertEqual(pattern, expectedPattern, file: file, line: line) + #expect(pattern == expectedPattern, sourceLocation: sourceLocation) // We should not see any kind of day period designator ("a" or "B") when showing 24-hour hour ("H"). if (expectedPattern.contains("H") || pattern.contains("H")) && (pattern.contains("a") || pattern.contains("B")) { - XCTFail("Pattern should not contain day period", file: file, line: line) + Issue.record("Pattern should not contain day period", sourceLocation: sourceLocation) } } @@ -40,6 +37,7 @@ final class ICUPatternGeneratorTests: XCTestCase { do { locale = Locale(identifier: "zh_TW") calendar = Calendar(identifier: .gregorian) + calendar.timeZone = .gmt test(symbols: .init(year: .defaultDigits, month: .defaultDigits, day: .defaultDigits, hour: .defaultDigitsWithWideAMPM), expectedPattern: "y/M/d BBBBh時") @@ -83,6 +81,7 @@ final class ICUPatternGeneratorTests: XCTestCase { do { locale = Locale(identifier: "zh_TW") calendar = Calendar(identifier: .republicOfChina) + calendar.timeZone = .gmt test(symbols: .init(year: .defaultDigits, month: .defaultDigits, day: .defaultDigits, hour: .defaultDigitsWithWideAMPM), expectedPattern: "G y/M/d BBBBh時") @@ -113,6 +112,7 @@ final class ICUPatternGeneratorTests: XCTestCase { do { locale = Locale(identifier: "zh_TW") calendar = Calendar(identifier: .gregorian) + calendar.timeZone = .gmt test(symbols: .init(year: .defaultDigits, month: .defaultDigits, day: .defaultDigits, hour: .defaultDigitsWithWideAMPM), expectedPattern: "y/M/d BBBBh時") @@ -159,6 +159,7 @@ final class ICUPatternGeneratorTests: XCTestCase { locale = Locale(components: localeUsing24hour) calendar = Calendar(identifier: .gregorian) + calendar.timeZone = .gmt test(symbols: .init(year: .defaultDigits, month: .defaultDigits, day: .defaultDigits, hour: .defaultDigitsWithWideAMPM), expectedPattern: "y/M/d H時") @@ -201,6 +202,7 @@ final class ICUPatternGeneratorTests: XCTestCase { do { locale = Locale(identifier: "zh_HK") calendar = Calendar(identifier: .gregorian) + calendar.timeZone = .gmt test(symbols: .init(year: .defaultDigits, month: .defaultDigits, day: .defaultDigits, hour: .defaultDigitsWithWideAMPM), expectedPattern: "d/M/y aaaah時") @@ -243,6 +245,7 @@ final class ICUPatternGeneratorTests: XCTestCase { // So there should be no "B" in the pattern locale = Locale(identifier: "en_TW") calendar = Calendar(identifier: .gregorian) + calendar.timeZone = .gmt test(symbols: .init(hour: .defaultDigitsWithAbbreviatedAMPM, minute: .defaultDigits), expectedPattern: "h:mm a") diff --git a/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleICUSkeletonTests.swift b/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleICUSkeletonTests.swift index 0d9ca0637..ea89d6212 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleICUSkeletonTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleICUSkeletonTests.swift @@ -5,128 +5,121 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// -// -// RUN: %target-run-simple-swift -// REQUIRES: executable_test -// REQUIRES: objc_interop -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if canImport(FoundationInternationalization) @testable import FoundationEssentials @testable import FoundationInternationalization -#endif - -#if FOUNDATION_FRAMEWORK +#elseif FOUNDATION_FRAMEWORK @testable import Foundation #endif -final class NumberFormatStyleICUSkeletonTests: XCTestCase { +@Suite("NumberFormatStyle ICU Skeleton") +private struct NumberFormatStyleICUSkeletonTests { - func testNumberConfigurationSkeleton() throws { + @Test func numberConfigurationSkeleton() throws { typealias Configuration = NumberFormatStyleConfiguration.Collection - XCTAssertEqual(Configuration().skeleton, "") + #expect(Configuration().skeleton == "") } - func testPrecisionSkeleton() throws { + @Test func precisionSkeleton() throws { typealias Precision = NumberFormatStyleConfiguration.Precision - XCTAssertEqual(Precision.significantDigits(3...3).skeleton, "@@@") - XCTAssertEqual(Precision.significantDigits(2...).skeleton, "@@+") - XCTAssertEqual(Precision.significantDigits(...4).skeleton, "@###") - XCTAssertEqual(Precision.significantDigits(3...4).skeleton, "@@@#") + #expect(Precision.significantDigits(3...3).skeleton == "@@@") + #expect(Precision.significantDigits(2...).skeleton == "@@+") + #expect(Precision.significantDigits(...4).skeleton == "@###") + #expect(Precision.significantDigits(3...4).skeleton == "@@@#") // Invalid configuration. We'll force at least one significant digits. - XCTAssertEqual(Precision.significantDigits(0...0).skeleton, "@") - XCTAssertEqual(Precision.significantDigits(...0).skeleton, "@") - - XCTAssertEqual(Precision.fractionLength(...0).skeleton, "precision-integer") - XCTAssertEqual(Precision.fractionLength(0...0).skeleton, "precision-integer") - XCTAssertEqual(Precision.fractionLength(3...3).skeleton, ".000") - XCTAssertEqual(Precision.fractionLength(1...).skeleton, ".0+") - XCTAssertEqual(Precision.fractionLength(...1).skeleton, ".#") - XCTAssertEqual(Precision.fractionLength(1...3).skeleton, ".0##") - - XCTAssertEqual(Precision.integerLength(0...).skeleton, "integer-width/+") - XCTAssertEqual(Precision.integerLength(1...).skeleton, "integer-width/+0") - XCTAssertEqual(Precision.integerLength(3...).skeleton, "integer-width/+000") - XCTAssertEqual(Precision.integerLength(1...3).skeleton, "integer-width/##0") - XCTAssertEqual(Precision.integerLength(2...2).skeleton, "integer-width/00") - XCTAssertEqual(Precision.integerLength(...3).skeleton, "integer-width/###") + #expect(Precision.significantDigits(0...0).skeleton == "@") + #expect(Precision.significantDigits(...0).skeleton == "@") + + #expect(Precision.fractionLength(...0).skeleton == "precision-integer") + #expect(Precision.fractionLength(0...0).skeleton == "precision-integer") + #expect(Precision.fractionLength(3...3).skeleton == ".000") + #expect(Precision.fractionLength(1...).skeleton == ".0+") + #expect(Precision.fractionLength(...1).skeleton == ".#") + #expect(Precision.fractionLength(1...3).skeleton == ".0##") + + #expect(Precision.integerLength(0...).skeleton == "integer-width/+") + #expect(Precision.integerLength(1...).skeleton == "integer-width/+0") + #expect(Precision.integerLength(3...).skeleton == "integer-width/+000") + #expect(Precision.integerLength(1...3).skeleton == "integer-width/##0") + #expect(Precision.integerLength(2...2).skeleton == "integer-width/00") + #expect(Precision.integerLength(...3).skeleton == "integer-width/###") // Special case - XCTAssertEqual(Precision.integerLength(...0).skeleton, "integer-width/*") + #expect(Precision.integerLength(...0).skeleton == "integer-width/*") } - func testSignDisplaySkeleton() throws { + @Test func signDisplaySkeleton() throws { typealias SignDisplay = NumberFormatStyleConfiguration.SignDisplayStrategy - XCTAssertEqual(SignDisplay.never.skeleton, "sign-never") - XCTAssertEqual(SignDisplay.always().skeleton, "sign-always") - XCTAssertEqual(SignDisplay.always(includingZero: true).skeleton, "sign-always") - XCTAssertEqual(SignDisplay.always(includingZero: false).skeleton, "sign-except-zero") - XCTAssertEqual(SignDisplay.automatic.skeleton, "sign-auto") + #expect(SignDisplay.never.skeleton == "sign-never") + #expect(SignDisplay.always().skeleton == "sign-always") + #expect(SignDisplay.always(includingZero: true).skeleton == "sign-always") + #expect(SignDisplay.always(includingZero: false).skeleton == "sign-except-zero") + #expect(SignDisplay.automatic.skeleton == "sign-auto") } - func testCurrencySkeleton() throws { + @Test func currencySkeleton() throws { typealias SignDisplay = CurrencyFormatStyleConfiguration.SignDisplayStrategy - XCTAssertEqual(SignDisplay.automatic.skeleton, "sign-auto") - XCTAssertEqual(SignDisplay.always().skeleton, "sign-always") - XCTAssertEqual(SignDisplay.always(showZero: true).skeleton, "sign-always") - XCTAssertEqual(SignDisplay.always(showZero: false).skeleton, "sign-except-zero") - XCTAssertEqual(SignDisplay.accounting.skeleton, "sign-accounting") - XCTAssertEqual(SignDisplay.accountingAlways().skeleton, "sign-accounting-except-zero") - XCTAssertEqual(SignDisplay.accountingAlways(showZero: true).skeleton, "sign-accounting-always") - XCTAssertEqual(SignDisplay.accountingAlways(showZero: false).skeleton, "sign-accounting-except-zero") - XCTAssertEqual(SignDisplay.never.skeleton, "sign-never") + #expect(SignDisplay.automatic.skeleton == "sign-auto") + #expect(SignDisplay.always().skeleton == "sign-always") + #expect(SignDisplay.always(showZero: true).skeleton == "sign-always") + #expect(SignDisplay.always(showZero: false).skeleton == "sign-except-zero") + #expect(SignDisplay.accounting.skeleton == "sign-accounting") + #expect(SignDisplay.accountingAlways().skeleton == "sign-accounting-except-zero") + #expect(SignDisplay.accountingAlways(showZero: true).skeleton == "sign-accounting-always") + #expect(SignDisplay.accountingAlways(showZero: false).skeleton == "sign-accounting-except-zero") + #expect(SignDisplay.never.skeleton == "sign-never") let style: IntegerFormatStyle.Currency = .init(code: "USD", locale: Locale(identifier: "en_US")) let formatter = ICUCurrencyNumberFormatter.create(for: style)! - XCTAssertEqual(formatter.skeleton, "currency/USD unit-width-short") + #expect(formatter.skeleton == "currency/USD unit-width-short") let accountingStyle = style.sign(strategy: .accounting) let accountingFormatter = ICUCurrencyNumberFormatter.create(for: accountingStyle)! - XCTAssertEqual(accountingFormatter.skeleton, "currency/USD unit-width-short sign-accounting") + #expect(accountingFormatter.skeleton == "currency/USD unit-width-short sign-accounting") let isoCodeStyle = style.sign(strategy: .never).presentation(.isoCode) let isoCodeFormatter = ICUCurrencyNumberFormatter.create(for: isoCodeStyle)! - XCTAssertEqual(isoCodeFormatter.skeleton, "currency/USD unit-width-iso-code sign-never") + #expect(isoCodeFormatter.skeleton == "currency/USD unit-width-iso-code sign-never") } - func testStyleSkeleton_integer_precisionAndRounding() throws { + @Test func styleSkeleton_integer_precisionAndRounding() throws { let style: IntegerFormatStyle = .init(locale: Locale(identifier: "en_US")) - XCTAssertEqual(style.precision(.fractionLength(3...3)).rounded(increment: 5).collection.skeleton, "precision-increment/5.000 rounding-mode-half-even") - XCTAssertEqual(style.precision(.fractionLength(3...)).rounded(increment: 5).collection.skeleton, "precision-increment/5.000 rounding-mode-half-even") - XCTAssertEqual(style.precision(.fractionLength(...3)).rounded(increment: 5).collection.skeleton, "precision-increment/5 rounding-mode-half-even") + #expect(style.precision(.fractionLength(3...3)).rounded(increment: 5).collection.skeleton == "precision-increment/5.000 rounding-mode-half-even") + #expect(style.precision(.fractionLength(3...)).rounded(increment: 5).collection.skeleton == "precision-increment/5.000 rounding-mode-half-even") + #expect(style.precision(.fractionLength(...3)).rounded(increment: 5).collection.skeleton == "precision-increment/5 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerLength(2...2)).rounded(increment: 5).collection.skeleton, "precision-increment/5 integer-width/00 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerLength(2...)).rounded(increment: 5).collection.skeleton, "precision-increment/5 integer-width/+00 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerLength(...2)).rounded(increment: 5).collection.skeleton, "precision-increment/5 integer-width/## rounding-mode-half-even") + #expect(style.precision(.integerLength(2...2)).rounded(increment: 5).collection.skeleton == "precision-increment/5 integer-width/00 rounding-mode-half-even") + #expect(style.precision(.integerLength(2...)).rounded(increment: 5).collection.skeleton == "precision-increment/5 integer-width/+00 rounding-mode-half-even") + #expect(style.precision(.integerLength(...2)).rounded(increment: 5).collection.skeleton == "precision-increment/5 integer-width/## rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerAndFractionLength(integerLimits: 2...2, fractionLimits: 3...3)).rounded(increment: 5).collection.skeleton, "precision-increment/5.000 integer-width/00 rounding-mode-half-even") + #expect(style.precision(.integerAndFractionLength(integerLimits: 2...2, fractionLimits: 3...3)).rounded(increment: 5).collection.skeleton == "precision-increment/5.000 integer-width/00 rounding-mode-half-even") } - func testStyleSkeleton_floatingPoint_precisionAndRounding() throws { + @Test func styleSkeleton_floatingPoint_precisionAndRounding() throws { let style: FloatingPointFormatStyle = .init(locale: Locale(identifier: "en_US")) - XCTAssertEqual(style.precision(.fractionLength(3...3)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 rounding-mode-half-even") - XCTAssertEqual(style.precision(.fractionLength(3...)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 rounding-mode-half-even") - XCTAssertEqual(style.precision(.fractionLength(...3)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 rounding-mode-half-even") - XCTAssertEqual(style.precision(.fractionLength(...1)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 rounding-mode-half-even") + #expect(style.precision(.fractionLength(3...3)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 rounding-mode-half-even") + #expect(style.precision(.fractionLength(3...)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 rounding-mode-half-even") + #expect(style.precision(.fractionLength(...3)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 rounding-mode-half-even") + #expect(style.precision(.fractionLength(...1)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 rounding-mode-half-even") - XCTAssertEqual(style.precision(.fractionLength(3...3)).rounded(increment: 0.3).collection.skeleton, "precision-increment/0.300 rounding-mode-half-even") - XCTAssertEqual(style.precision(.fractionLength(3...)).rounded(increment: 0.3).collection.skeleton, "precision-increment/0.300 rounding-mode-half-even") + #expect(style.precision(.fractionLength(3...3)).rounded(increment: 0.3).collection.skeleton == "precision-increment/0.300 rounding-mode-half-even") + #expect(style.precision(.fractionLength(3...)).rounded(increment: 0.3).collection.skeleton == "precision-increment/0.300 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerLength(2...2)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 integer-width/00 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerLength(2...)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 integer-width/+00 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerLength(...2)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 integer-width/## rounding-mode-half-even") + #expect(style.precision(.integerLength(2...2)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 integer-width/00 rounding-mode-half-even") + #expect(style.precision(.integerLength(2...)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 integer-width/+00 rounding-mode-half-even") + #expect(style.precision(.integerLength(...2)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 integer-width/## rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerAndFractionLength(integerLimits: 2...2, fractionLimits: 3...3)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 integer-width/00 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerAndFractionLength(integerLimits: 2..., fractionLimits: 3...3)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 integer-width/+00 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerAndFractionLength(integerLimits: ...2, fractionLimits: 3...3)).rounded(increment: 0.314).collection.skeleton, "precision-increment/0.314 integer-width/## rounding-mode-half-even") + #expect(style.precision(.integerAndFractionLength(integerLimits: 2...2, fractionLimits: 3...3)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 integer-width/00 rounding-mode-half-even") + #expect(style.precision(.integerAndFractionLength(integerLimits: 2..., fractionLimits: 3...3)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 integer-width/+00 rounding-mode-half-even") + #expect(style.precision(.integerAndFractionLength(integerLimits: ...2, fractionLimits: 3...3)).rounded(increment: 0.314).collection.skeleton == "precision-increment/0.314 integer-width/## rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerAndFractionLength(integerLimits: 2...2, fractionLimits: 3...3)).rounded(increment: 0.3).collection.skeleton, "precision-increment/0.300 integer-width/00 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerAndFractionLength(integerLimits: 2..., fractionLimits: 3...3)).rounded(increment: 0.3).collection.skeleton, "precision-increment/0.300 integer-width/+00 rounding-mode-half-even") - XCTAssertEqual(style.precision(.integerAndFractionLength(integerLimits: ...2, fractionLimits: 3...3)).rounded(increment: 0.3).collection.skeleton, "precision-increment/0.300 integer-width/## rounding-mode-half-even") + #expect(style.precision(.integerAndFractionLength(integerLimits: 2...2, fractionLimits: 3...3)).rounded(increment: 0.3).collection.skeleton == "precision-increment/0.300 integer-width/00 rounding-mode-half-even") + #expect(style.precision(.integerAndFractionLength(integerLimits: 2..., fractionLimits: 3...3)).rounded(increment: 0.3).collection.skeleton == "precision-increment/0.300 integer-width/+00 rounding-mode-half-even") + #expect(style.precision(.integerAndFractionLength(integerLimits: ...2, fractionLimits: 3...3)).rounded(increment: 0.3).collection.skeleton == "precision-increment/0.300 integer-width/## rounding-mode-half-even") } } diff --git a/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift b/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift index 0b9019c8f..6e684a957 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/ParseStrategy+RegexComponentTests.swift @@ -5,42 +5,39 @@ // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors // //===----------------------------------------------------------------------===// -// -// RUN: %target-run-simple-swift -// REQUIRES: executable_test -// REQUIRES: objc_interop import RegexBuilder +import Testing -#if canImport(TestSupport) -import TestSupport +#if canImport(FoundationInternationalization) +import FoundationEssentials +import FoundationInternationalization +#elseif FOUNDATION_FRAMEWORK +import Foundation #endif -final class ParseStrategyMatchTests: XCTestCase { - +@Suite("ParseStrategy Match") +private struct ParseStrategyMatchTests { let enUS = Locale(identifier: "en_US") let enGB = Locale(identifier: "en_GB") let gmt = TimeZone(secondsFromGMT: 0)! let pst = TimeZone(secondsFromGMT: -3600*8)! - func testDate() { + @Test func date() throws { let regex = Regex { OneOrMore { Capture { Date.ISO8601FormatStyle() } } } - guard let res = "💁🏽🏳️‍🌈2021-07-01T15:56:32Z".firstMatch(of: regex) else { - XCTFail() - return - } + let res = try #require("💁🏽🏳️‍🌈2021-07-01T15:56:32Z".firstMatch(of: regex)) - XCTAssertEqual(res.output.0, "2021-07-01T15:56:32Z") + #expect(res.output.0 == "2021-07-01T15:56:32Z") // dateFormatter.date(from: "2021-07-01 15:56:32.000")! - XCTAssertEqual(res.output.1, Date(timeIntervalSinceReferenceDate: 646847792.0)) + #expect(res.output.1 == Date(timeIntervalSinceReferenceDate: 646847792.0)) } - func testAPIHTTPHeader() { + @Test func apiHTTPHeader() throws { let header = """ HTTP/1.1 301 Redirect @@ -57,20 +54,17 @@ final class ParseStrategyMatchTests: XCTestCase { } } - guard let res = header.firstMatch(of: regex) else { - XCTFail() - return - } + let res = try #require(header.firstMatch(of: regex)) // dateFormatter.date(from: "2022-02-16 00:00:00.000")! let expectedDate = Date(timeIntervalSinceReferenceDate: 666662400.0) - XCTAssertEqual(res.output.0, "16 Feb 2022") - XCTAssertEqual(res.output.1, expectedDate) + #expect(res.output.0 == "16 Feb 2022") + #expect(res.output.1 == expectedDate) } // https://github.com/apple/swift-foundation/issues/60 #if FOUNDATION_FRAMEWORK - func testAPIStatement() { + @Test func apiStatement() { let statement = """ CREDIT 04/06/2020 Paypal transfer $4.99 @@ -100,8 +94,8 @@ DEBIT 03/24/2020 IRX tax payment ($52,249.98) let money = statement.matches(of: regex) - XCTAssertEqual(money.map(\.output.0), ["$4.99", "$3,020.85", "$69.73", "($38.25)", "($27.44)", "($52,249.98)"]) - XCTAssertEqual(money.map(\.output.1), expectedAmounts) + #expect(money.map(\.output.0) == ["$4.99", "$3,020.85", "$69.73", "($38.25)", "($27.44)", "($52,249.98)"]) + #expect(money.map(\.output.1) == expectedAmounts) let dateRegex = Regex { Capture { @@ -109,8 +103,8 @@ DEBIT 03/24/2020 IRX tax payment ($52,249.98) } } let dateMatches = statement.matches(of: dateRegex) - XCTAssertEqual(dateMatches.map(\.output.0), expectedDateStrings) - XCTAssertEqual(dateMatches.map(\.output.1), expectedDates) + #expect(dateMatches.map(\.output.0) == expectedDateStrings) + #expect(dateMatches.map(\.output.1) == expectedDates) let dot = try! Regex(#"."#) let dateCurrencyRegex = Regex { @@ -126,7 +120,7 @@ DEBIT 03/24/2020 IRX tax payment ($52,249.98) } let matches = statement.matches(of: dateCurrencyRegex) - XCTAssertEqual(matches.map(\.output.0), [ + #expect(matches.map(\.output.0) == [ "04/06/2020 Paypal transfer $4.99", "04/06/2020 REMOTE ONLINE DEPOSIT $3,020.85", "04/03/2020 PAYROLL $69.73", @@ -134,18 +128,18 @@ DEBIT 03/24/2020 IRX tax payment ($52,249.98) "03/31/2020 Payment to BoA card ($27.44)", "03/24/2020 IRX tax payment ($52,249.98)", ]) - XCTAssertEqual(matches.map(\.output.1), expectedDates) - XCTAssertEqual(matches.map(\.output.2), expectedAmounts) + #expect(matches.map(\.output.1) == expectedDates) + #expect(matches.map(\.output.2) == expectedAmounts) let numericMatches = statement.matches(of: Regex { Capture(.date(.numeric, locale: enUS, timeZone: gmt)) }) - XCTAssertEqual(numericMatches.map(\.output.0), expectedDateStrings) - XCTAssertEqual(numericMatches.map(\.output.1), expectedDates) + #expect(numericMatches.map(\.output.0) == expectedDateStrings) + #expect(numericMatches.map(\.output.1) == expectedDates) } - func testAPIStatements2() { + @Test func apiStatements2() { // Test dates and numbers appearing in unexpeted places let statement = """ CREDIT Apr 06/20 Zombie 5.29lb@$3.99/lb USD 21.11 @@ -178,18 +172,18 @@ DEBIT Mar 31/20 March Payment to BoA -USD 52,249.98 let expectedAmounts = [Decimal(string:"21.11")!, Decimal(string:"3020.85")!, Decimal(string:"69.73")!, Decimal(string:"-38.25")!, Decimal(string:"-52249.98")!] let matches = statement.matches(of: dateCurrencyRegex) - XCTAssertEqual(matches.map(\.output.0), [ + #expect(matches.map(\.output.0) == [ "Apr 06/20 Zombie 5.29lb@$3.99/lb USD 21.11", "Apr 06/20 GMT gain USD 3,020.85", "Apr 03/20 PAYROLL 03/29/20-04/02/20 USD 69.73", "Apr 02/20 ACH TRNSFR Apr 02/20 -USD 38.25", "Mar 31/20 March Payment to BoA -USD 52,249.98", ]) - XCTAssertEqual(matches.map(\.output.1), expectedDates) - XCTAssertEqual(matches.map(\.output.3), expectedAmounts) + #expect(matches.map(\.output.1) == expectedDates) + #expect(matches.map(\.output.3) == expectedAmounts) } - func testAPITestSuites() { + @Test func apiTestSuites() throws { let input = "Test Suite 'MergeableSetTests' started at 2021-07-08 10:19:35.418" let testSuiteLog = Regex { @@ -213,37 +207,34 @@ DEBIT Mar 31/20 March Payment to BoA -USD 52,249.98 } - guard let match = input.wholeMatch(of: testSuiteLog) else { - XCTFail() - return - } + let match = try #require(input.wholeMatch(of: testSuiteLog)) - XCTAssertEqual(match.output.0, "Test Suite 'MergeableSetTests' started at 2021-07-08 10:19:35.418") - XCTAssertEqual(match.output.1, "MergeableSetTests") - XCTAssertEqual(match.output.2, "started") + #expect(match.output.0 == "Test Suite 'MergeableSetTests' started at 2021-07-08 10:19:35.418") + #expect(match.output.1 == "MergeableSetTests") + #expect(match.output.2 == "started") // dateFormatter.date(from: "2021-07-08 10:19:35.418")! - XCTAssertEqual(match.output.3, Date(timeIntervalSinceReferenceDate: 647432375.418)) + #expect(match.output.3 == Date(timeIntervalSinceReferenceDate: 647432375.418)) } #endif - func testVariousDatesAndTimes() { - func verify(_ str: String, _ strategy: Date.ParseStrategy, _ expected: String?, file: StaticString = #filePath, line: UInt = #line) { + @Test func variousDatesAndTimes() { + func verify(_ str: String, _ strategy: Date.ParseStrategy, _ expected: String?, sourceLocation: SourceLocation = #_sourceLocation) { let match = str.wholeMatch(of: strategy) // Regex.Match? if let expected { guard let match else { - XCTFail("<\(str)> did not match, but it should", file: file, line: line) + var explanation = "" do { _ = try strategy.parse(str) } catch { - print(error) + explanation = String(describing: error) } - + Issue.record("<\(str)> did not match, but it should: \(explanation)", sourceLocation: sourceLocation) return } let expectedDate = try! Date(expected, strategy: .iso8601) - XCTAssertEqual(match.0, expectedDate, file: file, line: line) + #expect(match.0 == expectedDate, sourceLocation: sourceLocation) } else { - XCTAssertNil(match, "<\(str)> should not match, but it did", file: file, line: line) + #expect(match == nil, "<\(str)> should not match, but it did", sourceLocation: sourceLocation) } } @@ -269,8 +260,8 @@ DEBIT Mar 31/20 March Payment to BoA -USD 52,249.98 verify("03/05/2020", .date(.numeric, locale: enGB, timeZone: pst), "2020-05-03T00:00:00-08:00") } - func testMatchISO8601String() { - func verify(_ str: String, _ strategy: Date.ISO8601FormatStyle, _ expected: String?, file: StaticString = #filePath, line: UInt = #line) { + @Test func matchISO8601String() { + func verify(_ str: String, _ strategy: Date.ISO8601FormatStyle, _ expected: String?, sourceLocation: SourceLocation = #_sourceLocation) { let match = str.wholeMatch(of: strategy) // Regex.Match? if let expected { @@ -287,13 +278,13 @@ DEBIT Mar 31/20 March Payment to BoA -USD 52,249.98 message += "error: \(error)" } - XCTFail("<\(str)> did not match, but it should. Information: \(message)", file: file, line: line) + Issue.record("<\(str)> did not match, but it should. Information: \(message)", sourceLocation: sourceLocation) return } let expectedDate = try! Date(expected, strategy: .iso8601) - XCTAssertEqual(match.0, expectedDate, file: file, line: line) + #expect(match.0 == expectedDate, sourceLocation: sourceLocation) } else { - XCTAssertNil(match, "<\(str)> should not match, but it did", file: file, line: line) + #expect(match == nil, "<\(str)> should not match, but it did", sourceLocation: sourceLocation) } } diff --git a/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift b/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift index 62a8ecab6..195c18912 100644 --- a/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift +++ b/Tests/FoundationInternationalizationTests/GegorianCalendarInternationalizationTests.swift @@ -23,7 +23,7 @@ import Testing @Suite("Gregorian Calendar (Internationalization)") private struct GregorianCalendarInternationalizationTests { @Test func copy() { - let gregorianCalendar = _CalendarGregorian(identifier: .gregorian, timeZone: nil, locale: nil, firstWeekday: 5, minimumDaysInFirstWeek: 3, gregorianStartDate: nil) + let gregorianCalendar = _CalendarGregorian(identifier: .gregorian, timeZone: .gmt, locale: nil, firstWeekday: 5, minimumDaysInFirstWeek: 3, gregorianStartDate: nil) let newLocale = Locale(identifier: "new locale") let tz = TimeZone(identifier: "America/Los_Angeles")! @@ -35,7 +35,7 @@ private struct GregorianCalendarInternationalizationTests { #expect(copied.firstWeekday == 5) #expect(copied.minimumDaysInFirstWeek == 3) - let copied2 = gregorianCalendar.copy(changingLocale: nil, changingTimeZone: nil, changingFirstWeekday: 1, changingMinimumDaysInFirstWeek: 1) + let copied2 = gregorianCalendar.copy(changingLocale: nil, changingTimeZone: .gmt, changingFirstWeekday: 1, changingMinimumDaysInFirstWeek: 1) // unset values stay the same #expect(copied2.locale == gregorianCalendar.locale) @@ -2464,7 +2464,7 @@ private struct GregorianCalendarInternationalizationTests { // expect local time in dc.timeZone (UTC+8) #expect(gregorianCalendar.date(from: dc_customTimeZone)! == Date(timeIntervalSinceReferenceDate: 679024975.891)) // 2022-07-09T02:02:55Z - let dcCalendar_noTimeZone = Calendar(identifier: .japanese, locale: Locale(identifier: ""), timeZone: nil, firstWeekday: 1, minimumDaysInFirstWeek: 1, gregorianStartDate: nil) + let dcCalendar_noTimeZone = Calendar(identifier: .japanese, locale: Locale(identifier: ""), timeZone: .gmt, firstWeekday: 1, minimumDaysInFirstWeek: 1, gregorianStartDate: nil) var dc_customCalendarNoTimeZone_customTimeZone = dc dc_customCalendarNoTimeZone_customTimeZone.calendar = dcCalendar_noTimeZone dc_customCalendarNoTimeZone_customTimeZone.timeZone = .init(secondsFromGMT: 28800) diff --git a/Tests/FoundationInternationalizationTests/LocaleComponentsTests.swift b/Tests/FoundationInternationalizationTests/LocaleComponentsTests.swift index 23781d851..ce05cb1d4 100644 --- a/Tests/FoundationInternationalizationTests/LocaleComponentsTests.swift +++ b/Tests/FoundationInternationalizationTests/LocaleComponentsTests.swift @@ -10,9 +10,7 @@ // //===----------------------------------------------------------------------===// -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if FOUNDATION_FRAMEWORK @testable import Foundation @@ -21,103 +19,104 @@ import TestSupport @testable import FoundationInternationalization #endif // FOUNDATION_FRAMEWORK -final class LocaleComponentsTests: XCTestCase { +@Suite("Locale.Components") +private struct LocaleComponentsTests { - func testRegions() { + @Test func regions() { let region = Locale.Region("US") - XCTAssertTrue(region.isISORegion) - XCTAssertEqual(region.identifier, "US") - XCTAssertEqual(region.continent, Locale.Region("019")) - XCTAssertEqual(region.containingRegion, Locale.Region("021")) - XCTAssertEqual(region.subRegions.count, 0) - XCTAssert(Locale.Region.isoRegions.count > 0) + #expect(region.isISORegion) + #expect(region.identifier == "US") + #expect(region.continent == Locale.Region("019")) + #expect(region.containingRegion == Locale.Region("021")) + #expect(region.subRegions.count == 0) + #expect(Locale.Region.isoRegions.count > 0) let world = Locale.Region("001") - XCTAssertEqual(world.subRegions.count, 5) + #expect(world.subRegions.count == 5) let predefinedRegions: [Locale.Region] = [ .aruba, .belize, .chad, .côteDIvoire, .frenchSouthernTerritories, .heardMcdonaldIslands, .réunion ] for predefinedRegion in predefinedRegions { - XCTAssertTrue(predefinedRegion.isISORegion) + #expect(predefinedRegion.isISORegion) } } - func testCurrency() { + @Test func currency() { let usd = Locale.Currency("usd") - XCTAssertTrue(usd.isISOCurrency) - XCTAssertTrue(Locale.Currency.isoCurrencies.count > 0) + #expect(usd.isISOCurrency) + #expect(Locale.Currency.isoCurrencies.count > 0) } - func testLanguageCode() { + @Test func languageCode() { let isoLanguageCodes = Locale.LanguageCode.isoLanguageCodes - XCTAssertTrue(isoLanguageCodes.count > 0) + #expect(isoLanguageCodes.count > 0) let isoCodes: [Locale.LanguageCode] = [ "de", "ar", "en", "es", "ja", "und", "DE", "AR" ] for isoCode in isoCodes { - XCTAssertTrue(isoCode.isISOLanguage, "\(isoCode.identifier)") - XCTAssertTrue(isoLanguageCodes.contains(isoCode), "\(isoCode.identifier)") + #expect(isoCode.isISOLanguage, "\(isoCode.identifier)") + #expect(isoLanguageCodes.contains(isoCode), "\(isoCode.identifier)") } let invalidCodes: [Locale.LanguageCode] = [ "unk", "bogus", "foo", "root", "jp" ] for invalidCode in invalidCodes { - XCTAssertFalse(invalidCode.isISOLanguage, "\(invalidCode.identifier)") - XCTAssertNil(invalidCode.identifier(.alpha2)) - XCTAssertNil(invalidCode.identifier(.alpha3)) - XCTAssertFalse(isoLanguageCodes.contains(invalidCode)) + #expect(!invalidCode.isISOLanguage, "\(invalidCode.identifier)") + #expect(invalidCode.identifier(.alpha2) == nil) + #expect(invalidCode.identifier(.alpha3) == nil) + #expect(!isoLanguageCodes.contains(invalidCode)) } let isoCodes3: [Locale.LanguageCode] = [ "deu", "ara", "eng", "spa", "jpn", "und", "deu", "ara" ] for (alpha2, alpha3) in zip(isoCodes, isoCodes3) { let actualAlpha2 = alpha3.identifier(.alpha2) let actualAlpha3 = alpha2.identifier(.alpha3) - XCTAssertEqual(actualAlpha2, alpha2.identifier.lowercased()) - XCTAssertEqual(actualAlpha3, alpha3.identifier.lowercased()) + #expect(actualAlpha2 == alpha2.identifier.lowercased()) + #expect(actualAlpha3 == alpha3.identifier.lowercased()) } let reservedCodes: [Locale.LanguageCode] = [ .unidentified, .uncoded, .multiple, .unavailable ] for reservedCode in reservedCodes { - XCTAssertTrue(reservedCode.isISOLanguage, "\(reservedCode.identifier)") - XCTAssertEqual(reservedCode.identifier(.alpha2), reservedCode.identifier) - XCTAssertEqual(reservedCode.identifier(.alpha3), reservedCode.identifier) - XCTAssertTrue(isoLanguageCodes.contains(reservedCode)) + #expect(reservedCode.isISOLanguage, "\(reservedCode.identifier)") + #expect(reservedCode.identifier(.alpha2) == reservedCode.identifier) + #expect(reservedCode.identifier(.alpha3) == reservedCode.identifier) + #expect(isoLanguageCodes.contains(reservedCode)) } let predefinedCodes: [Locale.LanguageCode] = [ .arabic, .norwegianBokmål, .bulgarian, .māori, .norwegianNynorsk, .lithuanian ] for predefinedCode in predefinedCodes { - XCTAssertTrue(predefinedCode.isISOLanguage) + #expect(predefinedCode.isISOLanguage) } } - func testScript() { + @Test func script() { let someISOScripts: [Locale.Script] = [ "Latn", "Hani", "Hira", "Egyh", "Hans", "Arab", "Cyrl", "Deva", "Zzzz" ] for script in someISOScripts { - XCTAssertTrue(script.isISOScript) + #expect(script.isISOScript) } let notISOScripts: [Locale.Script] = [ "Wave", "Zombie", "Head", "Heart" ] for script in notISOScripts { - XCTAssertFalse(script.isISOScript) + #expect(!script.isISOScript) } let predefinedScripts: [Locale.Script] = [ .latin, .hanSimplified, .hanifiRohingya, .hiragana, .arabic, .cyrillic, .devanagari, .unknown, .hanTraditional, .kannada ] for script in predefinedScripts { - XCTAssertTrue(script.isISOScript) + #expect(script.isISOScript) } } - func testMisc() { - XCTAssertTrue(Locale.Collation.availableCollations.count > 0) + @Test func misc() { + #expect(Locale.Collation.availableCollations.count > 0) - XCTAssertEqual(Set(Locale.Collation.availableCollations(for: Locale.Language(identifier:"en"))), [ .standard, .searchRules, Locale.Collation("emoji"), Locale.Collation("eor") ]) + #expect(Set(Locale.Collation.availableCollations(for: Locale.Language(identifier:"en"))) == [ .standard, .searchRules, Locale.Collation("emoji"), Locale.Collation("eor") ]) - XCTAssertEqual(Set(Locale.Collation.availableCollations(for: Locale.Language(identifier:"de"))), [ .standard, .searchRules, Locale.Collation("emoji"), Locale.Collation("eor"), Locale.Collation("phonebook") ]) - XCTAssertEqual(Set(Locale.Collation.availableCollations(for: Locale.Language(identifier:"bogus"))), [ .standard, .searchRules, Locale.Collation("emoji"), Locale.Collation("eor") ]) + #expect(Set(Locale.Collation.availableCollations(for: Locale.Language(identifier:"de"))) == [ .standard, .searchRules, Locale.Collation("emoji"), Locale.Collation("eor"), Locale.Collation("phonebook") ]) + #expect(Set(Locale.Collation.availableCollations(for: Locale.Language(identifier:"bogus"))) == [ .standard, .searchRules, Locale.Collation("emoji"), Locale.Collation("eor") ]) - XCTAssertTrue(Locale.NumberingSystem.availableNumberingSystems.count > 0) - XCTAssertTrue(Locale.NumberingSystem.availableNumberingSystems.contains(Locale.NumberingSystem("java"))) + #expect(Locale.NumberingSystem.availableNumberingSystems.count > 0) + #expect(Locale.NumberingSystem.availableNumberingSystems.contains(Locale.NumberingSystem("java"))) } // The internal identifier getter would ignore invalid keywords and returns ICU-style identifier - func testInternalIdentifier() { + @Test func internalIdentifier() { // In previous versions Locale.Components(identifier:) would not include @va=posix and en_US_POSIX would result in simply en_US_POSIX. We now return the @va=posix for compatibility with CFLocale. let expectations = [ "en_GB" : "en_GB", @@ -133,15 +132,15 @@ final class LocaleComponentsTests: XCTestCase { ] for (key, value) in expectations { let comps = Locale.Components(identifier: key) - XCTAssertEqual(comps.icuIdentifier, value, "locale identifier: \(key)") + #expect(comps.icuIdentifier == value, "locale identifier: \(key)") } } - func testCreation_identifier() { - func verify(_ identifier: String, file: StaticString = #filePath, line: UInt = #line, expected components: () -> Locale.Components ) { + @Test func creation_identifier() { + func verify(_ identifier: String, sourceLocation: SourceLocation = #_sourceLocation, expected components: () -> Locale.Components ) { let comps = Locale.Components(identifier: identifier) let expected = components() - XCTAssertEqual(comps, expected, "expect: \"\(expected.icuIdentifier)\", actual: \"\(comps.icuIdentifier)\"", file: file, line: line) + #expect(comps == expected, "expect: \"\(expected.icuIdentifier)\", actual: \"\(comps.icuIdentifier)\"", sourceLocation: sourceLocation) } // keywords @@ -236,8 +235,8 @@ final class LocaleComponentsTests: XCTestCase { } } - func testCreation_roundTripLocale() { - func verify(_ identifier: String, file: StaticString = #filePath, line: UInt = #line) { + @Test func creation_roundTripLocale() { + func verify(_ identifier: String, sourceLocation: SourceLocation = #_sourceLocation) { let locale = Locale(identifier: identifier) @@ -247,8 +246,8 @@ final class LocaleComponentsTests: XCTestCase { let compsFromLocale = Locale.Components(locale: locale) let compsFromLocaleIdentifier = Locale.Components(identifier: locale.identifier) - XCTAssertEqual(compsFromLocale, comps, file: file, line: line) - XCTAssertEqual(compsFromLocale, compsFromLocaleIdentifier, file: file, line: line) + #expect(compsFromLocale == comps, sourceLocation: sourceLocation) + #expect(compsFromLocale == compsFromLocaleIdentifier, sourceLocation: sourceLocation) } verify("en_GB") @@ -256,11 +255,10 @@ final class LocaleComponentsTests: XCTestCase { verify("en-Latn_GB") } - func testLocaleComponentInitNoCrash() { + @Test func localeComponentInitNoCrash() { // Test that parsing invalid identifiers does not crash - func test(_ identifier: String, file: StaticString = #filePath, line: UInt = #line) { - let comp = Locale.Components(identifier: identifier) - XCTAssertNotNil(comp, file: file, line: line) + func test(_ identifier: String, sourceLocation: SourceLocation = #_sourceLocation) { + _ = Locale.Components(identifier: identifier) } test("en_US@calendar=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") @@ -268,24 +266,24 @@ final class LocaleComponentsTests: XCTestCase { test("en_US@aaaaaaaaaaaaaaaaaaaaaaaaaaaa=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") } - func test_userPreferenceOverride() { + @Test func userPreferenceOverride() { - func verifyHourCycle(_ localeID: String, _ expectDefault: Locale.HourCycle?, shouldRespectUserPref: Bool, file: StaticString = #filePath, line: UInt = #line) { + func verifyHourCycle(_ localeID: String, _ expectDefault: Locale.HourCycle?, shouldRespectUserPref: Bool, sourceLocation: SourceLocation = #_sourceLocation) { let loc = Locale(identifier: localeID) let nonCurrentDefault = Locale.Components(locale: loc) - XCTAssertEqual(nonCurrentDefault.hourCycle, expectDefault, "default did not match", file: file, line: line) + #expect(nonCurrentDefault.hourCycle == expectDefault, "default did not match", sourceLocation: sourceLocation) let defaultLoc = Locale.localeAsIfCurrent(name: localeID, overrides: .init()) let defaultComp = Locale.Components(locale: defaultLoc) - XCTAssertEqual(defaultComp.hourCycle, expectDefault, "explicit no override did not match", file: file, line: line) + #expect(defaultComp.hourCycle == expectDefault, "explicit no override did not match", sourceLocation: sourceLocation) let force24 = Locale.localeAsIfCurrent(name: localeID, overrides: .init(force24Hour: true)) let force24Comp = Locale.Components(locale: force24) - XCTAssertEqual(force24Comp.hourCycle, shouldRespectUserPref ? .zeroToTwentyThree : expectDefault, "force 24-hr did not match", file: file, line: line) + #expect(force24Comp.hourCycle == (shouldRespectUserPref ? .zeroToTwentyThree : expectDefault), "force 24-hr did not match", sourceLocation: sourceLocation) let force12 = Locale.localeAsIfCurrent(name: localeID, overrides: .init(force12Hour: true)) let force12Comp = Locale.Components(locale: force12) - XCTAssertEqual(force12Comp.hourCycle, shouldRespectUserPref ? .oneToTwelve : expectDefault, "force 12-hr did not match", file: file, line: line) + #expect(force12Comp.hourCycle == (shouldRespectUserPref ? .oneToTwelve : expectDefault), "force 12-hr did not match", sourceLocation: sourceLocation) } // expecting "nil" for hourCycle because no such information in the identifier @@ -300,202 +298,195 @@ final class LocaleComponentsTests: XCTestCase { verifyHourCycle("en_GB@hours=x", nil, shouldRespectUserPref: true) // invalid hour cycle is ignored } - func test_userPreferenceOverrideRoundtrip() { + @Test func userPreferenceOverrideRoundtrip() { let customLocale = Locale.localeAsIfCurrent(name: "en_US", overrides: .init(metricUnits: true, firstWeekday: [.gregorian: Locale.Weekday.wednesday.icuIndex], measurementUnits: .centimeters, force24Hour: true)) - XCTAssertEqual(customLocale.identifier, "en_US") - XCTAssertEqual(customLocale.hourCycle, .zeroToTwentyThree) - XCTAssertEqual(customLocale.firstDayOfWeek, .wednesday) - XCTAssertEqual(customLocale.measurementSystem, .metric) + #expect(customLocale.identifier == "en_US") + #expect(customLocale.hourCycle == .zeroToTwentyThree) + #expect(customLocale.firstDayOfWeek == .wednesday) + #expect(customLocale.measurementSystem == .metric) let components = Locale.Components(locale: customLocale) - XCTAssertEqual(components.icuIdentifier, "en_US@fw=wed;hours=h23;measure=metric") - XCTAssertEqual(components.hourCycle, .zeroToTwentyThree) - XCTAssertEqual(components.firstDayOfWeek, .wednesday) - XCTAssertEqual(components.measurementSystem, .metric) + #expect(components.icuIdentifier == "en_US@fw=wed;hours=h23;measure=metric") + #expect(components.hourCycle == .zeroToTwentyThree) + #expect(components.firstDayOfWeek == .wednesday) + #expect(components.measurementSystem == .metric) let locFromComp = Locale(components: components) - XCTAssertEqual(locFromComp.identifier, "en_US@fw=wed;hours=h23;measure=metric") - XCTAssertEqual(locFromComp.hourCycle, .zeroToTwentyThree) - XCTAssertEqual(locFromComp.firstDayOfWeek, .wednesday) - XCTAssertEqual(locFromComp.measurementSystem, .metric) + #expect(locFromComp.identifier == "en_US@fw=wed;hours=h23;measure=metric") + #expect(locFromComp.hourCycle == .zeroToTwentyThree) + #expect(locFromComp.firstDayOfWeek == .wednesday) + #expect(locFromComp.measurementSystem == .metric) var updatedComponents = components updatedComponents.firstDayOfWeek = .friday let locFromUpdatedComponents = Locale(components: updatedComponents) - XCTAssertEqual(locFromUpdatedComponents.identifier, "en_US@fw=fri;hours=h23;measure=metric") - XCTAssertEqual(locFromUpdatedComponents.hourCycle, .zeroToTwentyThree) - XCTAssertEqual(locFromUpdatedComponents.firstDayOfWeek, .friday) - XCTAssertEqual(locFromUpdatedComponents.measurementSystem, .metric) + #expect(locFromUpdatedComponents.identifier == "en_US@fw=fri;hours=h23;measure=metric") + #expect(locFromUpdatedComponents.hourCycle == .zeroToTwentyThree) + #expect(locFromUpdatedComponents.firstDayOfWeek == .friday) + #expect(locFromUpdatedComponents.measurementSystem == .metric) } } -final class LocaleCodableTests: XCTestCase { +@Suite("Locale Codable") +private struct LocaleCodableTests { // Test types that used to encode both `identifier` and `normalizdIdentifier` now only encodes `identifier` - func _testRoundtripCoding(_ obj: T, identifier: String, normalizedIdentifier: String, file: StaticString = #filePath, line: UInt = #line) -> T? { + func _testRoundtripCoding(_ obj: T, identifier: String, normalizedIdentifier: String, sourceLocation: SourceLocation = #_sourceLocation) throws -> T? { let previousEncoded = "{\"_identifier\":\"\(identifier)\",\"_normalizedIdentifier\":\"\(normalizedIdentifier)\"}" let previousEncodedData = previousEncoded.data(using: String._Encoding.utf8)! let decoder = JSONDecoder() - guard let decoded = try? decoder.decode(T.self, from: previousEncodedData) else { - XCTFail("Decoding \(obj) failed", file: file, line: line) - return nil - } + let decoded = try decoder.decode(T.self, from: previousEncodedData) let encoder = JSONEncoder() - guard let newEncoded = try? encoder.encode(decoded) else { - XCTFail("Encoding \(obj) failed", file: file, line: line) - return nil - } - XCTAssertEqual(String(data: newEncoded, encoding: .utf8)!, "\"\(identifier)\"") + let newEncoded = try encoder.encode(decoded) + #expect(String(data: newEncoded, encoding: .utf8)! == "\"\(identifier)\"") return decoded } - func test_compatibilityCoding() { + @Test func compatibilityCoding() throws { do { let codableObj = Locale.LanguageCode("HELLO") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.LanguageCode.armenian - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.LanguageCode("") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Region("My home") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Region.uganda - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Region("") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Script("BOGUS") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Script.hebrew - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Collation("BOGUS") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Collation.searchRules - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Currency("EXAMPLE") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Currency.unknown - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.NumberingSystem("UNKNOWN") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.NumberingSystem.latn - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.MeasurementSystem.metric - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.MeasurementSystem("EXAMPLE") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Subdivision("usca") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Variant("EXAMPLE") - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } do { let codableObj = Locale.Variant.posix - let decoded = _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) - XCTAssertEqual(decoded?.identifier, codableObj.identifier) - XCTAssertEqual(decoded?._normalizedIdentifier, codableObj._normalizedIdentifier) + let decoded = try _testRoundtripCoding(codableObj, identifier: codableObj.identifier, normalizedIdentifier: codableObj._normalizedIdentifier) + #expect(decoded?.identifier == codableObj.identifier) + #expect(decoded?._normalizedIdentifier == codableObj._normalizedIdentifier) } } - func test_decode_compatible_localeComponents() { - func expectDecode(_ encoded: String, _ expected: Locale.Components, file: StaticString = #filePath, line: UInt = #line) { - guard let data = encoded.data(using: String._Encoding.utf8), let decoded = try? JSONDecoder().decode(Locale.Components.self, from: data) else { - XCTFail(file: file, line: line) - return - } - XCTAssertEqual(decoded, expected, file: file, line: line) + @Test func decode_compatible_localeComponents() throws { + func expectDecode(_ encoded: String, _ expected: Locale.Components, sourceLocation: SourceLocation = #_sourceLocation) throws { + let data = try #require(encoded.data(using: String._Encoding.utf8)) + let decoded = try JSONDecoder().decode(Locale.Components.self, from: data) + #expect(decoded == expected, sourceLocation: sourceLocation) } do { @@ -510,178 +501,154 @@ final class LocaleCodableTests: XCTestCase { expected.currency = "GBP" expected.measurementSystem = .us - expectDecode(""" + try expectDecode(""" {"region":{"_identifier":"HK","_normalizedIdentifier":"HK"},"firstDayOfWeek":"mon","languageComponents":{"region":{"_identifier":"TW","_normalizedIdentifier":"TW"},"languageCode":{"_identifier":"zh","_normalizedIdentifier":"zh"}},"hourCycle":"h12","timeZone":{"identifier":"GMT"},"calendar":{"buddhist":{}},"currency":{"_identifier":"GBP","_normalizedIdentifier":"gbp"},"measurementSystem":{"_identifier":"ussystem","_normalizedIdentifier":"ussystem"}} """, expected) } do { - expectDecode(""" + try expectDecode(""" {"languageComponents":{}} """, Locale.Components(identifier: "")) } } - func test_decode_compatible_language() { + @Test func decode_compatible_language() throws { - func expectDecode(_ encoded: String, _ expected: Locale.Language, file: StaticString = #filePath, line: UInt = #line) { - guard let data = encoded.data(using: String._Encoding.utf8), let decoded = try? JSONDecoder().decode(Locale.Language.self, from: data) else { - XCTFail(file: file, line: line) - return - } - XCTAssertEqual(decoded, expected, file: file, line: line) + func expectDecode(_ encoded: String, _ expected: Locale.Language, sourceLocation: SourceLocation = #_sourceLocation) throws { + let data = try #require(encoded.data(using: String._Encoding.utf8)) + let decoded = try JSONDecoder().decode(Locale.Language.self, from: data) + #expect(decoded == expected, sourceLocation: sourceLocation) } - expectDecode(""" + try expectDecode(""" {"components":{"script":{"_identifier":"Hans","_normalizedIdentifier":"Hans"},"languageCode":{"_identifier":"zh","_normalizedIdentifier":"zh"},"region":{"_identifier":"HK","_normalizedIdentifier":"HK"}}} """, Locale.Language(identifier: "zh-Hans-HK")) - expectDecode(""" + try expectDecode(""" {"components":{}} """, Locale.Language(identifier: "")) } - func test_decode_compatible_languageComponents() { - func expectDecode(_ encoded: String, _ expected: Locale.Language.Components, file: StaticString = #filePath, line: UInt = #line) { - guard let data = encoded.data(using: String._Encoding.utf8), let decoded = try? JSONDecoder().decode(Locale.Language.Components.self, from: data) else { - XCTFail(file: file, line: line) - return - } - XCTAssertEqual(decoded, expected, file: file, line: line) + @Test func decode_compatible_languageComponents() throws { + func expectDecode(_ encoded: String, _ expected: Locale.Language.Components, sourceLocation: SourceLocation = #_sourceLocation) throws { + let data = try #require(encoded.data(using: String._Encoding.utf8)) + let decoded = try JSONDecoder().decode(Locale.Language.Components.self, from: data) + #expect(decoded == expected, sourceLocation: sourceLocation) } - expectDecode(""" + try expectDecode(""" {"script":{"_identifier":"Hans","_normalizedIdentifier":"Hans"},"languageCode":{"_identifier":"zh","_normalizedIdentifier":"zh"},"region":{"_identifier":"HK","_normalizedIdentifier":"HK"}} """, Locale.Language.Components(identifier: "zh-Hans-HK")) - expectDecode("{}", Locale.Language.Components(identifier: "")) + try expectDecode("{}", Locale.Language.Components(identifier: "")) } // Locale components are considered equal regardless of the identifier's case - func testCaseInsensitiveEquality() { - XCTAssertEqual(Locale.Collation("search"), Locale.Collation("SEARCH")) - XCTAssertEqual(Locale.NumberingSystem("latn"), Locale.NumberingSystem("Latn")) - XCTAssertEqual( - [ Locale.NumberingSystem("latn"), Locale.NumberingSystem("ARAB") ], + @Test func caseInsensitiveEquality() { + #expect(Locale.Collation("search") == Locale.Collation("SEARCH")) + #expect(Locale.NumberingSystem("latn") == Locale.NumberingSystem("Latn")) + #expect( + [ Locale.NumberingSystem("latn"), Locale.NumberingSystem("ARAB") ] == [ Locale.NumberingSystem("Latn"), Locale.NumberingSystem("arab") ]) - XCTAssertEqual( - Set([ Locale.NumberingSystem("latn"), Locale.NumberingSystem("ARAB") ]), + #expect( + Set([ Locale.NumberingSystem("latn"), Locale.NumberingSystem("ARAB") ]) == Set([ Locale.NumberingSystem("arab"), Locale.NumberingSystem("Latn") ])) - XCTAssertEqual(Locale.Region("US"), Locale.Region("us")) - XCTAssertEqual(Locale.Script("Hant"), Locale.Script("hant")) - XCTAssertEqual(Locale.LanguageCode("EN"), Locale.LanguageCode("en")) + #expect(Locale.Region("US") == Locale.Region("us")) + #expect(Locale.Script("Hant") == Locale.Script("hant")) + #expect(Locale.LanguageCode("EN") == Locale.LanguageCode("en")) } - func _encodeAsJSON(_ t: T) -> String? { + func _encodeAsJSON(_ t: T) throws -> String { let encoder = JSONEncoder() encoder.outputFormatting = [ .sortedKeys ] - guard let encoded = try? encoder.encode(t) else { - return nil - } - return String(data: encoded, encoding: .utf8) + let encoded = try encoder.encode(t) + return try #require(String(data: encoded, encoding: .utf8)) } - func test_encode_language() { - func expectEncode(_ lang: Locale.Language, _ expectedEncoded: String, file: StaticString = #filePath, line: UInt = #line) { - guard let encoded = _encodeAsJSON(lang) else { - XCTFail(file: file, line: line) - return - } + @Test func encode_language() throws { + func expectEncode(_ lang: Locale.Language, _ expectedEncoded: String, sourceLocation: SourceLocation = #_sourceLocation) throws { + let encoded = try _encodeAsJSON(lang) - XCTAssertEqual(encoded, expectedEncoded, file: file, line: line) + #expect(encoded == expectedEncoded, sourceLocation: sourceLocation) - let data = encoded.data(using: String._Encoding.utf8) - guard let data, let decoded = try? JSONDecoder().decode(Locale.Language.self, from: data) else { - XCTFail(file: file, line: line) - return - } + let data = try #require(encoded.data(using: String._Encoding.utf8)) + let decoded = try JSONDecoder().decode(Locale.Language.self, from: data) - XCTAssertEqual(lang, decoded, file: file, line: line) + #expect(lang == decoded, sourceLocation: sourceLocation) } - expectEncode(Locale.Language(identifier: "zh-Hans-hk"), """ + try expectEncode(Locale.Language(identifier: "zh-Hans-hk"), """ {"components":{"languageCode":"zh","region":"HK","script":"Hans"}} """) - expectEncode(Locale.Language(languageCode: .chinese, script: .hanSimplified, region: .hongKong), """ + try expectEncode(Locale.Language(languageCode: .chinese, script: .hanSimplified, region: .hongKong), """ {"components":{"languageCode":"zh","region":"HK","script":"Hans"}} """) let langComp = Locale.Language.Components(identifier: "zh-Hans-hk") - expectEncode(Locale.Language(components: langComp), """ + try expectEncode(Locale.Language(components: langComp), """ {"components":{"languageCode":"zh","region":"HK","script":"Hans"}} """) - expectEncode(Locale.Language(identifier: ""), """ + try expectEncode(Locale.Language(identifier: ""), """ {"components":{}} """) - expectEncode(Locale.Language(languageCode: nil), """ + try expectEncode(Locale.Language(languageCode: nil), """ {"components":{}} """) let empty = Locale.Language.Components(identifier: "") - expectEncode(Locale.Language(components: empty), """ + try expectEncode(Locale.Language(components: empty), """ {"components":{}} """) } - func test_encode_languageComponents() { - func expectEncode(_ lang: Locale.Language.Components, _ expectedEncoded: String, file: StaticString = #filePath, line: UInt = #line) { - guard let encoded = _encodeAsJSON(lang) else { - XCTFail(file: file, line: line) - return - } + @Test func encode_languageComponents() throws { + func expectEncode(_ lang: Locale.Language.Components, _ expectedEncoded: String, sourceLocation: SourceLocation = #_sourceLocation) throws { + let encoded = try _encodeAsJSON(lang) - XCTAssertEqual(encoded, expectedEncoded, file: file, line: line) + #expect(encoded == expectedEncoded, sourceLocation: sourceLocation) - let data = encoded.data(using: String._Encoding.utf8) - guard let data, let decoded = try? JSONDecoder().decode(Locale.Language.Components.self, from: data) else { - XCTFail(file: file, line: line) - return - } + let data = try #require(encoded.data(using: String._Encoding.utf8)) + let decoded = try JSONDecoder().decode(Locale.Language.Components.self, from: data) - XCTAssertEqual(lang, decoded, file: file, line: line) + #expect(lang == decoded, sourceLocation: sourceLocation) } - expectEncode(Locale.Language.Components(identifier: "zh-Hans-hk"), """ + try expectEncode(Locale.Language.Components(identifier: "zh-Hans-hk"), """ {"languageCode":"zh","region":"HK","script":"Hans"} """) - expectEncode(Locale.Language.Components(languageCode: .chinese, script: .hanSimplified, region: .hongKong), """ + try expectEncode(Locale.Language.Components(languageCode: .chinese, script: .hanSimplified, region: .hongKong), """ {"languageCode":"zh","region":"HK","script":"Hans"} """) let lang = Locale.Language(identifier: "zh-Hans-hk") - expectEncode(Locale.Language.Components(language: lang), """ + try expectEncode(Locale.Language.Components(language: lang), """ {"languageCode":"zh","region":"HK","script":"Hans"} """) - expectEncode(Locale.Language.Components(identifier: ""), """ + try expectEncode(Locale.Language.Components(identifier: ""), """ {} """) - expectEncode(Locale.Language.Components(languageCode: nil), "{}") + try expectEncode(Locale.Language.Components(languageCode: nil), "{}") } - func test_encode_localeComponents() { + @Test func encode_localeComponents() throws { - func expectEncode(_ lang: Locale.Components, _ expectedEncoded: String, file: StaticString = #filePath, line: UInt = #line) { - guard let encoded = _encodeAsJSON(lang) else { - XCTFail(file: file, line: line) - return - } + func expectEncode(_ lang: Locale.Components, _ expectedEncoded: String, sourceLocation: SourceLocation = #_sourceLocation) throws { + let encoded = try _encodeAsJSON(lang) - XCTAssertEqual(encoded, expectedEncoded, file: file, line: line) + #expect(encoded == expectedEncoded, sourceLocation: sourceLocation) - let data = encoded.data(using: String._Encoding.utf8) - guard let data, let decoded = try? JSONDecoder().decode(Locale.Components.self, from: data) else { - XCTFail(file: file, line: line) - return - } + let data = try #require(encoded.data(using: String._Encoding.utf8)) + let decoded = try JSONDecoder().decode(Locale.Components.self, from: data) - XCTAssertEqual(lang, decoded, file: file, line: line) + #expect(lang == decoded, sourceLocation: sourceLocation) } var comp = Locale.Components(languageCode: .chinese, languageRegion: .taiwan) @@ -693,15 +660,15 @@ final class LocaleCodableTests: XCTestCase { comp.measurementSystem = .us comp.timeZone = .gmt - expectEncode(comp, """ + try expectEncode(comp, """ {"calendar":{"buddhist":{}},"currency":"GBP","firstDayOfWeek":"mon","hourCycle":"h12","languageComponents":{"languageCode":"zh","region":"TW"},"measurementSystem":"ussystem","region":"HK","timeZone":{"identifier":"GMT"}} """) - expectEncode(Locale.Components(languageCode: nil), """ + try expectEncode(Locale.Components(languageCode: nil), """ {"languageComponents":{}} """) - expectEncode(Locale.Components(identifier: ""), """ + try expectEncode(Locale.Components(identifier: ""), """ {"languageComponents":{}} """) } diff --git a/Tests/FoundationInternationalizationTests/LocaleLanguageTests.swift b/Tests/FoundationInternationalizationTests/LocaleLanguageTests.swift index a6e421141..09177baac 100644 --- a/Tests/FoundationInternationalizationTests/LocaleLanguageTests.swift +++ b/Tests/FoundationInternationalizationTests/LocaleLanguageTests.swift @@ -10,9 +10,7 @@ // //===----------------------------------------------------------------------===// -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if FOUNDATION_FRAMEWORK @testable import Foundation @@ -21,20 +19,21 @@ import TestSupport @testable import FoundationInternationalization #endif // FOUNDATION_FRAMEWORK -final class LocaleLanguageComponentsTests : XCTestCase { +@Suite("Locale.Language.Components") +private struct LocaleLanguageComponentsTests { func verifyComponents(_ identifier: String, expectedLanguageCode: String?, expectedScriptCode: String?, expectedRegionCode: String?, - file: StaticString = #filePath, line: UInt = #line) { + sourceLocation: SourceLocation = #_sourceLocation) { let comp = Locale.Language.Components(identifier: identifier) - XCTAssertEqual(comp.languageCode?.identifier, expectedLanguageCode, file: file, line: line) - XCTAssertEqual(comp.script?.identifier, expectedScriptCode, file: file, line: line) - XCTAssertEqual(comp.region?.identifier, expectedRegionCode, file: file, line: line) + #expect(comp.languageCode?.identifier == expectedLanguageCode, sourceLocation: sourceLocation) + #expect(comp.script?.identifier == expectedScriptCode, sourceLocation: sourceLocation) + #expect(comp.region?.identifier == expectedRegionCode, sourceLocation: sourceLocation) } - func testCreateFromIdentifier() { + @Test func createFromIdentifier() { verifyComponents("en-US", expectedLanguageCode: "en", expectedScriptCode: nil, expectedRegionCode: "US") verifyComponents("en_US", expectedLanguageCode: "en", expectedScriptCode: nil, expectedRegionCode: "US") verifyComponents("en_US@rg=GBzzzz", expectedLanguageCode: "en", expectedScriptCode: nil, expectedRegionCode: "US") @@ -43,36 +42,37 @@ final class LocaleLanguageComponentsTests : XCTestCase { verifyComponents("hans-cn", expectedLanguageCode: "hans", expectedScriptCode: nil, expectedRegionCode: "CN") } - func testCreateFromInvalidIdentifier() { + @Test func createFromInvalidIdentifier() { verifyComponents("HANS", expectedLanguageCode: "hans", expectedScriptCode: nil, expectedRegionCode: nil) verifyComponents("zh-CN-Hant", expectedLanguageCode: "zh", expectedScriptCode: nil, expectedRegionCode: "CN") verifyComponents("bleh", expectedLanguageCode: "bleh", expectedScriptCode: nil, expectedRegionCode: nil) } // The internal identifier uses the ICU-style identifier - func testInternalIdentifier() { - XCTAssertEqual(Locale.Language.Components(languageCode: "en", script: "Hant", region: "US").identifier, "en-Hant_US") - XCTAssertEqual(Locale.Language.Components(languageCode: "en", script: nil, region: "US").identifier, "en_US") - XCTAssertEqual(Locale.Language.Components(languageCode: "EN", script: nil, region: "us").identifier, "en_US") - XCTAssertEqual(Locale.Language.Components(languageCode: "EN", script: "Latn").identifier, "en-Latn") + @Test func internalIdentifier() { + #expect(Locale.Language.Components(languageCode: "en", script: "Hant", region: "US").identifier == "en-Hant_US") + #expect(Locale.Language.Components(languageCode: "en", script: nil, region: "US").identifier == "en_US") + #expect(Locale.Language.Components(languageCode: "EN", script: nil, region: "us").identifier == "en_US") + #expect(Locale.Language.Components(languageCode: "EN", script: "Latn").identifier == "en-Latn") } } -class LocaleLanguageTests: XCTestCase { +@Suite("Locale.Language") +private struct LocaleLanguageTests { - func verify(_ identifier: String, expectedParent: Locale.Language, minBCP47: String, maxBCP47: String, langCode: Locale.LanguageCode?, script: Locale.Script?, region: Locale.Region?, lineDirection: Locale.LanguageDirection, characterDirection: Locale.LanguageDirection, file: StaticString = #filePath, line: UInt = #line) { + func verify(_ identifier: String, expectedParent: Locale.Language, minBCP47: String, maxBCP47: String, langCode: Locale.LanguageCode?, script: Locale.Script?, region: Locale.Region?, lineDirection: Locale.LanguageDirection, characterDirection: Locale.LanguageDirection, sourceLocation: SourceLocation = #_sourceLocation) { let lan = Locale.Language(identifier: identifier) - XCTAssertEqual(lan.parent, expectedParent, "Parents should be equal", file: file, line: line) - XCTAssertEqual(lan.minimalIdentifier, minBCP47, "minimalIdentifiers should be equal", file: file, line: line) - XCTAssertEqual(lan.maximalIdentifier, maxBCP47, "maximalIdentifiers should be equal", file: file, line: line) - XCTAssertEqual(lan.languageCode, langCode, "languageCodes should be equal", file: file, line: line) - XCTAssertEqual(lan.script, script, "languageCodes should be equal", file: file, line: line) - XCTAssertEqual(lan.region, region, "regions should be equal", file: file, line: line) - XCTAssertEqual(lan.lineLayoutDirection, lineDirection, "lineDirection should be equal", file: file, line: line) - XCTAssertEqual(lan.characterDirection, characterDirection, "characterDirection should be equal", file: file, line: line) + #expect(lan.parent == expectedParent, "Parents should be equal", sourceLocation: sourceLocation) + #expect(lan.minimalIdentifier == minBCP47, "minimalIdentifiers should be equal", sourceLocation: sourceLocation) + #expect(lan.maximalIdentifier == maxBCP47, "maximalIdentifiers should be equal", sourceLocation: sourceLocation) + #expect(lan.languageCode == langCode, "languageCodes should be equal", sourceLocation: sourceLocation) + #expect(lan.script == script, "languageCodes should be equal", sourceLocation: sourceLocation) + #expect(lan.region == region, "regions should be equal", sourceLocation: sourceLocation) + #expect(lan.lineLayoutDirection == lineDirection, "lineDirection should be equal", sourceLocation: sourceLocation) + #expect(lan.characterDirection == characterDirection, "characterDirection should be equal", sourceLocation: sourceLocation) } - func testProperties() { + @Test func properties() { verify("en-US", expectedParent: .init(identifier: "en"), minBCP47: "en", maxBCP47: "en-Latn-US", langCode: "en", script: "Latn", region: "US", lineDirection: .topToBottom, characterDirection: .leftToRight) verify("de-DE", expectedParent: .init(identifier: "de"), minBCP47: "de", maxBCP47: "de-Latn-DE", langCode: "de", script: "Latn", region: "DE", lineDirection: .topToBottom, characterDirection: .leftToRight) verify("en-Kore-US", expectedParent: .init(identifier: "en-Kore"), minBCP47: "en-Kore", maxBCP47: "en-Kore-US", langCode: "en", script: "Kore", region: "US", lineDirection: .topToBottom, characterDirection: .leftToRight) @@ -85,13 +85,13 @@ class LocaleLanguageTests: XCTestCase { verify("root", expectedParent: .init(identifier: "root"), minBCP47: "root", maxBCP47: "root", langCode: "root", script: nil, region: nil, lineDirection: .topToBottom, characterDirection: .leftToRight) } - func testEquivalent() { - func verify(lang1: String, lang2: String, isEqual: Bool, file: StaticString = #filePath, line: UInt = #line) { + @Test func equivalent() { + func verify(lang1: String, lang2: String, isEqual: Bool, sourceLocation: SourceLocation = #_sourceLocation) { let language1 = Locale.Language(identifier: lang1) let language2 = Locale.Language(identifier: lang2) - XCTAssert(language1.isEquivalent(to: language2) == isEqual, file: file, line: line) - XCTAssert(language2.isEquivalent(to: language1) == isEqual, file: file, line: line) + #expect(language1.isEquivalent(to: language2) == isEqual, sourceLocation: sourceLocation) + #expect(language2.isEquivalent(to: language1) == isEqual, sourceLocation: sourceLocation) } verify(lang1: "en", lang2: "en-Latn", isEqual: true) diff --git a/Tests/FoundationInternationalizationTests/LocaleTestUtilities.swift b/Tests/FoundationInternationalizationTests/LocaleTestUtilities.swift deleted file mode 100644 index 8922fc8eb..000000000 --- a/Tests/FoundationInternationalizationTests/LocaleTestUtilities.swift +++ /dev/null @@ -1,31 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2023 Apple Inc. and the Swift project authors -// Licensed under Apache License v2.0 with Runtime Library Exception -// -// See https://swift.org/LICENSE.txt for license information -// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors -// -//===----------------------------------------------------------------------===// -// -// RUN: %target-run-simple-swift -// REQUIRES: executable_test -// REQUIRES: objc_interop - -#if FOUNDATION_FRAMEWORK -@testable import Foundation -#else -@testable import FoundationEssentials -@testable import FoundationInternationalization -#endif // FOUNDATION_FRAMEWORK - -// MARK: - Stubs - -#if !FOUNDATION_FRAMEWORK -internal enum UnitTemperature { - case celsius - case fahrenheit -} -#endif // !FOUNDATION_FRAMEWORK diff --git a/Tests/FoundationInternationalizationTests/StringTests+Data.swift b/Tests/FoundationInternationalizationTests/StringICUEncodingTests.swift similarity index 75% rename from Tests/FoundationInternationalizationTests/StringTests+Data.swift rename to Tests/FoundationInternationalizationTests/StringICUEncodingTests.swift index 56e4e4944..f7f8edd5a 100644 --- a/Tests/FoundationInternationalizationTests/StringTests+Data.swift +++ b/Tests/FoundationInternationalizationTests/StringICUEncodingTests.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +import Testing + #if FOUNDATION_FRAMEWORK @testable import Foundation #else @@ -17,29 +19,25 @@ @testable import FoundationInternationalization #endif // FOUNDATION_FRAMEWORK -#if canImport(TestSupport) -import TestSupport -#endif - -final class StringConverterTests: XCTestCase { +@Suite("String (ICU Encoding)") +private struct StringICUEncodingTests { private func _test_roundTripConversion( string: String, data: Data, - encoding: String._Encoding, - file: StaticString = #filePath, - line: UInt = #line + encoding: String.Encoding, + sourceLocation: SourceLocation = #_sourceLocation ) { - XCTAssertEqual( - string.data(using: encoding), data, "Failed to convert string to data.", - file: file, line: line + #expect( + string.data(using: encoding) == data, "Failed to convert string to data.", + sourceLocation: sourceLocation ) - XCTAssertEqual( - string, String(data: data, encoding: encoding), "Failed to convert data to string.", - file: file, line: line + #expect( + string == String(data: data, encoding: encoding), "Failed to convert data to string.", + sourceLocation: sourceLocation ) } - func test_japaneseEUC() { + @Test func japaneseEUC() { // Confirm that https://github.com/swiftlang/swift-foundation/issues/1016 is fixed. // ASCII @@ -117,22 +115,22 @@ final class StringConverterTests: XCTestCase { // Unsupported characters let onsen = "Onsen♨" // BMP emoji let sushi = "Sushi🍣" // non-BMP emoji - XCTAssertNil(onsen.data(using: String._Encoding.japaneseEUC)) - XCTAssertNil(sushi.data(using: String._Encoding.japaneseEUC)) - XCTAssertEqual( - onsen.data(using: String._Encoding.japaneseEUC, allowLossyConversion: true), + #expect(onsen.data(using: .japaneseEUC) == nil) + #expect(sushi.data(using: .japaneseEUC) == nil) + #expect( + onsen.data(using: .japaneseEUC, allowLossyConversion: true) == "Onsen?".data(using: .utf8) ) #if FOUNDATION_FRAMEWORK // NOTE: Foundation framework replaces an unsupported non-BMP character // with "??"(two question marks). - XCTAssertEqual( - sushi.data(using: String._Encoding.japaneseEUC, allowLossyConversion: true), + #expect( + sushi.data(using: .japaneseEUC, allowLossyConversion: true) == "Sushi??".data(using: .utf8) ) #else - XCTAssertEqual( - sushi.data(using: String._Encoding.japaneseEUC, allowLossyConversion: true), + #expect( + sushi.data(using: .japaneseEUC, allowLossyConversion: true) == "Sushi?".data(using: .utf8) ) #endif diff --git a/Tests/FoundationInternationalizationTests/StringTests+Locale.swift b/Tests/FoundationInternationalizationTests/StringTests+Locale.swift index daf412a54..32ed89aa9 100644 --- a/Tests/FoundationInternationalizationTests/StringTests+Locale.swift +++ b/Tests/FoundationInternationalizationTests/StringTests+Locale.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +import Testing + #if FOUNDATION_FRAMEWORK @testable import Foundation #else @@ -17,26 +19,19 @@ @testable import FoundationInternationalization #endif // FOUNDATION_FRAMEWORK -#if canImport(TestSupport) -import TestSupport -#endif - extension String { var _scalarViewDescription: String { return unicodeScalars.map { "\\u{\(String($0.value, radix: 16, uppercase: true))}" }.joined() } } -final class StringLocaleTests: XCTestCase { +@Suite("String (Locale)") +private struct StringLocaleTests { - func testCapitalize_localized() { + @Test func capitalize_localized() { var locale: Locale? - // `extension StringProtocol { func capitalized(with: Locale) }` is - // declared twice on Darwin: once in FoundationInternationalization - // and once in SDK. Therefore it is ambiguous when building the package - // on Darwin. Workaround it by testing the internal implementation. - func test(_ string: String, _ expected: String, file: StaticString = #filePath, line: UInt = #line) { - XCTAssertEqual(string._capitalized(with: locale), expected, file: file, line: line) + func test(_ string: String, _ expected: String, sourceLocation: SourceLocation = #_sourceLocation) { + #expect(string.capitalized(with: locale) == expected, sourceLocation: sourceLocation) } do { @@ -84,18 +79,18 @@ final class StringLocaleTests: XCTestCase { } } - func testUppercase_localized() { + @Test func uppercase_localized() { - func test(_ localeID: String?, _ string: String, _ expected: String, file: StaticString = #filePath, line: UInt = #line) { + func test(_ localeID: String?, _ string: String, _ expected: String, sourceLocation: SourceLocation = #_sourceLocation) { let locale: Locale? if let localeID { locale = Locale(identifier: localeID) } else { locale = nil } - let actual = string._uppercased(with: locale) + let actual = string.uppercased(with: locale) - XCTAssertEqual(actual, expected, "actual: \(actual._scalarViewDescription), expected: \(expected._scalarViewDescription)", file: file, line: line) + #expect(actual == expected, "actual: \(actual._scalarViewDescription), expected: \(expected._scalarViewDescription)", sourceLocation: sourceLocation) } test(nil, "ffl", "FFL") // 0xFB04 @@ -128,17 +123,17 @@ final class StringLocaleTests: XCTestCase { test("el_GR", "\u{03B9}\u{0308}\u{0301}", "\u{0399}\u{0308}") } - func testLowercase_localized() { - func test(_ localeID: String?, _ string: String, _ expected: String, file: StaticString = #filePath, line: UInt = #line) { + @Test func lowercase_localized() { + func test(_ localeID: String?, _ string: String, _ expected: String, sourceLocation: SourceLocation = #_sourceLocation) { let locale: Locale? if let localeID { locale = Locale(identifier: localeID) } else { locale = nil } - let actual = string._lowercased(with: locale) + let actual = string.lowercased(with: locale) - XCTAssertEqual(actual, expected, "actual: \(actual._scalarViewDescription), expected: \(expected._scalarViewDescription)", file: file, line: line) + #expect(actual == expected, "actual: \(actual._scalarViewDescription), expected: \(expected._scalarViewDescription)", sourceLocation: sourceLocation) } test(nil, "ᾈ", "ᾀ") // 0x1F88 @@ -156,8 +151,9 @@ final class StringLocaleTests: XCTestCase { test("tr", "İİ", "ii") } - func testFuzzFailure() throws { - let input = String(data: Data(base64Encoded: "77+977+977+977+977+977+977+977+977+977+9Cg==")!, encoding: .utf8)! + @Test func fuzzFailure() throws { + let data = try #require(Data(base64Encoded: "77+977+977+977+977+977+977+977+977+977+9Cg==")) + let input = try #require(String(data: data, encoding: .utf8)) _ = input.lowercased(with: Locale(identifier: "en_US")) _ = input.capitalized(with: Locale(identifier: "en_US")) _ = input.capitalized(with: Locale(identifier: "en_US")) diff --git a/Tests/FoundationInternationalizationTests/TimeZoneTests.swift b/Tests/FoundationInternationalizationTests/TimeZoneTests.swift index 31991d16f..724b15799 100644 --- a/Tests/FoundationInternationalizationTests/TimeZoneTests.swift +++ b/Tests/FoundationInternationalizationTests/TimeZoneTests.swift @@ -10,63 +10,64 @@ // //===----------------------------------------------------------------------===// -#if canImport(TestSupport) -import TestSupport -#endif +import Testing #if FOUNDATION_FRAMEWORK @testable import Foundation #elseif canImport(FoundationInternationalization) @testable import FoundationInternationalization +@testable import FoundationEssentials #endif -final class TimeZoneTests : XCTestCase { - - func test_timeZoneBasics() { +@Suite("TimeZone") +private struct TimeZoneTests { + @Test func basics() { let tz = TimeZone(identifier: "America/Los_Angeles")! - XCTAssertTrue(!tz.identifier.isEmpty) + #expect(!tz.identifier.isEmpty) } - func test_equality() { - let autoupdating = TimeZone.autoupdatingCurrent - let autoupdating2 = TimeZone.autoupdatingCurrent - - XCTAssertEqual(autoupdating, autoupdating2) - - let current = TimeZone.current - - XCTAssertNotEqual(autoupdating, current) + @Test func equality() async { + await usingCurrentInternationalizationPreferences { + let autoupdating = TimeZone.autoupdatingCurrent + let autoupdating2 = TimeZone.autoupdatingCurrent + + #expect(autoupdating == autoupdating2) + + let current = TimeZone.current + + #expect(autoupdating != current) + } } - func test_AnyHashableContainingTimeZone() { + @Test func anyHashableContainingTimeZone() { let values: [TimeZone] = [ TimeZone(identifier: "America/Los_Angeles")!, TimeZone(identifier: "Europe/Kiev")!, TimeZone(identifier: "Europe/Kiev")!, ] let anyHashables = values.map(AnyHashable.init) - expectEqual(TimeZone.self, type(of: anyHashables[0].base)) - expectEqual(TimeZone.self, type(of: anyHashables[1].base)) - expectEqual(TimeZone.self, type(of: anyHashables[2].base)) - XCTAssertNotEqual(anyHashables[0], anyHashables[1]) - XCTAssertEqual(anyHashables[1], anyHashables[2]) + #expect(TimeZone.self == type(of: anyHashables[0].base)) + #expect(TimeZone.self == type(of: anyHashables[1].base)) + #expect(TimeZone.self == type(of: anyHashables[2].base)) + #expect(anyHashables[0] != anyHashables[1]) + #expect(anyHashables[1] == anyHashables[2]) } - func testPredefinedTimeZone() { - XCTAssertEqual(TimeZone.gmt, TimeZone(identifier: "GMT")) + @Test func predefinedTimeZone() { + #expect(TimeZone.gmt == TimeZone(identifier: "GMT")) } - func testLocalizedName_103036605() { - func test(_ tzIdentifier: String, _ localeIdentifier: String, _ style: TimeZone.NameStyle, _ expected: String?, file: StaticString = #filePath, line: UInt = #line) { + @Test func localizedName_103036605() { + func test(_ tzIdentifier: String, _ localeIdentifier: String, _ style: TimeZone.NameStyle, _ expected: String?, sourceLocation: SourceLocation = #_sourceLocation) { let tz = TimeZone(identifier: tzIdentifier) guard let expected else { - XCTAssertNil(tz, file: file, line: line) + #expect(tz == nil, sourceLocation: sourceLocation) return } let locale = Locale(identifier: localeIdentifier) - XCTAssertEqual(tz?.localizedName(for: .generic, locale: locale), expected, file: file, line: line) + #expect(tz?.localizedName(for: .generic, locale: locale) == expected, sourceLocation: sourceLocation) } test("America/Los_Angeles", "en_US", .generic, "Pacific Time") @@ -91,235 +92,237 @@ final class TimeZoneTests : XCTestCase { test("BOGUS/BOGUS", "en_US", .standard, nil) } - func testTimeZoneName_103097012() { + @Test func timeZoneName_103097012() throws { - func _verify(_ tz: TimeZone?, _ expectedOffset: Int?, _ createdId: String?, file: StaticString = #filePath, line: UInt = #line) { + func _verify(_ tz: TimeZone?, _ expectedOffset: Int?, _ createdId: String?, sourceLocation: SourceLocation = #_sourceLocation) throws { if let expectedOffset { - XCTAssertNotNil(tz, file: file, line: line) - XCTAssertEqual(tz!.secondsFromGMT(for: Date(timeIntervalSince1970: 0)), expectedOffset, file: file, line: line) - XCTAssertEqual(tz!.identifier, createdId, file: file, line: line) + #expect(tz?.secondsFromGMT(for: Date(timeIntervalSince1970: 0)) == expectedOffset, sourceLocation: sourceLocation) + #expect(tz?.identifier == createdId, sourceLocation: sourceLocation) } else { - XCTAssertNil(tz, file: file, line: line) + #expect(tz == nil, sourceLocation: sourceLocation) } } - func testIdentifier(_ tzID: String, _ expectedOffset: Int?, _ createdId: String?, file: StaticString = #filePath, line: UInt = #line) { - _verify(TimeZone(identifier: tzID), expectedOffset, createdId, file: file, line: line) + func testIdentifier(_ tzID: String, _ expectedOffset: Int?, _ createdId: String?, sourceLocation: SourceLocation = #_sourceLocation) throws { + try _verify(TimeZone(identifier: tzID), expectedOffset, createdId, sourceLocation: sourceLocation) } - func testAbbreviation(_ abb: String, _ expectedOffset: Int?, _ createdId: String?, file: StaticString = #filePath, line: UInt = #line) { - _verify(TimeZone(abbreviation: abb), expectedOffset, createdId, file: file, line: line) + func testAbbreviation(_ abb: String, _ expectedOffset: Int?, _ createdId: String?, sourceLocation: SourceLocation = #_sourceLocation) throws { + try _verify(TimeZone(abbreviation: abb), expectedOffset, createdId, sourceLocation: sourceLocation) } - testIdentifier("America/Los_Angeles", -28800, "America/Los_Angeles") - testIdentifier("GMT", 0, "GMT") - testIdentifier("PST", -28800, "PST") - testIdentifier("GMT+8", 28800, "GMT+0800") - testIdentifier("GMT+8:00", 28800, "GMT+0800") - testIdentifier("BOGUS", nil, nil) - testIdentifier("XYZ", nil, nil) - testIdentifier("UTC", 0, "GMT") - - testAbbreviation("America/Los_Angeles", nil, nil) - testAbbreviation("XYZ", nil, nil) - testAbbreviation("GMT", 0, "GMT") - testAbbreviation("PST", -28800, "America/Los_Angeles") - testAbbreviation("GMT+8", 28800, "GMT+0800") - testAbbreviation("GMT+8:00", 28800, "GMT+0800") - testAbbreviation("GMT+0800", 28800, "GMT+0800") - testAbbreviation("UTC", 0, "GMT") + try testIdentifier("America/Los_Angeles", -28800, "America/Los_Angeles") + try testIdentifier("GMT", 0, "GMT") + try testIdentifier("PST", -28800, "PST") + try testIdentifier("GMT+8", 28800, "GMT+0800") + try testIdentifier("GMT+8:00", 28800, "GMT+0800") + try testIdentifier("BOGUS", nil, nil) + try testIdentifier("XYZ", nil, nil) + try testIdentifier("UTC", 0, "GMT") + + try testAbbreviation("America/Los_Angeles", nil, nil) + try testAbbreviation("XYZ", nil, nil) + try testAbbreviation("GMT", 0, "GMT") + try testAbbreviation("PST", -28800, "America/Los_Angeles") + try testAbbreviation("GMT+8", 28800, "GMT+0800") + try testAbbreviation("GMT+8:00", 28800, "GMT+0800") + try testAbbreviation("GMT+0800", 28800, "GMT+0800") + try testAbbreviation("UTC", 0, "GMT") } - func testSecondsFromGMT_RemoteDates() { + @Test func secondsFromGMT_RemoteDates() { let date = Date(timeIntervalSinceReferenceDate: -5001243627) // "1842-07-09T05:39:33+0000" let europeRome = TimeZone(identifier: "Europe/Rome")! let secondsFromGMT = europeRome.secondsFromGMT(for: date) - XCTAssertEqual(secondsFromGMT, 2996) // Before 1893 the time zone is UTC+00:49:56 + #expect(secondsFromGMT == 2996) // Before 1893 the time zone is UTC+00:49:56 + } + + func decodeHelper(_ l: TimeZone) throws -> TimeZone { + let je = JSONEncoder() + let data = try je.encode(l) + let jd = JSONDecoder() + return try jd.decode(TimeZone.self, from: data) + } + + @Test func serializationOfCurrent() async throws { + try await usingCurrentInternationalizationPreferences { + let current = TimeZone.current + let decodedCurrent = try decodeHelper(current) + #expect(decodedCurrent == current) + + let autoupdatingCurrent = TimeZone.autoupdatingCurrent + let decodedAutoupdatingCurrent = try decodeHelper(autoupdatingCurrent) + #expect(decodedAutoupdatingCurrent == autoupdatingCurrent) + + #expect(decodedCurrent != decodedAutoupdatingCurrent) + #expect(current != autoupdatingCurrent) + #expect(decodedCurrent != autoupdatingCurrent) + #expect(current != decodedAutoupdatingCurrent) + } } } -final class TimeZoneGMTTests : XCTestCase { +@Suite("TimeZone GMT") +private struct TimeZoneGMTTests { var tz: TimeZone { TimeZone(identifier: "GMT")! } - func testIdentifier() { - XCTAssertEqual(tz.identifier, "GMT") + @Test func identifier() { + #expect(tz.identifier == "GMT") } - func testSecondsFromGMT() { - XCTAssertEqual(tz.secondsFromGMT(), 0) + @Test func secondsFromGMT() { + #expect(tz.secondsFromGMT() == 0) } - func testSecondsFromGMTForDate() { - XCTAssertEqual(tz.secondsFromGMT(for: Date.now), 0) - XCTAssertEqual(tz.secondsFromGMT(for: Date.distantFuture), 0) - XCTAssertEqual(tz.secondsFromGMT(for: Date.distantPast), 0) + @Test func secondsFromGMTForDate() { + #expect(tz.secondsFromGMT(for: Date.now) == 0) + #expect(tz.secondsFromGMT(for: Date.distantFuture) == 0) + #expect(tz.secondsFromGMT(for: Date.distantPast) == 0) } - func testAbbreviationForDate() { - XCTAssertEqual(tz.abbreviation(for: Date.now), "GMT") - XCTAssertEqual(tz.abbreviation(for: Date.distantFuture), "GMT") - XCTAssertEqual(tz.abbreviation(for: Date.distantPast), "GMT") + @Test func abbreviationForDate() { + #expect(tz.abbreviation(for: Date.now) == "GMT") + #expect(tz.abbreviation(for: Date.distantFuture) == "GMT") + #expect(tz.abbreviation(for: Date.distantPast) == "GMT") } - func testDaylightSavingTimeOffsetForDate() { - XCTAssertEqual(tz.daylightSavingTimeOffset(for: Date.now), 0) - XCTAssertEqual(tz.daylightSavingTimeOffset(for: Date.distantFuture), 0) - XCTAssertEqual(tz.daylightSavingTimeOffset(for: Date.distantPast), 0) + @Test func daylightSavingTimeOffsetForDate() { + #expect(tz.daylightSavingTimeOffset(for: Date.now) == 0) + #expect(tz.daylightSavingTimeOffset(for: Date.distantFuture) == 0) + #expect(tz.daylightSavingTimeOffset(for: Date.distantPast) == 0) } - func testNextDaylightSavingTimeTransitionAfterDate() { - XCTAssertNil(tz.nextDaylightSavingTimeTransition(after: Date.now)) - XCTAssertNil(tz.nextDaylightSavingTimeTransition(after: Date.distantFuture)) - XCTAssertNil(tz.nextDaylightSavingTimeTransition(after: Date.distantPast)) + @Test func nextDaylightSavingTimeTransitionAfterDate() { + #expect(tz.nextDaylightSavingTimeTransition(after: Date.now) == nil) + #expect(tz.nextDaylightSavingTimeTransition(after: Date.distantFuture) == nil) + #expect(tz.nextDaylightSavingTimeTransition(after: Date.distantPast) == nil) } - func testNextDaylightSavingTimeTransition() { - XCTAssertNil(tz.nextDaylightSavingTimeTransition) - XCTAssertNil(tz.nextDaylightSavingTimeTransition) - XCTAssertNil(tz.nextDaylightSavingTimeTransition) + @Test func nextDaylightSavingTimeTransition() { + #expect(tz.nextDaylightSavingTimeTransition == nil) + #expect(tz.nextDaylightSavingTimeTransition == nil) + #expect(tz.nextDaylightSavingTimeTransition == nil) } - func testLocalizedName() { - XCTAssertEqual(tz.localizedName(for: .standard, locale: Locale(identifier: "en_US")), "Greenwich Mean Time") - XCTAssertEqual(tz.localizedName(for: .shortStandard, locale: Locale(identifier: "en_US")), "GMT") - XCTAssertEqual(tz.localizedName(for: .daylightSaving, locale: Locale(identifier: "en_US")), "Greenwich Mean Time") - XCTAssertEqual(tz.localizedName(for: .shortDaylightSaving, locale: Locale(identifier: "en_US")), "GMT") - XCTAssertEqual(tz.localizedName(for: .generic, locale: Locale(identifier: "en_US")), "Greenwich Mean Time") - XCTAssertEqual(tz.localizedName(for: .shortGeneric, locale: Locale(identifier: "en_US")), "GMT") + @Test func localizedName() { + #expect(tz.localizedName(for: .standard, locale: Locale(identifier: "en_US")) == "Greenwich Mean Time") + #expect(tz.localizedName(for: .shortStandard, locale: Locale(identifier: "en_US")) == "GMT") + #expect(tz.localizedName(for: .daylightSaving, locale: Locale(identifier: "en_US")) == "Greenwich Mean Time") + #expect(tz.localizedName(for: .shortDaylightSaving, locale: Locale(identifier: "en_US")) == "GMT") + #expect(tz.localizedName(for: .generic, locale: Locale(identifier: "en_US")) == "Greenwich Mean Time") + #expect(tz.localizedName(for: .shortGeneric, locale: Locale(identifier: "en_US")) == "GMT") // TODO: In non-framework, no FoundationInternationalization cases, return nil for all of tehse } - func testEqual() { - XCTAssertEqual(TimeZone(identifier: "UTC"), TimeZone(identifier: "UTC")) + @Test func equal() { + #expect(TimeZone(identifier: "UTC") == TimeZone(identifier: "UTC")) } - func test_abbreviated() { + @Test func abbreviated() throws { // A sampling of expected values for abbreviated GMT names let expected : [(Int, String)] = [(-64800, "GMT-18"), (-64769, "GMT-17:59"), (-64709, "GMT-17:58"), (-61769, "GMT-17:09"), (-61229, "GMT-17"), (-36029, "GMT-10"), (-35969, "GMT-9:59"), (-35909, "GMT-9:58"), (-32489, "GMT-9:01"), (-32429, "GMT-9"), (-3629, "GMT-1"), (-1829, "GMT-0:30"), (-89, "GMT-0:01"), (-29, "GMT"), (-1, "GMT"), (0, "GMT"), (29, "GMT"), (30, "GMT+0:01"), (90, "GMT+0:02"), (1770, "GMT+0:30"), (3570, "GMT+1"), (3630, "GMT+1:01"), (34170, "GMT+9:30"), (35910, "GMT+9:59"), (35970, "GMT+10"), (36030, "GMT+10:01"), (64650, "GMT+17:58"), (64710, "GMT+17:59"), (64770, "GMT+18")] for (offset, expect) in expected { - let tz = TimeZone(secondsFromGMT: offset)! - XCTAssertEqual(tz.abbreviation(), expect) + let tz = try #require(TimeZone(secondsFromGMT: offset)) + #expect(tz.abbreviation() == expect) } } } -final class TimeZoneICUTests: XCTestCase { - func testTimeZoneOffset() { +@Suite("TimeZone ICU") +private struct TimeZoneICUTests { + @Test func timeZoneOffset() throws { let tz = _TimeZoneICU(identifier: "America/Los_Angeles")! var c = Calendar(identifier: .gregorian) c.timeZone = TimeZone(identifier: "America/Los_Angeles")! var gmt_calendar = Calendar(identifier: .gregorian) gmt_calendar.timeZone = .gmt - func test(_ dateComponent: DateComponents, expectedRawOffset: Int, expectedDSTOffset: TimeInterval, file: StaticString = #filePath, line: UInt = #line) { - let d = gmt_calendar.date(from: dateComponent)! // date in GMT + func test(_ dateComponent: DateComponents, expectedRawOffset: Int, expectedDSTOffset: TimeInterval, sourceLocation: SourceLocation = #_sourceLocation) throws { + let d = try #require(gmt_calendar.date(from: dateComponent)) // date in GMT let (rawOffset, dstOffset) = tz.rawAndDaylightSavingTimeOffset(for: d) - XCTAssertEqual(rawOffset, expectedRawOffset, file: file, line: line) - XCTAssertEqual(dstOffset, expectedDSTOffset, file: file, line: line) + #expect(rawOffset == expectedRawOffset, sourceLocation: sourceLocation) + #expect(dstOffset == expectedDSTOffset, sourceLocation: sourceLocation) } // Not in DST - test(.init(year: 2023, month: 3, day: 12, hour: 1, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 0) - test(.init(year: 2023, month: 3, day: 12, hour: 1, minute: 00, second: 00, nanosecond: 1), expectedRawOffset: -28800, expectedDSTOffset: 0) - test(.init(year: 2023, month: 3, day: 12, hour: 1, minute: 00, second: 01), expectedRawOffset: -28800, expectedDSTOffset: 0) - test(.init(year: 2023, month: 3, day: 12, hour: 1, minute: 59, second: 59), expectedRawOffset: -28800, expectedDSTOffset: 0) + try test(.init(year: 2023, month: 3, day: 12, hour: 1, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 0) + try test(.init(year: 2023, month: 3, day: 12, hour: 1, minute: 00, second: 00, nanosecond: 1), expectedRawOffset: -28800, expectedDSTOffset: 0) + try test(.init(year: 2023, month: 3, day: 12, hour: 1, minute: 00, second: 01), expectedRawOffset: -28800, expectedDSTOffset: 0) + try test(.init(year: 2023, month: 3, day: 12, hour: 1, minute: 59, second: 59), expectedRawOffset: -28800, expectedDSTOffset: 0) // These times do not exist; we treat it as if in the previous time zone, i.e. not in DST - test(.init(year: 2023, month: 3, day: 12, hour: 2, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 0) - test(.init(year: 2023, month: 3, day: 12, hour: 2, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 0) + try test(.init(year: 2023, month: 3, day: 12, hour: 2, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 0) + try test(.init(year: 2023, month: 3, day: 12, hour: 2, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 0) // After DST starts - test(.init(year: 2023, month: 3, day: 12, hour: 3, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 3600) - test(.init(year: 2023, month: 3, day: 12, hour: 3, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 3600) - test(.init(year: 2023, month: 3, day: 12, hour: 4, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 3600) + try test(.init(year: 2023, month: 3, day: 12, hour: 3, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 3600) + try test(.init(year: 2023, month: 3, day: 12, hour: 3, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 3600) + try test(.init(year: 2023, month: 3, day: 12, hour: 4, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 3600) // These times happen twice; we treat it as if in the previous time zone, i.e. still in DST - test(.init(year: 2023, month: 11, day: 5, hour: 1, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 3600) - test(.init(year: 2023, month: 11, day: 5, hour: 1, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 3600) + try test(.init(year: 2023, month: 11, day: 5, hour: 1, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 3600) + try test(.init(year: 2023, month: 11, day: 5, hour: 1, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 3600) // Clock should turn right back as this moment, so if we insist on being at this point, then we've moved past the transition point -- hence not DST - test(.init(year: 2023, month: 11, day: 5, hour: 2, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 0) + try test(.init(year: 2023, month: 11, day: 5, hour: 2, minute: 00, second: 00), expectedRawOffset: -28800, expectedDSTOffset: 0) // Not in DST - test(.init(year: 2023, month: 11, day: 5, hour: 2, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 0) - test(.init(year: 2023, month: 11, day: 5, hour: 3, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 0) + try test(.init(year: 2023, month: 11, day: 5, hour: 2, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 0) + try test(.init(year: 2023, month: 11, day: 5, hour: 3, minute: 34, second: 52), expectedRawOffset: -28800, expectedDSTOffset: 0) } } // MARK: - FoundationPreview disabled tests -#if FOUNDATION_FRAMEWORK -extension TimeZoneTests { - func decodeHelper(_ l: TimeZone) -> TimeZone { - let je = JSONEncoder() - let data = try! je.encode(l) - let jd = JSONDecoder() - return try! jd.decode(TimeZone.self, from: data) - } - - // Reenable once JSONEncoder/Decoder are moved - func test_serializationOfCurrent() { - let current = TimeZone.current - let decodedCurrent = decodeHelper(current) - XCTAssertEqual(decodedCurrent, current) - - let autoupdatingCurrent = TimeZone.autoupdatingCurrent - let decodedAutoupdatingCurrent = decodeHelper(autoupdatingCurrent) - XCTAssertEqual(decodedAutoupdatingCurrent, autoupdatingCurrent) - - XCTAssertNotEqual(decodedCurrent, decodedAutoupdatingCurrent) - XCTAssertNotEqual(current, autoupdatingCurrent) - XCTAssertNotEqual(decodedCurrent, autoupdatingCurrent) - XCTAssertNotEqual(current, decodedAutoupdatingCurrent) - } -} -#endif // FOUNDATION_FRAMEWORK // MARK: - Bridging Tests #if FOUNDATION_FRAMEWORK -final class TimeZoneBridgingTests : XCTestCase { - func testCustomNSTimeZone() { +@Suite("TimeZone Bridging") +private struct TimeZoneBridgingTests { + @Test func customNSTimeZone() { // This test verifies that a custom ObjC subclass of NSTimeZone, bridged into Swift, still calls back into ObjC. `customTimeZone` returns an instances of "MyCustomTimeZone : NSTimeZone". let myTZ = customTimeZone() - XCTAssertEqual(myTZ.identifier, "MyCustomTimeZone") - XCTAssertEqual(myTZ.nextDaylightSavingTimeTransition(after: Date.now), Date(timeIntervalSince1970: 1000000)) - XCTAssertEqual(myTZ.secondsFromGMT(), 42) - XCTAssertEqual(myTZ.abbreviation(), "hello") - XCTAssertEqual(myTZ.isDaylightSavingTime(), true) - XCTAssertEqual(myTZ.daylightSavingTimeOffset(), 12345) - } - - func testCustomNSTimeZoneAsDefault() { - // Set a custom subclass of NSTimeZone as the default time zone - setCustomTimeZoneAsDefault() - - // Calendar uses the default time zone - let defaultTZ = Calendar.current.timeZone - XCTAssertEqual(defaultTZ.identifier, "MyCustomTimeZone") - XCTAssertEqual(defaultTZ.nextDaylightSavingTimeTransition(after: Date.now), Date(timeIntervalSince1970: 1000000)) - XCTAssertEqual(defaultTZ.secondsFromGMT(), 42) - XCTAssertEqual(defaultTZ.abbreviation(), "hello") - XCTAssertEqual(defaultTZ.isDaylightSavingTime(), true) - XCTAssertEqual(defaultTZ.daylightSavingTimeOffset(), 12345) + #expect(myTZ.identifier == "MyCustomTimeZone") + #expect(myTZ.nextDaylightSavingTimeTransition(after: Date.now) == Date(timeIntervalSince1970: 1000000)) + #expect(myTZ.secondsFromGMT() == 42) + #expect(myTZ.abbreviation() == "hello") + #expect(myTZ.isDaylightSavingTime() == true) + #expect(myTZ.daylightSavingTimeOffset() == 12345) } - func test_AnyHashableCreatedFromNSTimeZone() { + @Test func anyHashableCreatedFromNSTimeZone() { let values: [NSTimeZone] = [ NSTimeZone(name: "America/Los_Angeles")!, NSTimeZone(name: "Europe/Kiev")!, NSTimeZone(name: "Europe/Kiev")!, ] let anyHashables = values.map(AnyHashable.init) - expectEqual(TimeZone.self, type(of: anyHashables[0].base)) - expectEqual(TimeZone.self, type(of: anyHashables[1].base)) - expectEqual(TimeZone.self, type(of: anyHashables[2].base)) - XCTAssertNotEqual(anyHashables[0], anyHashables[1]) - XCTAssertEqual(anyHashables[1], anyHashables[2]) + #expect(TimeZone.self == type(of: anyHashables[0].base)) + #expect(TimeZone.self == type(of: anyHashables[1].base)) + #expect(TimeZone.self == type(of: anyHashables[2].base)) + #expect(anyHashables[0] != anyHashables[1]) + #expect(anyHashables[1] == anyHashables[2]) + } + + @Test func customNSTimeZoneAsDefault() async { + await usingCurrentInternationalizationPreferences { + // Set a custom subclass of NSTimeZone as the default time zone + setCustomTimeZoneAsDefault() + + // Calendar uses the default time zone + let defaultTZ = Calendar.current.timeZone + #expect(defaultTZ.identifier == "MyCustomTimeZone") + #expect(defaultTZ.nextDaylightSavingTimeTransition(after: Date.now) == Date(timeIntervalSince1970: 1000000)) + #expect(defaultTZ.secondsFromGMT() == 42) + #expect(defaultTZ.abbreviation() == "hello") + #expect(defaultTZ.isDaylightSavingTime() == true) + #expect(defaultTZ.daylightSavingTimeOffset() == 12345) + } } } #endif // FOUNDATION_FRAMEWORK diff --git a/Tests/FoundationInternationalizationTests/URLTests+UIDNA.swift b/Tests/FoundationInternationalizationTests/URLTests+UIDNA.swift index 142e66bdf..489ccdc76 100644 --- a/Tests/FoundationInternationalizationTests/URLTests+UIDNA.swift +++ b/Tests/FoundationInternationalizationTests/URLTests+UIDNA.swift @@ -10,6 +10,8 @@ // //===----------------------------------------------------------------------===// +import Testing + #if FOUNDATION_FRAMEWORK @testable import Foundation #else @@ -17,20 +19,17 @@ @testable import FoundationInternationalization #endif // FOUNDATION_FRAMEWORK -#if canImport(TestSupport) -import TestSupport -#endif - -final class URLUIDNATests: XCTestCase { - func testURLHostUIDNAEncoding() { +@Suite("URL UIDNA") +private struct URLUIDNATests { + @Test func urlHostUIDNAEncoding() { let emojiURL = URL(string: "https://i❤️tacos.ws/🏳️‍🌈/冰淇淋") let emojiURLEncoded = "https://xn--itacos-i50d.ws/%F0%9F%8F%B3%EF%B8%8F%E2%80%8D%F0%9F%8C%88/%E5%86%B0%E6%B7%87%E6%B7%8B" - XCTAssertEqual(emojiURL?.absoluteString, emojiURLEncoded) - XCTAssertEqual(emojiURL?.host(percentEncoded: false), "xn--itacos-i50d.ws") + #expect(emojiURL?.absoluteString == emojiURLEncoded) + #expect(emojiURL?.host(percentEncoded: false) == "xn--itacos-i50d.ws") let chineseURL = URL(string: "http://見.香港/热狗/🌭") let chineseURLEncoded = "http://xn--nw2a.xn--j6w193g/%E7%83%AD%E7%8B%97/%F0%9F%8C%AD" - XCTAssertEqual(chineseURL?.absoluteString, chineseURLEncoded) - XCTAssertEqual(chineseURL?.host(percentEncoded: false), "xn--nw2a.xn--j6w193g") + #expect(chineseURL?.absoluteString == chineseURLEncoded) + #expect(chineseURL?.host(percentEncoded: false) == "xn--nw2a.xn--j6w193g") } }