Skip to content

Commit 522d154

Browse files
committed
Firestore: add test bloomFilterShouldCorrectlyEncodeComplexUnicodeCharacters() to QueryTest.java
1 parent d68ed5c commit 522d154

File tree

1 file changed

+157
-0
lines changed
  • firebase-firestore/src/androidTest/java/com/google/firebase/firestore

1 file changed

+157
-0
lines changed

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.firestore;
1616

17+
import static com.google.common.truth.Truth.assertThat;
1718
import static com.google.common.truth.Truth.assertWithMessage;
1819
import static com.google.firebase.firestore.remote.TestingHooksUtil.captureExistenceFilterMismatches;
1920
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.isRunningAgainstEmulator;
@@ -32,6 +33,7 @@
3233
import static org.junit.Assert.assertFalse;
3334
import static org.junit.Assert.assertNull;
3435
import static org.junit.Assert.assertTrue;
36+
import static org.junit.Assume.assumeFalse;
3537
import static org.junit.Assume.assumeTrue;
3638

3739
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -42,6 +44,7 @@
4244
import com.google.firebase.firestore.remote.TestingHooksUtil.ExistenceFilterMismatchInfo;
4345
import com.google.firebase.firestore.testutil.EventAccumulator;
4446
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
47+
import java.text.Normalizer;
4548
import java.util.ArrayList;
4649
import java.util.HashMap;
4750
import java.util.HashSet;
@@ -1169,6 +1172,160 @@ public void resumingAQueryShouldUseBloomFilterToAvoidFullRequery() throws Except
11691172
}
11701173
}
11711174

1175+
private static String unicodeNormalize(String s) {
1176+
return Normalizer.normalize(s, Normalizer.Form.NFC);
1177+
}
1178+
1179+
@Test
1180+
public void bloomFilterShouldCorrectlyEncodeComplexUnicodeCharacters() throws Exception {
1181+
assumeFalse(
1182+
"Skip this test when running against the Firestore emulator because the Firestore emulator "
1183+
+ "fails to send existence filters when queries are resumed (b/270731363), and even "
1184+
+ "if it did send an existence filter it probably wouldn't include a bloom filter.",
1185+
isRunningAgainstEmulator());
1186+
1187+
// Firestore does not do any Unicode normalization on the document IDs. Therefore, two document
1188+
// IDs that are canonically-equivalent (i.e. they visually appear identical) but are represented
1189+
// by a different sequence of Unicode code points are treated as distinct document IDs.
1190+
ArrayList<String> testDocIds = new ArrayList<>();
1191+
testDocIds.add("DocumentToDelete");
1192+
// The next two strings both end with "e" with an accent: the first uses the dedicated Unicode
1193+
// code point for this character, while the second uses the standard lowercase "e" followed by
1194+
// the accent combining character.
1195+
testDocIds.add("LowercaseEWithAcuteAccent_\u00E9");
1196+
testDocIds.add("LowercaseEWithAcuteAccent_\u0065\u0301");
1197+
// The next two strings both end with an "e" with two different accents applied via the
1198+
// following two combining characters. The combining characters are specified in a different
1199+
// order and Firestore treats these document IDs as unique, despite the order of the combining
1200+
// characters being irrelevant.
1201+
testDocIds.add("LowercaseEWithMultipleAccents_\u0065\u0301\u0327");
1202+
testDocIds.add("LowercaseEWithMultipleAccents_\u0065\u0327\u0301");
1203+
// The next string contains a character outside the BMP (the "basic multilingual plane"); that
1204+
// is, its code point is greater than 0xFFFF. Since "The Java programming language represents
1205+
// text in sequences of 16-bit code units, using the UTF-16 encoding" (according to the "Java
1206+
// Language Specification" at https://docs.oracle.com/javase/specs/jls/se11/html/index.html)
1207+
// this requires a surrogate pair, two 16-bit code units, to represent this character. Make sure
1208+
// that its presence is correctly tested in the bloom filter, which uses UTF-8 encoding.
1209+
testDocIds.add("Smiley_\uD83D\uDE00");
1210+
1211+
// Verify assumptions about the equivalence of strings in `testDocIds`.
1212+
assertThat(unicodeNormalize(testDocIds.get(1))).isEqualTo(unicodeNormalize(testDocIds.get(2)));
1213+
assertThat(unicodeNormalize(testDocIds.get(3))).isEqualTo(unicodeNormalize(testDocIds.get(4)));
1214+
assertThat(testDocIds.get(5).codePointAt(7)).isEqualTo(0x1F600);
1215+
1216+
// Create the mapping from document ID to document data for the document IDs specified in
1217+
// `testDocIds`.
1218+
Map<String, Map<String, Object>> testDocs = new HashMap<>();
1219+
for (String docId : testDocIds) {
1220+
testDocs.put(docId, map("foo", 42));
1221+
}
1222+
1223+
// Each iteration of the "while" loop below runs a single iteration of the test. The test will
1224+
// be run multiple times only if a bloom filter false positive occurs.
1225+
int attemptNumber = 0;
1226+
while (true) {
1227+
attemptNumber++;
1228+
1229+
// Create the documents whose names contain complex Unicode characters in a new collection.
1230+
CollectionReference collection = testCollectionWithDocs(testDocs);
1231+
1232+
// Run a query to populate the local cache with documents that have names with complex Unicode
1233+
// characters.
1234+
List<DocumentReference> createdDocuments = new ArrayList<>();
1235+
{
1236+
QuerySnapshot querySnapshot1 = waitFor(collection.get());
1237+
for (DocumentSnapshot documentSnapshot : querySnapshot1.getDocuments()) {
1238+
createdDocuments.add(documentSnapshot.getReference());
1239+
}
1240+
HashSet<String> createdDocumentIds = new HashSet<>();
1241+
for (DocumentSnapshot documentSnapshot : querySnapshot1.getDocuments()) {
1242+
createdDocumentIds.add(documentSnapshot.getId());
1243+
}
1244+
assertWithMessage("createdDocumentIds")
1245+
.that(createdDocumentIds)
1246+
.containsExactlyElementsIn(testDocIds);
1247+
}
1248+
1249+
// Delete one of the documents so that the next call to getDocs() will
1250+
// experience an existence filter mismatch. Do this deletion in a
1251+
// transaction, rather than using deleteDoc(), to avoid affecting the
1252+
// local cache.
1253+
waitFor(
1254+
collection
1255+
.getFirestore()
1256+
.runTransaction(
1257+
transaction -> {
1258+
DocumentReference documentToDelete = collection.document("DocumentToDelete");
1259+
DocumentSnapshot documentToDeleteSnapshot = transaction.get(documentToDelete);
1260+
assertWithMessage("documentToDeleteSnapshot.exists()")
1261+
.that(documentToDeleteSnapshot.exists())
1262+
.isTrue();
1263+
transaction.delete(documentToDelete);
1264+
return null;
1265+
}));
1266+
1267+
// Wait for 10 seconds, during which Watch will stop tracking the query and will send an
1268+
// existence filter rather than "delete" events when the query is resumed.
1269+
Thread.sleep(10000);
1270+
1271+
// Resume the query and save the resulting snapshot for verification. Use some internal
1272+
// testing hooks to "capture" the existence filter mismatches.
1273+
AtomicReference<QuerySnapshot> querySnapshot2Ref = new AtomicReference<>();
1274+
ArrayList<ExistenceFilterMismatchInfo> existenceFilterMismatches =
1275+
captureExistenceFilterMismatches(
1276+
() -> {
1277+
QuerySnapshot querySnapshot = waitFor(collection.get());
1278+
querySnapshot2Ref.set(querySnapshot);
1279+
});
1280+
QuerySnapshot querySnapshot2 = querySnapshot2Ref.get();
1281+
1282+
// Verify that the snapshot from the resumed query contains the expected documents; that is,
1283+
// that it contains the documents whose names contain complex Unicode characters and _not_ the
1284+
// document that was deleted.
1285+
HashSet<String> querySnapshot2DocumentIds = new HashSet<>();
1286+
for (DocumentSnapshot documentSnapshot : querySnapshot2.getDocuments()) {
1287+
querySnapshot2DocumentIds.add(documentSnapshot.getId());
1288+
}
1289+
HashSet<String> querySnapshot2ExpectedDocumentIds = new HashSet<>(testDocIds);
1290+
querySnapshot2ExpectedDocumentIds.remove("DocumentToDelete");
1291+
assertWithMessage("querySnapshot2DocumentIds")
1292+
.that(querySnapshot2DocumentIds)
1293+
.containsExactlyElementsIn(querySnapshot2ExpectedDocumentIds);
1294+
1295+
// Verify that Watch sent an existence filter with the correct counts.
1296+
assertWithMessage("Watch should have sent exactly 1 existence filter")
1297+
.that(existenceFilterMismatches)
1298+
.hasSize(1);
1299+
ExistenceFilterMismatchInfo existenceFilterMismatchInfo = existenceFilterMismatches.get(0);
1300+
assertWithMessage("localCacheCount")
1301+
.that(existenceFilterMismatchInfo.localCacheCount())
1302+
.isEqualTo(testDocIds.size());
1303+
assertWithMessage("existenceFilterCount")
1304+
.that(existenceFilterMismatchInfo.existenceFilterCount())
1305+
.isEqualTo(testDocIds.size() - 1);
1306+
1307+
// Verify that Watch sent a valid bloom filter.
1308+
ExistenceFilterBloomFilterInfo bloomFilter = existenceFilterMismatchInfo.bloomFilter();
1309+
assertWithMessage("The bloom filter specified in the existence filter")
1310+
.that(bloomFilter)
1311+
.isNotNull();
1312+
1313+
// Verify that the bloom filter was successfully used to avert a full requery. If a false
1314+
// positive occurred, which is statistically rare, but technically possible, then retry the
1315+
// entire test.
1316+
if (attemptNumber == 1 && !bloomFilter.applied()) {
1317+
continue;
1318+
}
1319+
1320+
assertWithMessage("bloom filter successfully applied with attemptNumber=" + attemptNumber)
1321+
.that(bloomFilter.applied())
1322+
.isTrue();
1323+
1324+
// Break out of the test loop now that the test passes.
1325+
break;
1326+
}
1327+
}
1328+
11721329
@Test
11731330
public void testOrQueries() {
11741331
Map<String, Map<String, Object>> testDocs =

0 commit comments

Comments
 (0)