Skip to content

Commit 5c2b381

Browse files
authored
Tidy up TestingHooks code to be simpler and more consistent with the Web and iOS SDKs (#4975)
1 parent 441ebc4 commit 5c2b381

File tree

4 files changed

+110
-187
lines changed

4 files changed

+110
-187
lines changed

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

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.firestore;
1616

1717
import static com.google.common.truth.Truth.assertWithMessage;
18+
import static com.google.firebase.firestore.remote.TestingHooksUtil.captureExistenceFilterMismatches;
1819
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.isRunningAgainstEmulator;
1920
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.nullList;
2021
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.querySnapshotToIds;
@@ -37,7 +38,7 @@
3738
import com.google.android.gms.tasks.Task;
3839
import com.google.common.collect.Lists;
3940
import com.google.firebase.firestore.Query.Direction;
40-
import com.google.firebase.firestore.remote.ExistenceFilterMismatchListener;
41+
import com.google.firebase.firestore.remote.TestingHooksUtil.ExistenceFilterMismatchInfo;
4142
import com.google.firebase.firestore.testutil.EventAccumulator;
4243
import com.google.firebase.firestore.testutil.IntegrationTestUtil;
4344
import java.util.ArrayList;
@@ -47,6 +48,7 @@
4748
import java.util.List;
4849
import java.util.Map;
4950
import java.util.concurrent.Semaphore;
51+
import java.util.concurrent.atomic.AtomicReference;
5052
import org.junit.After;
5153
import org.junit.Test;
5254
import org.junit.runner.RunWith;
@@ -1079,25 +1081,14 @@ public void resumingAQueryShouldUseExistenceFilterToDetectDeletes() throws Excep
10791081

10801082
// Resume the query and save the resulting snapshot for verification. Use some internal testing
10811083
// hooks to "capture" the existence filter mismatches to verify them.
1082-
ExistenceFilterMismatchListener existenceFilterMismatchListener =
1083-
new ExistenceFilterMismatchListener();
1084-
QuerySnapshot snapshot2;
1085-
ExistenceFilterMismatchListener.ExistenceFilterMismatchInfo existenceFilterMismatchInfo;
1086-
try {
1087-
existenceFilterMismatchListener.startListening();
1088-
snapshot2 = waitFor(collection.get());
1089-
// TODO(b/270731363): Remove the "if" condition below once the Firestore Emulator is fixed
1090-
// to send an existence filter.
1091-
if (isRunningAgainstEmulator()) {
1092-
existenceFilterMismatchInfo = null;
1093-
} else {
1094-
existenceFilterMismatchInfo =
1095-
existenceFilterMismatchListener.getOrWaitForExistenceFilterMismatch(
1096-
/*timeoutMillis=*/ 5000);
1097-
}
1098-
} finally {
1099-
existenceFilterMismatchListener.stopListening();
1100-
}
1084+
AtomicReference<QuerySnapshot> snapshot2Ref = new AtomicReference<>();
1085+
ArrayList<ExistenceFilterMismatchInfo> existenceFilterMismatches =
1086+
captureExistenceFilterMismatches(
1087+
() -> {
1088+
QuerySnapshot querySnapshot = waitFor(collection.get());
1089+
snapshot2Ref.set(querySnapshot);
1090+
});
1091+
QuerySnapshot snapshot2 = snapshot2Ref.get();
11011092

11021093
// Verify that the snapshot from the resumed query contains the expected documents; that is,
11031094
// that it contains the 50 documents that were _not_ deleted.
@@ -1131,9 +1122,10 @@ public void resumingAQueryShouldUseExistenceFilterToDetectDeletes() throws Excep
11311122

11321123
// Verify that Watch sent an existence filter with the correct counts when the query was
11331124
// resumed.
1134-
assertWithMessage("Watch should have sent an existence filter")
1135-
.that(existenceFilterMismatchInfo)
1136-
.isNotNull();
1125+
assertWithMessage("Watch should have sent exactly 1 existence filter")
1126+
.that(existenceFilterMismatches)
1127+
.hasSize(1);
1128+
ExistenceFilterMismatchInfo existenceFilterMismatchInfo = existenceFilterMismatches.get(0);
11371129
assertWithMessage("localCacheCount")
11381130
.that(existenceFilterMismatchInfo.localCacheCount())
11391131
.isEqualTo(100);

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/ExistenceFilterMismatchListener.java

Lines changed: 0 additions & 150 deletions
This file was deleted.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.remote;
16+
17+
import androidx.annotation.NonNull;
18+
import com.google.firebase.firestore.ListenerRegistration;
19+
import java.util.ArrayList;
20+
21+
/** Convenience functions for accessing the testing hooks from {@link TestingHooks}. */
22+
public final class TestingHooksUtil {
23+
24+
/** Private constructor to prevent instantiation. */
25+
private TestingHooksUtil() {}
26+
27+
/**
28+
* Captures all existence filter mismatches in the Watch 'Listen' stream that occur during the
29+
* execution of the given callback.
30+
*
31+
* @param callback The callback to invoke; during the invocation of this callback all existence
32+
* filter mismatches will be captured.
33+
* @return the captured existence filter mismatches.
34+
*/
35+
public static ArrayList<ExistenceFilterMismatchInfo> captureExistenceFilterMismatches(
36+
Runnable callback) {
37+
if (callback == null) {
38+
throw new NullPointerException("the given callback must not be null");
39+
}
40+
41+
ArrayList<ExistenceFilterMismatchInfo> existenceFilterMismatches = new ArrayList<>();
42+
43+
ListenerRegistration listenerRegistration =
44+
TestingHooks.getInstance()
45+
.addExistenceFilterMismatchListener(
46+
info -> {
47+
synchronized (existenceFilterMismatches) {
48+
existenceFilterMismatches.add(new ExistenceFilterMismatchInfo(info));
49+
}
50+
});
51+
52+
try {
53+
callback.run();
54+
} finally {
55+
listenerRegistration.remove();
56+
}
57+
58+
// Return a *copy* of the `existenceFilterMismatches` list because, as documented in
59+
// addExistenceFilterMismatchListener(), it could *still* get notifications after it is
60+
// unregistered and that would be a race condition with the caller accessing the list.
61+
synchronized (existenceFilterMismatches) {
62+
return new ArrayList<>(existenceFilterMismatches);
63+
}
64+
}
65+
66+
/** @see TestingHooks.ExistenceFilterMismatchInfo */
67+
public static final class ExistenceFilterMismatchInfo {
68+
69+
private final TestingHooks.ExistenceFilterMismatchInfo info;
70+
71+
ExistenceFilterMismatchInfo(@NonNull TestingHooks.ExistenceFilterMismatchInfo info) {
72+
this.info = info;
73+
}
74+
75+
public int localCacheCount() {
76+
return info.localCacheCount();
77+
}
78+
79+
public int existenceFilterCount() {
80+
return info.existenceFilterCount();
81+
}
82+
}
83+
}

firebase-firestore/src/main/java/com/google/firebase/firestore/remote/TestingHooks.java

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import androidx.annotation.VisibleForTesting;
2222
import com.google.auto.value.AutoValue;
2323
import com.google.firebase.firestore.ListenerRegistration;
24-
import com.google.firebase.firestore.util.Executors;
2524
import java.util.concurrent.CopyOnWriteArrayList;
2625
import java.util.concurrent.atomic.AtomicReference;
2726

@@ -50,21 +49,18 @@ static TestingHooks getInstance() {
5049
}
5150

5251
/**
53-
* Asynchronously notifies all registered {@link ExistenceFilterMismatchListener}` listeners
52+
* Synchronously notifies all registered {@link ExistenceFilterMismatchListener}` listeners
5453
* registered via {@link #addExistenceFilterMismatchListener}.
5554
*
5655
* @param info Information about the existence filter mismatch to deliver to the listeners.
5756
*/
5857
void notifyOnExistenceFilterMismatch(@NonNull ExistenceFilterMismatchInfo info) {
5958
for (AtomicReference<ExistenceFilterMismatchListener> listenerRef :
6059
existenceFilterMismatchListeners) {
61-
Executors.BACKGROUND_EXECUTOR.execute(
62-
() -> {
63-
ExistenceFilterMismatchListener listener = listenerRef.get();
64-
if (listener != null) {
65-
listener.onExistenceFilterMismatch(info);
66-
}
67-
});
60+
ExistenceFilterMismatchListener listener = listenerRef.get();
61+
if (listener != null) {
62+
listener.onExistenceFilterMismatch(info);
63+
}
6864
}
6965
}
7066

@@ -76,15 +72,17 @@ void notifyOnExistenceFilterMismatch(@NonNull ExistenceFilterMismatchInfo info)
7672
* particular ordering. If a given callback is registered multiple times then it will be notified
7773
* multiple times, once per registration.
7874
*
79-
* <p>The thread on which the callback occurs is unspecified; listeners should perform their work
80-
* as quickly as possible and return to avoid blocking any critical work. In particular, the
81-
* listener callbacks should <em>not</em> block or perform long-running operations. Listener
82-
* callbacks can occur concurrently with other callbacks on the same and other listeners.
75+
* <p>The listener callbacks are performed synchronously in `NotifyOnExistenceFilterMismatch()`;
76+
* therefore, listeners should perform their work as quickly as possible and return to avoid
77+
* blocking any critical work. In particular, the listener callbacks should <em>not</em> block or
78+
* perform long-running operations.
8379
*
8480
* @param listener the listener to register.
8581
* @return an object that unregisters the given listener via its {@link
8682
* ListenerRegistration#remove} method; only the first unregistration request does anything;
87-
* all subsequent requests do nothing.
83+
* all subsequent requests do nothing. Note that due to inherent race conditions it is
84+
* technically possible, although unlikely, that callbacks could still occur <em>after</em>
85+
* unregistering.
8886
*/
8987
ListenerRegistration addExistenceFilterMismatchListener(
9088
@NonNull ExistenceFilterMismatchListener listener) {

0 commit comments

Comments
 (0)