@@ -1192,7 +1192,10 @@ - (void)testOrderByEquality {
1192
1192
matchesResult: @[ @" doc6" , @" doc3" ]];
1193
1193
}
1194
1194
1195
- - (void )testResumingAQueryShouldUseExistenceFilterToDetectDeletes {
1195
+ - (void )testResumingAQueryShouldUseBloomFilterToAvoidFullRequery {
1196
+ using firebase::firestore::testutil::CaptureExistenceFilterMismatches;
1197
+ using firebase::firestore::util::TestingHooks;
1198
+
1196
1199
// Set this test to stop when the first failure occurs because some test assertion failures make
1197
1200
// the rest of the test not applicable or will even crash.
1198
1201
[self setContinueAfterFailure: NO ];
@@ -1204,100 +1207,135 @@ - (void)testResumingAQueryShouldUseExistenceFilterToDetectDeletes {
1204
1207
[testDocs setValue: @{@" key" : @42 } forKey: [NSString stringWithFormat: @" doc%@ " , @(1000 + i)]];
1205
1208
}
1206
1209
1207
- // Create 100 documents in a new collection.
1208
- FIRCollectionReference *collRef = [self collectionRefWithDocuments: testDocs];
1209
-
1210
- // Run a query to populate the local cache with the 100 documents and a resume token.
1211
- FIRQuerySnapshot *querySnapshot1 = [self readDocumentSetForRef: collRef
1212
- source: FIRFirestoreSourceDefault];
1213
- XCTAssertEqual (querySnapshot1.count , 100 , @" querySnapshot1.count has an unexpected value" );
1214
- NSArray <FIRDocumentReference *> *createdDocuments =
1215
- FIRDocumentReferenceArrayFromQuerySnapshot (querySnapshot1);
1216
-
1217
- // Delete 50 of the 100 documents. Do this in a transaction, rather than
1218
- // [FIRDocumentReference deleteDocument], to avoid affecting the local cache.
1219
- NSSet <NSString *> *deletedDocumentIds;
1220
- {
1221
- NSMutableArray <NSString *> *deletedDocumentIdsAccumulator = [[NSMutableArray alloc ] init ];
1222
- XCTestExpectation *expectation = [self expectationWithDescription: @" DeleteTransaction" ];
1223
- [collRef.firestore
1224
- runTransactionWithBlock: ^id _Nullable (FIRTransaction *transaction, NSError **) {
1225
- for (decltype (createdDocuments.count ) i = 0 ; i < createdDocuments.count ; i += 2 ) {
1226
- FIRDocumentReference *documentToDelete = createdDocuments[i];
1227
- [transaction deleteDocument: documentToDelete];
1228
- [deletedDocumentIdsAccumulator addObject: documentToDelete.documentID];
1229
- }
1230
- return @" document deletion successful" ;
1231
- }
1232
- completion: ^(id , NSError *) {
1233
- [expectation fulfill ];
1234
- }];
1235
- [self awaitExpectation: expectation];
1236
- deletedDocumentIds = [NSSet setWithArray: deletedDocumentIdsAccumulator];
1237
- }
1238
- XCTAssertEqual (deletedDocumentIds.count , 50u , @" deletedDocumentIds has the wrong size" );
1239
-
1240
- // Wait for 10 seconds, during which Watch will stop tracking the query and will send an existence
1241
- // filter rather than "delete" events when the query is resumed.
1242
- [NSThread sleepForTimeInterval: 10 .0f ];
1243
-
1244
- // Resume the query and save the resulting snapshot for verification.
1245
- // Use some internal testing hooks to "capture" the existence filter mismatches to verify them.
1246
- FIRQuerySnapshot *querySnapshot2;
1247
- std::vector<firebase::firestore::util::TestingHooks::ExistenceFilterMismatchInfo>
1248
- existence_filter_mismatches =
1249
- firebase::firestore::testutil::CaptureExistenceFilterMismatches ([&] {
1250
- querySnapshot2 = [self readDocumentSetForRef: collRef source: FIRFirestoreSourceDefault];
1251
- });
1252
-
1253
- // Verify that the snapshot from the resumed query contains the expected documents; that is,
1254
- // that it contains the 50 documents that were _not_ deleted.
1255
- // TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed to
1256
- // send an existence filter. At the time of writing, the Firestore emulator fails to send an
1257
- // existence filter, resulting in the client including the deleted documents in the snapshot
1258
- // of the resumed query.
1259
- if (!([FSTIntegrationTestCase isRunningAgainstEmulator ] && querySnapshot2.count == 100 )) {
1260
- NSSet <NSString *> *actualDocumentIds =
1261
- [NSSet setWithArray: FIRQuerySnapshotGetIDs (querySnapshot2)];
1262
- NSSet <NSString *> *expectedDocumentIds;
1210
+ // Each iteration of the "while" loop below runs a single iteration of the test. The test will
1211
+ // be run multiple times only if a bloom filter false positive occurs.
1212
+ int attemptNumber = 0 ;
1213
+ while (true ) {
1214
+ attemptNumber++;
1215
+
1216
+ // Create 100 documents in a new collection.
1217
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments: testDocs];
1218
+
1219
+ // Run a query to populate the local cache with the 100 documents and a resume token.
1220
+ FIRQuerySnapshot *querySnapshot1 = [self readDocumentSetForRef: collRef
1221
+ source: FIRFirestoreSourceDefault];
1222
+ XCTAssertEqual (querySnapshot1.count , 100 , @" querySnapshot1.count has an unexpected value" );
1223
+ NSArray <FIRDocumentReference *> *createdDocuments =
1224
+ FIRDocumentReferenceArrayFromQuerySnapshot (querySnapshot1);
1225
+
1226
+ // Delete 50 of the 100 documents. Do this in a transaction, rather than
1227
+ // [FIRDocumentReference deleteDocument], to avoid affecting the local cache.
1228
+ NSSet <NSString *> *deletedDocumentIds;
1263
1229
{
1264
- NSMutableArray <NSString *> *expectedDocumentIdsAccumulator = [[NSMutableArray alloc ] init ];
1265
- for (FIRDocumentReference *documentRef in createdDocuments) {
1266
- if (![deletedDocumentIds containsObject: documentRef.documentID]) {
1267
- [expectedDocumentIdsAccumulator addObject: documentRef.documentID];
1230
+ NSMutableArray <NSString *> *deletedDocumentIdsAccumulator = [[NSMutableArray alloc ] init ];
1231
+ XCTestExpectation *expectation = [self expectationWithDescription: @" DeleteTransaction" ];
1232
+ [collRef.firestore
1233
+ runTransactionWithBlock: ^id _Nullable (FIRTransaction *transaction, NSError **) {
1234
+ for (decltype (createdDocuments.count ) i = 0 ; i < createdDocuments.count ; i += 2 ) {
1235
+ FIRDocumentReference *documentToDelete = createdDocuments[i];
1236
+ [transaction deleteDocument: documentToDelete];
1237
+ [deletedDocumentIdsAccumulator addObject: documentToDelete.documentID];
1238
+ }
1239
+ return @" document deletion successful" ;
1240
+ }
1241
+ completion: ^(id , NSError *) {
1242
+ [expectation fulfill ];
1243
+ }];
1244
+ [self awaitExpectation: expectation];
1245
+ deletedDocumentIds = [NSSet setWithArray: deletedDocumentIdsAccumulator];
1246
+ }
1247
+ XCTAssertEqual (deletedDocumentIds.count , 50u , @" deletedDocumentIds has the wrong size" );
1248
+
1249
+ // Wait for 10 seconds, during which Watch will stop tracking the query and will send an
1250
+ // existence filter rather than "delete" events when the query is resumed.
1251
+ [NSThread sleepForTimeInterval: 10 .0f ];
1252
+
1253
+ // Resume the query and save the resulting snapshot for verification.
1254
+ // Use some internal testing hooks to "capture" the existence filter mismatches to verify that
1255
+ // Watch sent a bloom filter, and it was used to avert a full requery.
1256
+ FIRQuerySnapshot *querySnapshot2;
1257
+ std::vector<TestingHooks::ExistenceFilterMismatchInfo> existence_filter_mismatches =
1258
+ CaptureExistenceFilterMismatches ([&] {
1259
+ querySnapshot2 = [self readDocumentSetForRef: collRef source: FIRFirestoreSourceDefault];
1260
+ });
1261
+
1262
+ // Verify that the snapshot from the resumed query contains the expected documents; that is,
1263
+ // that it contains the 50 documents that were _not_ deleted.
1264
+ // TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed to
1265
+ // send an existence filter. At the time of writing, the Firestore emulator fails to send an
1266
+ // existence filter, resulting in the client including the deleted documents in the snapshot
1267
+ // of the resumed query.
1268
+ if (!([FSTIntegrationTestCase isRunningAgainstEmulator ] && querySnapshot2.count == 100 )) {
1269
+ NSSet <NSString *> *actualDocumentIds =
1270
+ [NSSet setWithArray: FIRQuerySnapshotGetIDs (querySnapshot2)];
1271
+ NSSet <NSString *> *expectedDocumentIds;
1272
+ {
1273
+ NSMutableArray <NSString *> *expectedDocumentIdsAccumulator = [[NSMutableArray alloc ] init ];
1274
+ for (FIRDocumentReference *documentRef in createdDocuments) {
1275
+ if (![deletedDocumentIds containsObject: documentRef.documentID]) {
1276
+ [expectedDocumentIdsAccumulator addObject: documentRef.documentID];
1277
+ }
1268
1278
}
1279
+ expectedDocumentIds = [NSSet setWithArray: expectedDocumentIdsAccumulator];
1280
+ }
1281
+ if (![actualDocumentIds isEqualToSet: expectedDocumentIds]) {
1282
+ NSArray <NSString *> *unexpectedDocumentIds =
1283
+ SortedStringsNotIn (actualDocumentIds, expectedDocumentIds);
1284
+ NSArray <NSString *> *missingDocumentIds =
1285
+ SortedStringsNotIn (expectedDocumentIds, actualDocumentIds);
1286
+ XCTFail (@" querySnapshot2 contained %lu documents (expected %lu ): "
1287
+ @" %lu unexpected and %lu missing; "
1288
+ @" unexpected documents: %@ ; missing documents: %@ " ,
1289
+ (unsigned long )actualDocumentIds.count , (unsigned long )expectedDocumentIds.count ,
1290
+ (unsigned long )unexpectedDocumentIds.count , (unsigned long )missingDocumentIds.count ,
1291
+ [unexpectedDocumentIds componentsJoinedByString: @" , " ],
1292
+ [missingDocumentIds componentsJoinedByString: @" , " ]);
1269
1293
}
1270
- expectedDocumentIds = [NSSet setWithArray: expectedDocumentIdsAccumulator];
1271
1294
}
1272
- if (![actualDocumentIds isEqualToSet: expectedDocumentIds]) {
1273
- NSArray <NSString *> *unexpectedDocumentIds =
1274
- SortedStringsNotIn (actualDocumentIds, expectedDocumentIds);
1275
- NSArray <NSString *> *missingDocumentIds =
1276
- SortedStringsNotIn (expectedDocumentIds, actualDocumentIds);
1277
- XCTFail (@" querySnapshot2 contained %lu documents (expected %lu ): "
1278
- @" %lu unexpected and %lu missing; "
1279
- @" unexpected documents: %@ ; missing documents: %@ " ,
1280
- (unsigned long )actualDocumentIds.count , (unsigned long )expectedDocumentIds.count ,
1281
- (unsigned long )unexpectedDocumentIds.count , (unsigned long )missingDocumentIds.count ,
1282
- [unexpectedDocumentIds componentsJoinedByString: @" , " ],
1283
- [missingDocumentIds componentsJoinedByString: @" , " ]);
1295
+
1296
+ // Skip the verification of the existence filter mismatch when testing against the Firestore
1297
+ // emulator because the Firestore emulator fails to to send an existence filter at all.
1298
+ // TODO(b/270731363): Enable the verification of the existence filter mismatch once the
1299
+ // Firestore emulator is fixed to send an existence filter.
1300
+ if ([FSTIntegrationTestCase isRunningAgainstEmulator ]) {
1301
+ return ;
1284
1302
}
1285
- }
1286
1303
1287
- // Skip the verification of the existence filter mismatch when testing against the Firestore
1288
- // emulator because the Firestore emulator fails to to send an existence filter at all.
1289
- // TODO(b/270731363): Enable the verification of the existence filter mismatch once the Firestore
1290
- // emulator is fixed to send an existence filter.
1291
- if ([FSTIntegrationTestCase isRunningAgainstEmulator ]) {
1292
- return ;
1293
- }
1304
+ // Verify that Watch sent an existence filter with the correct counts when the query was
1305
+ // resumed.
1306
+ XCTAssertEqual (existence_filter_mismatches.size (), size_t {1 },
1307
+ @" Watch should have sent exactly 1 existence filter" );
1308
+ const TestingHooks::ExistenceFilterMismatchInfo &existenceFilterMismatchInfo =
1309
+ existence_filter_mismatches[0 ];
1310
+ XCTAssertEqual (existenceFilterMismatchInfo.local_cache_count , 100 );
1311
+ XCTAssertEqual (existenceFilterMismatchInfo.existence_filter_count , 50 );
1312
+
1313
+ // Verify that Watch sent a valid bloom filter.
1314
+ const absl::optional<TestingHooks::BloomFilterInfo> &bloom_filter =
1315
+ existence_filter_mismatches[0 ].bloom_filter ;
1316
+ XCTAssertTrue (bloom_filter.has_value (),
1317
+ " Watch should have included a bloom filter in the existence filter" );
1318
+ XCTAssertGreaterThan (bloom_filter->hash_count , 0 );
1319
+ XCTAssertGreaterThan (bloom_filter->bitmap_length , 0 );
1320
+ XCTAssertGreaterThan (bloom_filter->padding , 0 );
1321
+ XCTAssertLessThan (bloom_filter->padding , 8 );
1322
+
1323
+ // Verify that the bloom filter was successfully used to avert a full requery. If a false
1324
+ // positive occurred then retry the entire test. Although statistically rare, false positives
1325
+ // are expected to happen occasionally. When a false positive _does_ happen, just retry the test
1326
+ // with a different set of documents. If that retry _also_ experiences a false positive, then
1327
+ // fail the test because that is so improbable that something must have gone wrong.
1328
+ if (attemptNumber == 1 && !bloom_filter->applied ) {
1329
+ continue ;
1330
+ }
1331
+
1332
+ XCTAssertTrue (bloom_filter->applied ,
1333
+ @" The bloom filter should have been successfully applied with attemptNumber=%@ " ,
1334
+ @(attemptNumber));
1294
1335
1295
- // Verify that Watch sent an existence filter with the correct counts when the query was resumed.
1296
- XCTAssertEqual (static_cast <int >(existence_filter_mismatches.size ()), 1 );
1297
- firebase::firestore::util::TestingHooks::ExistenceFilterMismatchInfo &info =
1298
- existence_filter_mismatches[0 ];
1299
- XCTAssertEqual (info.local_cache_count , 100 );
1300
- XCTAssertEqual (info.existence_filter_count , 50 );
1336
+ // Break out of the test loop now that the test passes.
1337
+ break ;
1338
+ }
1301
1339
}
1302
1340
1303
1341
@end
0 commit comments