Skip to content

Commit a08f405

Browse files
authored
Add test cases for existence filter with updated, removed, added documents (#11782)
1 parent 0e3f78b commit a08f405

File tree

2 files changed

+1527
-0
lines changed

2 files changed

+1527
-0
lines changed

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

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,164 @@ - (void)testResumingAQueryShouldUseBloomFilterToAvoidFullRequery {
12851285
}
12861286
}
12871287

1288+
- (void)
1289+
testBloomFilterShouldAvertAFullRequeryWhenDocumentsWereAddedDeletedRemovedUpdatedAndUnchangedSinceTheResumeToken {
1290+
// TODO(b/291365820): Stop skipping this test when running against the Firestore emulator once
1291+
// the emulator is improved to include a bloom filter in the existence filter messages that it
1292+
// sends.
1293+
XCTSkipIf([FSTIntegrationTestCase isRunningAgainstEmulator],
1294+
"Skip this test when running against the Firestore emulator because the emulator does "
1295+
"not include a bloom filter when it sends existence filter messages, making it "
1296+
"impossible for this test to verify the correctness of the bloom filter.");
1297+
1298+
// Set this test to stop when the first failure occurs because some test assertion failures make
1299+
// the rest of the test not applicable or will even crash.
1300+
[self setContinueAfterFailure:NO];
1301+
1302+
// Prepare the names and contents of the 20 documents to create.
1303+
NSMutableDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs =
1304+
[[NSMutableDictionary alloc] init];
1305+
for (int i = 0; i < 20; i++) {
1306+
[testDocs setValue:@{@"key" : @42, @"removed" : @NO}
1307+
forKey:[NSString stringWithFormat:@"doc%@", @(1000 + i)]];
1308+
}
1309+
1310+
// Each iteration of the "while" loop below runs a single iteration of the test. The test will
1311+
// be run multiple times only if a bloom filter false positive occurs.
1312+
int attemptNumber = 0;
1313+
while (true) {
1314+
attemptNumber++;
1315+
1316+
// Create 20 documents in a new collection.
1317+
FIRCollectionReference *collRef = [self collectionRefWithDocuments:testDocs];
1318+
FIRQuery *query = [collRef queryWhereField:@"removed" isEqualTo:@NO];
1319+
1320+
// Run a query to populate the local cache with the 20 documents and a resume token.
1321+
FIRQuerySnapshot *querySnapshot1 = [self readDocumentSetForRef:query
1322+
source:FIRFirestoreSourceDefault];
1323+
XCTAssertEqual(querySnapshot1.count, 20u, @"querySnapshot1.count has an unexpected value");
1324+
NSArray<FIRDocumentReference *> *createdDocuments =
1325+
FIRDocumentReferenceArrayFromQuerySnapshot(querySnapshot1);
1326+
1327+
// Out of the 20 existing documents, leave 5 docs untouched, delete 5 docs, remove 5 docs,
1328+
// update 5 docs, and add 15 new docs.
1329+
NSSet<NSString *> *deletedDocumentIds;
1330+
NSSet<NSString *> *removedDocumentIds;
1331+
NSSet<NSString *> *updatedDocumentIds;
1332+
NSMutableArray<NSString *> *addedDocumentIds = [[NSMutableArray alloc] init];
1333+
1334+
{
1335+
FIRFirestore *db2 = [self firestore];
1336+
FIRWriteBatch *batch = [db2 batch];
1337+
1338+
NSMutableArray<NSString *> *deletedDocumentIdsAccumulator = [[NSMutableArray alloc] init];
1339+
for (decltype(createdDocuments.count) i = 0; i < createdDocuments.count; i += 4) {
1340+
FIRDocumentReference *documentToDelete = [db2 documentWithPath:createdDocuments[i].path];
1341+
[batch deleteDocument:documentToDelete];
1342+
[deletedDocumentIdsAccumulator addObject:documentToDelete.documentID];
1343+
}
1344+
deletedDocumentIds = [NSSet setWithArray:deletedDocumentIdsAccumulator];
1345+
XCTAssertEqual(deletedDocumentIds.count, 5u, @"deletedDocumentIds has the wrong size");
1346+
1347+
// Update 5 documents to no longer match the query.
1348+
NSMutableArray<NSString *> *removedDocumentIdsAccumulator = [[NSMutableArray alloc] init];
1349+
for (decltype(createdDocuments.count) i = 1; i < createdDocuments.count; i += 4) {
1350+
FIRDocumentReference *documentToRemove = [db2 documentWithPath:createdDocuments[i].path];
1351+
[batch updateData:@{@"removed" : @YES} forDocument:documentToRemove];
1352+
[removedDocumentIdsAccumulator addObject:documentToRemove.documentID];
1353+
}
1354+
removedDocumentIds = [NSSet setWithArray:removedDocumentIdsAccumulator];
1355+
XCTAssertEqual(removedDocumentIds.count, 5u, @"removedDocumentIds has the wrong size");
1356+
1357+
// Update 5 documents, but ensure they still match the query.
1358+
NSMutableArray<NSString *> *updatedDocumentIdsAccumulator = [[NSMutableArray alloc] init];
1359+
for (decltype(createdDocuments.count) i = 2; i < createdDocuments.count; i += 4) {
1360+
FIRDocumentReference *documentToUpdate = [db2 documentWithPath:createdDocuments[i].path];
1361+
[batch updateData:@{@"key" : @43} forDocument:documentToUpdate];
1362+
[updatedDocumentIdsAccumulator addObject:documentToUpdate.documentID];
1363+
}
1364+
updatedDocumentIds = [NSSet setWithArray:updatedDocumentIdsAccumulator];
1365+
XCTAssertEqual(updatedDocumentIds.count, 5u, @"updatedDocumentIds has the wrong size");
1366+
1367+
for (int i = 0; i < 15; i += 1) {
1368+
FIRDocumentReference *documentToAdd = [db2
1369+
documentWithPath:[NSString stringWithFormat:@"%@/newDoc%@", collRef.path, @(1000 + i)]];
1370+
[batch setData:@{@"key" : @42, @"removed" : @NO} forDocument:documentToAdd];
1371+
[addedDocumentIds addObject:documentToAdd.documentID];
1372+
}
1373+
1374+
// Ensure the documentIds above are mutually exclusive.
1375+
NSMutableSet<NSString *> *mergedSet = [NSMutableSet setWithArray:addedDocumentIds];
1376+
[mergedSet unionSet:deletedDocumentIds];
1377+
[mergedSet unionSet:removedDocumentIds];
1378+
[mergedSet unionSet:updatedDocumentIds];
1379+
XCTAssertEqual(mergedSet.count, 30u, @"There are documents experienced multiple operations.");
1380+
1381+
[self commitWriteBatch:batch];
1382+
}
1383+
1384+
// Wait for 10 seconds, during which Watch will stop tracking the query and will send an
1385+
// existence filter rather than "delete" events when the query is resumed.
1386+
[NSThread sleepForTimeInterval:10.0f];
1387+
1388+
// Resume the query and save the resulting snapshot for verification. Use some internal testing
1389+
// hooks to "capture" the existence filter mismatches to verify that Watch sent a bloom
1390+
// filter, and it was used to avert a full requery.
1391+
__block FIRQuerySnapshot *querySnapshot2;
1392+
NSArray<FSTTestingHooksExistenceFilterMismatchInfo *> *existenceFilterMismatches =
1393+
[FSTTestingHooks captureExistenceFilterMismatchesDuringBlock:^{
1394+
querySnapshot2 = [self readDocumentSetForRef:query source:FIRFirestoreSourceDefault];
1395+
}];
1396+
XCTAssertEqual(querySnapshot2.count, 25u, @"querySnapshot1.count has an unexpected value");
1397+
1398+
// Verify that the snapshot from the resumed query contains the expected documents; that is, 10
1399+
// existing documents that still match the query, and 15 documents that are newly added.
1400+
{
1401+
NSMutableArray<NSString *> *expectedDocumentIds = [[NSMutableArray alloc] init];
1402+
for (FIRDocumentReference *documentRef in createdDocuments) {
1403+
if (![deletedDocumentIds containsObject:documentRef.documentID] &&
1404+
![removedDocumentIds containsObject:documentRef.documentID]) {
1405+
[expectedDocumentIds addObject:documentRef.documentID];
1406+
}
1407+
}
1408+
[expectedDocumentIds addObjectsFromArray:addedDocumentIds];
1409+
XCTAssertEqualObjects([NSSet setWithArray:FIRQuerySnapshotGetIDs(querySnapshot2)],
1410+
[NSSet setWithArray:expectedDocumentIds],
1411+
@"querySnapshot2 has the wrong documents");
1412+
}
1413+
1414+
// Verify that Watch sent an existence filter with the correct counts when the query was
1415+
// resumed.
1416+
XCTAssertEqual(existenceFilterMismatches.count, 1u,
1417+
@"Watch should have sent exactly 1 existence filter");
1418+
FSTTestingHooksExistenceFilterMismatchInfo *existenceFilterMismatchInfo =
1419+
existenceFilterMismatches[0];
1420+
XCTAssertEqual(existenceFilterMismatchInfo.localCacheCount, 35);
1421+
XCTAssertEqual(existenceFilterMismatchInfo.existenceFilterCount, 25);
1422+
1423+
// Verify that Watch sent a valid bloom filter.
1424+
FSTTestingHooksBloomFilter *bloomFilter = existenceFilterMismatchInfo.bloomFilter;
1425+
XCTAssertNotNil(bloomFilter,
1426+
"Watch should have included a bloom filter in the existence filter");
1427+
1428+
// Verify that the bloom filter was successfully used to avert a full requery. If a false
1429+
// positive occurred then retry the entire test. Although statistically rare, false positives
1430+
// are expected to happen occasionally. When a false positive _does_ happen, just retry the test
1431+
// with a different set of documents. If that retry _also_ experiences a false positive, then
1432+
// fail the test because that is so improbable that something must have gone wrong.
1433+
if (attemptNumber == 1 && !bloomFilter.applied) {
1434+
continue;
1435+
}
1436+
1437+
XCTAssertTrue(bloomFilter.applied,
1438+
@"The bloom filter should have been successfully applied with attemptNumber=%@",
1439+
@(attemptNumber));
1440+
1441+
// Break out of the test loop now that the test passes.
1442+
break;
1443+
}
1444+
}
1445+
12881446
- (void)testBloomFilterShouldCorrectlyEncodeComplexUnicodeCharacters {
12891447
// TODO(b/291365820): Stop skipping this test when running against the Firestore emulator once
12901448
// the emulator is improved to include a bloom filter in the existence filter messages that it

0 commit comments

Comments
 (0)