Skip to content

Commit 0c58855

Browse files
authored
Firestore: FIRQueryTests.mm: add a test that resumes a query with existence filter (#10974)
1 parent 216956f commit 0c58855

File tree

3 files changed

+147
-2
lines changed

3 files changed

+147
-2
lines changed

Firestore/Example/Tests/Integration/API/FIRQueryTests.mm

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@
2222
#import "Firestore/Example/Tests/Util/FSTHelpers.h"
2323
#import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h"
2424

25+
namespace {
26+
27+
NSArray<NSString *> *SortedStringsNotIn(NSSet<NSString *> *set, NSSet<NSString *> *remove) {
28+
NSMutableSet<NSString *> *mutableSet = [NSMutableSet setWithSet:set];
29+
[mutableSet minusSet:remove];
30+
return [mutableSet.allObjects sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
31+
}
32+
33+
} // namespace
34+
2535
@interface FIRQueryTests : FSTIntegrationTestCase
2636
@end
2737

@@ -1180,4 +1190,88 @@ - (void)testOrderByEquality {
11801190
matchesResult:@[ @"doc6", @"doc3" ]];
11811191
}
11821192

1193+
- (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes {
1194+
// Prepare the names and contents of the 100 documents to create.
1195+
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs =
1196+
[[NSMutableDictionary alloc] init];
1197+
for (int i = 0; i < 100; i++) {
1198+
[testDocs setValue:@{@"key" : @42} forKey:[NSString stringWithFormat:@"doc%@", @(1000 + i)]];
1199+
}
1200+
1201+
// Create 100 documents in a new collection.
1202+
FIRCollectionReference *collRef = [self collectionRefWithDocuments:testDocs];
1203+
1204+
// Run a query to populate the local cache with the 100 documents and a resume token.
1205+
FIRQuerySnapshot *querySnapshot1 = [self readDocumentSetForRef:collRef
1206+
source:FIRFirestoreSourceDefault];
1207+
XCTAssertEqual(querySnapshot1.count, 100, @"querySnapshot1.count has an unexpected value");
1208+
NSArray<FIRDocumentReference *> *createdDocuments =
1209+
FIRDocumentReferenceArrayFromQuerySnapshot(querySnapshot1);
1210+
1211+
// Delete 50 of the 100 documents. Do this in a transaction, rather than
1212+
// [FIRDocumentReference deleteDocument], to avoid affecting the local cache.
1213+
NSSet<NSString *> *deletedDocumentIds;
1214+
{
1215+
NSMutableArray<NSString *> *deletedDocumentIdsAccumulator = [[NSMutableArray alloc] init];
1216+
XCTestExpectation *expectation = [self expectationWithDescription:@"DeleteTransaction"];
1217+
[collRef.firestore
1218+
runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **) {
1219+
for (decltype(createdDocuments.count) i = 0; i < createdDocuments.count; i += 2) {
1220+
FIRDocumentReference *documentToDelete = createdDocuments[i];
1221+
[transaction deleteDocument:documentToDelete];
1222+
[deletedDocumentIdsAccumulator addObject:documentToDelete.documentID];
1223+
}
1224+
return @"document deletion successful";
1225+
}
1226+
completion:^(id, NSError *) {
1227+
[expectation fulfill];
1228+
}];
1229+
[self awaitExpectation:expectation];
1230+
deletedDocumentIds = [NSSet setWithArray:deletedDocumentIdsAccumulator];
1231+
}
1232+
XCTAssertEqual(deletedDocumentIds.count, 50u, @"deletedDocumentIds has the wrong size");
1233+
1234+
// Wait for 10 seconds, during which Watch will stop tracking the query and will send an existence
1235+
// filter rather than "delete" events when the query is resumed.
1236+
[NSThread sleepForTimeInterval:10.0f];
1237+
1238+
// Resume the query and save the resulting snapshot for verification.
1239+
FIRQuerySnapshot *querySnapshot2 = [self readDocumentSetForRef:collRef
1240+
source:FIRFirestoreSourceDefault];
1241+
1242+
// Verify that the snapshot from the resumed query contains the expected documents; that is,
1243+
// that it contains the 50 documents that were _not_ deleted.
1244+
// TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed to
1245+
// send an existence filter. At the time of writing, the Firestore emulator fails to send an
1246+
// existence filter, resulting in the client including the deleted documents in the snapshot
1247+
// of the resumed query.
1248+
if (!([FSTIntegrationTestCase isRunningAgainstEmulator] && querySnapshot2.count == 100)) {
1249+
NSSet<NSString *> *actualDocumentIds =
1250+
[NSSet setWithArray:FIRQuerySnapshotGetIDs(querySnapshot2)];
1251+
NSSet<NSString *> *expectedDocumentIds;
1252+
{
1253+
NSMutableArray<NSString *> *expectedDocumentIdsAccumulator = [[NSMutableArray alloc] init];
1254+
for (FIRDocumentReference *documentRef in createdDocuments) {
1255+
if (![deletedDocumentIds containsObject:documentRef.documentID]) {
1256+
[expectedDocumentIdsAccumulator addObject:documentRef.documentID];
1257+
}
1258+
}
1259+
expectedDocumentIds = [NSSet setWithArray:expectedDocumentIdsAccumulator];
1260+
}
1261+
if (![actualDocumentIds isEqualToSet:expectedDocumentIds]) {
1262+
NSArray<NSString *> *unexpectedDocumentIds =
1263+
SortedStringsNotIn(actualDocumentIds, expectedDocumentIds);
1264+
NSArray<NSString *> *missingDocumentIds =
1265+
SortedStringsNotIn(expectedDocumentIds, actualDocumentIds);
1266+
XCTFail(@"querySnapshot2 contained %lu documents (expected %lu): "
1267+
@"%lu unexpected and %lu missing; "
1268+
@"unexpected documents: %@; missing documents: %@",
1269+
(unsigned long)actualDocumentIds.count, (unsigned long)expectedDocumentIds.count,
1270+
(unsigned long)unexpectedDocumentIds.count, (unsigned long)missingDocumentIds.count,
1271+
[unexpectedDocumentIds componentsJoinedByString:@", "],
1272+
[missingDocumentIds componentsJoinedByString:@", "]);
1273+
}
1274+
}
1275+
}
1276+
11831277
@end

Firestore/Example/Tests/Util/FSTIntegrationTestCase.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@ NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs);
142142
* in order of { type, doc title, doc data }. */
143143
NSArray<NSArray<id> *> *FIRQuerySnapshotGetDocChangesData(FIRQuerySnapshot *docs);
144144

145+
/** Gets the FIRDocumentReference objects from a FIRQuerySnapshot and returns them. */
146+
NSArray<FIRDocumentReference *> *FIRDocumentReferenceArrayFromQuerySnapshot(FIRQuerySnapshot *);
147+
145148
#if __cplusplus
146149
} // extern "C"
147150
#endif

Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#import <FirebaseFirestore/FIRQuerySnapshot.h>
2626
#import <FirebaseFirestore/FIRSnapshotMetadata.h>
2727
#import <FirebaseFirestore/FIRTransaction.h>
28+
#import <FirebaseFirestore/FIRWriteBatch.h>
2829

2930
#include <exception>
3031
#include <memory>
@@ -393,11 +394,48 @@ - (FIRCollectionReference *)collectionRefWithDocuments:
393394

394395
- (void)writeAllDocuments:(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents
395396
toCollection:(FIRCollectionReference *)collection {
397+
NSMutableArray<XCTestExpectation *> *commits = [[NSMutableArray alloc] init];
398+
__block FIRWriteBatch *writeBatch = nil;
399+
__block int writeBatchSize = 0;
400+
396401
[documents enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary<NSString *, id> *value,
397402
BOOL *) {
398-
FIRDocumentReference *ref = [collection documentWithPath:key];
399-
[self writeDocumentRef:ref data:value];
403+
if (writeBatch == nil) {
404+
writeBatch = [collection.firestore batch];
405+
}
406+
407+
[writeBatch setData:value forDocument:[collection documentWithPath:key]];
408+
writeBatchSize++;
409+
410+
// Write batches are capped at 500 writes. Use 400 just to be safe.
411+
if (writeBatchSize == 400) {
412+
XCTestExpectation *commitExpectation = [self expectationWithDescription:@"WriteBatch commit"];
413+
[writeBatch commitWithCompletion:^(NSError *_Nullable error) {
414+
[commitExpectation fulfill];
415+
if (error != nil) {
416+
XCTFail(@"WriteBatch commit failed: %@", error);
417+
}
418+
}];
419+
[commits addObject:commitExpectation];
420+
writeBatch = nil;
421+
writeBatchSize = 0;
422+
}
400423
}];
424+
425+
if (writeBatch != nil) {
426+
XCTestExpectation *commitExpectation = [self expectationWithDescription:@"WriteBatch commit"];
427+
[writeBatch commitWithCompletion:^(NSError *_Nullable error) {
428+
[commitExpectation fulfill];
429+
if (error != nil) {
430+
XCTFail(@"WriteBatch commit failed: %@", error);
431+
}
432+
}];
433+
[commits addObject:commitExpectation];
434+
}
435+
436+
for (XCTestExpectation *commitExpectation in commits) {
437+
[self awaitExpectation:commitExpectation];
438+
}
401439
}
402440

403441
- (void)readerAndWriterOnDocumentRef:(void (^)(FIRDocumentReference *readerRef,
@@ -590,6 +628,16 @@ - (void)waitUntil:(BOOL (^)())predicate {
590628
return result;
591629
}
592630

631+
extern "C" NSArray<FIRDocumentReference *> *FIRDocumentReferenceArrayFromQuerySnapshot(
632+
FIRQuerySnapshot *docs) {
633+
NSMutableArray<FIRDocumentReference *> *documentReferenceAccumulator =
634+
[[NSMutableArray alloc] init];
635+
for (FIRDocumentSnapshot *documentSnapshot in docs.documents) {
636+
[documentReferenceAccumulator addObject:documentSnapshot.reference];
637+
}
638+
return [documentReferenceAccumulator copy];
639+
}
640+
593641
@end
594642

595643
NS_ASSUME_NONNULL_END

0 commit comments

Comments
 (0)