Skip to content

Commit 04ecd57

Browse files
authored
Merge pull request #37 from PureSwift/feature/predicate
Improve predicate test coverage and stabilize CoreData tests
2 parents 65503f5 + c34b014 commit 04ecd57

7 files changed

Lines changed: 89 additions & 10 deletions

File tree

Sources/CoreModel/Predicate/Evaluate.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,6 @@ internal extension PredicateValue {
8888
return value
8989
}
9090

91-
/// The values traversed through a to-many relationship, if any.
92-
var aggregateValues: [PredicateValue]? {
93-
guard case let .aggregate(values) = self else { return nil }
94-
return values
95-
}
96-
9791
/// An object identifier this value can represent, for relationship comparisons.
9892
var objectIDValue: ObjectID? {
9993
switch self {

Tests/CoreModelTests/BatchInsertTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Testing
1313
@testable import CoreModel
1414
@testable import CoreDataModel
1515

16-
@Suite
16+
@Suite(.serialized)
1717
struct BatchInsertTests {
1818

1919
/// Synthetic catalog payload: many events sharing a small set of people through

Tests/CoreModelTests/CoreDataModelTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Testing
1818
// suite exercises (`ManagedObjectViewContext`, `NSPersistentContainer.syncLoadPersistentStores()`,
1919
// etc.) need macOS 12/iOS 15/watchOS 8/tvOS 15, below this package's deployment target, so
2020
// each test guards its body with a runtime `if #available` instead.
21-
@Suite struct CoreDataModelTests {
21+
@Suite(.serialized) struct CoreDataModelTests {
2222

2323
static func makeContext() throws -> NSManagedObjectContext {
2424
let model = Model(entities: Person.self, Event.self)

Tests/CoreModelTests/CoreDataTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Testing
1313
@testable import CoreModel
1414
@testable import CoreDataModel
1515

16-
@Suite
16+
@Suite(.serialized)
1717
struct CoreDataTests {
1818

1919
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)

Tests/CoreModelTests/FoundationPredicateTests.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,56 @@ import Testing
6767
let greaterThanOrEqual = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.age >= 20 })
6868
#expect(greaterThanOrEqual == .comparison(.init(left: .keyPath("age"), right: .attribute(.int64(20)), type: .greaterThanOrEqualTo)))
6969
#expect(Self.people.filtered(by: greaterThanOrEqual).map(\.id.rawValue) == ["Alice", "Alina"])
70+
71+
let lessThanOrEqual = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.age <= 20 })
72+
#expect(lessThanOrEqual == .comparison(.init(left: .keyPath("age"), right: .attribute(.int64(20)), type: .lessThanOrEqualTo)))
73+
#expect(Self.people.filtered(by: lessThanOrEqual).map(\.id.rawValue) == ["Bob", "Alina"])
74+
75+
let greaterThan = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.age > 20 })
76+
#expect(greaterThan == .comparison(.init(left: .keyPath("age"), right: .attribute(.int64(20)), type: .greaterThan)))
77+
#expect(Self.people.filtered(by: greaterThan).map(\.id.rawValue) == ["Alice"])
78+
}
79+
80+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
81+
@Test func sequenceContains() throws {
82+
83+
// an element tested against a collection property
84+
let predicate = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.scores.contains(10) })
85+
#expect(predicate == .comparison(.init(
86+
left: .keyPath("scores"),
87+
right: .attribute(.int64(10)),
88+
type: .contains
89+
)))
90+
}
91+
92+
#if canImport(Darwin)
93+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
94+
@Test func localizedStandardContains() throws {
95+
96+
// maps to a case- and diacritic-insensitive CONTAINS
97+
let predicate = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.name.localizedStandardContains("ali") })
98+
#expect(predicate == .comparison(.init(
99+
left: .keyPath("name"),
100+
right: .attribute(.string("ali")),
101+
type: .contains,
102+
options: [.caseInsensitive, .diacriticInsensitive]
103+
)))
104+
// the case-insensitive option applies when evaluating in memory
105+
#expect(Self.people.filtered(by: predicate).map(\.id.rawValue) == ["Alice", "Alina"])
106+
}
107+
108+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
109+
@Test func objectKeyPath() throws {
110+
111+
// an @objc root resolves its key paths through Key-Value Coding
112+
let predicate = try FetchRequest.Predicate(#Predicate<EventObject> { $0.name == "Event 1" })
113+
#expect(predicate == .comparison(.init(
114+
left: .keyPath("name"),
115+
right: .attribute(.string("Event 1")),
116+
type: .equalTo
117+
)))
70118
}
119+
#endif
71120

72121
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
73122
@Test func compound() throws {

Tests/CoreModelTests/KeyPathTraversalTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,42 @@ import Testing
101101
#expect(request.evaluate(Fixture.all).map(\.id.rawValue) == ["alice"])
102102
}
103103

104+
@Test func nullRelationship() {
105+
106+
// a null relationship has nothing to traverse into
107+
let dave = ModelData(
108+
entity: "Person",
109+
id: ObjectID(rawValue: "dave"),
110+
attributes: ["name": .string("Dave")],
111+
relationships: ["events": .null, "favorite": .null]
112+
)
113+
let objects = Fixture.all + [dave]
114+
let any = FetchRequest(entity: "Person", predicate: "events.name".compare(.any, .equalTo, [], .attribute(.string("WWDC"))))
115+
#expect(any.evaluate(objects).map(\.id.rawValue) == ["alice"])
116+
let toOne = FetchRequest(entity: "Person", predicate: FetchRequest.Predicate.comparison(.init(
117+
left: .keyPath("favorite.name"),
118+
right: .attribute(.string("WWDC")),
119+
type: .equalTo
120+
)))
121+
#expect(toOne.evaluate(objects).map(\.id.rawValue) == ["alice"])
122+
}
123+
124+
@Test func modifierOnNonCollection() {
125+
126+
// an ALL/ANY modifier on a plain attribute falls back to a direct comparison
127+
let predicate = "name".compare(.any, .equalTo, [], .attribute(.string("Alice")))
128+
let request = FetchRequest(entity: "Person", predicate: predicate)
129+
#expect(request.evaluate(Fixture.all).map(\.id.rawValue) == ["alice"])
130+
}
131+
132+
@Test func missingRelationshipProperty() {
133+
134+
// a key path whose leading key isn't a relationship doesn't resolve
135+
let predicate = "name.length".compare(.any, .equalTo, [], .attribute(.int64(5)))
136+
let request = FetchRequest(entity: "Person", predicate: predicate)
137+
#expect(request.evaluate(Fixture.all).isEmpty)
138+
}
139+
104140
@Test func unresolvedRelatedObjects() {
105141

106142
// without the related objects, a traversing key path can't resolve

Tests/CoreModelTests/PersistentStorageTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ struct AllTypes: Equatable, Hashable, Codable, Identifiable {
111111
// - Note: `@Test`/`@Suite` can't be combined with a declaration-level `@available` — see
112112
// CoreDataModelTests.swift for the same note. Each test guards its body with a runtime
113113
// `if #available` instead.
114-
@Suite struct PersistentStorageTests {
114+
@Suite(.serialized) struct PersistentStorageTests {
115115

116116
static func makeStorage(model: Model = Model(entities: Person.self, Event.self, AllTypes.self)) -> PersistentContainerStorage {
117117
let description = NSPersistentStoreDescription()

0 commit comments

Comments
 (0)