Skip to content

Commit 58540de

Browse files
authored
Test Harness for gRPC Integration Test of full Firestore stack (#6043)
A single test of existing Write stream handshake. - Test is written using new ComponentProvider to mock out gRPC layer. - Test uses `FirebaseFirestore` instance to write document data. - Server integration on the gRPC layer is faked and verified, such that each proto exchanged with server is written into the test. This test harness will be used to test cache clearing semantics in future PR.
1 parent 45d0c83 commit 58540de

File tree

9 files changed

+605
-8
lines changed

9 files changed

+605
-8
lines changed

buildSrc/src/main/java/com/google/firebase/gradle/plugins/ci/device/FirebaseTestServer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public void uploadApks(String variantName, File testApk, File testedApk) {
7878
"--type=instrumentation",
7979
"--app=" + testedApkPath,
8080
"--test=" + testApk,
81-
"--timeout=30m",
81+
"--timeout=45m",
8282
"--use-orchestrator",
8383
"--no-auto-google-login",
8484
"--no-record-video",

firebase-firestore/firebase-firestore.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ firebaseLibrary {
2424
publishSources = true
2525
testLab {
2626
enabled = true
27-
timeout = '30m'
27+
timeout = '45m'
2828
}
2929
}
3030

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import static com.google.firebase.firestore.Filter.notEqualTo;
2525
import static com.google.firebase.firestore.Filter.notInArray;
2626
import static com.google.firebase.firestore.Filter.or;
27+
import static com.google.firebase.firestore.testutil.CompositeIndexTestHelper.COMPOSITE_INDEX_TEST_COLLECTION;
2728
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.nullList;
2829
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore;
2930
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
@@ -113,7 +114,6 @@ public void testOrQueriesWithCompositeIndexes() {
113114
@Test
114115
public void testCanRunAggregateCollectionGroupQuery() {
115116
CompositeIndexTestHelper testHelper = new CompositeIndexTestHelper();
116-
String collectionGroup = testHelper.withTestCollection().getId();
117117

118118
FirebaseFirestore db = testFirestore();
119119

@@ -134,15 +134,15 @@ public void testCanRunAggregateCollectionGroupQuery() {
134134
WriteBatch batch = db.batch();
135135
for (String path : docPaths) {
136136
batch.set(
137-
db.document(path.replace("${collectionGroup}", collectionGroup)),
137+
db.document(path.replace("${collectionGroup}", COMPOSITE_INDEX_TEST_COLLECTION)),
138138
testHelper.addTestSpecificFieldsToDoc(map("a", 2)));
139139
}
140140
waitFor(batch.commit());
141141

142142
AggregateQuerySnapshot snapshot =
143143
waitFor(
144144
testHelper
145-
.query(db.collectionGroup(collectionGroup))
145+
.query(db.collectionGroup(COMPOSITE_INDEX_TEST_COLLECTION))
146146
.aggregate(AggregateField.count(), sum("a"), average("a"))
147147
.get(AggregateSource.SERVER));
148148
assertEquals(

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/CompositeIndexTestHelper.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.checkOnlineAndOfflineResultsMatch;
1818
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.querySnapshotToIds;
1919
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore;
20+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testInMemoryFirestore;
2021
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
2122
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.writeAllDocs;
2223
import static com.google.firebase.firestore.util.Util.autoId;
@@ -53,7 +54,7 @@ public class CompositeIndexTestHelper {
5354
private final String testId;
5455
private static final String TEST_ID_FIELD = "testId";
5556
private static final String TTL_FIELD = "expireAt";
56-
private static final String COMPOSITE_INDEX_TEST_COLLECTION = "composite-index-test-collection";
57+
public static final String COMPOSITE_INDEX_TEST_COLLECTION = "composite-index-test-collection";
5758

5859
// Creates a new instance of the CompositeIndexTestHelper class, with a unique test
5960
// identifier for data isolation.
@@ -69,7 +70,7 @@ public CollectionReference withTestCollection() {
6970
// Runs a test with specified documents in the COMPOSITE_INDEX_TEST_COLLECTION.
7071
@NonNull
7172
public CollectionReference withTestDocs(@NonNull Map<String, Map<String, Object>> docs) {
72-
CollectionReference writer = withTestCollection();
73+
CollectionReference writer = testInMemoryFirestore().collection(COMPOSITE_INDEX_TEST_COLLECTION);
7374
writeAllDocs(writer, prepareTestDocuments(docs));
7475
CollectionReference reader = testFirestore().collection(writer.getPath());
7576
return reader;

firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ public enum TargetBackend {
124124
private static final FirestoreProvider provider = new FirestoreProvider();
125125

126126
private static boolean strictModeEnabled = false;
127-
private static boolean backendPrimed = false;
128127

129128
// FirebaseOptions needed to create a test FirebaseApp.
130129
private static final FirebaseOptions OPTIONS =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
// Copyright 2024 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+
//
6+
// You may obtain a copy of the License at
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;
16+
17+
import static org.mockito.ArgumentMatchers.eq;
18+
import static org.mockito.Mockito.mock;
19+
import static org.mockito.Mockito.when;
20+
21+
import androidx.test.core.app.ApplicationProvider;
22+
23+
import com.google.android.gms.tasks.Task;
24+
import com.google.android.gms.tasks.TaskCompletionSource;
25+
import com.google.android.gms.tasks.Tasks;
26+
import com.google.firebase.firestore.core.ComponentProvider;
27+
import com.google.firebase.firestore.integration.AsyncTaskAccumulator;
28+
import com.google.firebase.firestore.model.DatabaseId;
29+
import com.google.firebase.firestore.remote.GrpcCallProvider;
30+
import com.google.firebase.firestore.remote.RemoteComponenetProvider;
31+
import com.google.firebase.firestore.testutil.EmptyAppCheckTokenProvider;
32+
import com.google.firebase.firestore.testutil.EmptyCredentialsProvider;
33+
import com.google.firebase.firestore.util.AsyncQueue;
34+
import com.google.firestore.v1.FirestoreGrpc;
35+
import com.google.firestore.v1.ListenRequest;
36+
import com.google.firestore.v1.ListenResponse;
37+
import com.google.firestore.v1.WriteRequest;
38+
import com.google.firestore.v1.WriteResponse;
39+
40+
import com.google.firebase.firestore.integration.TestClientCall;
41+
42+
/**
43+
* Factory for producing FirebaseFirestore instances that has mocked gRPC layer.
44+
*
45+
* <ol>
46+
* <li>Response protos from server can be faked.
47+
* <li>Request protos from SDK can be verified.
48+
* </ol>
49+
*
50+
* <p>
51+
* The FirebaseFirestoreIntegrationTestFactory is located in this package to gain package-private
52+
* access to FirebaseFirestore methods.
53+
*/
54+
public final class FirebaseFirestoreIntegrationTestFactory {
55+
56+
/**
57+
* Everytime the `componentProviderFactory` on FirebaseFirestore is run, a new instance is added.
58+
*/
59+
public final AsyncTaskAccumulator<Instance> instances = new AsyncTaskAccumulator<>();
60+
61+
/**
62+
* Instance of Firestore components.
63+
*/
64+
public static class Instance {
65+
66+
/** Instance of ComponentProvider */
67+
public final ComponentProvider componentProvider;
68+
69+
/** Every listen stream created is captured here. */
70+
private final AsyncTaskAccumulator<TestClientCall<ListenRequest, ListenResponse>> listens = new AsyncTaskAccumulator<>();
71+
72+
/** Every write stream created is captured here. */
73+
private final AsyncTaskAccumulator<TestClientCall<WriteRequest, WriteResponse>> writes = new AsyncTaskAccumulator<>();
74+
75+
private Instance(ComponentProvider componentProvider) {
76+
this.componentProvider = componentProvider;
77+
}
78+
79+
/**
80+
* Queues work on AsyncQueue. This is required when faking responses from server since they
81+
* must be handled through the AsyncQueue of the FirestoreClient.
82+
*/
83+
public Task<Void> enqueue(Runnable runnable) {
84+
return configuration.asyncQueue.enqueue(runnable);
85+
}
86+
87+
/**
88+
* Configuration passed to `ComponentProvider`
89+
*
90+
* <p>
91+
* This is never null because `Task<Instance>` completes after initialization. The
92+
* `FirebaseFirestoreIntegrationTestFactory` will set `Instance.configuration` from within
93+
* the ComponentProvider override.
94+
*/
95+
private ComponentProvider.Configuration configuration;
96+
97+
/** Every listen stream created */
98+
public Task<TestClientCall<ListenRequest, ListenResponse>> getListenClient(int i) {
99+
return listens.get(i);
100+
}
101+
102+
/** Every write stream created */
103+
public Task<TestClientCall<WriteRequest, WriteResponse>> getWriteClient(int i) {
104+
return writes.get(i);
105+
}
106+
107+
}
108+
109+
/**
110+
* The FirebaseFirestore instance.
111+
*/
112+
public final FirebaseFirestore firestore;
113+
114+
/**
115+
* Mockito Mock of `FirebaseFirestore.InstanceRegistry` that was passed into FirebaseFirestore
116+
* constructor.
117+
*/
118+
public final FirebaseFirestore.InstanceRegistry instanceRegistry = mock(FirebaseFirestore.InstanceRegistry.class);
119+
120+
public FirebaseFirestoreIntegrationTestFactory(DatabaseId databaseId) {
121+
firestore = new FirebaseFirestore(
122+
ApplicationProvider.getApplicationContext(),
123+
databaseId,
124+
"k",
125+
new EmptyCredentialsProvider(),
126+
new EmptyAppCheckTokenProvider(),
127+
new AsyncQueue(),
128+
this::componentProvider,
129+
null,
130+
instanceRegistry,
131+
null
132+
);
133+
}
134+
135+
public void useMemoryCache() {
136+
FirebaseFirestoreSettings.Builder builder = new FirebaseFirestoreSettings.Builder(firestore.getFirestoreSettings());
137+
builder.setLocalCacheSettings(MemoryCacheSettings.newBuilder().build());
138+
firestore.setFirestoreSettings(builder.build());
139+
}
140+
141+
private GrpcCallProvider mockGrpcCallProvider(Instance instance) {
142+
GrpcCallProvider mockGrpcCallProvider = mock(GrpcCallProvider.class);
143+
when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getListenMethod())))
144+
.thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.listens.next())));
145+
when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getWriteMethod())))
146+
.thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.writes.next())));
147+
return mockGrpcCallProvider;
148+
}
149+
150+
private ComponentProvider componentProvider(FirebaseFirestoreSettings settings) {
151+
TaskCompletionSource<Instance> next = instances.next();
152+
ComponentProvider componentProvider = ComponentProvider.defaultFactory(settings);
153+
Instance instance = new Instance(componentProvider);
154+
componentProvider.setRemoteProvider(new RemoteComponenetProvider() {
155+
@Override
156+
protected GrpcCallProvider createGrpcCallProvider(ComponentProvider.Configuration configuration) {
157+
instance.configuration = configuration;
158+
next.setResult(instance);
159+
return mockGrpcCallProvider(instance);
160+
}
161+
});
162+
return componentProvider;
163+
}
164+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright 2024 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+
//
6+
// You may obtain a copy of the License at
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+
package com.google.firebase.firestore.integration;
15+
16+
import androidx.annotation.NonNull;
17+
18+
import com.google.android.gms.tasks.Task;
19+
import com.google.android.gms.tasks.TaskCompletionSource;
20+
21+
import java.util.ArrayList;
22+
import java.util.Iterator;
23+
import java.util.List;
24+
import java.util.NoSuchElementException;
25+
26+
27+
/**
28+
* Collects asynchronous `onResult` and `onException` callback invocations.
29+
*
30+
* <p> As part of a test, a callback can be asynchronously invoked many times. This class retains
31+
* all callback invocations as a List of Task. The test code can await a future callback.
32+
*
33+
* <p> Just like a stream, no more results are expected after an exception.
34+
*/
35+
public class AsyncTaskAccumulator<T> implements Iterable<Task<T>> {
36+
37+
private int eventCount;
38+
private List<TaskCompletionSource<T>> events;
39+
40+
public AsyncTaskAccumulator() {
41+
eventCount = 0;
42+
events = new ArrayList<>();
43+
}
44+
45+
/**
46+
* Callback for next `onResult` or `onException`. Calling this method repeatedly will
47+
* provide callbacks further into the future. Each callback should only be exactly once.
48+
*/
49+
public synchronized TaskCompletionSource<T> next() {
50+
return computeIfAbsentIndex(eventCount++);
51+
}
52+
53+
/**
54+
* Callback that can be invoked as part of test code.
55+
*/
56+
public void onResult(T result) {
57+
next().setResult(result);
58+
}
59+
60+
/**
61+
* Callback that can be invoked as part of test code.
62+
*/
63+
public void onException(Exception e) {
64+
next().setException(e);
65+
}
66+
67+
private TaskCompletionSource<T> computeIfAbsentIndex(int i) {
68+
while (events.size() <= i) {
69+
events.add(new TaskCompletionSource<>());
70+
}
71+
return events.get(i);
72+
}
73+
74+
/**
75+
* Get task that completes when result arrives.
76+
*
77+
* @param index 0 indexed arrival sequence of results.
78+
* @return Task.
79+
*/
80+
@NonNull
81+
public synchronized Task<T> get(int index) {
82+
return computeIfAbsentIndex(index).getTask();
83+
}
84+
85+
/**
86+
* Iterates over results.
87+
* <p>
88+
* The Iterator is thread safe.
89+
* Iteration will stop upon task that is failed, cancelled or incomplete.
90+
* <p>
91+
* A loop that waits for task to complete before getting next task will continue to iterate
92+
* indefinitely. Attempting to iterate past a task that is not yet successful will throw
93+
* {#code NoSuchElementException} and {@code #hasNext()} will be false. In this way, iteration
94+
* in nonblocking. Last element will be failed, cancelled or awaiting result.
95+
*
96+
* @return Iterator of Tasks that complete.
97+
*/
98+
@NonNull
99+
@Override
100+
public Iterator<Task<T>> iterator() {
101+
return new Iterator<Task<T>>() {
102+
103+
private int i = -1;
104+
private Task<T> current;
105+
106+
@Override
107+
public synchronized boolean hasNext() {
108+
// We always return first, and continue to return tasks so long as previous
109+
// is successful. A task that hasn't completed, will also mark the end of
110+
// iteration.
111+
return i < 0 || current.isSuccessful();
112+
}
113+
114+
@Override
115+
public synchronized Task<T> next() {
116+
if (!hasNext()) {
117+
throw new NoSuchElementException();
118+
}
119+
i++;
120+
return current = get(i);
121+
}
122+
};
123+
}
124+
}

0 commit comments

Comments
 (0)