Skip to content

Commit 818c137

Browse files
committed
#15 Add tests for Foundation.Predicate conversion
1 parent 1c9eae3 commit 818c137

1 file changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//
2+
// FoundationPredicateTests.swift
3+
// CoreModel
4+
//
5+
// Created by Alsey Coleman Miller on 8/16/26.
6+
//
7+
8+
#if canImport(FoundationEssentials)
9+
import FoundationEssentials
10+
#elseif canImport(Foundation)
11+
import Foundation
12+
#endif
13+
import Testing
14+
@testable import CoreModel
15+
16+
@Suite struct FoundationPredicateTests {
17+
18+
struct PersonModel {
19+
20+
var name: String
21+
var age: Int
22+
var isActive: Bool
23+
var nickname: String?
24+
}
25+
26+
static var people: [ModelData] {
27+
[
28+
("Alice", 30, true),
29+
("Bob", 17, false),
30+
("Alina", 20, true)
31+
].map { name, age, isActive in
32+
ModelData(
33+
entity: "Person",
34+
id: ObjectID(rawValue: name),
35+
attributes: [
36+
"name": .string(name),
37+
"age": .int64(numericCast(age)),
38+
"isActive": .bool(isActive)
39+
]
40+
)
41+
}
42+
}
43+
44+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
45+
@Test func comparison() throws {
46+
47+
let predicate = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.name == "Alice" })
48+
#expect(predicate == .comparison(.init(left: .keyPath("name"), right: .attribute(.string("Alice")), type: .equalTo)))
49+
#expect(Self.people.filtered(by: predicate).map(\.id.rawValue) == ["Alice"])
50+
51+
let notEqual = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.name != "Alice" })
52+
#expect(notEqual == .comparison(.init(left: .keyPath("name"), right: .attribute(.string("Alice")), type: .notEqualTo)))
53+
54+
let lessThan = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.age < 18 })
55+
#expect(lessThan == .comparison(.init(left: .keyPath("age"), right: .attribute(.int64(18)), type: .lessThan)))
56+
#expect(Self.people.filtered(by: lessThan).map(\.id.rawValue) == ["Bob"])
57+
58+
let greaterThanOrEqual = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.age >= 20 })
59+
#expect(greaterThanOrEqual == .comparison(.init(left: .keyPath("age"), right: .attribute(.int64(20)), type: .greaterThanOrEqualTo)))
60+
#expect(Self.people.filtered(by: greaterThanOrEqual).map(\.id.rawValue) == ["Alice", "Alina"])
61+
}
62+
63+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
64+
@Test func compound() throws {
65+
66+
// nested conjunctions are flattened into a single AND
67+
let and = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.age >= 18 && $0.isActive && $0.name != "Bob" })
68+
#expect(and == .compound(.and([
69+
.comparison(.init(left: .keyPath("age"), right: .attribute(.int64(18)), type: .greaterThanOrEqualTo)),
70+
.comparison(.init(left: .keyPath("isActive"), right: .attribute(.bool(true)), type: .equalTo)),
71+
.comparison(.init(left: .keyPath("name"), right: .attribute(.string("Bob")), type: .notEqualTo))
72+
])))
73+
#expect(Self.people.filtered(by: and).map(\.id.rawValue) == ["Alice", "Alina"])
74+
75+
let or = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.name == "Bob" || $0.age > 25 })
76+
#expect(or == .compound(.or([
77+
.comparison(.init(left: .keyPath("name"), right: .attribute(.string("Bob")), type: .equalTo)),
78+
.comparison(.init(left: .keyPath("age"), right: .attribute(.int64(25)), type: .greaterThan))
79+
])))
80+
#expect(Self.people.filtered(by: or).map(\.id.rawValue) == ["Alice", "Bob"])
81+
82+
let not = try FetchRequest.Predicate(#Predicate<PersonModel> { !$0.isActive })
83+
#expect(not == .compound(.not(.comparison(.init(left: .keyPath("isActive"), right: .attribute(.bool(true)), type: .equalTo)))))
84+
#expect(Self.people.filtered(by: not).map(\.id.rawValue) == ["Bob"])
85+
}
86+
87+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
88+
@Test func string() throws {
89+
90+
let contains = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.name.contains("li") })
91+
#expect(contains == .comparison(.init(left: .keyPath("name"), right: .attribute(.string("li")), type: .contains)))
92+
#expect(Self.people.filtered(by: contains).map(\.id.rawValue) == ["Alice", "Alina"])
93+
94+
let starts = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.name.starts(with: "Al") })
95+
#expect(starts == .comparison(.init(left: .keyPath("name"), right: .attribute(.string("Al")), type: .beginsWith)))
96+
#expect(Self.people.filtered(by: starts).map(\.id.rawValue) == ["Alice", "Alina"])
97+
}
98+
99+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
100+
@Test func range() throws {
101+
102+
// ranges lower to a compound comparison since BETWEEN isn't evaluatable
103+
let range = try FetchRequest.Predicate(#Predicate<PersonModel> { (18...25).contains($0.age) })
104+
#expect(range == .compound(.and([
105+
.comparison(.init(left: .keyPath("age"), right: .attribute(.int64(18)), type: .greaterThanOrEqualTo)),
106+
.comparison(.init(left: .keyPath("age"), right: .attribute(.int64(25)), type: .lessThanOrEqualTo))
107+
])))
108+
#expect(Self.people.filtered(by: range).map(\.id.rawValue) == ["Alina"])
109+
}
110+
111+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
112+
@Test func booleanProperty() throws {
113+
114+
// a boolean property used directly becomes an equality comparison
115+
let predicate = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.isActive })
116+
#expect(predicate == .comparison(.init(left: .keyPath("isActive"), right: .attribute(.bool(true)), type: .equalTo)))
117+
#expect(Self.people.filtered(by: predicate).map(\.id.rawValue) == ["Alice", "Alina"])
118+
}
119+
120+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
121+
@Test func constant() throws {
122+
123+
let predicate = try FetchRequest.Predicate(#Predicate<PersonModel> { _ in true })
124+
#expect(predicate == .value(true))
125+
#expect(Self.people.filtered(by: predicate).count == 3)
126+
}
127+
128+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
129+
@Test func optionalValue() throws {
130+
131+
let predicate = try FetchRequest.Predicate(#Predicate<PersonModel> { $0.nickname == nil })
132+
#expect(predicate == .comparison(.init(left: .keyPath("nickname"), right: .attribute(.null), type: .equalTo)))
133+
}
134+
135+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
136+
@Test func unsupported() {
137+
138+
// arithmetic has no CoreModel equivalent
139+
#expect(throws: FetchRequest.Predicate.ConversionError.self) {
140+
try FetchRequest.Predicate(#Predicate<PersonModel> { $0.age + 1 > 18 })
141+
}
142+
}
143+
144+
@available(macOS 14.0, iOS 17.0, tvOS 17.0, watchOS 10.0, *)
145+
@Test func fetchRequestEvaluation() throws {
146+
147+
let request = FetchRequest(
148+
entity: "Person",
149+
sortDescriptors: [.init(property: "age", ascending: true)],
150+
predicate: try FetchRequest.Predicate(#Predicate<PersonModel> { $0.age >= 18 })
151+
)
152+
let results = request.evaluate(Self.people)
153+
#expect(results.map(\.id.rawValue) == ["Alina", "Alice"])
154+
}
155+
}

0 commit comments

Comments
 (0)