@@ -1285,4 +1285,144 @@ - (void)testResumingAQueryShouldUseBloomFilterToAvoidFullRequery {
1285
1285
}
1286
1286
}
1287
1287
1288
+ - (void )testBloomFilterShouldCorrectlyEncodeComplexUnicodeCharacters {
1289
+ // TODO(b/291365820): Stop skipping this test when running against the Firestore emulator once
1290
+ // the emulator is improved to include a bloom filter in the existence filter messages that it
1291
+ // sends.
1292
+ XCTSkipIf ([FSTIntegrationTestCase isRunningAgainstEmulator ],
1293
+ " Skip this test when running against the Firestore emulator because the emulator does "
1294
+ " not include a bloom filter when it sends existence filter messages, making it "
1295
+ " impossible for this test to verify the correctness of the bloom filter." );
1296
+
1297
+ // Set this test to stop when the first failure occurs because some test assertion failures make
1298
+ // the rest of the test not applicable or will even crash.
1299
+ [self setContinueAfterFailure: NO ];
1300
+
1301
+ // Define a comparator that compares `NSString` objects in a way that orders canonically-
1302
+ // equivalent, but distinct, strings in a consistent manner by using `NSForcedOrderingSearch`.
1303
+ // Otherwise, the bare `[NSString compare:]` method considers canonically-equivalent, but
1304
+ // distinct, strings as "equal" and orders them indeterminately.
1305
+ NSComparator sortComparator = ^(NSString *string1, NSString *string2) {
1306
+ return [string1 compare: string2 options: NSForcedOrderingSearch];
1307
+ };
1308
+
1309
+ // Firestore does not do any Unicode normalization on the document IDs. Therefore, two document
1310
+ // IDs that are canonically-equivalent (i.e. they visually appear identical) but are represented
1311
+ // by a different sequence of Unicode code points are treated as distinct document IDs.
1312
+ NSArray <NSString *> *testDocIds;
1313
+ {
1314
+ NSMutableArray <NSString *> *testDocIdsAccumulator = [[NSMutableArray alloc ] init ];
1315
+ [testDocIdsAccumulator addObject: @" DocumentToDelete" ];
1316
+ // The next two strings both end with "e" with an accent: the first uses the dedicated Unicode
1317
+ // code point for this character, while the second uses the standard lowercase "e" followed by
1318
+ // the accent combining character.
1319
+ [testDocIdsAccumulator addObject: @" LowercaseEWithAcuteAccent_\u00E9 " ];
1320
+ [testDocIdsAccumulator addObject: @" LowercaseEWithAcuteAccent_\u0065\u0301 " ];
1321
+ // The next two strings both end with an "e" with two different accents applied via the
1322
+ // following two combining characters. The combining characters are specified in a different
1323
+ // order and Firestore treats these document IDs as unique, despite the order of the combining
1324
+ // characters being irrelevant.
1325
+ [testDocIdsAccumulator addObject: @" LowercaseEWithMultipleAccents_\u0065\u0301\u0327 " ];
1326
+ [testDocIdsAccumulator addObject: @" LowercaseEWithMultipleAccents_\u0065\u0327\u0301 " ];
1327
+ // The next string contains a character outside the BMP (the "basic multilingual plane"); that
1328
+ // is, its code point is greater than 0xFFFF. Since NSString stores text in sequences of 16-bit
1329
+ // code units, using the UTF-16 encoding (according to
1330
+ // https://www.objc.io/issues/9-strings/unicode) it is stored as a surrogate pair, two 16-bit
1331
+ // code units U+D83D and U+DE00, to represent this character. Make sure that its presence is
1332
+ // correctly tested in the bloom filter, which uses UTF-8 encoding.
1333
+ [testDocIdsAccumulator addObject: @" Smiley_\U0001F600 " ];
1334
+
1335
+ testDocIds = [NSArray arrayWithArray: testDocIdsAccumulator];
1336
+ }
1337
+
1338
+ // Verify assumptions about the equivalence of strings in `testDocIds`.
1339
+ XCTAssertEqualObjects (testDocIds[1 ].decomposedStringWithCanonicalMapping ,
1340
+ testDocIds[2 ].decomposedStringWithCanonicalMapping );
1341
+ XCTAssertEqualObjects (testDocIds[3 ].decomposedStringWithCanonicalMapping ,
1342
+ testDocIds[4 ].decomposedStringWithCanonicalMapping );
1343
+ XCTAssertEqual ([testDocIds[5 ] characterAtIndex: 7 ], 0xD83D );
1344
+ XCTAssertEqual ([testDocIds[5 ] characterAtIndex: 8 ], 0xDE00 );
1345
+
1346
+ // Create the mapping from document ID to document data for the document IDs specified in
1347
+ // `testDocIds`.
1348
+ NSMutableDictionary <NSString *, NSDictionary <NSString *, id > *> *testDocs =
1349
+ [[NSMutableDictionary alloc ] init ];
1350
+ for (NSString *testDocId in testDocIds) {
1351
+ [testDocs setValue: @{@" foo" : @42 } forKey: testDocId];
1352
+ }
1353
+
1354
+ // Create the documents whose names contain complex Unicode characters in a new collection.
1355
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments: testDocs];
1356
+
1357
+ // Run a query to populate the local cache with documents that have names with complex Unicode
1358
+ // characters.
1359
+ {
1360
+ FIRQuerySnapshot *querySnapshot1 = [self readDocumentSetForRef: collRef
1361
+ source: FIRFirestoreSourceDefault];
1362
+ XCTAssertEqualObjects (
1363
+ [FIRQuerySnapshotGetIDs (querySnapshot1) sortedArrayUsingComparator: sortComparator],
1364
+ [testDocIds sortedArrayUsingComparator: sortComparator],
1365
+ @" querySnapshot1 has the wrong documents" );
1366
+ }
1367
+
1368
+ // Delete one of the documents so that the next call to collection.get() will experience an
1369
+ // existence filter mismatch. Use a different Firestore instance to avoid affecting the local
1370
+ // cache.
1371
+ FIRDocumentReference *documentToDelete = [collRef documentWithPath: @" DocumentToDelete" ];
1372
+ {
1373
+ FIRFirestore *db2 = [self firestore ];
1374
+ [self deleteDocumentRef: [db2 documentWithPath: documentToDelete.path]];
1375
+ }
1376
+
1377
+ // Wait for 10 seconds, during which Watch will stop tracking the query and will send an
1378
+ // existence filter rather than "delete" events when the query is resumed.
1379
+ [NSThread sleepForTimeInterval: 10 .0f ];
1380
+
1381
+ // Resume the query and save the resulting snapshot for verification. Use some internal testing
1382
+ // hooks to "capture" the existence filter mismatches.
1383
+ __block FIRQuerySnapshot *querySnapshot2;
1384
+ NSArray <FSTTestingHooksExistenceFilterMismatchInfo *> *existenceFilterMismatches =
1385
+ [FSTTestingHooks captureExistenceFilterMismatchesDuringBlock: ^{
1386
+ querySnapshot2 = [self readDocumentSetForRef: collRef source: FIRFirestoreSourceDefault];
1387
+ }];
1388
+
1389
+ // Verify that the snapshot from the resumed query contains the expected documents; that is, that
1390
+ // it contains the documents whose names contain complex Unicode characters and _not_ the document
1391
+ // that was deleted.
1392
+ {
1393
+ NSMutableArray <NSString *> *querySnapshot2ExpectedDocumentIds =
1394
+ [NSMutableArray arrayWithArray: testDocIds];
1395
+ [querySnapshot2ExpectedDocumentIds removeObject: documentToDelete.documentID];
1396
+ XCTAssertEqualObjects (
1397
+ [FIRQuerySnapshotGetIDs (querySnapshot2) sortedArrayUsingComparator: sortComparator],
1398
+ [querySnapshot2ExpectedDocumentIds sortedArrayUsingComparator: sortComparator],
1399
+ @" querySnapshot2 has the wrong documents" );
1400
+ }
1401
+
1402
+ // Verify that Watch sent an existence filter with the correct counts.
1403
+ XCTAssertEqual (existenceFilterMismatches.count , 1u ,
1404
+ @" Watch should have sent exactly 1 existence filter" );
1405
+ FSTTestingHooksExistenceFilterMismatchInfo *existenceFilterMismatchInfo =
1406
+ existenceFilterMismatches[0 ];
1407
+ XCTAssertEqual (existenceFilterMismatchInfo.localCacheCount , (int )testDocIds.count );
1408
+ XCTAssertEqual (existenceFilterMismatchInfo.existenceFilterCount , (int )testDocIds.count - 1 );
1409
+
1410
+ // Verify that Watch sent a valid bloom filter.
1411
+ FSTTestingHooksBloomFilter *bloomFilter = existenceFilterMismatchInfo.bloomFilter ;
1412
+ XCTAssertNotNil (bloomFilter, " Watch should have included a bloom filter in the existence filter" );
1413
+
1414
+ // The bloom filter application should statistically be successful almost every time; the _only_
1415
+ // time when it would _not_ be successful is if there is a false positive when testing for
1416
+ // 'DocumentToDelete' in the bloom filter. So verify that the bloom filter application is
1417
+ // successful, unless there was a false positive.
1418
+ BOOL isFalsePositive = [bloomFilter mightContain: documentToDelete];
1419
+ XCTAssertEqual (bloomFilter.applied , !isFalsePositive);
1420
+
1421
+ // Verify that the bloom filter contains the document paths with complex Unicode characters.
1422
+ for (FIRDocumentSnapshot *documentSnapshot in querySnapshot2.documents ) {
1423
+ XCTAssertTrue ([bloomFilter mightContain: documentSnapshot.reference],
1424
+ @" The bloom filter should contain %@ " , documentSnapshot.documentID );
1425
+ }
1426
+ }
1427
+
1288
1428
@end
0 commit comments