diff --git a/Sources/CoreModel/InMemoryStorage.swift b/Sources/CoreModel/InMemoryStorage.swift index 328f2f0..6b1be41 100644 --- a/Sources/CoreModel/InMemoryStorage.swift +++ b/Sources/CoreModel/InMemoryStorage.swift @@ -52,18 +52,84 @@ public final class InMemoryStorage { public func fetch(_ entity: EntityName, for id: ObjectID) throws(CoreModelError) -> ModelData? { try withLock { () throws(CoreModelError) in try validate(entity) - return objects[entity]?[id] + return objects[entity]?[id].map { normalized(entity: entity, $0) } } } public func fetch(_ fetchRequest: FetchRequest) throws(CoreModelError) -> [ModelData] { try withLock { () throws(CoreModelError) in try validate(fetchRequest.entity) - let values = objects[fetchRequest.entity].map { Array($0.values) } ?? [] + let values = (objects[fetchRequest.entity].map { Array($0.values) } ?? []) + .map { normalized(entity: fetchRequest.entity, $0) } return fetchRequest.evaluate(values, functions: functions) } } + /// Materializes every attribute and to-many relationship the schema declares, the way a + /// SQL row or a Core Data managed object does automatically on read. + /// + /// A SQL column binds `NULL` for anything an `INSERT` doesn't provide, and a Core Data + /// managed object always has a value (`nil` for an unset optional) for every attribute + /// its model declares — a row is never partially formed on either backend. This store + /// keeps only the keys `insert` was actually given, so an attribute nobody has ever + /// explicitly set to `.null` (rather than simply never provided) previously decoded as + /// `keyNotFound` instead of the absence it actually represents. + /// + /// To-many relationships get the same treatment along a different axis: neither backend + /// stores a to-many value on the row that owns it in the first place. A one-to-many + /// (whose inverse is a to-one foreign key, e.g. `WalletCard.user` → `User`) is answered + /// by scanning the destination table for rows whose foreign key points back here, and a + /// many-to-many (whose inverse is also to-many) by a join table either side can add a + /// link to. A row whose to-many relationship key was never explicitly set — which + /// includes every one-to-many, which is supposed to be entirely computed and never + /// assigned — previously decoded as `keyNotFound` instead of the collection it actually + /// has. To-one relationships are left alone: an absent required reference is a real data + /// problem, not a default. + private func normalized(entity: EntityName, _ value: ModelData) -> ModelData { + guard let description = model[entity] else { return value } + var value = value + for attribute in description.attributes where value.attributes[attribute.id] == nil { + value.attributes[attribute.id] = .null + } + for relationship in description.relationships where relationship.type == .toMany { + guard let destination = model[relationship.destinationEntity], + let inverse = destination.relationships.first(where: { $0.id == relationship.inverseRelationship }) + else { + if value.relationships[relationship.id] == nil { + value.relationships[relationship.id] = .toMany([]) + } + continue + } + let candidates = objects[relationship.destinationEntity].map { Array($0.values) } ?? [] + switch inverse.type { + case .toOne: + // One-to-many: always derived live from the destination rows' foreign key, + // matching SQL/Core Data — this row never stores it itself. + let derived = candidates + .filter { $0.relationships[inverse.id] == .toOne(value.id) } + .map { $0.id } + value.relationships[relationship.id] = .toMany(derived) + case .toMany: + // Many-to-many: no join table here, so union whatever this row already + // records with anything the destination rows record pointing back — a link + // added from either side is visible from both. + var ids = Set() + if case let .toMany(existing)? = value.relationships[relationship.id] { + ids.formUnion(existing) + } + for candidate in candidates { + if case let .toMany(backLinks)? = candidate.relationships[inverse.id], + backLinks.contains(value.id) + { + ids.insert(candidate.id) + } + } + value.relationships[relationship.id] = .toMany(Array(ids)) + } + } + return value + } + public func fetchID(_ fetchRequest: FetchRequest) throws(CoreModelError) -> [ObjectID] { try fetch(fetchRequest).map { $0.id } } @@ -75,7 +141,27 @@ public final class InMemoryStorage { public func insert(_ value: ModelData) throws(CoreModelError) { try withLock { () throws(CoreModelError) in try validate(value.entity) - objects[value.entity, default: [:]][value.id] = value + // A key present in `value` overrides; a key the existing row already had that + // `value` doesn't mention is preserved — the same "only touch the columns you + // provided" semantics a SQL `ON CONFLICT DO UPDATE` or a Core Data managed object + // gives for free. Without this, re-inserting a row from a batch that doesn't + // touch every relationship (e.g. a site catalog refresh that never re-states + // `parkingReservations`, which is written by an entirely separate sync) would + // silently wipe those links instead of leaving them alone. + if var existing = objects[value.entity]?[value.id] { + // - Note: Explicit loops rather than `Dictionary.merge(_:uniquingKeysWith:)` — + // the closure-based overload does dynamic casting internally, which is + // disallowed under Embedded Swift. + for (key, attribute) in value.attributes { + existing.attributes[key] = attribute + } + for (key, relationship) in value.relationships { + existing.relationships[key] = relationship + } + objects[value.entity, default: [:]][value.id] = existing + } else { + objects[value.entity, default: [:]][value.id] = value + } } } diff --git a/Tests/CoreModelMacrosTests/EntityMacroTests.swift b/Tests/CoreModelMacrosTests/EntityMacroTests.swift index 29ca4f9..0aed73c 100644 --- a/Tests/CoreModelMacrosTests/EntityMacroTests.swift +++ b/Tests/CoreModelMacrosTests/EntityMacroTests.swift @@ -11,16 +11,16 @@ #if os(macOS) || os(Linux) import Foundation -import XCTest +import Testing import SwiftSyntax import SwiftParser import SwiftSyntaxMacros import SwiftSyntaxMacroExpansion @testable import CoreModelMacros -final class EntityMacroTests: XCTestCase { +@Suite struct EntityMacroTests { - func testStructExpansion() throws { + @Test func structExpansion() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -35,20 +35,20 @@ final class EntityMacroTests: XCTestCase { """) let context = BasicMacroExpansionContext() let members = try expandMembers(of: node, attachedTo: declaration, in: context) - XCTAssertEqual(members.count, 5) + #expect(members.count == 5) let source = members.map { $0.description }.joined(separator: "\n") - XCTAssert(source.contains(#"public static var entityName: EntityName { "Person" }"#)) - XCTAssert(source.contains(".name: .string")) - XCTAssert(source.contains(".age: .int64")) - XCTAssert(source.contains(".created: .date")) - XCTAssert(source.contains("public static var relationships: [CodingKeys: Relationship] { [:] }")) - XCTAssert(source.contains("public init(from container: ModelData) throws")) - XCTAssert(source.contains("self.name = try container.decode(String.self, forKey: Person.CodingKeys.name)")) - XCTAssert(source.contains("public func encode() -> ModelData")) - XCTAssert(source.contains("container.encode(self.age, forKey: Person.CodingKeys.age)")) + #expect(source.contains(#"public static var entityName: EntityName { "Person" }"#)) + #expect(source.contains(".name: .string")) + #expect(source.contains(".age: .int64")) + #expect(source.contains(".created: .date")) + #expect(source.contains("public static var relationships: [CodingKeys: Relationship] { [:] }")) + #expect(source.contains("public init(from container: ModelData) throws")) + #expect(source.contains("self.name = try container.decode(String.self, forKey: Person.CodingKeys.name)")) + #expect(source.contains("public func encode() -> ModelData")) + #expect(source.contains("container.encode(self.age, forKey: Person.CodingKeys.age)")) } - func testExtensionExpansion() throws { + @Test func extensionExpansion() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -63,11 +63,11 @@ final class EntityMacroTests: XCTestCase { conformingTo: [], in: context ) - XCTAssertEqual(extensions.count, 1) - XCTAssert(extensions[0].description.contains("CoreModel.Entity")) + #expect(extensions.count == 1) + #expect(extensions[0].description.contains("CoreModel.Entity")) } - func testExplicitEntityName() throws { + @Test func explicitEntityName() throws { let (node, declaration) = try parse(""" @Entity("PersonEntity") struct Person { @@ -76,10 +76,10 @@ final class EntityMacroTests: XCTestCase { """) let context = BasicMacroExpansionContext() let decl = try EntityMacro.entityNameDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) - XCTAssert(decl.description.contains(#""PersonEntity""#)) + #expect(decl.description.contains(#""PersonEntity""#)) } - func testOptionalAttributes() throws { + @Test func optionalAttributes() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -92,11 +92,11 @@ final class EntityMacroTests: XCTestCase { """) let context = BasicMacroExpansionContext() let decl = try EntityMacro.attributesDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) - XCTAssert(decl.description.contains(".nickname: .string")) - XCTAssert(decl.description.contains(".count: .int64")) + #expect(decl.description.contains(".nickname: .string")) + #expect(decl.description.contains(".count: .int64")) } - func testExplicitAttributeType() throws { + @Test func explicitAttributeType() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -107,10 +107,10 @@ final class EntityMacroTests: XCTestCase { """) let context = BasicMacroExpansionContext() let decl = try EntityMacro.attributesDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) - XCTAssert(decl.description.contains(".avatar: .data")) + #expect(decl.description.contains(".avatar: .data")) } - func testUnknownAttributeType() throws { + @Test func unknownAttributeType() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -120,15 +120,17 @@ final class EntityMacroTests: XCTestCase { } """) let context = BasicMacroExpansionContext() - XCTAssertThrowsError(try EntityMacro.attributesDeclarationSyntax(of: node, providingMembersOf: declaration, in: context)) { error in - guard case MacroError.unknownAttributeType(let name) = error else { - return XCTFail("Expected unknownAttributeType, got \(error)") - } - XCTAssertEqual(name, "point") + do { + _ = try EntityMacro.attributesDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) + Issue.record("Expected an error") + } catch MacroError.unknownAttributeType(let name) { + #expect(name == "point") + } catch { + Issue.record("Expected unknownAttributeType, got \(error)") } } - func testRelationships() throws { + @Test func relationships() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -142,15 +144,15 @@ final class EntityMacroTests: XCTestCase { let context = BasicMacroExpansionContext() let decl = try EntityMacro.relationshipsDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) let source = decl.description - XCTAssert(source.contains("destination: Pet.self")) - XCTAssert(source.contains("type: .toMany")) - XCTAssert(source.contains("inverseRelationship: .owner")) - XCTAssert(source.contains("destination: Company.self")) - XCTAssert(source.contains("type: .toOne")) - XCTAssert(source.contains("inverseRelationship: .employees")) + #expect(source.contains("destination: Pet.self")) + #expect(source.contains("type: .toMany")) + #expect(source.contains("inverseRelationship: .owner")) + #expect(source.contains("destination: Company.self")) + #expect(source.contains("type: .toOne")) + #expect(source.contains("inverseRelationship: .employees")) } - func testMissingInverseRelationship() throws { + @Test func missingInverseRelationship() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -160,15 +162,17 @@ final class EntityMacroTests: XCTestCase { } """) let context = BasicMacroExpansionContext() - XCTAssertThrowsError(try EntityMacro.relationshipsDeclarationSyntax(of: node, providingMembersOf: declaration, in: context)) { error in - guard case MacroError.unknownInverseRelationship(let name) = error else { - return XCTFail("Expected unknownInverseRelationship, got \(error)") - } - XCTAssertEqual(name, "pets") + do { + _ = try EntityMacro.relationshipsDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) + Issue.record("Expected an error") + } catch MacroError.unknownInverseRelationship(let name) { + #expect(name == "pets") + } catch { + Issue.record("Expected unknownInverseRelationship, got \(error)") } } - func testInvalidInverseExpression() throws { + @Test func invalidInverseExpression() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -178,14 +182,17 @@ final class EntityMacroTests: XCTestCase { } """) let context = BasicMacroExpansionContext() - XCTAssertThrowsError(try EntityMacro.relationshipsDeclarationSyntax(of: node, providingMembersOf: declaration, in: context)) { error in - guard case MacroError.unknownInverseRelationship = error else { - return XCTFail("Expected unknownInverseRelationship, got \(error)") - } + do { + _ = try EntityMacro.relationshipsDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) + Issue.record("Expected an error") + } catch MacroError.unknownInverseRelationship { + // expected + } catch { + Issue.record("Expected unknownInverseRelationship, got \(error)") } } - func testRelationshipDecodeEncode() throws { + @Test func relationshipDecodeEncode() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -198,12 +205,12 @@ final class EntityMacroTests: XCTestCase { """) let context = BasicMacroExpansionContext() let initDecl = try EntityMacro.initDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) - XCTAssert(initDecl.description.contains("self.pets = try container.decodeRelationship([Pet.ID].self, forKey: Person.CodingKeys.pets)")) + #expect(initDecl.description.contains("self.pets = try container.decodeRelationship([Pet.ID].self, forKey: Person.CodingKeys.pets)")) let encodeDecl = try EntityMacro.encodeDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) - XCTAssert(encodeDecl.description.contains("container.encodeRelationship(self.pets, forKey: Person.CodingKeys.pets)")) + #expect(encodeDecl.description.contains("container.encodeRelationship(self.pets, forKey: Person.CodingKeys.pets)")) } - func testUnrelatedPropertyAttributeIgnored() throws { + @Test func unrelatedPropertyAttributeIgnored() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -216,12 +223,12 @@ final class EntityMacroTests: XCTestCase { """) let context = BasicMacroExpansionContext() let properties = EntityMacro.codableProperties(of: declaration) - XCTAssertEqual(properties.map { $0.name }, ["name"]) + #expect(properties.map { $0.name } == ["name"]) let initDecl = try EntityMacro.initDeclarationSyntax(of: node, providingMembersOf: declaration, in: context) - XCTAssertFalse(initDecl.description.contains("ignored")) + #expect(initDecl.description.contains("ignored") == false) } - func testClassTypeName() throws { + @Test func classTypeName() throws { let (node, declaration) = try parse(""" @Entity class Animal { @@ -229,10 +236,10 @@ final class EntityMacroTests: XCTestCase { } """) let context = BasicMacroExpansionContext() - XCTAssertEqual(try EntityMacro.typeName(of: node, providingMembersOf: declaration, in: context), "Animal") + #expect(try EntityMacro.typeName(of: node, providingMembersOf: declaration, in: context) == "Animal") } - func testEnumTypeName() throws { + @Test func enumTypeName() throws { let (node, declaration) = try parse(""" @Entity enum Kind { @@ -240,10 +247,10 @@ final class EntityMacroTests: XCTestCase { } """) let context = BasicMacroExpansionContext() - XCTAssertEqual(try EntityMacro.typeName(of: node, providingMembersOf: declaration, in: context), "Kind") + #expect(try EntityMacro.typeName(of: node, providingMembersOf: declaration, in: context) == "Kind") } - func testInvalidType() throws { + @Test func invalidType() throws { let (node, declaration) = try parse(""" @Entity actor Worker { @@ -251,31 +258,34 @@ final class EntityMacroTests: XCTestCase { } """) let context = BasicMacroExpansionContext() - XCTAssertThrowsError(try EntityMacro.typeName(of: node, providingMembersOf: declaration, in: context)) { error in - guard case MacroError.invalidType = error else { - return XCTFail("Expected invalidType, got \(error)") - } + do { + _ = try EntityMacro.typeName(of: node, providingMembersOf: declaration, in: context) + Issue.record("Expected an error") + } catch MacroError.invalidType { + // expected + } catch { + Issue.record("Expected invalidType, got \(error)") } } - func testInferAttributeType() { - XCTAssertEqual(inferAttributeType(from: "String"), ".string") - XCTAssertEqual(inferAttributeType(from: "Data"), ".data") - XCTAssertEqual(inferAttributeType(from: "Bool"), ".bool") - XCTAssertEqual(inferAttributeType(from: "Int16"), ".int16") - XCTAssertEqual(inferAttributeType(from: "Int32"), ".int32") - XCTAssertEqual(inferAttributeType(from: "Int64"), ".int64") - XCTAssertEqual(inferAttributeType(from: "Int"), ".int64") - XCTAssertEqual(inferAttributeType(from: "Float"), ".float") - XCTAssertEqual(inferAttributeType(from: "Double"), ".double") - XCTAssertEqual(inferAttributeType(from: "Date"), ".date") - XCTAssertEqual(inferAttributeType(from: "UUID"), ".uuid") - XCTAssertEqual(inferAttributeType(from: "URL"), ".url") - XCTAssertEqual(inferAttributeType(from: "Decimal"), ".decimal") - XCTAssertNil(inferAttributeType(from: "CGPoint")) + @Test func inferAttributeTypeMapping() { + #expect(inferAttributeType(from: "String") == ".string") + #expect(inferAttributeType(from: "Data") == ".data") + #expect(inferAttributeType(from: "Bool") == ".bool") + #expect(inferAttributeType(from: "Int16") == ".int16") + #expect(inferAttributeType(from: "Int32") == ".int32") + #expect(inferAttributeType(from: "Int64") == ".int64") + #expect(inferAttributeType(from: "Int") == ".int64") + #expect(inferAttributeType(from: "Float") == ".float") + #expect(inferAttributeType(from: "Double") == ".double") + #expect(inferAttributeType(from: "Date") == ".date") + #expect(inferAttributeType(from: "UUID") == ".uuid") + #expect(inferAttributeType(from: "URL") == ".url") + #expect(inferAttributeType(from: "Decimal") == ".decimal") + #expect(inferAttributeType(from: "CGPoint") == nil) } - func testPeerMacrosExpandToNothing() throws { + @Test func peerMacrosExpandToNothing() throws { let (node, declaration) = try parse(""" @Entity struct Person { @@ -283,26 +293,26 @@ final class EntityMacroTests: XCTestCase { } """) let context = BasicMacroExpansionContext() - XCTAssertEqual(try AttributeMacro.expansion(of: node, providingPeersOf: declaration, in: context).count, 0) - XCTAssertEqual(try RelationshipMacro.expansion(of: node, providingPeersOf: declaration, in: context).count, 0) + #expect(try AttributeMacro.expansion(of: node, providingPeersOf: declaration, in: context).count == 0) + #expect(try RelationshipMacro.expansion(of: node, providingPeersOf: declaration, in: context).count == 0) } - func testExpansionNames() { - XCTAssertEqual(EntityMacro.expansionNames.count, 5) + @Test func expansionNames() { + #expect(EntityMacro.expansionNames.count == 5) } #if canImport(Darwin) - func testMacroErrorDescriptions() { - XCTAssertNotNil(MacroError.invalidType.errorDescription) - XCTAssertNotNil(MacroError.unknownAttributeType(for: "point").errorDescription) - XCTAssertNotNil(MacroError.unknownInverseRelationship(for: "pets").errorDescription) + @Test func macroErrorDescriptions() { + #expect(MacroError.invalidType.errorDescription != nil) + #expect(MacroError.unknownAttributeType(for: "point").errorDescription != nil) + #expect(MacroError.unknownInverseRelationship(for: "pets").errorDescription != nil) } #endif } // MARK: - Helpers -private extension EntityMacroTests { +extension EntityMacroTests { /// Parse source containing a single attributed type declaration, returning the /// macro attribute node and the declaration group it is attached to. diff --git a/Tests/CoreModelTests/AttributeCodingTests.swift b/Tests/CoreModelTests/AttributeCodingTests.swift index e4d7fb6..6968805 100644 --- a/Tests/CoreModelTests/AttributeCodingTests.swift +++ b/Tests/CoreModelTests/AttributeCodingTests.swift @@ -6,10 +6,10 @@ // import Foundation -import XCTest +import Testing @testable import CoreModel -final class AttributeCodingTests: XCTestCase { +@Suite struct AttributeCodingTests { enum Key: CodingKey { case value @@ -23,90 +23,90 @@ final class AttributeCodingTests: XCTestCase { // MARK: - AttributeEncodable - func testEncodeAttributeValues() { - XCTAssertEqual(true.attributeValue, .bool(true)) - XCTAssertEqual("test".attributeValue, .string("test")) - XCTAssertEqual(Int(1).attributeValue, .int64(1)) - XCTAssertEqual(Int8(2).attributeValue, .int16(2)) - XCTAssertEqual(Int16(3).attributeValue, .int16(3)) - XCTAssertEqual(Int32(4).attributeValue, .int32(4)) - XCTAssertEqual(Int64(5).attributeValue, .int64(5)) - XCTAssertEqual(UInt(6).attributeValue, .int64(6)) - XCTAssertEqual(UInt8(7).attributeValue, .int16(7)) - XCTAssertEqual(UInt16(8).attributeValue, .int32(8)) - XCTAssertEqual(UInt32(9).attributeValue, .int64(9)) - XCTAssertEqual(UInt64(10).attributeValue, .int64(10)) - XCTAssertEqual(Float(1.5).attributeValue, .float(1.5)) - XCTAssertEqual(Double(2.5).attributeValue, .double(2.5)) + @Test func encodeAttributeValues() { + #expect(true.attributeValue == .bool(true)) + #expect("test".attributeValue == .string("test")) + #expect(Int(1).attributeValue == .int64(1)) + #expect(Int8(2).attributeValue == .int16(2)) + #expect(Int16(3).attributeValue == .int16(3)) + #expect(Int32(4).attributeValue == .int32(4)) + #expect(Int64(5).attributeValue == .int64(5)) + #expect(UInt(6).attributeValue == .int64(6)) + #expect(UInt8(7).attributeValue == .int16(7)) + #expect(UInt16(8).attributeValue == .int32(8)) + #expect(UInt32(9).attributeValue == .int64(9)) + #expect(UInt64(10).attributeValue == .int64(10)) + #expect(Float(1.5).attributeValue == .float(1.5)) + #expect(Double(2.5).attributeValue == .double(2.5)) let date = Date(timeIntervalSince1970: 100) - XCTAssertEqual(date.attributeValue, .date(date)) + #expect(date.attributeValue == .date(date)) let data = Data([0x01, 0x02]) - XCTAssertEqual(data.attributeValue, .data(data)) + #expect(data.attributeValue == .data(data)) let uuid = UUID() - XCTAssertEqual(uuid.attributeValue, .uuid(uuid)) + #expect(uuid.attributeValue == .uuid(uuid)) let url = URL(string: "https://example.com")! - XCTAssertEqual(url.attributeValue, .url(url)) + #expect(url.attributeValue == .url(url)) let decimal = Decimal(string: "3.14")! - XCTAssertEqual(decimal.attributeValue, .decimal(decimal)) + #expect(decimal.attributeValue == .decimal(decimal)) // RawRepresentable - XCTAssertEqual(Color.red.attributeValue, .string("red")) + #expect(Color.red.attributeValue == .string("red")) // Optional - XCTAssertEqual(Optional.none.attributeValue, .null) - XCTAssertEqual(Optional.some("test").attributeValue, .string("test")) + #expect(Optional.none.attributeValue == .null) + #expect(Optional.some("test").attributeValue == .string("test")) } // MARK: - AttributeDecodable - func testDecodeAttributeValues() { - XCTAssertEqual(Bool(attributeValue: .bool(true)), true) - XCTAssertNil(Bool(attributeValue: .string("true"))) - XCTAssertEqual(String(attributeValue: .string("test")), "test") - XCTAssertNil(String(attributeValue: .bool(false))) + @Test func decodeAttributeValues() { + #expect(Bool(attributeValue: .bool(true)) == true) + #expect(Bool(attributeValue: .string("true")) == nil) + #expect(String(attributeValue: .string("test")) == "test") + #expect(String(attributeValue: .bool(false)) == nil) let uuid = UUID() - XCTAssertEqual(UUID(attributeValue: .uuid(uuid)), uuid) - XCTAssertNil(UUID(attributeValue: .null)) + #expect(UUID(attributeValue: .uuid(uuid)) == uuid) + #expect(UUID(attributeValue: .null) == nil) let url = URL(string: "https://example.com")! - XCTAssertEqual(URL(attributeValue: .url(url)), url) - XCTAssertNil(URL(attributeValue: .null)) + #expect(URL(attributeValue: .url(url)) == url) + #expect(URL(attributeValue: .null) == nil) let date = Date(timeIntervalSince1970: 100) - XCTAssertEqual(Date(attributeValue: .date(date)), date) - XCTAssertNil(Date(attributeValue: .null)) + #expect(Date(attributeValue: .date(date)) == date) + #expect(Date(attributeValue: .null) == nil) let data = Data([0x01]) - XCTAssertEqual(Data(attributeValue: .data(data)), data) - XCTAssertNil(Data(attributeValue: .null)) + #expect(Data(attributeValue: .data(data)) == data) + #expect(Data(attributeValue: .null) == nil) let decimal = Decimal(string: "3.14")! - XCTAssertEqual(Decimal(attributeValue: .decimal(decimal)), decimal) - XCTAssertNil(Decimal(attributeValue: .double(3.14))) - XCTAssertEqual(Float(attributeValue: .float(1.5)), 1.5) - XCTAssertNil(Float(attributeValue: .double(1.5))) - XCTAssertEqual(Double(attributeValue: .double(2.5)), 2.5) - XCTAssertNil(Double(attributeValue: .float(2.5))) + #expect(Decimal(attributeValue: .decimal(decimal)) == decimal) + #expect(Decimal(attributeValue: .double(3.14)) == nil) + #expect(Float(attributeValue: .float(1.5)) == 1.5) + #expect(Float(attributeValue: .double(1.5)) == nil) + #expect(Double(attributeValue: .double(2.5)) == 2.5) + #expect(Double(attributeValue: .float(2.5)) == nil) // RawRepresentable - XCTAssertEqual(Color(attributeValue: .string("red")), .red) - XCTAssertNil(Color(attributeValue: .string("green"))) - XCTAssertNil(Color(attributeValue: .bool(true))) + #expect(Color(attributeValue: .string("red")) == .red) + #expect(Color(attributeValue: .string("green")) == nil) + #expect(Color(attributeValue: .bool(true)) == nil) // Optional - XCTAssertEqual(Optional(attributeValue: .null), .some(.none)) - XCTAssertEqual(Optional(attributeValue: .string("x")), "x") - XCTAssertNil(Optional(attributeValue: .bool(true))) + #expect(Optional(attributeValue: .null) == .some(.none)) + #expect(Optional(attributeValue: .string("x")) == "x") + #expect(Optional(attributeValue: .bool(true)) == nil) } - func testDecodeIntegerValues() { + @Test func decodeIntegerValues() { // every integer type decodes from all three stored widths func verify(_ type: T.Type) where T: AttributeDecodable & FixedWidthInteger { - XCTAssertEqual(T(attributeValue: .int16(16)), 16) - XCTAssertEqual(T(attributeValue: .int32(32)), 32) - XCTAssertEqual(T(attributeValue: .int64(64)), 64) - XCTAssertNil(T(attributeValue: .null)) - XCTAssertNil(T(attributeValue: .string("1"))) - XCTAssertNil(T(attributeValue: .bool(true))) - XCTAssertNil(T(attributeValue: .float(1))) - XCTAssertNil(T(attributeValue: .double(1))) - XCTAssertNil(T(attributeValue: .date(Date()))) - XCTAssertNil(T(attributeValue: .uuid(UUID()))) - XCTAssertNil(T(attributeValue: .url(URL(string: "https://example.com")!))) - XCTAssertNil(T(attributeValue: .data(Data()))) - XCTAssertNil(T(attributeValue: .decimal(1))) + #expect(T(attributeValue: .int16(16)) == 16) + #expect(T(attributeValue: .int32(32)) == 32) + #expect(T(attributeValue: .int64(64)) == 64) + #expect(T(attributeValue: .null) == nil) + #expect(T(attributeValue: .string("1")) == nil) + #expect(T(attributeValue: .bool(true)) == nil) + #expect(T(attributeValue: .float(1)) == nil) + #expect(T(attributeValue: .double(1)) == nil) + #expect(T(attributeValue: .date(Date())) == nil) + #expect(T(attributeValue: .uuid(UUID())) == nil) + #expect(T(attributeValue: .url(URL(string: "https://example.com")!)) == nil) + #expect(T(attributeValue: .data(Data())) == nil) + #expect(T(attributeValue: .decimal(1)) == nil) } verify(Int.self) verify(Int8.self) @@ -122,103 +122,109 @@ final class AttributeCodingTests: XCTestCase { // MARK: - ModelData attribute decoding - func testModelDataDecode() throws { + @Test func modelDataDecode() throws { var model = ModelData(entity: "Test", id: "1") model.encode("value", forKey: Key.value) - XCTAssertEqual(try model.decode(String.self, forKey: Key.value), "value") + #expect(try model.decode(String.self, forKey: Key.value) == "value") // key not found - XCTAssertThrowsError(try model.decode(String.self, forKey: Key.other)) { error in - guard case DecodingError.keyNotFound = error else { - return XCTFail("Expected keyNotFound, got \(error)") - } + do { + _ = try model.decode(String.self, forKey: Key.other) + Issue.record("Expected an error") + } catch DecodingError.keyNotFound { + // expected + } catch { + Issue.record("Expected keyNotFound, got \(error)") } // type mismatch - XCTAssertThrowsError(try model.decode(Bool.self, forKey: Key.value)) { error in - guard case DecodingError.typeMismatch = error else { - return XCTFail("Expected typeMismatch, got \(error)") - } + do { + _ = try model.decode(Bool.self, forKey: Key.value) + Issue.record("Expected an error") + } catch DecodingError.typeMismatch { + // expected + } catch { + Issue.record("Expected typeMismatch, got \(error)") } } // MARK: - ModelData relationship decoding - func testDecodeToOneRelationship() throws { + @Test func decodeToOneRelationship() throws { let uuid = UUID() var model = ModelData(entity: "Test", id: "1") model.encodeRelationship(uuid, forKey: Key.value) - XCTAssertEqual(try model.decodeRelationship(UUID.self, forKey: Key.value), uuid) + #expect(try model.decodeRelationship(UUID.self, forKey: Key.value) == uuid) // key not found - XCTAssertThrowsError(try model.decodeRelationship(UUID.self, forKey: Key.other)) + #expect(throws: (any Error).self) { try model.decodeRelationship(UUID.self, forKey: Key.other) } // null throws for non-optional model.relationships[PropertyKey(Key.value)] = .null - XCTAssertThrowsError(try model.decodeRelationship(UUID.self, forKey: Key.value)) + #expect(throws: (any Error).self) { try model.decodeRelationship(UUID.self, forKey: Key.value) } // to-many mismatch model.relationships[PropertyKey(Key.value)] = .toMany([ObjectID(uuid)]) - XCTAssertThrowsError(try model.decodeRelationship(UUID.self, forKey: Key.value)) + #expect(throws: (any Error).self) { try model.decodeRelationship(UUID.self, forKey: Key.value) } // invalid identifier model.relationships[PropertyKey(Key.value)] = .toOne("not-a-uuid") - XCTAssertThrowsError(try model.decodeRelationship(UUID.self, forKey: Key.value)) + #expect(throws: (any Error).self) { try model.decodeRelationship(UUID.self, forKey: Key.value) } } - func testDecodeOptionalRelationship() throws { + @Test func decodeOptionalRelationship() throws { let uuid = UUID() var model = ModelData(entity: "Test", id: "1") // missing key decodes as nil - XCTAssertNil(try model.decodeRelationship(UUID?.self, forKey: Key.value)) + #expect(try model.decodeRelationship(UUID?.self, forKey: Key.value) == nil) // null decodes as nil model.encodeRelationship(UUID?.none, forKey: Key.value) - XCTAssertNil(try model.decodeRelationship(UUID?.self, forKey: Key.value)) + #expect(try model.decodeRelationship(UUID?.self, forKey: Key.value) == nil) // value decodes model.encodeRelationship(UUID?.some(uuid), forKey: Key.value) - XCTAssertEqual(try model.decodeRelationship(UUID?.self, forKey: Key.value), uuid) + #expect(try model.decodeRelationship(UUID?.self, forKey: Key.value) == uuid) // to-many mismatch model.relationships[PropertyKey(Key.value)] = .toMany([ObjectID(uuid)]) - XCTAssertThrowsError(try model.decodeRelationship(UUID?.self, forKey: Key.value)) + #expect(throws: (any Error).self) { try model.decodeRelationship(UUID?.self, forKey: Key.value) } // invalid identifier model.relationships[PropertyKey(Key.value)] = .toOne("not-a-uuid") - XCTAssertThrowsError(try model.decodeRelationship(UUID?.self, forKey: Key.value)) + #expect(throws: (any Error).self) { try model.decodeRelationship(UUID?.self, forKey: Key.value) } } - func testDecodeToManyRelationship() throws { + @Test func decodeToManyRelationship() throws { let ids = [UUID(), UUID()] var model = ModelData(entity: "Test", id: "1") // missing key throws - XCTAssertThrowsError(try model.decodeRelationship([UUID].self, forKey: Key.value)) + #expect(throws: (any Error).self) { try model.decodeRelationship([UUID].self, forKey: Key.value) } // values decode model.encodeRelationship(ids, forKey: Key.value) - XCTAssertEqual(try model.decodeRelationship([UUID].self, forKey: Key.value), ids) + #expect(try model.decodeRelationship([UUID].self, forKey: Key.value) == ids) // null decodes as empty model.relationships[PropertyKey(Key.value)] = .null - XCTAssertEqual(try model.decodeRelationship([UUID].self, forKey: Key.value), []) + #expect(try model.decodeRelationship([UUID].self, forKey: Key.value) == []) // to-one mismatch model.relationships[PropertyKey(Key.value)] = .toOne(ObjectID(ids[0])) - XCTAssertThrowsError(try model.decodeRelationship([UUID].self, forKey: Key.value)) + #expect(throws: (any Error).self) { try model.decodeRelationship([UUID].self, forKey: Key.value) } // invalid identifier model.relationships[PropertyKey(Key.value)] = .toMany(["not-a-uuid"]) - XCTAssertThrowsError(try model.decodeRelationship([UUID].self, forKey: Key.value)) + #expect(throws: (any Error).self) { try model.decodeRelationship([UUID].self, forKey: Key.value) } } // MARK: - ObjectID - func testObjectID() { + @Test func objectID() { let id: ObjectID = "test-id" - XCTAssertEqual(id.rawValue, "test-id") - XCTAssertEqual(id.description, "test-id") - XCTAssertEqual(id.debugDescription, "test-id") + #expect(id.rawValue == "test-id") + #expect(id.description == "test-id") + #expect(id.debugDescription == "test-id") let uuid = UUID() - XCTAssertEqual(ObjectID(uuid).rawValue, uuid.uuidString) - XCTAssertEqual(UUID(objectID: ObjectID(uuid)), uuid) - XCTAssertNil(UUID(objectID: "invalid")) - XCTAssertEqual(String(objectID: "value"), "value") + #expect(ObjectID(uuid).rawValue == uuid.uuidString) + #expect(UUID(objectID: ObjectID(uuid)) == uuid) + #expect(UUID(objectID: "invalid") == nil) + #expect(String(objectID: "value") == "value") // RawRepresentable conversion - XCTAssertEqual(Color(objectID: "red"), Color.red) - XCTAssertNil(Color(objectID: "green")) + #expect(Color(objectID: "red") == Color.red) + #expect(Color(objectID: "green") == nil) // Optional conversion - XCTAssertEqual(UUID?(objectID: ObjectID(uuid)), uuid) - XCTAssertNil(UUID?(objectID: "invalid")) + #expect(UUID?(objectID: ObjectID(uuid)) == uuid) + #expect(UUID?(objectID: "invalid") == nil) // Optional description - XCTAssertEqual(UUID?.none.description, "") - XCTAssertEqual(UUID?.some(uuid).description, uuid.uuidString) + #expect(UUID?.none.description == "") + #expect(UUID?.some(uuid).description == uuid.uuidString) } } diff --git a/Tests/CoreModelTests/CoreDataModelTests.swift b/Tests/CoreModelTests/CoreDataModelTests.swift index 4eaebf8..19543c9 100644 --- a/Tests/CoreModelTests/CoreDataModelTests.swift +++ b/Tests/CoreModelTests/CoreDataModelTests.swift @@ -9,12 +9,16 @@ import Foundation import CoreData -import XCTest +import Testing @testable import CoreModel @testable import CoreDataModel -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -final class CoreDataModelTests: XCTestCase { +// - Note: `@Test`/`@Suite` can't be combined with a declaration-level `@available` — the +// macro expansion requires the declaration to be unconditionally available. The APIs this +// suite exercises (`ManagedObjectViewContext`, `NSPersistentContainer.syncLoadPersistentStores()`, +// etc.) need macOS 12/iOS 15/watchOS 8/tvOS 15, below this package's deployment target, so +// each test guards its body with a runtime `if #available` instead. +@Suite struct CoreDataModelTests { static func makeContext() throws -> NSManagedObjectContext { let model = Model(entities: Person.self, Event.self) @@ -25,39 +29,48 @@ final class CoreDataModelTests: XCTestCase { return context } - func testAttributeTypeConversion() { + @Test func attributeTypeConversion() throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } // CoreModel -> CoreData - XCTAssertEqual(NSAttributeType(attributeType: .bool), .booleanAttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .int16), .integer16AttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .int32), .integer32AttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .int64), .integer64AttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .float), .floatAttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .double), .doubleAttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .string), .stringAttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .data), .binaryDataAttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .date), .dateAttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .uuid), .UUIDAttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .url), .URIAttributeType) - XCTAssertEqual(NSAttributeType(attributeType: .decimal), .decimalAttributeType) + #expect(NSAttributeType(attributeType: .bool) == .booleanAttributeType) + #expect(NSAttributeType(attributeType: .int16) == .integer16AttributeType) + #expect(NSAttributeType(attributeType: .int32) == .integer32AttributeType) + #expect(NSAttributeType(attributeType: .int64) == .integer64AttributeType) + #expect(NSAttributeType(attributeType: .float) == .floatAttributeType) + #expect(NSAttributeType(attributeType: .double) == .doubleAttributeType) + #expect(NSAttributeType(attributeType: .string) == .stringAttributeType) + #expect(NSAttributeType(attributeType: .data) == .binaryDataAttributeType) + #expect(NSAttributeType(attributeType: .date) == .dateAttributeType) + #expect(NSAttributeType(attributeType: .uuid) == .UUIDAttributeType) + #expect(NSAttributeType(attributeType: .url) == .URIAttributeType) + #expect(NSAttributeType(attributeType: .decimal) == .decimalAttributeType) // CoreData -> CoreModel round trip for type in [AttributeType.bool, .int16, .int32, .int64, .float, .double, .string, .data, .date, .uuid, .url, .decimal] { - XCTAssertEqual(AttributeType(attributeType: NSAttributeType(attributeType: type)), type) + #expect(AttributeType(attributeType: NSAttributeType(attributeType: type)) == type) } // unsupported CoreData types - XCTAssertNil(AttributeType(attributeType: .undefinedAttributeType)) - XCTAssertNil(AttributeType(attributeType: .transformableAttributeType)) - XCTAssertNil(AttributeType(attributeType: .objectIDAttributeType)) + #expect(AttributeType(attributeType: .undefinedAttributeType) == nil) + #expect(AttributeType(attributeType: .transformableAttributeType) == nil) + #expect(AttributeType(attributeType: .objectIDAttributeType) == nil) #if swift(>=5.9) - XCTAssertNil(AttributeType(attributeType: .compositeAttributeType)) + #expect(AttributeType(attributeType: .compositeAttributeType) == nil) #endif } - func testComparisonModifierConversion() { - XCTAssertEqual(FetchRequest.Predicate.Comparison.Modifier.all.toFoundation(), .all) - XCTAssertEqual(FetchRequest.Predicate.Comparison.Modifier.any.toFoundation(), .any) + @Test func comparisonModifierConversion() throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } + #expect(FetchRequest.Predicate.Comparison.Modifier.all.toFoundation() == .all) + #expect(FetchRequest.Predicate.Comparison.Modifier.any.toFoundation() == .any) } - func testFunctionSortDescriptorNotConvertible() { + @Test func functionSortDescriptorNotConvertible() throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } // function-based sort terms can't be represented in NSFetchRequest and are dropped let request = FetchRequest( entity: "Person", @@ -68,29 +81,35 @@ final class CoreDataModelTests: XCTestCase { ) let sortDescriptors = request.toFoundation().sortDescriptors ?? [] // only the property sort plus the built-in id tiebreaker survive - XCTAssertEqual(sortDescriptors.count, 2) - XCTAssertEqual(sortDescriptors.first?.key, "name") + #expect(sortDescriptors.count == 2) + #expect(sortDescriptors.first?.key == "name") } - func testContextModelStorageInsert() throws { + @Test func contextModelStorageInsert() throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let context = try Self.makeContext() let person = Person(name: "Alice", age: 30) // single insert through the ModelStorage conformance try context.insert(person.encode()) - XCTAssertEqual(try context.count(FetchRequest(entity: Person.entityName)), 1) + #expect(try context.count(FetchRequest(entity: Person.entityName)) == 1) // batch insert through the ModelStorage conformance let more = [Person(name: "Bob", age: 25), Person(name: "Charlie", age: 35)] try context.insert(more.map { try! $0.encode() }) - XCTAssertEqual(try context.count(FetchRequest(entity: Person.entityName)), 3) + #expect(try context.count(FetchRequest(entity: Person.entityName)) == 3) // single delete try context.delete(Person.entityName, for: ObjectID(person.id)) - XCTAssertEqual(try context.count(FetchRequest(entity: Person.entityName)), 2) + #expect(try context.count(FetchRequest(entity: Person.entityName)) == 2) // deleting a missing object is a no-op try context.delete(Person.entityName, for: ObjectID(UUID())) - XCTAssertEqual(try context.count(FetchRequest(entity: Person.entityName)), 2) + #expect(try context.count(FetchRequest(entity: Person.entityName)) == 2) } - func testContextInMemoryFetchID() throws { + @Test func contextInMemoryFetchID() throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let context = try Self.makeContext() try context.register(function: DatabaseFunction(name: "lower", argumentCount: 1) { arguments in guard case let .string(value) = arguments[0] else { return nil } @@ -103,7 +122,7 @@ final class CoreDataModelTests: XCTestCase { entity: Person.entityName, predicate: lower.compare(.equalTo, .attribute(.string("alice"))) ) - XCTAssertEqual(try context.fetchID(request), [ObjectID(person.id)]) + #expect(try context.fetchID(request) == [ObjectID(person.id)]) // in-memory path with limit and offset try context.insert(Person(name: "alina", age: 20).encode()) let paged = FetchRequest( @@ -114,11 +133,14 @@ final class CoreDataModelTests: XCTestCase { fetchOffset: 1 ) let results = try context.fetch(paged) - XCTAssertEqual(results.count, 1) - XCTAssertEqual(results[0].attributes["name"], .string("alina")) + #expect(results.count == 1) + #expect(results[0].attributes["name"] == .string("alina")) } - func testNullRelationshipInsert() throws { + @Test func nullRelationshipInsert() throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let context = try Self.makeContext() var data = Person(name: "Loner", age: 40).encode() data.relationships[PropertyKey(Person.CodingKeys.events)] = .null @@ -126,11 +148,14 @@ final class CoreDataModelTests: XCTestCase { try context.insert([data]) let fetched = try context.fetch(Person.entityName, for: data.id) // CoreData represents an empty to-many relationship as an empty set - XCTAssertEqual(fetched?.relationships[PropertyKey(Person.CodingKeys.events)], .toMany([])) + #expect(fetched?.relationships[PropertyKey(Person.CodingKeys.events)] == .toMany([])) } @MainActor - func testManagedObjectViewContextObservation() throws { + @Test func managedObjectViewContextObservation() async throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let context = try Self.makeContext() let viewContext = ManagedObjectViewContext(context: context) var changes = 0 @@ -138,15 +163,23 @@ final class CoreDataModelTests: XCTestCase { defer { cancellable.cancel() } // mutate the observed context to trigger change and save notifications try context.insert(Person(name: "Alice", age: 30).encode()) - RunLoop.main.run(until: Date().addingTimeInterval(0.1)) - XCTAssertGreaterThan(changes, 0) + // - Note: `RunLoop.main.run(until:)` pumped the real main run loop under XCTest's + // synchronous main-thread execution; Swift Testing's concurrency model doesn't + // guarantee the same thing, so poll briefly instead of blocking on it. + for _ in 0..<20 where changes == 0 { + try await Task.sleep(for: .milliseconds(10)) + } + #expect(changes > 0) // ViewContext conformance - XCTAssertEqual(try viewContext.count(FetchRequest(entity: Person.entityName)), 1) - XCTAssertEqual(try viewContext.fetchID(FetchRequest(entity: Person.entityName)).count, 1) - XCTAssertEqual(try viewContext.fetch(FetchRequest(entity: Person.entityName)).count, 1) + #expect(try viewContext.count(FetchRequest(entity: Person.entityName)) == 1) + #expect(try viewContext.fetchID(FetchRequest(entity: Person.entityName)).count == 1) + #expect(try viewContext.fetch(FetchRequest(entity: Person.entityName)).count == 1) } - func testPersistentContainerFetchAndDelete() async throws { + @Test func persistentContainerFetchAndDelete() async throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let model = Model(entities: Person.self, Event.self) let container = NSPersistentContainer( name: "Test\(UUID())", @@ -157,14 +190,17 @@ final class CoreDataModelTests: XCTestCase { try await container.insert(person.encode()) // fetch with a fetch request let results = try await container.fetch(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(results.count, 1) + #expect(results.count == 1) // delete a single object try await container.delete(Person.entityName, for: ObjectID(person.id)) let remaining = try await container.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(remaining, 0) + #expect(remaining == 0) } - func testStorageLoadFailure() async throws { + @Test func storageLoadFailure() async throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } // a store URL inside a nonexistent directory fails to load let description = NSPersistentStoreDescription( url: URL(fileURLWithPath: "/nonexistent-\(UUID())/store.sqlite") @@ -178,7 +214,7 @@ final class CoreDataModelTests: XCTestCase { ) do { _ = try await failing.fetch(FetchRequest(entity: Person.entityName)) - XCTFail("Expected store load to fail") + Issue.record("Expected store load to fail") } catch { // expected } @@ -187,7 +223,10 @@ final class CoreDataModelTests: XCTestCase { } @MainActor - func testViewContextLoadFailure() throws { + @Test func viewContextLoadFailure() throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let description = NSPersistentStoreDescription( url: URL(fileURLWithPath: "/nonexistent-\(UUID())/store.sqlite") ) @@ -201,7 +240,7 @@ final class CoreDataModelTests: XCTestCase { // fetches simply return no results against the unloaded store let viewContext = try failing.viewContext let results = try? viewContext.fetch(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(results ?? [], []) + #expect(results ?? [] == []) } } diff --git a/Tests/CoreModelTests/FunctionEvaluationTests.swift b/Tests/CoreModelTests/FunctionEvaluationTests.swift index f2d131c..4d662af 100644 --- a/Tests/CoreModelTests/FunctionEvaluationTests.swift +++ b/Tests/CoreModelTests/FunctionEvaluationTests.swift @@ -8,11 +8,11 @@ #if canImport(CoreData) import Foundation -import XCTest +import Testing @testable import CoreModel @testable import CoreDataModel -final class FunctionEvaluationTests: XCTestCase { +@Suite struct FunctionEvaluationTests { typealias Predicate = FetchRequest.Predicate @@ -35,77 +35,77 @@ final class FunctionEvaluationTests: XCTestCase { ) } - func testRequiresInMemoryEvaluation() { + @Test func requiresInMemoryEvaluation() { let native = FetchRequest(entity: "Person", predicate: "name".compare(.equalTo, .attribute(.string("x")))) - XCTAssertFalse(native.requiresInMemoryEvaluation) + #expect(native.requiresInMemoryEvaluation == false) let functionPredicate = FetchRequest( entity: "Person", predicate: Self.functionExpression.compare(.equalTo, .attribute(.string("x"))) ) - XCTAssert(functionPredicate.requiresInMemoryEvaluation) + #expect(functionPredicate.requiresInMemoryEvaluation) let functionSort = FetchRequest( entity: "Person", sortDescriptors: [.init(term: .function(.init(name: "lowercase", arguments: [.keyPath("name")])), ascending: true)] ) - XCTAssert(functionSort.requiresInMemoryEvaluation) + #expect(functionSort.requiresInMemoryEvaluation) } - func testContainsFunction() { - XCTAssertFalse(Predicate.value(true).containsFunction) - XCTAssertFalse("name".compare(.equalTo, .attribute(.string("x"))).containsFunction) - XCTAssert(Self.functionExpression.compare(.equalTo, .attribute(.string("x"))).containsFunction) + @Test func containsFunction() { + #expect(Predicate.value(true).containsFunction == false) + #expect("name".compare(.equalTo, .attribute(.string("x"))).containsFunction == false) + #expect(Self.functionExpression.compare(.equalTo, .attribute(.string("x"))).containsFunction) // function on the right side - XCTAssert(Predicate.Expression.keyPath("name").compare(.equalTo, Self.functionExpression).containsFunction) - XCTAssert(Predicate.compound(.and([.value(true), Self.functionExpression.compare(.equalTo, .attribute(.null))])).containsFunction) - XCTAssertFalse(Predicate.compound(.or([.value(false)])).containsFunction) + #expect(Predicate.Expression.keyPath("name").compare(.equalTo, Self.functionExpression).containsFunction) + #expect(Predicate.compound(.and([.value(true), Self.functionExpression.compare(.equalTo, .attribute(.null))])).containsFunction) + #expect(Predicate.compound(.or([.value(false)])).containsFunction == false) } - func testStrippingFunctionComparisons() { + @Test func strippingFunctionComparisons() { let function = Self.functionExpression.compare(.equalTo, .attribute(.string("x"))) let native = "name".compare(.equalTo, .attribute(.string("x"))) - XCTAssertEqual(Predicate.value(false).strippingFunctionComparisons(), .value(false)) - XCTAssertEqual(native.strippingFunctionComparisons(), native) - XCTAssertEqual(function.strippingFunctionComparisons(), .value(true)) - XCTAssertEqual( - Predicate.compound(.and([native, function])).strippingFunctionComparisons(), - .compound(.and([native, .value(true)])) + #expect(Predicate.value(false).strippingFunctionComparisons() == .value(false)) + #expect(native.strippingFunctionComparisons() == native) + #expect(function.strippingFunctionComparisons() == .value(true)) + #expect( + Predicate.compound(.and([native, function])).strippingFunctionComparisons() + == .compound(.and([native, .value(true)])) ) - XCTAssertEqual( - Predicate.compound(.or([function])).strippingFunctionComparisons(), - .compound(.or([.value(true)])) + #expect( + Predicate.compound(.or([function])).strippingFunctionComparisons() + == .compound(.or([.value(true)])) ) - XCTAssertEqual( - Predicate.compound(.not(function)).strippingFunctionComparisons(), - .compound(.not(.value(true))) + #expect( + Predicate.compound(.not(function)).strippingFunctionComparisons() + == .compound(.not(.value(true))) ) } - func testPredicateEvaluation() { + @Test func predicateEvaluation() { let data = Self.makeData() - XCTAssert(Predicate.value(true).evaluate(with: data, functions: [:])) - XCTAssertFalse(Predicate.value(false).evaluate(with: data, functions: [:])) + #expect(Predicate.value(true).evaluate(with: data, functions: [:])) + #expect(Predicate.value(false).evaluate(with: data, functions: [:]) == false) let isAlice = Self.functionExpression.compare(.equalTo, .attribute(.string("alice"))) - XCTAssert(isAlice.evaluate(with: data, functions: Self.functions)) + #expect(isAlice.evaluate(with: data, functions: Self.functions)) // compound evaluation - XCTAssert(Predicate.compound(.and([.value(true), isAlice])).evaluate(with: data, functions: Self.functions)) - XCTAssertFalse(Predicate.compound(.and([.value(false), isAlice])).evaluate(with: data, functions: Self.functions)) - XCTAssert(Predicate.compound(.or([.value(false), isAlice])).evaluate(with: data, functions: Self.functions)) - XCTAssertFalse(Predicate.compound(.not(isAlice)).evaluate(with: data, functions: Self.functions)) + #expect(Predicate.compound(.and([.value(true), isAlice])).evaluate(with: data, functions: Self.functions)) + #expect(Predicate.compound(.and([.value(false), isAlice])).evaluate(with: data, functions: Self.functions) == false) + #expect(Predicate.compound(.or([.value(false), isAlice])).evaluate(with: data, functions: Self.functions)) + #expect(Predicate.compound(.not(isAlice)).evaluate(with: data, functions: Self.functions) == false) } - func testExpressionEvaluation() { + @Test func expressionEvaluation() { let data = Self.makeData() - XCTAssertEqual(Predicate.Expression.attribute(.int64(1)).evaluate(with: data, functions: [:]), .attribute(.int64(1))) - XCTAssertEqual(Predicate.Expression.keyPath("name").evaluate(with: data, functions: [:]), .attribute(.string("Alice"))) - XCTAssertNil(Predicate.Expression.keyPath("missing").evaluate(with: data, functions: [:])) - XCTAssertEqual(Self.functionExpression.evaluate(with: data, functions: Self.functions), .attribute(.string("alice"))) + #expect(Predicate.Expression.attribute(.int64(1)).evaluate(with: data, functions: [:]) == .attribute(.int64(1))) + #expect(Predicate.Expression.keyPath("name").evaluate(with: data, functions: [:]) == .attribute(.string("Alice"))) + #expect(Predicate.Expression.keyPath("missing").evaluate(with: data, functions: [:]) == nil) + #expect(Self.functionExpression.evaluate(with: data, functions: Self.functions) == .attribute(.string("alice"))) // unregistered function - XCTAssertNil(Self.functionExpression.evaluate(with: data, functions: [:])) + #expect(Self.functionExpression.evaluate(with: data, functions: [:]) == nil) // relationship expressions resolve to relationship values - XCTAssertEqual(Predicate.Expression.relationship(.toOne("x")).evaluate(with: data, functions: [:]), .relationship(.toOne("x"))) + #expect(Predicate.Expression.relationship(.toOne("x")).evaluate(with: data, functions: [:]) == .relationship(.toOne("x"))) } - func testOperatorEvaluation() { + @Test func operatorEvaluation() { let data = Self.makeData() func evaluate( _ type: Predicate.Comparison.Operator, @@ -119,79 +119,79 @@ final class FunctionEvaluationTests: XCTestCase { let name = Predicate.Expression.keyPath("name") let age = Predicate.Expression.keyPath("age") // equality - XCTAssert(evaluate(.equalTo, name, .attribute(.string("Alice")))) - XCTAssert(evaluate(.equalTo, name, .attribute(.string("ALICE")), options: [.caseInsensitive])) - XCTAssertFalse(evaluate(.equalTo, name, .attribute(.string("ALICE")))) - XCTAssert(evaluate(.notEqualTo, name, .attribute(.string("Bob")))) + #expect(evaluate(.equalTo, name, .attribute(.string("Alice")))) + #expect(evaluate(.equalTo, name, .attribute(.string("ALICE")), options: [.caseInsensitive])) + #expect(evaluate(.equalTo, name, .attribute(.string("ALICE"))) == false) + #expect(evaluate(.notEqualTo, name, .attribute(.string("Bob")))) // null equality - XCTAssert(evaluate(.equalTo, .attribute(.null), .attribute(.null))) - XCTAssert(evaluate(.equalTo, .keyPath("missing"), .attribute(.null))) - XCTAssertFalse(evaluate(.equalTo, name, .attribute(.null))) - XCTAssertFalse(evaluate(.equalTo, name, .keyPath("missing"))) + #expect(evaluate(.equalTo, .attribute(.null), .attribute(.null))) + #expect(evaluate(.equalTo, .keyPath("missing"), .attribute(.null))) + #expect(evaluate(.equalTo, name, .attribute(.null)) == false) + #expect(evaluate(.equalTo, name, .keyPath("missing")) == false) // ordering (numeric) - XCTAssert(evaluate(.lessThan, age, .attribute(.int64(40)))) - XCTAssertFalse(evaluate(.lessThan, age, .attribute(.int64(30)))) - XCTAssert(evaluate(.lessThanOrEqualTo, age, .attribute(.int64(30)))) - XCTAssert(evaluate(.greaterThan, age, .attribute(.int64(20)))) - XCTAssert(evaluate(.greaterThanOrEqualTo, age, .attribute(.int64(30)))) + #expect(evaluate(.lessThan, age, .attribute(.int64(40)))) + #expect(evaluate(.lessThan, age, .attribute(.int64(30))) == false) + #expect(evaluate(.lessThanOrEqualTo, age, .attribute(.int64(30)))) + #expect(evaluate(.greaterThan, age, .attribute(.int64(20)))) + #expect(evaluate(.greaterThanOrEqualTo, age, .attribute(.int64(30)))) // ordering with mixed numeric types - XCTAssert(evaluate(.lessThan, age, .attribute(.double(30.5)))) - XCTAssert(evaluate(.greaterThan, age, .attribute(.float(29.5)))) - XCTAssert(evaluate(.greaterThan, age, .attribute(.int16(29)))) - XCTAssert(evaluate(.lessThan, age, .attribute(.int32(31)))) - XCTAssert(evaluate(.greaterThan, age, .attribute(.bool(true)))) - XCTAssert(evaluate(.lessThan, age, .attribute(.decimal(Decimal(50))))) + #expect(evaluate(.lessThan, age, .attribute(.double(30.5)))) + #expect(evaluate(.greaterThan, age, .attribute(.float(29.5)))) + #expect(evaluate(.greaterThan, age, .attribute(.int16(29)))) + #expect(evaluate(.lessThan, age, .attribute(.int32(31)))) + #expect(evaluate(.greaterThan, age, .attribute(.bool(true)))) + #expect(evaluate(.lessThan, age, .attribute(.decimal(Decimal(50))))) // date ordering - XCTAssert(evaluate(.lessThan, .attribute(.date(Date(timeIntervalSinceReferenceDate: 0))), .attribute(.date(Date(timeIntervalSinceReferenceDate: 100))))) + #expect(evaluate(.lessThan, .attribute(.date(Date(timeIntervalSinceReferenceDate: 0))), .attribute(.date(Date(timeIntervalSinceReferenceDate: 100))))) // string ordering - XCTAssert(evaluate(.lessThan, name, .attribute(.string("Bob")))) - XCTAssertFalse(evaluate(.greaterThan, name, .attribute(.string("Bob")))) + #expect(evaluate(.lessThan, name, .attribute(.string("Bob")))) + #expect(evaluate(.greaterThan, name, .attribute(.string("Bob"))) == false) // non-comparable ordering - XCTAssertFalse(evaluate(.lessThan, name, .attribute(.int64(1)))) - XCTAssertFalse(evaluate(.lessThan, .attribute(.null), age)) + #expect(evaluate(.lessThan, name, .attribute(.int64(1))) == false) + #expect(evaluate(.lessThan, .attribute(.null), age) == false) // string operators - XCTAssert(evaluate(.beginsWith, name, .attribute(.string("Al")))) - XCTAssert(evaluate(.beginsWith, name, .attribute(.string("AL")), options: [.caseInsensitive])) - XCTAssert(evaluate(.endsWith, name, .attribute(.string("ice")))) - XCTAssert(evaluate(.contains, name, .attribute(.string("lic")))) - XCTAssertFalse(evaluate(.contains, name, .attribute(.string("bob")))) - XCTAssertFalse(evaluate(.contains, age, .attribute(.string("3")))) + #expect(evaluate(.beginsWith, name, .attribute(.string("Al")))) + #expect(evaluate(.beginsWith, name, .attribute(.string("AL")), options: [.caseInsensitive])) + #expect(evaluate(.endsWith, name, .attribute(.string("ice")))) + #expect(evaluate(.contains, name, .attribute(.string("lic")))) + #expect(evaluate(.contains, name, .attribute(.string("bob"))) == false) + #expect(evaluate(.contains, age, .attribute(.string("3"))) == false) // like / matches - XCTAssert(evaluate(.like, name, .attribute(.string("A*e")))) - XCTAssert(evaluate(.like, name, .attribute(.string("Alic?")))) - XCTAssertFalse(evaluate(.like, name, .attribute(.string("B*")))) - XCTAssert(evaluate(.matches, name, .attribute(.string("^A[a-z]+e$")))) - XCTAssertFalse(evaluate(.matches, name, .attribute(.string("^[0-9]+$")))) + #expect(evaluate(.like, name, .attribute(.string("A*e")))) + #expect(evaluate(.like, name, .attribute(.string("Alic?")))) + #expect(evaluate(.like, name, .attribute(.string("B*"))) == false) + #expect(evaluate(.matches, name, .attribute(.string("^A[a-z]+e$")))) + #expect(evaluate(.matches, name, .attribute(.string("^[0-9]+$"))) == false) // IN: left hand side is a substring of the right hand side - XCTAssert(evaluate(.in, name, .attribute(.string("Alice in Wonderland")))) - XCTAssertFalse(evaluate(.in, name, .attribute(.string("Bob")))) + #expect(evaluate(.in, name, .attribute(.string("Alice in Wonderland")))) + #expect(evaluate(.in, name, .attribute(.string("Bob"))) == false) // BETWEEN bounds aren't representable as a single expression value - XCTAssertFalse(evaluate(.between, age, .attribute(.int64(50)))) + #expect(evaluate(.between, age, .attribute(.int64(50))) == false) } - func testSortedInMemory() { + @Test func sortedInMemory() { let people = [ Self.makeData(name: "Charlie", age: 35, id: "3"), Self.makeData(name: "alice", age: 30, id: "1"), Self.makeData(name: "Bob", age: 30, id: "2") ] // no descriptors sorts by identifier - XCTAssertEqual(people.sorted(by: [], functions: [:]).map { $0.id.rawValue }, ["1", "2", "3"]) + #expect(people.sorted(by: [], functions: [:]).map { $0.id.rawValue } == ["1", "2", "3"]) // property ascending let byAge = people.sorted(by: [.init(property: "age", ascending: true)], functions: [:]) - XCTAssertEqual(byAge.map { $0.id.rawValue }, ["1", "2", "3"]) + #expect(byAge.map { $0.id.rawValue } == ["1", "2", "3"]) // property descending let byAgeDesc = people.sorted(by: [.init(property: "age", ascending: false)], functions: [:]) - XCTAssertEqual(byAgeDesc.first?.id.rawValue, "3") + #expect(byAgeDesc.first?.id.rawValue == "3") // function term (case-insensitive name order) let byName = people.sorted( by: [.init(term: .function(.init(name: "lowercase", arguments: [.keyPath("name")])), ascending: true)], functions: Self.functions ) - XCTAssertEqual(byName.map { $0.id.rawValue }, ["1", "2", "3"]) + #expect(byName.map { $0.id.rawValue } == ["1", "2", "3"]) // ties fall back to id ordering let tied = people.sorted(by: [.init(property: "missing", ascending: true)], functions: [:]) - XCTAssertEqual(tied.map { $0.id.rawValue }, ["1", "2", "3"]) + #expect(tied.map { $0.id.rawValue } == ["1", "2", "3"]) } } diff --git a/Tests/CoreModelTests/InMemoryStoreTests.swift b/Tests/CoreModelTests/InMemoryStoreTests.swift index d568f93..d5ed7a0 100644 --- a/Tests/CoreModelTests/InMemoryStoreTests.swift +++ b/Tests/CoreModelTests/InMemoryStoreTests.swift @@ -6,48 +6,48 @@ // import Foundation -import XCTest +import Testing @testable import CoreModel -final class InMemoryStoreTests: XCTestCase { +@Suite struct InMemoryStoreTests { static let model = Model(entities: [ EntityDescription(entity: Person.self), EntityDescription(entity: Event.self) ]) - func testInsertAndFetch() async throws { + @Test func insertAndFetch() async throws { let store = InMemoryModelStorage(model: Self.model) let person = Person(name: "Alice", age: 30) try await store.insert(person) let fetched = try await store.fetch(Person.self, for: person.id) - XCTAssertEqual(fetched, person) + #expect(fetched == person) // fetching an unknown identifier returns nil let missing = try await store.fetch(Person.self, for: UUID()) - XCTAssertNil(missing) + #expect(missing == nil) } - func testUpdate() async throws { + @Test func update() async throws { let store = InMemoryModelStorage(model: Self.model) var person = Person(name: "Alice", age: 30) try await store.insert(person) person.age = 31 try await store.insert(person) let fetched = try await store.fetch(Person.self, for: person.id) - XCTAssertEqual(fetched?.age, 31) + #expect(fetched?.age == 31) let count = try await store.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 1) + #expect(count == 1) } - func testBatchInsert() async throws { + @Test func batchInsert() async throws { let store = InMemoryModelStorage(model: Self.model) let people = (1...10).map { Person(name: "Person \($0)", age: UInt(20 + $0)) } try await store.insert(people.map { try $0.encode() }) let count = try await store.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 10) + #expect(count == 10) } - func testFetchRequest() async throws { + @Test func fetchRequest() async throws { let store = InMemoryModelStorage(model: Self.model) let people = (1...5).map { Person(name: "Person \($0)", age: UInt(20 + $0)) } try await store.insert(people.map { try $0.encode() }) @@ -56,14 +56,14 @@ final class InMemoryStoreTests: XCTestCase { Person.self, predicate: Person.CodingKeys.age > 22 ) - XCTAssertEqual(adults.count, 3) - XCTAssert(adults.allSatisfy { $0.age > 22 }) + #expect(adults.count == 3) + #expect(adults.allSatisfy { $0.age > 22 }) // sorting let sorted: [Person] = try await store.fetch( Person.self, sortDescriptors: [.init(property: "age", ascending: false)] ) - XCTAssertEqual(sorted.map { $0.age }, [25, 24, 23, 22, 21]) + #expect(sorted.map { $0.age } == [25, 24, 23, 22, 21]) // limit and offset let page: [Person] = try await store.fetch( Person.self, @@ -71,34 +71,34 @@ final class InMemoryStoreTests: XCTestCase { fetchLimit: 2, fetchOffset: 1 ) - XCTAssertEqual(page.map { $0.age }, [22, 23]) + #expect(page.map { $0.age } == [22, 23]) // count with predicate let count = try await store.count(Person.self, predicate: Person.CodingKeys.age <= 22) - XCTAssertEqual(count, 2) + #expect(count == 2) } - func testFetchID() async throws { + @Test func fetchID() async throws { let store = InMemoryModelStorage(model: Self.model) let person = Person(name: "Alice", age: 30) try await store.insert(person) let ids = try await store.fetchID(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(ids, [ObjectID(person.id)]) + #expect(ids == [ObjectID(person.id)]) } - func testDelete() async throws { + @Test func delete() async throws { let store = InMemoryModelStorage(model: Self.model) let people = (1...3).map { Person(name: "Person \($0)", age: UInt(20 + $0)) } try await store.insert(people.map { try $0.encode() }) try await store.delete(Person.self, for: people[0].id) var count = try await store.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 2) + #expect(count == 2) // batch delete try await store.delete(Person.entityName, for: people.map { ObjectID($0.id) }) count = try await store.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 0) + #expect(count == 0) } - func testRelationshipPredicate() async throws { + @Test func relationshipPredicate() async throws { let store = InMemoryModelStorage(model: Self.model) let event = Event(name: "WWDC", date: Date()) let attendee = Person(name: "Alice", age: 30, events: [event.id]) @@ -108,10 +108,10 @@ final class InMemoryStoreTests: XCTestCase { Person.self, predicate: Person.CodingKeys.events.compare(.contains, .attribute(.string(event.id.uuidString))) ) - XCTAssertEqual(attendees, [attendee]) + #expect(attendees == [attendee]) } - func testCustomFunction() async throws { + @Test func customFunction() async throws { let store = InMemoryModelStorage(model: Self.model) let stringLength = DatabaseFunction(name: "LENGTH", argumentCount: 1) { arguments in guard case let .string(value)? = arguments.first ?? nil else { return nil } @@ -133,21 +133,21 @@ final class InMemoryStoreTests: XCTestCase { ) ) ) - XCTAssertEqual(longNames.map { $0.name }, ["Alexandra"]) + #expect(longNames.map { $0.name } == ["Alexandra"]) } - func testModelValidation() async throws { + @Test func modelValidation() async throws { let store = InMemoryModelStorage(model: Self.model) let person = Person(name: "Alice", age: 30) try await store.insert(person) let count = try await store.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 1) + #expect(count == 1) // unknown entities are rejected do { _ = try await store.fetch(FetchRequest(entity: "Unknown")) - XCTFail("Expected an error") + Issue.record("Expected an error") } catch CoreModelError.invalidEntity(let entity) { - XCTAssertEqual(entity, "Unknown") + #expect(entity == "Unknown") } } } diff --git a/Tests/CoreModelTests/InMemoryViewContextTests.swift b/Tests/CoreModelTests/InMemoryViewContextTests.swift index 091d2a1..5740177 100644 --- a/Tests/CoreModelTests/InMemoryViewContextTests.swift +++ b/Tests/CoreModelTests/InMemoryViewContextTests.swift @@ -6,49 +6,49 @@ // import Foundation -import XCTest +import Testing @testable import CoreModel @MainActor -final class InMemoryViewContextTests: XCTestCase { +@Suite struct InMemoryViewContextTests { static let model = Model(entities: [ EntityDescription(entity: Person.self), EntityDescription(entity: Event.self) ]) - func testInsertAndFetch() throws { + @Test func insertAndFetch() throws { let context = InMemoryViewContext(model: Self.model) let person = Person(name: "Alice", age: 30) try context.insert(person.encode()) let fetched = try context.fetch(Person.self, for: person.id) - XCTAssertEqual(fetched, person) + #expect(fetched == person) // fetching an unknown identifier returns nil let missing = try context.fetch(Person.self, for: UUID()) - XCTAssertNil(missing) + #expect(missing == nil) } - func testUpdate() throws { + @Test func update() throws { let context = InMemoryViewContext(model: Self.model) var person = Person(name: "Alice", age: 30) try context.insert(person.encode()) person.age = 31 try context.insert(person.encode()) let fetched = try context.fetch(Person.self, for: person.id) - XCTAssertEqual(fetched?.age, 31) + #expect(fetched?.age == 31) let count = try context.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 1) + #expect(count == 1) } - func testBatchInsert() throws { + @Test func batchInsert() throws { let context = InMemoryViewContext(model: Self.model) let people = (1...10).map { Person(name: "Person \($0)", age: UInt(20 + $0)) } try context.insert(people.map { try $0.encode() }) let count = try context.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 10) + #expect(count == 10) } - func testFetchRequest() throws { + @Test func fetchRequest() throws { let context = InMemoryViewContext(model: Self.model) let people = (1...5).map { Person(name: "Person \($0)", age: UInt(20 + $0)) } try context.insert(people.map { try $0.encode() }) @@ -57,14 +57,14 @@ final class InMemoryViewContextTests: XCTestCase { Person.self, predicate: Person.CodingKeys.age > 22 ) - XCTAssertEqual(adults.count, 3) - XCTAssert(adults.allSatisfy { $0.age > 22 }) + #expect(adults.count == 3) + #expect(adults.allSatisfy { $0.age > 22 }) // sorting let sorted: [Person] = try context.fetch( Person.self, sortDescriptors: [.init(property: "age", ascending: false)] ) - XCTAssertEqual(sorted.map { $0.age }, [25, 24, 23, 22, 21]) + #expect(sorted.map { $0.age } == [25, 24, 23, 22, 21]) // limit and offset let page: [Person] = try context.fetch( Person.self, @@ -72,34 +72,34 @@ final class InMemoryViewContextTests: XCTestCase { fetchLimit: 2, fetchOffset: 1 ) - XCTAssertEqual(page.map { $0.age }, [22, 23]) + #expect(page.map { $0.age } == [22, 23]) // count with predicate let count = try context.count(Person.self, predicate: Person.CodingKeys.age <= 22) - XCTAssertEqual(count, 2) + #expect(count == 2) } - func testFetchID() throws { + @Test func fetchID() throws { let context = InMemoryViewContext(model: Self.model) let person = Person(name: "Alice", age: 30) try context.insert(person.encode()) let ids = try context.fetchID(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(ids, [ObjectID(person.id)]) + #expect(ids == [ObjectID(person.id)]) } - func testDelete() throws { + @Test func delete() throws { let context = InMemoryViewContext(model: Self.model) let people = (1...3).map { Person(name: "Person \($0)", age: UInt(20 + $0)) } try context.insert(people.map { try $0.encode() }) try context.delete(Person.entityName, for: ObjectID(people[0].id)) var count = try context.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 2) + #expect(count == 2) // batch delete try context.delete(Person.entityName, for: people.map { ObjectID($0.id) }) count = try context.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 0) + #expect(count == 0) } - func testRelationshipPredicate() throws { + @Test func relationshipPredicate() throws { let context = InMemoryViewContext(model: Self.model) let event = Event(name: "WWDC", date: Date()) let attendee = Person(name: "Alice", age: 30, events: [event.id]) @@ -109,10 +109,10 @@ final class InMemoryViewContextTests: XCTestCase { Person.self, predicate: Person.CodingKeys.events.compare(.contains, .attribute(.string(event.id.uuidString))) ) - XCTAssertEqual(attendees, [attendee]) + #expect(attendees == [attendee]) } - func testCustomFunction() throws { + @Test func customFunction() throws { let context = InMemoryViewContext(model: Self.model) let stringLength = DatabaseFunction(name: "LENGTH", argumentCount: 1) { arguments in guard case let .string(value)? = arguments.first ?? nil else { return nil } @@ -134,42 +134,42 @@ final class InMemoryViewContextTests: XCTestCase { ) ) ) - XCTAssertEqual(longNames.map { $0.name }, ["Alexandra"]) + #expect(longNames.map { $0.name } == ["Alexandra"]) } - func testSharedDataWithStore() async throws { + @Test func sharedDataWithStore() async throws { let store = InMemoryModelStorage(model: Self.model) let context = store.viewContext // the same cached instance is returned on every access - XCTAssertTrue(context === store.viewContext) + #expect(context === store.viewContext) // objects inserted through the store are visible to the view context let alice = Person(name: "Alice", age: 30) try await store.insert(alice) - XCTAssertEqual(try context.fetch(Person.self, for: alice.id), alice) - XCTAssertEqual(try context.count(FetchRequest(entity: Person.entityName)), 1) + #expect(try context.fetch(Person.self, for: alice.id) == alice) + #expect(try context.count(FetchRequest(entity: Person.entityName)) == 1) // objects inserted through the view context are visible to the store let bob = Person(name: "Bob", age: 25) try context.insert(bob.encode()) let fetchedByStore = try await store.fetch(Person.self, for: bob.id) - XCTAssertEqual(fetchedByStore, bob) + #expect(fetchedByStore == bob) // deletes propagate as well try await store.delete(Person.self, for: alice.id) - XCTAssertNil(try context.fetch(Person.self, for: alice.id)) - XCTAssertEqual(try context.count(FetchRequest(entity: Person.entityName)), 1) + #expect(try context.fetch(Person.self, for: alice.id) == nil) + #expect(try context.count(FetchRequest(entity: Person.entityName)) == 1) } - func testModelValidation() throws { + @Test func modelValidation() throws { let context = InMemoryViewContext(model: Self.model) let person = Person(name: "Alice", age: 30) try context.insert(person.encode()) let count = try context.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 1) + #expect(count == 1) // unknown entities are rejected do { _ = try context.fetch(FetchRequest(entity: "Unknown")) - XCTFail("Expected an error") + Issue.record("Expected an error") } catch CoreModelError.invalidEntity(let entity) { - XCTAssertEqual(entity, "Unknown") + #expect(entity == "Unknown") } } } diff --git a/Tests/CoreModelTests/ModelTests.swift b/Tests/CoreModelTests/ModelTests.swift index e3bc376..e2c5386 100644 --- a/Tests/CoreModelTests/ModelTests.swift +++ b/Tests/CoreModelTests/ModelTests.swift @@ -6,47 +6,47 @@ // import Foundation -import XCTest +import Testing @testable import CoreModel #if canImport(CoreData) @testable import CoreDataModel #endif -final class ModelTests: XCTestCase { +@Suite struct ModelTests { - func testModel() throws { + @Test func model() throws { let model = Model(entities: Person.self, Event.self) - XCTAssertEqual(model.entities.count, 2) + #expect(model.entities.count == 2) // subscript - XCTAssertNotNil(model[Person.entityName]) - XCTAssertNotNil(model["Event"]) - XCTAssertNil(model["Missing"]) + #expect(model[Person.entityName] != nil) + #expect(model["Event"] != nil) + #expect(model["Missing"] == nil) // codable round trip let data = try JSONEncoder().encode(model) let decoded = try JSONDecoder().decode(Model.self, from: data) - XCTAssertEqual(decoded, model) + #expect(decoded == model) } - func testEntityName() throws { + @Test func entityName() throws { let name: EntityName = "Person" - XCTAssertEqual(name.rawValue, "Person") - XCTAssertEqual(name.description, "Person") - XCTAssertEqual(name.debugDescription, "Person") + #expect(name.rawValue == "Person") + #expect(name.description == "Person") + #expect(name.debugDescription == "Person") let data = try JSONEncoder().encode(name) - XCTAssertEqual(try JSONDecoder().decode(EntityName.self, from: data), name) + #expect(try JSONDecoder().decode(EntityName.self, from: data) == name) } - func testPropertyKey() throws { + @Test func propertyKey() throws { let key: PropertyKey = "name" - XCTAssertEqual(key.rawValue, "name") - XCTAssertEqual(key.description, "name") - XCTAssertEqual(key.debugDescription, "name") - XCTAssertEqual(PropertyKey(Person.CodingKeys.name), key) + #expect(key.rawValue == "name") + #expect(key.description == "name") + #expect(key.debugDescription == "name") + #expect(PropertyKey(Person.CodingKeys.name) == key) let data = try JSONEncoder().encode(key) - XCTAssertEqual(try JSONDecoder().decode(PropertyKey.self, from: data), key) + #expect(try JSONDecoder().decode(PropertyKey.self, from: data) == key) } - func testEntityDefaultImplementations() { + @Test func entityDefaultImplementations() { // entity with no attributes or relationships uses protocol defaults struct Empty: Entity { typealias ID = UUID @@ -64,34 +64,34 @@ final class ModelTests: XCTestCase { ModelData(entity: Self.entityName, id: ObjectID(id)) } } - XCTAssertEqual(Empty.entityName.rawValue, "Empty") - XCTAssertEqual(Empty.attributes, [:]) - XCTAssertEqual(Empty.relationships, [:]) + #expect(Empty.entityName.rawValue == "Empty") + #expect(Empty.attributes == [:]) + #expect(Empty.relationships == [:]) let description = EntityDescription(entity: Empty.self) - XCTAssertEqual(description.id, Empty.entityName) - XCTAssertEqual(description.attributes, []) - XCTAssertEqual(description.relationships, []) + #expect(description.id == Empty.entityName) + #expect(description.attributes == []) + #expect(description.relationships == []) } - func testModelDataCodable() throws { + @Test func modelDataCodable() throws { var data = ModelData(entity: "Person", id: "1") data.encode("Alice", forKey: PredicateCodingTests.Key.name) data.encodeRelationship([UUID()], forKey: PredicateCodingTests.Key.age) let encoded = try JSONEncoder().encode(data) let decoded = try JSONDecoder().decode(ModelData.self, from: encoded) - XCTAssertEqual(decoded, data) + #expect(decoded == data) } #if canImport(CoreData) - func testNSNumberConversion() { - XCTAssertEqual(NSNumber(value: .bool(true)), NSNumber(value: true)) - XCTAssertEqual(NSNumber(value: .int16(16)), NSNumber(value: Int16(16))) - XCTAssertEqual(NSNumber(value: .int32(32)), NSNumber(value: Int32(32))) - XCTAssertEqual(NSNumber(value: .int64(64)), NSNumber(value: Int64(64))) - XCTAssertEqual(NSNumber(value: .float(1.5)), NSNumber(value: Float(1.5))) - XCTAssertEqual(NSNumber(value: .double(2.5)), NSNumber(value: Double(2.5))) - XCTAssertNil(NSNumber(value: .string("x"))) - XCTAssertNil(NSNumber(value: .null)) + @Test func nsNumberConversion() { + #expect(NSNumber(value: .bool(true)) == NSNumber(value: true)) + #expect(NSNumber(value: .int16(16)) == NSNumber(value: Int16(16))) + #expect(NSNumber(value: .int32(32)) == NSNumber(value: Int32(32))) + #expect(NSNumber(value: .int64(64)) == NSNumber(value: Int64(64))) + #expect(NSNumber(value: .float(1.5)) == NSNumber(value: Float(1.5))) + #expect(NSNumber(value: .double(2.5)) == NSNumber(value: Double(2.5))) + #expect(NSNumber(value: .string("x")) == nil) + #expect(NSNumber(value: .null) == nil) } #endif } diff --git a/Tests/CoreModelTests/PersistentStorageTests.swift b/Tests/CoreModelTests/PersistentStorageTests.swift index 2128aad..7d84903 100644 --- a/Tests/CoreModelTests/PersistentStorageTests.swift +++ b/Tests/CoreModelTests/PersistentStorageTests.swift @@ -9,7 +9,7 @@ import Foundation import CoreData -import XCTest +import Testing @testable import CoreModel @testable import CoreDataModel @@ -108,8 +108,10 @@ struct AllTypes: Equatable, Hashable, Codable, Identifiable { } } -@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -final class PersistentStorageTests: XCTestCase { +// - Note: `@Test`/`@Suite` can't be combined with a declaration-level `@available` — see +// CoreDataModelTests.swift for the same note. Each test guards its body with a runtime +// `if #available` instead. +@Suite struct PersistentStorageTests { static func makeStorage(model: Model = Model(entities: Person.self, Event.self, AllTypes.self)) -> PersistentContainerStorage { let description = NSPersistentStoreDescription() @@ -140,22 +142,28 @@ final class PersistentStorageTests: XCTestCase { ) } - func testAllAttributeTypesRoundTrip() async throws { + @Test func allAttributeTypesRoundTrip() async throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let storage = Self.makeStorage() var value = Self.makeAllTypes() try await storage.insert(value) var fetched = try await storage.fetch(AllTypes.self, for: value.id) - XCTAssertEqual(fetched, value) + #expect(fetched == value) // update with non-nil optional value.optionalString = "present" value.stringValue = "updated" try await storage.insert(value) fetched = try await storage.fetch(AllTypes.self, for: value.id) - XCTAssertEqual(fetched, value) - XCTAssertEqual(fetched?.optionalString, "present") + #expect(fetched == value) + #expect(fetched?.optionalString == "present") } - func testStorageCRUD() async throws { + @Test func storageCRUD() async throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let storage = Self.makeStorage() let people = [ Person(name: "Alice", age: 30), @@ -167,20 +175,20 @@ final class PersistentStorageTests: XCTestCase { // count let fetchRequest = FetchRequest(entity: Person.entityName) let total = try await storage.count(fetchRequest) - XCTAssertEqual(total, 3) + #expect(total == 3) // typed count let typedCount = try await storage.count(Person.self) - XCTAssertEqual(typedCount, 3) + #expect(typedCount == 3) // fetchID let ids = try await storage.fetchID(fetchRequest) - XCTAssertEqual(Set(ids), Set(people.map { ObjectID($0.id) })) + #expect(Set(ids) == Set(people.map { ObjectID($0.id) })) // typed fetch with sort and predicate let sorted: [Person] = try await storage.fetch( Person.self, sortDescriptors: [.init(property: PropertyKey(Person.CodingKeys.name), ascending: false)], predicate: Person.CodingKeys.age.compare(.greaterThan, .attribute(.int16(26))) ) - XCTAssertEqual(sorted.map { $0.name }, ["Charlie", "Alice"]) + #expect(sorted.map { $0.name } == ["Charlie", "Alice"]) // fetch with limit and offset let limited = try await storage.fetch( FetchRequest( @@ -190,20 +198,23 @@ final class PersistentStorageTests: XCTestCase { fetchOffset: 1 ) ) - XCTAssertEqual(limited.count, 1) - XCTAssertEqual(limited[0].attributes[PropertyKey(Person.CodingKeys.name)], .string("Bob")) + #expect(limited.count == 1) + #expect(limited[0].attributes[PropertyKey(Person.CodingKeys.name)] == .string("Bob")) // fetch missing object let missing = try await storage.fetch(Person.self, for: UUID()) - XCTAssertNil(missing) + #expect(missing == nil) // typed delete try await storage.delete(Person.self, for: people[0].id) // batch delete by id try await storage.delete(Person.entityName, for: [ObjectID(people[1].id), ObjectID(people[2].id)]) let remaining = try await storage.count(fetchRequest) - XCTAssertEqual(remaining, 0) + #expect(remaining == 0) } - func testStorageCustomFunction() async throws { + @Test func storageCustomFunction() async throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let storage = Self.makeStorage() try await storage.register(function: DatabaseFunction(name: "upperName", argumentCount: 1) { arguments in guard case let .string(name) = arguments[0] else { return nil } @@ -218,11 +229,14 @@ final class PersistentStorageTests: XCTestCase { predicate: .comparison(.init(left: upperName, right: .attribute(.string("ALICE")), type: .equalTo)) ) let matches = try await storage.fetch(request) - XCTAssertEqual(matches.count, 1) + #expect(matches.count == 1) } @MainActor - func testViewContext() async throws { + @Test func viewContext() async throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let storage = Self.makeStorage() let person = Person(name: "Alice", age: 30) try await storage.insert(person) @@ -231,23 +245,26 @@ final class PersistentStorageTests: XCTestCase { _ = try storage.viewContext // typed fetch by id let fetched = try viewContext.fetch(Person.self, for: person.id) - XCTAssertEqual(fetched, person) + #expect(fetched == person) // typed fetch with predicate let all: [Person] = try viewContext.fetch( Person.self, sortDescriptors: [.init(property: PropertyKey(Person.CodingKeys.name), ascending: true)], predicate: Person.CodingKeys.name.compare(.equalTo, .attribute(.string("Alice"))) ) - XCTAssertEqual(all, [person]) + #expect(all == [person]) // typed count - XCTAssertEqual(try viewContext.count(Person.self), 1) + #expect(try viewContext.count(Person.self) == 1) // count with fetch request - XCTAssertEqual(try viewContext.count(FetchRequest(entity: Person.entityName)), 1) + #expect(try viewContext.count(FetchRequest(entity: Person.entityName)) == 1) // fetch missing - XCTAssertNil(try viewContext.fetch(Person.self, for: UUID())) + #expect(try viewContext.fetch(Person.self, for: UUID()) == nil) } - func testNSPersistentContainerStorage() async throws { + @Test func nsPersistentContainerStorage() async throws { + guard #available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) else { + return + } let model = Model(entities: Person.self, Event.self, AllTypes.self) let container = NSPersistentContainer( name: "Test\(UUID())", @@ -262,13 +279,13 @@ final class PersistentStorageTests: XCTestCase { try await container.insert(people.map { try! $0.encode() }) let fetchRequest = FetchRequest(entity: Person.entityName) let count = try await container.count(fetchRequest) - XCTAssertEqual(count, 2) + #expect(count == 2) let ids = try await container.fetchID(fetchRequest) - XCTAssertEqual(Set(ids), Set(people.map { ObjectID($0.id) })) + #expect(Set(ids) == Set(people.map { ObjectID($0.id) })) try await container.register(function: DatabaseFunction(name: "identity", argumentCount: 1) { arguments in arguments[0] }) try await container.delete(Person.entityName, for: ids) let remaining = try await container.count(fetchRequest) - XCTAssertEqual(remaining, 0) + #expect(remaining == 0) } } diff --git a/Tests/CoreModelTests/PredicateCodingTests.swift b/Tests/CoreModelTests/PredicateCodingTests.swift index a3c80e6..16aa3e8 100644 --- a/Tests/CoreModelTests/PredicateCodingTests.swift +++ b/Tests/CoreModelTests/PredicateCodingTests.swift @@ -6,10 +6,10 @@ // import Foundation -import XCTest +import Testing @testable import CoreModel -final class PredicateCodingTests: XCTestCase { +@Suite struct PredicateCodingTests { typealias Predicate = FetchRequest.Predicate @@ -18,40 +18,40 @@ final class PredicateCodingTests: XCTestCase { case age } - func roundTrip(_ predicate: Predicate, file: StaticString = #filePath, line: UInt = #line) { + func roundTrip(_ predicate: Predicate, sourceLocation: SourceLocation = #_sourceLocation) { do { let data = try JSONEncoder().encode(predicate) let decoded = try JSONDecoder().decode(Predicate.self, from: data) - XCTAssertEqual(decoded, predicate, file: file, line: line) + #expect(decoded == predicate, sourceLocation: sourceLocation) } catch { - XCTFail("Failed to round trip: \(error)", file: file, line: line) + Issue.record("Failed to round trip: \(error)", sourceLocation: sourceLocation) } } - func testPredicateType() { - XCTAssertEqual(Predicate.value(true).type, .value) - XCTAssertEqual(Predicate.comparison(.init(left: .attribute(.null), right: .attribute(.null))).type, .comparison) - XCTAssertEqual(Predicate.compound(.and([])).type, .compound) + @Test func predicateType() { + #expect(Predicate.value(true).type == .value) + #expect(Predicate.comparison(.init(left: .attribute(.null), right: .attribute(.null))).type == .comparison) + #expect(Predicate.compound(.and([])).type == .compound) } - func testExpressionType() { - XCTAssertEqual(Predicate.Expression.attribute(.null).type, .attribute) - XCTAssertEqual(Predicate.Expression.relationship(.null).type, .relationship) - XCTAssertEqual(Predicate.Expression.keyPath("name").type, .keyPath) - XCTAssertEqual(Predicate.Expression.function(.init(name: "f", arguments: [])).type, .function) + @Test func expressionType() { + #expect(Predicate.Expression.attribute(.null).type == .attribute) + #expect(Predicate.Expression.relationship(.null).type == .relationship) + #expect(Predicate.Expression.keyPath("name").type == .keyPath) + #expect(Predicate.Expression.function(.init(name: "f", arguments: [])).type == .function) } - func testCompoundAccessors() { + @Test func compoundAccessors() { let comparison = Predicate.comparison(.init(left: .keyPath("name"), right: .attribute(.string("x")))) - XCTAssertEqual(Predicate.Compound.and([comparison]).type, .and) - XCTAssertEqual(Predicate.Compound.or([comparison]).type, .or) - XCTAssertEqual(Predicate.Compound.not(comparison).type, .not) - XCTAssertEqual(Predicate.Compound.and([comparison, comparison]).subpredicates.count, 2) - XCTAssertEqual(Predicate.Compound.or([comparison]).subpredicates.count, 1) - XCTAssertEqual(Predicate.Compound.not(comparison).subpredicates, [comparison]) + #expect(Predicate.Compound.and([comparison]).type == .and) + #expect(Predicate.Compound.or([comparison]).type == .or) + #expect(Predicate.Compound.not(comparison).type == .not) + #expect(Predicate.Compound.and([comparison, comparison]).subpredicates.count == 2) + #expect(Predicate.Compound.or([comparison]).subpredicates.count == 1) + #expect(Predicate.Compound.not(comparison).subpredicates == [comparison]) } - func testPredicateCodable() { + @Test func predicateCodable() { let comparison = Predicate.comparison( .init( left: .keyPath("name"), @@ -71,7 +71,7 @@ final class PredicateCodingTests: XCTestCase { roundTrip(.compound(.not(.compound(.and([comparison, .compound(.or([comparison]))]))))) } - func testExpressionCodable() throws { + @Test func expressionCodable() throws { let expressions: [Predicate.Expression] = [ .attribute(.string("test")), .attribute(.null), @@ -83,18 +83,18 @@ final class PredicateCodingTests: XCTestCase { for expression in expressions { let data = try JSONEncoder().encode(expression) let decoded = try JSONDecoder().decode(Predicate.Expression.self, from: data) - XCTAssertEqual(decoded, expression) + #expect(decoded == expression) } } - func testDescriptions() { - XCTAssertEqual(Predicate.value(true).description, "true") + @Test func descriptions() { + #expect(Predicate.value(true).description == "true") let comparison = Predicate.Comparison( left: .keyPath("name"), right: .attribute(.string("John")), type: .equalTo ) - XCTAssertEqual(comparison.description, #"name == "John""#) + #expect(comparison.description == #"name == "John""#) let modified = Predicate.Comparison( left: .keyPath("name"), right: .attribute(.string("j")), @@ -102,44 +102,44 @@ final class PredicateCodingTests: XCTestCase { modifier: .all, options: [.caseInsensitive, .diacriticInsensitive] ) - XCTAssertEqual(modified.description, #"ALL name BEGINSWITH[cd] "j""#) - XCTAssertEqual(Predicate.comparison(comparison).description, comparison.description) + #expect(modified.description == #"ALL name BEGINSWITH[cd] "j""#) + #expect(Predicate.comparison(comparison).description == comparison.description) // compound descriptions let and = Predicate.compound(.and([.comparison(comparison), .value(true)])) - XCTAssertEqual(and.description, #"name == "John" AND true"#) + #expect(and.description == #"name == "John" AND true"#) let notNested = Predicate.compound(.not(and)) - XCTAssert(notNested.description.contains("NOT (")) - XCTAssertEqual(Predicate.Compound.and([]).description, "(Empty and predicate)") + #expect(notNested.description.contains("NOT (")) + #expect(Predicate.Compound.and([]).description == "(Empty and predicate)") // function expression description let function = Predicate.FunctionExpression(name: "f", arguments: [.keyPath("name"), .attribute(.int64(1))]) - XCTAssertEqual(function.description, "f(name, 1)") - XCTAssertEqual(Predicate.Expression.function(function).description, "f(name, 1)") + #expect(function.description == "f(name, 1)") + #expect(Predicate.Expression.function(function).description == "f(name, 1)") } - func testAttributeValuePredicateDescriptions() { + @Test func attributeValuePredicateDescriptions() { let date = Date(timeIntervalSince1970: 0) let uuid = UUID() let url = URL(string: "https://example.com")! - XCTAssertEqual(Predicate.Expression.attribute(.null).description, "nil") - XCTAssertEqual(Predicate.Expression.attribute(.string("x")).description, "\"x\"") - XCTAssertEqual(Predicate.Expression.attribute(.bool(true)).description, "true") - XCTAssertEqual(Predicate.Expression.attribute(.int16(1)).description, "1") - XCTAssertEqual(Predicate.Expression.attribute(.int32(2)).description, "2") - XCTAssertEqual(Predicate.Expression.attribute(.int64(3)).description, "3") - XCTAssertEqual(Predicate.Expression.attribute(.float(1.5)).description, "1.5") - XCTAssertEqual(Predicate.Expression.attribute(.double(2.5)).description, "2.5") - XCTAssertEqual(Predicate.Expression.attribute(.date(date)).description, date.description) - XCTAssertEqual(Predicate.Expression.attribute(.uuid(uuid)).description, uuid.uuidString) - XCTAssertEqual(Predicate.Expression.attribute(.url(url)).description, url.description) - XCTAssertEqual(Predicate.Expression.attribute(.data(Data([0x01]))).description, Data([0x01]).description) - XCTAssertEqual(Predicate.Expression.attribute(.decimal(3)).description, "3") + #expect(Predicate.Expression.attribute(.null).description == "nil") + #expect(Predicate.Expression.attribute(.string("x")).description == "\"x\"") + #expect(Predicate.Expression.attribute(.bool(true)).description == "true") + #expect(Predicate.Expression.attribute(.int16(1)).description == "1") + #expect(Predicate.Expression.attribute(.int32(2)).description == "2") + #expect(Predicate.Expression.attribute(.int64(3)).description == "3") + #expect(Predicate.Expression.attribute(.float(1.5)).description == "1.5") + #expect(Predicate.Expression.attribute(.double(2.5)).description == "2.5") + #expect(Predicate.Expression.attribute(.date(date)).description == date.description) + #expect(Predicate.Expression.attribute(.uuid(uuid)).description == uuid.uuidString) + #expect(Predicate.Expression.attribute(.url(url)).description == url.description) + #expect(Predicate.Expression.attribute(.data(Data([0x01]))).description == Data([0x01]).description) + #expect(Predicate.Expression.attribute(.decimal(3)).description == "3") // relationship values - XCTAssertEqual(Predicate.Expression.relationship(.null).description, "nil") - XCTAssertEqual(Predicate.Expression.relationship(.toOne("a")).description, "a") - XCTAssertEqual(Predicate.Expression.relationship(.toMany(["a", "b"])).description, "{a, b}") + #expect(Predicate.Expression.relationship(.null).description == "nil") + #expect(Predicate.Expression.relationship(.toOne("a")).description == "a") + #expect(Predicate.Expression.relationship(.toMany(["a", "b"])).description == "{a, b}") } - func testComparisonOperators() { + @Test func comparisonOperators() { let name = Predicate.Expression.keyPath("name") let value = Predicate.Expression.attribute(.string("x")) func comparisonType(_ predicate: Predicate) -> Predicate.Comparison.Operator? { @@ -147,123 +147,127 @@ final class PredicateCodingTests: XCTestCase { return comparison.type } // expression op expression - XCTAssertEqual(comparisonType(name < value), .lessThan) - XCTAssertEqual(comparisonType(name <= value), .lessThanOrEqualTo) - XCTAssertEqual(comparisonType(name > value), .greaterThan) - XCTAssertEqual(comparisonType(name >= value), .greaterThanOrEqualTo) - XCTAssertEqual(comparisonType(name == value), .equalTo) - XCTAssertEqual(comparisonType(name != value), .notEqualTo) + #expect(comparisonType(name < value) == .lessThan) + #expect(comparisonType(name <= value) == .lessThanOrEqualTo) + #expect(comparisonType(name > value) == .greaterThan) + #expect(comparisonType(name >= value) == .greaterThanOrEqualTo) + #expect(comparisonType(name == value) == .equalTo) + #expect(comparisonType(name != value) == .notEqualTo) // string op value - XCTAssertEqual(comparisonType("age" < 1), .lessThan) - XCTAssertEqual(comparisonType("age" <= 1), .lessThanOrEqualTo) - XCTAssertEqual(comparisonType("age" > 1), .greaterThan) - XCTAssertEqual(comparisonType("age" >= 1), .greaterThanOrEqualTo) - XCTAssertEqual(comparisonType("age" == 1), .equalTo) - XCTAssertEqual(comparisonType("age" != 1), .notEqualTo) + #expect(comparisonType("age" < 1) == .lessThan) + #expect(comparisonType("age" <= 1) == .lessThanOrEqualTo) + #expect(comparisonType("age" > 1) == .greaterThan) + #expect(comparisonType("age" >= 1) == .greaterThanOrEqualTo) + #expect(comparisonType("age" == 1) == .equalTo) + #expect(comparisonType("age" != 1) == .notEqualTo) // coding key op value - XCTAssertEqual(comparisonType(Key.age < 1), .lessThan) - XCTAssertEqual(comparisonType(Key.age <= 1), .lessThanOrEqualTo) - XCTAssertEqual(comparisonType(Key.age > 1), .greaterThan) - XCTAssertEqual(comparisonType(Key.age >= 1), .greaterThanOrEqualTo) - XCTAssertEqual(comparisonType(Key.age == 1), .equalTo) - XCTAssertEqual(comparisonType(Key.age != 1), .notEqualTo) + #expect(comparisonType(Key.age < 1) == .lessThan) + #expect(comparisonType(Key.age <= 1) == .lessThanOrEqualTo) + #expect(comparisonType(Key.age > 1) == .greaterThan) + #expect(comparisonType(Key.age >= 1) == .greaterThanOrEqualTo) + #expect(comparisonType(Key.age == 1) == .equalTo) + #expect(comparisonType(Key.age != 1) == .notEqualTo) } - func testCompareExtensions() { + @Test func compareExtensions() { let rhs = Predicate.Expression.attribute(.string("x")) // string - XCTAssertEqual("name".compare(.equalTo, rhs).type, .comparison) - XCTAssertEqual("name".compare(.like, [.caseInsensitive], rhs).type, .comparison) - XCTAssertEqual("name".compare(.any, .contains, [.diacriticInsensitive], rhs).type, .comparison) + #expect("name".compare(.equalTo, rhs).type == .comparison) + #expect("name".compare(.like, [.caseInsensitive], rhs).type == .comparison) + #expect("name".compare(.any, .contains, [.diacriticInsensitive], rhs).type == .comparison) // coding key - XCTAssertEqual(Key.name.compare(.equalTo, rhs).type, .comparison) - XCTAssertEqual(Key.name.compare(.matches, [.normalized], rhs).type, .comparison) - XCTAssertEqual(Key.name.compare(.all, .endsWith, [.localeSensitive], rhs).type, .comparison) + #expect(Key.name.compare(.equalTo, rhs).type == .comparison) + #expect(Key.name.compare(.matches, [.normalized], rhs).type == .comparison) + #expect(Key.name.compare(.all, .endsWith, [.localeSensitive], rhs).type == .comparison) // expression let lhs = Predicate.Expression.keyPath("name") - XCTAssertEqual(lhs.compare(.in, rhs).type, .comparison) - XCTAssertEqual(lhs.compare(.between, [.caseInsensitive], rhs).type, .comparison) - XCTAssertEqual(lhs.compare(.any, .beginsWith, [.caseInsensitive], rhs).type, .comparison) + #expect(lhs.compare(.in, rhs).type == .comparison) + #expect(lhs.compare(.between, [.caseInsensitive], rhs).type == .comparison) + #expect(lhs.compare(.any, .beginsWith, [.caseInsensitive], rhs).type == .comparison) } - func testCompoundOperators() { + @Test func compoundOperators() { let a = Predicate.value(true) let b = Predicate.value(false) - XCTAssertEqual(a && b, .compound(.and([a, b]))) - XCTAssertEqual(a && [b, a], .compound(.and([a, b, a]))) - XCTAssertEqual(a || b, .compound(.or([a, b]))) - XCTAssertEqual(a || [b, a], .compound(.or([a, b, a]))) - XCTAssertEqual(!a, .compound(.not(a))) + #expect((a && b) == .compound(.and([a, b]))) + #expect((a && [b, a]) == .compound(.and([a, b, a]))) + #expect((a || b) == .compound(.or([a, b]))) + #expect((a || [b, a]) == .compound(.or([a, b, a]))) + #expect((!a) == .compound(.not(a))) } - func testKeyPath() { + @Test func keyPath() { var keyPath: PredicateKeyPath = [.property("events"), .property("name")] - XCTAssertEqual(keyPath.keys, [.property("events"), .property("name")]) - XCTAssertEqual(keyPath.rawValue, "events.name") - XCTAssertEqual(keyPath.description, "events.name") + #expect(keyPath.keys == [.property("events"), .property("name")]) + #expect(keyPath.rawValue == "events.name") + #expect(keyPath.description == "events.name") // append / removal keyPath.append(.index(0)) - XCTAssertEqual(keyPath.rawValue, "events.name.0") - XCTAssertEqual(keyPath.appending(.operator(.count)).rawValue, "events.name.0.@count") + #expect(keyPath.rawValue == "events.name.0") + #expect(keyPath.appending(.operator(.count)).rawValue == "events.name.0.@count") keyPath.append(contentsOf: [.property("id")]) - XCTAssertEqual(keyPath.appending(contentsOf: [PredicateKeyPath.Key.property("x")]).keys.count, 5) - XCTAssertEqual(keyPath.removeFirst(), .property("events")) - XCTAssertEqual(keyPath.removingFirst().keys.first, .index(0)) - XCTAssertEqual(keyPath.removeLast(), .property("id")) - XCTAssertEqual(keyPath.removingLast().keys.count, keyPath.keys.count - 1) + #expect(keyPath.appending(contentsOf: [PredicateKeyPath.Key.property("x")]).keys.count == 5) + #expect(keyPath.removeFirst() == .property("events")) + #expect(keyPath.removingFirst().keys.first == .index(0)) + #expect(keyPath.removeLast() == .property("id")) + #expect(keyPath.removingLast().keys.count == keyPath.keys.count - 1) // begins(with:) let path: PredicateKeyPath = "events.name" - XCTAssert(path.begins(with: "events")) - XCTAssertFalse(path.begins(with: "people")) + #expect(path.begins(with: "events")) + #expect(path.begins(with: "people") == false) // raw value parsing let parsed = PredicateKeyPath(rawValue: "events.0.@count") - XCTAssertEqual(parsed.keys, [.property("events"), .index(0), .operator(.count)]) + #expect(parsed.keys == [.property("events"), .index(0), .operator(.count)]) // operators for op in [PredicateKeyPath.Operator.count, .sum, .min, .max, .average] { - XCTAssertEqual(PredicateKeyPath.Key(rawValue: op.rawValue), .operator(op)) - XCTAssertEqual(op.description, op.rawValue) + #expect(PredicateKeyPath.Key(rawValue: op.rawValue) == .operator(op)) + #expect(op.description == op.rawValue) } - XCTAssertEqual(PredicateKeyPath.Key.index(1).description, "1") - XCTAssertEqual(PredicateKeyPath.Key.property("a").description, "a") + #expect(PredicateKeyPath.Key.index(1).description == "1") + #expect(PredicateKeyPath.Key.property("a").description == "a") } - func testStringComparisonHelpers() { + @Test func stringComparisonHelpers() { let locale = Locale(identifier: "en_US") - XCTAssert("apple".compare("APPLE", [.caseInsensitive], nil, .orderedSame)) - XCTAssertFalse("apple".compare("banana", [], nil, .orderedSame)) - XCTAssert("apple".compare("banana", [.localeSensitive], locale, .orderedAscending)) - XCTAssertNotNil("hello world".range(of: "WORLD", [.caseInsensitive], nil)) - XCTAssertNil("hello".range(of: "xyz", [], locale)) - XCTAssert("hello123".matches("[a-z]+[0-9]+", [], nil)) - XCTAssertFalse("hello".matches("^[0-9]+$", [.caseInsensitive], locale)) - XCTAssert("hello world".begins(with: "HELLO", [.caseInsensitive], nil)) - XCTAssertFalse("hello world".begins(with: "world", [], locale)) - XCTAssert("hello world".ends(with: "WORLD", [.caseInsensitive], nil)) - XCTAssertFalse("hello world".ends(with: "hello", [], locale)) - XCTAssert("héllo".compare("hello", [.diacriticInsensitive], nil, .orderedSame)) - XCTAssert("hello"[...].begins(with: "he")) - XCTAssertFalse("hello"[...].begins(with: "lo")) + let caseInsensitive: Set = [.caseInsensitive] + let localeSensitive: Set = [.localeSensitive] + let diacriticInsensitive: Set = [.diacriticInsensitive] + let noOptions: Set = [] + #expect("apple".compare("APPLE", caseInsensitive, nil, .orderedSame)) + #expect("apple".compare("banana", noOptions, nil, .orderedSame) == false) + #expect("apple".compare("banana", localeSensitive, locale, .orderedAscending)) + #expect("hello world".range(of: "WORLD", caseInsensitive, nil) != nil) + #expect("hello".range(of: "xyz", noOptions, locale) == nil) + #expect("hello123".matches("[a-z]+[0-9]+", noOptions, nil)) + #expect("hello".matches("^[0-9]+$", caseInsensitive, locale) == false) + #expect("hello world".begins(with: "HELLO", caseInsensitive, nil)) + #expect("hello world".begins(with: "world", noOptions, locale) == false) + #expect("hello world".ends(with: "WORLD", caseInsensitive, nil)) + #expect("hello world".ends(with: "hello", noOptions, locale) == false) + #expect("héllo".compare("hello", diacriticInsensitive, nil, .orderedSame)) + #expect("hello"[...].begins(with: "he")) + #expect("hello"[...].begins(with: "lo") == false) // CompareOptions conversion - XCTAssertEqual(String.CompareOptions(.caseInsensitive), .caseInsensitive) - XCTAssertEqual(String.CompareOptions(.diacriticInsensitive), .diacriticInsensitive) - XCTAssertNil(String.CompareOptions(.normalized)) - XCTAssertNil(String.CompareOptions(.localeSensitive)) + #expect(String.CompareOptions(.caseInsensitive) == .caseInsensitive) + #expect(String.CompareOptions(.diacriticInsensitive) == .diacriticInsensitive) + #expect(String.CompareOptions(.normalized) == nil) + #expect(String.CompareOptions(.localeSensitive) == nil) } - func testCollectionHelpers() { - XCTAssert([1, 2, 3].begins(with: [1, 2])) - XCTAssertFalse([1, 2, 3].begins(with: [2])) + @Test func collectionHelpers() { + #expect([1, 2, 3].begins(with: [1, 2])) + #expect([1, 2, 3].begins(with: [2]) == false) // contains(_:) is a *contiguous subsequence* search — the string // `.contains` predicate resolves to it on platforms without // Foundation's `StringProtocol.contains` (Embedded Swift), so // every-element membership is not enough: searching locations for // "mill" must not match "1150 Timber Lane" just because all of // m/i/l/l appear somewhere in it. - XCTAssert([1, 2, 3].contains([2, 3])) - XCTAssert([1, 2, 3].contains([1, 2, 3])) - XCTAssertFalse([1, 2, 3].contains([3, 1])) - XCTAssertFalse([1, 2].contains([1, 4])) - XCTAssert(Array("millbrook").contains(Array("mill"))) - XCTAssertFalse(Array("1150 timber lane").contains(Array("mill"))) + #expect([1, 2, 3].contains([2, 3])) + #expect([1, 2, 3].contains([1, 2, 3])) + #expect([1, 2, 3].contains([3, 1]) == false) + #expect([1, 2].contains([1, 4]) == false) + #expect(Array("millbrook").contains(Array("mill"))) + #expect(Array("1150 timber lane").contains(Array("mill")) == false) } } diff --git a/Tests/CoreModelTests/PredicateEvaluationTests.swift b/Tests/CoreModelTests/PredicateEvaluationTests.swift index d153f7a..40454f6 100644 --- a/Tests/CoreModelTests/PredicateEvaluationTests.swift +++ b/Tests/CoreModelTests/PredicateEvaluationTests.swift @@ -6,10 +6,10 @@ // import Foundation -import XCTest +import Testing @testable import CoreModel -final class PredicateEvaluationTests: XCTestCase { +@Suite struct PredicateEvaluationTests { private let person = ModelData( entity: "Person", @@ -27,111 +27,111 @@ final class PredicateEvaluationTests: XCTestCase { ] ) - func testValue() { - XCTAssert(FetchRequest.Predicate.value(true).evaluate(with: person)) - XCTAssertFalse(FetchRequest.Predicate.value(false).evaluate(with: person)) + @Test func value() { + #expect(FetchRequest.Predicate.value(true).evaluate(with: person)) + #expect(FetchRequest.Predicate.value(false).evaluate(with: person) == false) } - func testEquality() { - XCTAssert(("name" == "Alice").evaluate(with: person)) - XCTAssertFalse(("name" == "Bob").evaluate(with: person)) - XCTAssert(("name" != "Bob").evaluate(with: person)) - XCTAssert(("age" == 30).evaluate(with: person)) - XCTAssert(("verified" == true).evaluate(with: person)) + @Test func equality() { + #expect(("name" == "Alice").evaluate(with: person)) + #expect(("name" == "Bob").evaluate(with: person) == false) + #expect(("name" != "Bob").evaluate(with: person)) + #expect(("age" == 30).evaluate(with: person)) + #expect(("verified" == true).evaluate(with: person)) // numeric comparison across integer types - XCTAssert(("age" == Int64(30)).evaluate(with: person)) + #expect(("age" == Int64(30)).evaluate(with: person)) } - func testCaseInsensitiveEquality() { + @Test func caseInsensitiveEquality() { let predicate = "name".compare(.equalTo, [.caseInsensitive], .attribute(.string("ALICE"))) - XCTAssert(predicate.evaluate(with: person)) - XCTAssertFalse(("name" == "ALICE").evaluate(with: person)) + #expect(predicate.evaluate(with: person)) + #expect(("name" == "ALICE").evaluate(with: person) == false) } - func testNull() { + @Test func null() { // a null attribute equals a nil / missing value - XCTAssert("nickname".compare(.equalTo, .attribute(.null)).evaluate(with: person)) - XCTAssert("missingKey".compare(.equalTo, .attribute(.null)).evaluate(with: person)) - XCTAssertFalse("name".compare(.equalTo, .attribute(.null)).evaluate(with: person)) - } - - func testOrdering() { - XCTAssert(("age" > 21).evaluate(with: person)) - XCTAssert(("age" >= 30).evaluate(with: person)) - XCTAssert(("age" < 31).evaluate(with: person)) - XCTAssert(("age" <= 30).evaluate(with: person)) - XCTAssertFalse(("age" > 30).evaluate(with: person)) - XCTAssert(("score" > 4.0).evaluate(with: person)) + #expect("nickname".compare(.equalTo, .attribute(.null)).evaluate(with: person)) + #expect("missingKey".compare(.equalTo, .attribute(.null)).evaluate(with: person)) + #expect("name".compare(.equalTo, .attribute(.null)).evaluate(with: person) == false) + } + + @Test func ordering() { + #expect(("age" > 21).evaluate(with: person)) + #expect(("age" >= 30).evaluate(with: person)) + #expect(("age" < 31).evaluate(with: person)) + #expect(("age" <= 30).evaluate(with: person)) + #expect(("age" > 30).evaluate(with: person) == false) + #expect(("score" > 4.0).evaluate(with: person)) // string ordering - XCTAssert(("name" < "Bob").evaluate(with: person)) + #expect(("name" < "Bob").evaluate(with: person)) // values that aren't order-comparable - XCTAssertFalse(("verified" > "Alice").evaluate(with: person)) + #expect(("verified" > "Alice").evaluate(with: person) == false) } - func testStringOperators() { - XCTAssert("name".compare(.beginsWith, .attribute(.string("Al"))).evaluate(with: person)) - XCTAssert("name".compare(.endsWith, .attribute(.string("ice"))).evaluate(with: person)) - XCTAssert("name".compare(.contains, .attribute(.string("lic"))).evaluate(with: person)) - XCTAssertFalse("name".compare(.beginsWith, .attribute(.string("Bo"))).evaluate(with: person)) - XCTAssert("name".compare(.beginsWith, [.caseInsensitive], .attribute(.string("al"))).evaluate(with: person)) + @Test func stringOperators() { + #expect("name".compare(.beginsWith, .attribute(.string("Al"))).evaluate(with: person)) + #expect("name".compare(.endsWith, .attribute(.string("ice"))).evaluate(with: person)) + #expect("name".compare(.contains, .attribute(.string("lic"))).evaluate(with: person)) + #expect("name".compare(.beginsWith, .attribute(.string("Bo"))).evaluate(with: person) == false) + #expect("name".compare(.beginsWith, [.caseInsensitive], .attribute(.string("al"))).evaluate(with: person)) // IN: left hand side is a substring of the right hand side - XCTAssert("name".compare(.in, .attribute(.string("Alice in Wonderland"))).evaluate(with: person)) + #expect("name".compare(.in, .attribute(.string("Alice in Wonderland"))).evaluate(with: person)) } - func testLike() { - XCTAssert("name".compare(.like, .attribute(.string("A*"))).evaluate(with: person)) - XCTAssert("name".compare(.like, .attribute(.string("?lice"))).evaluate(with: person)) - XCTAssert("name".compare(.like, .attribute(.string("*ice"))).evaluate(with: person)) - XCTAssert("name".compare(.like, .attribute(.string("A*e"))).evaluate(with: person)) - XCTAssertFalse("name".compare(.like, .attribute(.string("B*"))).evaluate(with: person)) - XCTAssertFalse("name".compare(.like, .attribute(.string("Alic?e"))).evaluate(with: person)) - XCTAssert("name".compare(.like, [.caseInsensitive], .attribute(.string("a*"))).evaluate(with: person)) + @Test func like() { + #expect("name".compare(.like, .attribute(.string("A*"))).evaluate(with: person)) + #expect("name".compare(.like, .attribute(.string("?lice"))).evaluate(with: person)) + #expect("name".compare(.like, .attribute(.string("*ice"))).evaluate(with: person)) + #expect("name".compare(.like, .attribute(.string("A*e"))).evaluate(with: person)) + #expect("name".compare(.like, .attribute(.string("B*"))).evaluate(with: person) == false) + #expect("name".compare(.like, .attribute(.string("Alic?e"))).evaluate(with: person) == false) + #expect("name".compare(.like, [.caseInsensitive], .attribute(.string("a*"))).evaluate(with: person)) } - func testMatches() { - XCTAssert("name".compare(.matches, .attribute(.string("A[a-z]+e"))).evaluate(with: person)) - XCTAssertFalse("name".compare(.matches, .attribute(.string("^B.*"))).evaluate(with: person)) + @Test func matches() { + #expect("name".compare(.matches, .attribute(.string("A[a-z]+e"))).evaluate(with: person)) + #expect("name".compare(.matches, .attribute(.string("^B.*"))).evaluate(with: person) == false) } - func testRelationships() { + @Test func relationships() { // to-one equality against an identifier - XCTAssert("boss".compare(.equalTo, .relationship(.toOne("100"))).evaluate(with: person)) - XCTAssert("boss".compare(.equalTo, .attribute(.string("100"))).evaluate(with: person)) - XCTAssertFalse("boss".compare(.equalTo, .relationship(.toOne("999"))).evaluate(with: person)) + #expect("boss".compare(.equalTo, .relationship(.toOne("100"))).evaluate(with: person)) + #expect("boss".compare(.equalTo, .attribute(.string("100"))).evaluate(with: person)) + #expect("boss".compare(.equalTo, .relationship(.toOne("999"))).evaluate(with: person) == false) // to-many contains an identifier - XCTAssert("events".compare(.contains, .relationship(.toOne("10"))).evaluate(with: person)) - XCTAssert("events".compare(.contains, .attribute(.string("20"))).evaluate(with: person)) - XCTAssertFalse("events".compare(.contains, .attribute(.string("30"))).evaluate(with: person)) + #expect("events".compare(.contains, .relationship(.toOne("10"))).evaluate(with: person)) + #expect("events".compare(.contains, .attribute(.string("20"))).evaluate(with: person)) + #expect("events".compare(.contains, .attribute(.string("30"))).evaluate(with: person) == false) // identifier is in a to-many relationship let predicate = FetchRequest.Predicate.comparison( .init(left: .attribute(.string("10")), right: .keyPath("events"), type: .in) ) - XCTAssert(predicate.evaluate(with: person)) + #expect(predicate.evaluate(with: person)) } - func testModifiers() { + @Test func modifiers() { let anyMatch = "events".compare(.any, .equalTo, [], .attribute(.string("10"))) - XCTAssert(anyMatch.evaluate(with: person)) + #expect(anyMatch.evaluate(with: person)) let anyMiss = "events".compare(.any, .equalTo, [], .attribute(.string("30"))) - XCTAssertFalse(anyMiss.evaluate(with: person)) + #expect(anyMiss.evaluate(with: person) == false) let allMatch = "events".compare(.all, .in, [], .relationship(.toMany(["10", "20", "30"]))) - XCTAssert(allMatch.evaluate(with: person)) + #expect(allMatch.evaluate(with: person)) let allMiss = "events".compare(.all, .equalTo, [], .attribute(.string("10"))) - XCTAssertFalse(allMiss.evaluate(with: person)) + #expect(allMiss.evaluate(with: person) == false) } - func testCompound() { + @Test func compound() { let isAlice: FetchRequest.Predicate = "name" == "Alice" let isAdult: FetchRequest.Predicate = "age" >= 18 let isBob: FetchRequest.Predicate = "name" == "Bob" - XCTAssert((isAlice && isAdult).evaluate(with: person)) - XCTAssertFalse((isAlice && isBob).evaluate(with: person)) - XCTAssert((isBob || isAdult).evaluate(with: person)) - XCTAssert((!isBob).evaluate(with: person)) - XCTAssertFalse((!isAlice).evaluate(with: person)) + #expect((isAlice && isAdult).evaluate(with: person)) + #expect((isAlice && isBob).evaluate(with: person) == false) + #expect((isBob || isAdult).evaluate(with: person)) + #expect((!isBob).evaluate(with: person)) + #expect((!isAlice).evaluate(with: person) == false) } - func testFunctionExpression() { + @Test func functionExpression() { let uppercase = DatabaseFunction(name: "UPPERCASE", argumentCount: 1) { arguments in guard case let .string(value)? = arguments.first ?? nil else { return nil } return .string(value.uppercased()) @@ -142,25 +142,25 @@ final class PredicateEvaluationTests: XCTestCase { right: .attribute(.string("ALICE")) ) ) - XCTAssert(predicate.evaluate(with: person, functions: ["UPPERCASE": uppercase])) + #expect(predicate.evaluate(with: person, functions: ["UPPERCASE": uppercase])) // unregistered functions evaluate to nil, which doesn't equal a string - XCTAssertFalse(predicate.evaluate(with: person)) + #expect(predicate.evaluate(with: person) == false) } - func testWildcardMatch() { - XCTAssert(String.wildcardMatch("", pattern: "")) - XCTAssert(String.wildcardMatch("", pattern: "*")) - XCTAssertFalse(String.wildcardMatch("", pattern: "?")) - XCTAssert(String.wildcardMatch("abc", pattern: "*")) - XCTAssert(String.wildcardMatch("abc", pattern: "a*c")) - XCTAssert(String.wildcardMatch("abbbc", pattern: "a*c")) - XCTAssert(String.wildcardMatch("abc", pattern: "a**c")) - XCTAssertFalse(String.wildcardMatch("abd", pattern: "a*c")) - XCTAssert(String.wildcardMatch("abc", pattern: "???")) - XCTAssertFalse(String.wildcardMatch("abc", pattern: "??")) + @Test func wildcardMatch() { + #expect(String.wildcardMatch("", pattern: "")) + #expect(String.wildcardMatch("", pattern: "*")) + #expect(String.wildcardMatch("", pattern: "?") == false) + #expect(String.wildcardMatch("abc", pattern: "*")) + #expect(String.wildcardMatch("abc", pattern: "a*c")) + #expect(String.wildcardMatch("abbbc", pattern: "a*c")) + #expect(String.wildcardMatch("abc", pattern: "a**c")) + #expect(String.wildcardMatch("abd", pattern: "a*c") == false) + #expect(String.wildcardMatch("abc", pattern: "???")) + #expect(String.wildcardMatch("abc", pattern: "??") == false) } - func testFetchRequestEvaluation() { + @Test func fetchRequestEvaluation() { let people: [ModelData] = (1...5).map { index in ModelData( entity: "Person", @@ -174,16 +174,16 @@ final class PredicateEvaluationTests: XCTestCase { let other = ModelData(entity: "Event", id: "99") let all = people + [other] // filters by entity - XCTAssertEqual(FetchRequest(entity: "Person").evaluate(all).count, 5) + #expect(FetchRequest(entity: "Person").evaluate(all).count == 5) // predicate let adults = FetchRequest(entity: "Person", predicate: "age" > 22).evaluate(all) - XCTAssertEqual(adults.map { $0.id }, ["3", "4", "5"]) + #expect(adults.map { $0.id } == ["3", "4", "5"]) // sort descending let sorted = FetchRequest( entity: "Person", sortDescriptors: [.init(property: "age", ascending: false)] ).evaluate(all) - XCTAssertEqual(sorted.map { $0.id }, ["5", "4", "3", "2", "1"]) + #expect(sorted.map { $0.id } == ["5", "4", "3", "2", "1"]) // limit and offset let page = FetchRequest( entity: "Person", @@ -191,29 +191,29 @@ final class PredicateEvaluationTests: XCTestCase { fetchLimit: 2, fetchOffset: 1 ).evaluate(all) - XCTAssertEqual(page.map { $0.id }, ["2", "3"]) + #expect(page.map { $0.id } == ["2", "3"]) // offset past the end let empty = FetchRequest(entity: "Person", fetchOffset: 10).evaluate(all) - XCTAssertEqual(empty, []) + #expect(empty == []) } - func testSorting() { + @Test func sorting() { let people: [ModelData] = [ ModelData(entity: "Person", id: "b", attributes: ["age": .int16(30), "name": .string("Bob")]), ModelData(entity: "Person", id: "a", attributes: ["age": .int16(30), "name": .string("Alice")]), ModelData(entity: "Person", id: "c", attributes: ["age": .int16(25), "name": .string("Charlie")]) ] // empty descriptors sort by identifier - XCTAssertEqual(people.sorted(by: []).map { $0.id }, ["a", "b", "c"]) + #expect(people.sorted(by: []).map { $0.id } == ["a", "b", "c"]) // ties broken by identifier let byAge = people.sorted(by: [.init(property: "age", ascending: true)]) - XCTAssertEqual(byAge.map { $0.id }, ["c", "a", "b"]) + #expect(byAge.map { $0.id } == ["c", "a", "b"]) // multiple descriptors let byAgeThenName = people.sorted(by: [ .init(property: "age", ascending: false), .init(property: "name", ascending: false) ]) - XCTAssertEqual(byAgeThenName.map { $0.id }, ["b", "a", "c"]) + #expect(byAgeThenName.map { $0.id } == ["b", "a", "c"]) // function sort term let negate = DatabaseFunction(name: "NEGATE", argumentCount: 1) { arguments in guard case let .int16(value)? = arguments.first ?? nil else { return nil } @@ -223,15 +223,15 @@ final class PredicateEvaluationTests: XCTestCase { by: [.init(term: .function(.init(name: "NEGATE", arguments: [.keyPath("age")])), ascending: true)], functions: ["NEGATE": negate] ) - XCTAssertEqual(byNegatedAge.map { $0.id }, ["a", "b", "c"]) + #expect(byNegatedAge.map { $0.id } == ["a", "b", "c"]) } - func testFiltering() { + @Test func filtering() { let people: [ModelData] = [ ModelData(entity: "Person", id: "a", attributes: ["name": .string("Alice")]), ModelData(entity: "Person", id: "b", attributes: ["name": .string("Bob")]) ] - XCTAssertEqual(people.filtered(by: "name" == "Bob").map { $0.id }, ["b"]) - XCTAssertEqual(people.filtered(by: .value(false)), []) + #expect(people.filtered(by: "name" == "Bob").map { $0.id } == ["b"]) + #expect(people.filtered(by: .value(false)) == []) } } diff --git a/Tests/CoreModelTests/StoreDefaultsTests.swift b/Tests/CoreModelTests/StoreDefaultsTests.swift index cd956f8..85186ec 100644 --- a/Tests/CoreModelTests/StoreDefaultsTests.swift +++ b/Tests/CoreModelTests/StoreDefaultsTests.swift @@ -6,7 +6,7 @@ // import Foundation -import XCTest +import Testing @testable import CoreModel /// Minimal in-memory `ModelStorage` conformer that relies on the protocol's @@ -46,9 +46,9 @@ private final class MinimalStore: ModelStorage, @unchecked Sendable { func register(function: DatabaseFunction) async throws { } } -final class StoreDefaultsTests: XCTestCase { +@Suite struct StoreDefaultsTests { - func testDefaultImplementations() async throws { + @Test func defaultImplementations() async throws { let store = MinimalStore() let people = [ Person(name: "Alice", age: 30), @@ -58,9 +58,9 @@ final class StoreDefaultsTests: XCTestCase { try await store.insert(people.map { try! $0.encode() }) // default count(_:) falls back to fetching and counting let count = try await store.count(FetchRequest(entity: Person.entityName)) - XCTAssertEqual(count, 2) + #expect(count == 2) // typed convenience still works through the defaults let fetched = try await store.fetch(Person.self, for: people[0].id) - XCTAssertEqual(fetched, people[0]) + #expect(fetched == people[0]) } }