Skip to content

Commit be45ad4

Browse files
committed
add more tests
1 parent aad77ae commit be45ad4

File tree

5 files changed

+239
-41
lines changed

5 files changed

+239
-41
lines changed

Firestore/Source/API/FIRPipelineBridge.mm

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -817,12 +817,20 @@ - (id)initWithName:(NSString *)name
817817
} else if ([value isKindOfClass:[FIRAggregateFunctionBridge class]]) {
818818
return [((FIRAggregateFunctionBridge *)value) cppExprWithReader:reader]->to_proto();
819819
} else if ([value isKindOfClass:[NSDictionary class]]) {
820-
NSDictionary<NSString *, FIRExprBridge *> *dictionary =
821-
(NSDictionary<NSString *, FIRExprBridge *> *)value;
820+
NSDictionary<NSString *, id> *dictionary = (NSDictionary<NSString *, id> *)value;
822821

823822
std::unordered_map<std::string, firebase::firestore::google_firestore_v1_Value> cpp_dictionary;
824823
for (NSString *key in dictionary) {
825-
cpp_dictionary[MakeString(key)] = [dictionary[key] cppExprWithReader:reader]->to_proto();
824+
if ([dictionary[key] isKindOfClass:[FIRExprBridge class]]) {
825+
cpp_dictionary[MakeString(key)] =
826+
[((FIRExprBridge *)dictionary[key]) cppExprWithReader:reader]->to_proto();
827+
} else if ([dictionary[key] isKindOfClass:[FIRAggregateFunctionBridge class]]) {
828+
cpp_dictionary[MakeString(key)] =
829+
[((FIRAggregateFunctionBridge *)dictionary[key]) cppExprWithReader:reader]->to_proto();
830+
} else {
831+
ThrowInvalidArgument(
832+
"Dictionary value must be an FIRExprBridge or FIRAggregateFunctionBridge.");
833+
}
826834
}
827835

828836
firebase::firestore::google_firestore_v1_Value result;
@@ -836,7 +844,7 @@ - (id)initWithName:(NSString *)name
836844
});
837845
return result;
838846
} else {
839-
ThrowInvalidArgument("Subscript key must be an NSString or FIRFieldPath.");
847+
ThrowInvalidArgument("Invalid value to convert to google_firestore_v1_Value.");
840848
}
841849
}
842850

Firestore/Swift/Source/Helper/PipelineHelper.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ enum Helper {
5959
return Constant.nil.bridge
6060
}
6161

62-
// Step 2: Check for NSNull.
6362
guard !(value is NSNull) else {
6463
return Constant.nil.bridge
6564
}
@@ -69,7 +68,12 @@ enum Helper {
6968
} else if value is AggregateFunction {
7069
return (value as! AggregateFunction).toBridge()
7170
} else if value is [String: Sendable?] {
72-
let mappedValue = (value as! [String: Sendable?]).mapValues { sendableToExpr($0).toBridge() }
71+
let mappedValue: [String: Sendable?] = (value as! [String: Sendable?]).mapValues {
72+
if $0 is AggregateFunction {
73+
return ($0 as! AggregateFunction).toBridge()
74+
}
75+
return sendableToExpr($0).toBridge()
76+
}
7377
return mappedValue as NSDictionary
7478
} else {
7579
return Constant(value).bridge

Firestore/Swift/Source/SwiftAPI/Stages.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ class Unnest: Stage {
340340
bridge = UnnestStageBridge(
341341
field: self.field.toBridge(),
342342
alias: alias.toBridge(),
343-
indexField: indexField.map { Field($0) } ?? nil
343+
indexField: indexField.map { Field($0).toBridge() } ?? nil
344344
)
345345
}
346346
}

Firestore/Swift/Tests/Integration/PipelineTests.swift

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,164 @@ class PipelineIntegrationTests: FSTIntegrationTestCase {
11221122
)
11231123
}
11241124

1125-
// TODO:
1125+
func testCanAddFields() async throws {
1126+
let collRef = collectionRef(withDocuments: bookDocs)
1127+
let db = collRef.firestore
1128+
1129+
let pipeline = db.pipeline()
1130+
.collection(collRef.path)
1131+
.sort(Field("author").ascending())
1132+
.limit(1)
1133+
.select("title", "author")
1134+
.rawStage(
1135+
name: "add_fields",
1136+
params: [
1137+
[
1138+
"display": Field("title").strConcat(
1139+
Constant(" - "),
1140+
Field("author")
1141+
),
1142+
],
1143+
]
1144+
)
1145+
1146+
let snapshot = try await pipeline.execute()
1147+
1148+
TestHelper.compare(
1149+
pipelineSnapshot: snapshot,
1150+
expected: [
1151+
[
1152+
"title": "The Hitchhiker's Guide to the Galaxy",
1153+
"author": "Douglas Adams",
1154+
"display": "The Hitchhiker's Guide to the Galaxy - Douglas Adams",
1155+
],
1156+
],
1157+
enforceOrder: false
1158+
)
1159+
}
1160+
1161+
func testCanPerformDistinctQuery() async throws {
1162+
let collRef = collectionRef(withDocuments: bookDocs)
1163+
let db = collRef.firestore
1164+
1165+
let pipeline = db.pipeline()
1166+
.collection(collRef.path)
1167+
.select("title", "author", "rating")
1168+
.rawStage(
1169+
name: "distinct",
1170+
params: [
1171+
["rating": Field("rating")],
1172+
]
1173+
)
1174+
.sort(Field("rating").descending())
1175+
1176+
let snapshot = try await pipeline.execute()
1177+
1178+
TestHelper.compare(
1179+
pipelineSnapshot: snapshot,
1180+
expected: [
1181+
["rating": 4.7],
1182+
["rating": 4.6],
1183+
["rating": 4.5],
1184+
["rating": 4.3],
1185+
["rating": 4.2],
1186+
["rating": 4.1],
1187+
["rating": 4.0],
1188+
],
1189+
enforceOrder: true
1190+
)
1191+
}
1192+
1193+
func testCanPerformAggregateQuery() async throws {
1194+
let collRef = collectionRef(withDocuments: bookDocs)
1195+
let db = collRef.firestore
1196+
1197+
let emptySendableDictionary: [String: Sendable?] = [:]
1198+
1199+
let pipeline = db.pipeline()
1200+
.collection(collRef.path)
1201+
.select("title", "author", "rating")
1202+
.rawStage(
1203+
name: "aggregate",
1204+
params: [
1205+
[
1206+
"averageRating": Field("rating").avg(),
1207+
],
1208+
emptySendableDictionary,
1209+
]
1210+
)
1211+
1212+
let snapshot = try await pipeline.execute()
1213+
1214+
TestHelper.compare(
1215+
pipelineSnapshot: snapshot,
1216+
expected: [
1217+
[
1218+
"averageRating": 4.3100000000000005,
1219+
],
1220+
],
1221+
enforceOrder: true
1222+
)
1223+
}
1224+
1225+
func testCanFilterWithWhere() async throws {
1226+
let collRef = collectionRef(withDocuments: bookDocs)
1227+
let db = collRef.firestore
1228+
1229+
let pipeline = db.pipeline()
1230+
.collection(collRef.path)
1231+
.select("title", "author")
1232+
.rawStage(
1233+
name: "where",
1234+
params: [Field("author").eq("Douglas Adams")]
1235+
)
1236+
1237+
let snapshot = try await pipeline.execute()
1238+
1239+
TestHelper.compare(
1240+
pipelineSnapshot: snapshot,
1241+
expected: [
1242+
[
1243+
"title": "The Hitchhiker's Guide to the Galaxy",
1244+
"author": "Douglas Adams",
1245+
],
1246+
],
1247+
enforceOrder: false
1248+
)
1249+
}
1250+
1251+
func testCanLimitOffsetAndSort() async throws {
1252+
let collRef = collectionRef(withDocuments: bookDocs)
1253+
let db = collRef.firestore
1254+
1255+
let pipeline = db.pipeline()
1256+
.collection(collRef.path)
1257+
.select("title", "author")
1258+
.rawStage(
1259+
name: "sort",
1260+
params: [
1261+
[
1262+
"direction": "ascending",
1263+
"expression": Field("author"),
1264+
],
1265+
]
1266+
)
1267+
.rawStage(name: "offset", params: [3])
1268+
.rawStage(name: "limit", params: [1])
1269+
1270+
let snapshot = try await pipeline.execute()
1271+
1272+
TestHelper.compare(
1273+
pipelineSnapshot: snapshot,
1274+
expected: [
1275+
[
1276+
"author": "Fyodor Dostoevsky",
1277+
"title": "Crime and Punishment",
1278+
],
1279+
],
1280+
enforceOrder: false
1281+
)
1282+
}
11261283

11271284
// MARK: - Replace Stage Test
11281285

@@ -1153,6 +1310,31 @@ class PipelineIntegrationTests: FSTIntegrationTestCase {
11531310
)
11541311
}
11551312

1313+
func testReplaceWithExprResult() async throws {
1314+
let collRef = collectionRef(withDocuments: bookDocs)
1315+
let db = collRef.firestore
1316+
1317+
let pipeline = db.pipeline()
1318+
.collection(collRef.path)
1319+
.where(Field("title").eq("The Hitchhiker's Guide to the Galaxy"))
1320+
.replace(with:
1321+
MapExpression([
1322+
"foo": "bar",
1323+
"baz": MapExpression([
1324+
"title": Field("title"),
1325+
]),
1326+
]))
1327+
1328+
let snapshot = try await pipeline.execute()
1329+
1330+
let expectedResults: [String: Sendable?] = [
1331+
"foo": "bar",
1332+
"baz": ["title": "The Hitchhiker's Guide to the Galaxy"],
1333+
]
1334+
1335+
TestHelper.compare(pipelineSnapshot: snapshot, expected: [expectedResults], enforceOrder: false)
1336+
}
1337+
11561338
// MARK: - Sample Stage Tests
11571339

11581340
func testSampleStageLimit3() async throws {
@@ -1212,7 +1394,7 @@ class PipelineIntegrationTests: FSTIntegrationTestCase {
12121394
let pipeline = db.pipeline()
12131395
.collection(collRef.path)
12141396
.where(Field("title").eq("The Hitchhiker's Guide to the Galaxy"))
1215-
.unnest(Field("tags").as("tag"))
1397+
.unnest(Field("tags").as("tag"), indexField: "tagsIndex")
12161398
.select(
12171399
"title",
12181400
"author",

Firestore/Swift/Tests/TestHelper/TestHelper.swift

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ public enum TestHelper {
9898
}
9999

100100
// MARK: - Internal helper
101-
101+
102102
private static func isNilOrNSNull(_ value: Sendable?) -> Bool {
103103
// First, use a `guard` to safely unwrap the optional.
104104
// If it's nil, we can immediately return true.
@@ -137,9 +137,11 @@ public enum TestHelper {
137137
case let (v1 as Int, v2 as Int):
138138
return v1 == v2
139139
case let (v1 as Double, v2 as Double):
140-
return v1 == v2
140+
let doubleEpsilon = 0.000001
141+
return abs(v1 - v2) <= doubleEpsilon
141142
case let (v1 as Float, v2 as Float):
142-
return v1 == v2
143+
let floatEpsilon: Float = 0.00001
144+
return abs(v1 - v2) <= floatEpsilon
143145
case let (v1 as String, v2 as String):
144146
return v1 == v2
145147
case let (v1 as Bool, v2 as Bool):
@@ -159,10 +161,13 @@ public enum TestHelper {
159161

160162
for (key, value1) in dict1 {
161163
guard let value2 = dict2[key], areEqual(value1, value2) else {
162-
print("The Dictionary value is not equal.")
163-
print("key1: \(key)")
164-
print("value1: \(String(describing: value1))")
165-
print("value2: \(String(describing: dict2[key]))")
164+
XCTFail("""
165+
Dictionary value mismatch for key: '\(key)'
166+
Expected value: '\(String(describing: value1))' (from dict1)
167+
Actual value: '\(String(describing: dict2[key]))' (from dict2)
168+
Full dict1: \(String(describing: dict1))
169+
Full dict2: \(String(describing: dict2))
170+
""")
166171
return false
167172
}
168173
}
@@ -175,9 +180,11 @@ public enum TestHelper {
175180
for (index, value1) in array1.enumerated() {
176181
let value2 = array2[index]
177182
if !areEqual(value1, value2) {
178-
print("The Array value is not equal.")
179-
print("value1: \(String(describing: value1))")
180-
print("value2: \(String(describing: value2))")
183+
XCTFail("""
184+
Array value mismatch.
185+
Expected array value: '\(String(describing: value1))'
186+
Actual array value: '\(String(describing: value2))'
187+
""")
181188
return false
182189
}
183190
}
@@ -192,32 +199,29 @@ public enum TestHelper {
192199
return false
193200
}
194201

195-
// 2. Sort both arrays based on a canonical representation of the dictionaries.
196-
let sortedArray1 = array1.sorted { dict1, dict2 -> Bool in
197-
// Create a comparable representation of the dictionaries.
198-
// Here, we sort by keys and create a descriptive string.
199-
let dict1String = dict1.keys.sorted().map { "\($0):\(String(describing: dict1[$0]!))" }
200-
.joined(separator: ",")
201-
let dict2String = dict2.keys.sorted().map { "\($0):\(String(describing: dict2[$0]!))" }
202-
.joined(separator: ",")
203-
return dict1String < dict2String
204-
}
205-
206-
let sortedArray2 = array2.sorted { dict1, dict2 -> Bool in
207-
let dict1String = dict1.keys.sorted().map { "\($0):\(String(describing: dict1[$0]!))" }
208-
.joined(separator: ",")
209-
let dict2String = dict2.keys.sorted().map { "\($0):\(String(describing: dict2[$0]!))" }
210-
.joined(separator: ",")
211-
return dict1String < dict2String
212-
}
202+
// Create a mutable copy of array2 to remove matched dictionaries
203+
var mutableArray2 = array2
204+
205+
// Iterate through each dictionary in array1
206+
for dict1 in array1 {
207+
var foundMatch = false
208+
// Try to find an equivalent dictionary in mutableArray2
209+
if let index = mutableArray2.firstIndex(where: { dict2 in
210+
areDictionariesEqual(dict1, dict2) // Use our deep comparison function
211+
}) {
212+
// If a match is found, remove it from mutableArray2 to handle duplicates
213+
mutableArray2.remove(at: index)
214+
foundMatch = true
215+
}
213216

214-
// 3. Compare the sorted arrays element by element using our dictionary comparison function.
215-
for (dict1, dict2) in zip(sortedArray1, sortedArray2) {
216-
if !areDictionariesEqual(dict1, dict2) {
217+
// If no match was found for the current dictionary from array1, arrays are not equal
218+
if !foundMatch {
217219
return false
218220
}
219221
}
220222

221-
return true
223+
// If we've iterated through all of array1 and mutableArray2 is empty,
224+
// it means all dictionaries found a unique match.
225+
return mutableArray2.isEmpty
222226
}
223227
}

0 commit comments

Comments
 (0)