Skip to content

Commit 1b63d4a

Browse files
authored
test(firestore): add integration tests for large documents (#8367)
Add integration tests for 16MB documents. Tests reads, writes, snapshot reads from server and the cache.
1 parent 8482c84 commit 1b63d4a

2 files changed

Lines changed: 348 additions & 0 deletions

File tree

firebase-firestore/firebase-firestore.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,14 @@ android {
7272
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
7373
consumerProguardFiles 'proguard.txt'
7474

75+
// By default, exclude large tests because they are slow.
76+
// Run with `./gradlew :firebase-firestore:connectedCheck -PrunLargeTests` to run ONLY large tests.
77+
if (project.hasProperty('runLargeTests')) {
78+
testInstrumentationRunnerArguments annotation: 'androidx.test.filters.LargeTest'
79+
} else {
80+
testInstrumentationRunnerArguments notAnnotation: 'androidx.test.filters.LargeTest'
81+
}
82+
7583
// Acceptable values are: 'emulator', 'qa', 'nightly', and 'prod'.
7684
def targetBackend = findProperty("targetBackend") ?: "emulator"
7785
buildConfigField("String", "TARGET_BACKEND", "\"$targetBackend\"")
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
// Copyright 2026 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;
16+
17+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.testFirestore;
18+
import static com.google.firebase.firestore.testutil.IntegrationTestUtil.waitFor;
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertTrue;
21+
import static org.junit.Assert.fail;
22+
23+
import androidx.test.ext.junit.runners.AndroidJUnit4;
24+
import androidx.test.filters.LargeTest;
25+
import com.google.android.gms.tasks.Task;
26+
import java.util.Arrays;
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.concurrent.CountDownLatch;
31+
import java.util.concurrent.TimeUnit;
32+
import org.junit.After;
33+
import org.junit.AfterClass;
34+
import org.junit.BeforeClass;
35+
import org.junit.Test;
36+
import org.junit.runner.RunWith;
37+
38+
/**
39+
* Tests for handling large documents in Firestore.
40+
* <p>
41+
* <b>How to run these tests:</b>
42+
* <ul>
43+
* <li><b>Command Line:</b> {@code ./gradlew :firebase-firestore:connectedCheck -PrunLargeTests}</li>
44+
* <li><b>Android Studio:</b> Add {@code runLargeTests=true} to your root {@code gradle.properties},
45+
* sync the project, and click the Run button next to the class or test method.
46+
* (Remember to remove it when done to keep local builds fast).</li>
47+
* </ul>
48+
*/
49+
@LargeTest
50+
@RunWith(AndroidJUnit4.class)
51+
public class LargeDocumentTest {
52+
53+
private static String seedCollection;
54+
private static String unicodePayload;
55+
private static String asciiPayload;
56+
57+
// Extended timeout because these tests can be slow.
58+
private static final int TIMEOUT_MS = 120000;
59+
60+
private static String generateUnicodeString(int targetUtf8Bytes) {
61+
StringBuilder sb = new StringBuilder();
62+
String emoji = "🚀"; // 4 bytes in UTF-8
63+
int bytes = 0;
64+
while (bytes < targetUtf8Bytes) {
65+
if (bytes % 2 == 0 && bytes + 4 <= targetUtf8Bytes) {
66+
sb.append(emoji);
67+
bytes += 4;
68+
} else {
69+
sb.append('a');
70+
bytes += 1;
71+
}
72+
}
73+
return sb.toString();
74+
}
75+
76+
private static String generateAsciiString(int sizeInBytes) {
77+
char[] chars = new char[sizeInBytes];
78+
Arrays.fill(chars, 'a');
79+
return new String(chars);
80+
}
81+
82+
@BeforeClass
83+
public static void setUpClass() {
84+
FirebaseFirestore db = testFirestore();
85+
seedCollection = "large_doc_tests_" + System.currentTimeMillis();
86+
87+
int targetBytes = (int) Math.floor(15.9 * 1024 * 1024);
88+
unicodePayload = generateUnicodeString(targetBytes);
89+
asciiPayload = generateAsciiString(targetBytes);
90+
91+
DocumentReference docRef = db.collection(seedCollection).document("doc_15_9MB_unicode");
92+
DocumentReference docA = db.collection(seedCollection).document("doc_a");
93+
DocumentReference docB = db.collection(seedCollection).document("doc_b");
94+
95+
Map<String, Object> dataUnicode = new HashMap<>();
96+
dataUnicode.put("chunk", unicodePayload);
97+
dataUnicode.put("tag", "unicode_large_doc");
98+
Map<String, Object> dataAscii = new HashMap<>();
99+
dataAscii.put("chunk", asciiPayload);
100+
dataAscii.put("tag", "ascii_large_doc");
101+
102+
waitFor(docRef.set(dataUnicode));
103+
waitFor(docA.set(dataAscii));
104+
waitFor(docB.set(dataAscii));
105+
}
106+
107+
@AfterClass
108+
public static void tearDownClass() {
109+
if (seedCollection != null) {
110+
FirebaseFirestore db = testFirestore();
111+
try {
112+
waitFor(db.collection(seedCollection).document("doc_15_9MB_unicode").delete());
113+
waitFor(db.collection(seedCollection).document("doc_a").delete());
114+
waitFor(db.collection(seedCollection).document("doc_b").delete());
115+
} catch (Exception e) {
116+
// Suppress cleanup exceptions
117+
}
118+
}
119+
}
120+
121+
@After
122+
public void tearDown() {
123+
com.google.firebase.firestore.testutil.IntegrationTestUtil.tearDown();
124+
}
125+
126+
@Test(timeout = TIMEOUT_MS)
127+
public void testReadAndCacheLargeUnicodeDocument() {
128+
FirebaseFirestore db = testFirestore();
129+
DocumentReference docRef = db.collection(seedCollection).document("doc_15_9MB_unicode");
130+
131+
DocumentSnapshot serverSnapshot = waitFor(docRef.get(Source.SERVER));
132+
assertTrue(serverSnapshot.exists());
133+
134+
waitFor(db.disableNetwork());
135+
136+
DocumentSnapshot cacheSnapshot = waitFor(docRef.get(Source.CACHE));
137+
assertTrue(cacheSnapshot.exists());
138+
139+
assertEquals(serverSnapshot.getData(), cacheSnapshot.getData());
140+
141+
waitFor(db.enableNetwork());
142+
}
143+
144+
@Test(timeout = TIMEOUT_MS)
145+
public void testCacheIntegrityWithMultipleLargeDocuments() {
146+
FirebaseFirestore db = testFirestore();
147+
148+
CollectionReference colRef = db.collection(seedCollection);
149+
DocumentReference docA = colRef.document("doc_a");
150+
DocumentReference docB = colRef.document("doc_b");
151+
152+
waitFor(docA.get(Source.SERVER));
153+
waitFor(docB.get(Source.SERVER));
154+
155+
waitFor(db.disableNetwork());
156+
157+
DocumentSnapshot cacheSnapshotA = waitFor(docA.get(Source.CACHE));
158+
DocumentSnapshot cacheSnapshotB = waitFor(docB.get(Source.CACHE));
159+
160+
assertTrue("docA should exist in cache", cacheSnapshotA.exists());
161+
assertTrue("docB should exist in cache", cacheSnapshotB.exists());
162+
163+
assertEquals(asciiPayload, cacheSnapshotA.getString("chunk"));
164+
assertEquals(asciiPayload, cacheSnapshotB.getString("chunk"));
165+
166+
waitFor(db.enableNetwork());
167+
}
168+
169+
@Test(timeout = TIMEOUT_MS)
170+
public void testWatchStreamInitializationAndDiff() throws Exception {
171+
FirebaseFirestore db = testFirestore();
172+
DocumentReference docRef = db.collection(seedCollection).document("doc_15_9MB_unicode");
173+
174+
String expectedValue = "updated_val_" + System.currentTimeMillis();
175+
176+
// Verify that the initial snapshot of a large document is received successfully
177+
// without triggering stream cancellation loops.
178+
CountDownLatch updateLatch = new CountDownLatch(1);
179+
ListenerRegistration registration =
180+
docRef.addSnapshotListener(
181+
(snapshot, error) -> {
182+
if (snapshot != null
183+
&& snapshot.exists()
184+
&& expectedValue.equals(snapshot.getString("differential_field"))) {
185+
updateLatch.countDown();
186+
}
187+
});
188+
189+
try {
190+
Task<DocumentSnapshot> firstSnapshotTask = docRef.get(Source.SERVER);
191+
DocumentSnapshot firstSnapshot = waitFor(firstSnapshotTask);
192+
assertTrue(firstSnapshot.exists());
193+
194+
Map<String, Object> updateData = new HashMap<>();
195+
updateData.put("differential_field", expectedValue);
196+
waitFor(docRef.update(updateData));
197+
198+
assertTrue(
199+
"Watch stream should deliver differential update",
200+
updateLatch.await(60, TimeUnit.SECONDS));
201+
} finally {
202+
registration.remove();
203+
}
204+
}
205+
206+
@Test(timeout = TIMEOUT_MS)
207+
public void testOversizedPayloadRejection() {
208+
FirebaseFirestore db = testFirestore();
209+
DocumentReference docRef = db.collection(seedCollection).document("temp_oversized_doc");
210+
211+
Map<String, Object> data = new HashMap<>();
212+
// 16.1MB payload
213+
int oversizedPayloadBytes = (16 * 1024 * 1024) + 102400;
214+
data.put("largeField", generateAsciiString(oversizedPayloadBytes));
215+
216+
try {
217+
waitFor(docRef.set(data));
218+
fail("Setting a document exceeding the maximum size limit should fail.");
219+
} catch (Exception e) {
220+
assertTrue(e.getCause() instanceof FirebaseFirestoreException);
221+
FirebaseFirestoreException firestoreException = (FirebaseFirestoreException) e.getCause();
222+
assertEquals(FirebaseFirestoreException.Code.INVALID_ARGUMENT, firestoreException.getCode());
223+
}
224+
}
225+
226+
@Test(timeout = TIMEOUT_MS)
227+
public void testWriteValidLargeDocument() {
228+
FirebaseFirestore db = testFirestore();
229+
String tempDocId = "temp_valid_large_doc_" + System.currentTimeMillis();
230+
DocumentReference docRef = db.collection(seedCollection).document(tempDocId);
231+
232+
try {
233+
int targetBytes = (int) Math.floor(15.9 * 1024 * 1024);
234+
String largePayload = generateAsciiString(targetBytes);
235+
Map<String, Object> data = new HashMap<>();
236+
data.put("chunk", largePayload);
237+
238+
waitFor(docRef.set(data));
239+
240+
DocumentSnapshot snapshot = waitFor(docRef.get(Source.SERVER));
241+
assertTrue(snapshot.exists());
242+
assertEquals(largePayload, snapshot.getString("chunk"));
243+
} finally {
244+
try {
245+
waitFor(docRef.delete());
246+
} catch (Exception e) {
247+
// Suppress cleanup exceptions
248+
}
249+
}
250+
}
251+
252+
@Test(timeout = TIMEOUT_MS)
253+
public void testTransactionReadModifyWrite() {
254+
FirebaseFirestore db = testFirestore();
255+
DocumentReference docRef = db.collection(seedCollection).document("doc_15_9MB_unicode");
256+
257+
long timestamp = System.currentTimeMillis();
258+
Task<Void> transactionTask =
259+
db.runTransaction(
260+
transaction -> {
261+
DocumentSnapshot snapshot = transaction.get(docRef);
262+
assertTrue(snapshot.exists());
263+
264+
transaction.update(docRef, "transaction_timestamp", timestamp);
265+
return null;
266+
});
267+
268+
waitFor(transactionTask);
269+
270+
DocumentSnapshot updatedSnapshot = waitFor(docRef.get(Source.SERVER));
271+
assertTrue(updatedSnapshot.exists());
272+
assertEquals(Long.valueOf(timestamp), updatedSnapshot.getLong("transaction_timestamp"));
273+
assertEquals(unicodePayload, updatedSnapshot.getString("chunk"));
274+
}
275+
276+
@Test(timeout = TIMEOUT_MS)
277+
public void testQueryLargeDocuments() {
278+
FirebaseFirestore db = testFirestore();
279+
CollectionReference colRef = db.collection(seedCollection);
280+
281+
Query query = colRef.whereIn(FieldPath.documentId(), Arrays.asList("doc_a", "doc_b"));
282+
283+
QuerySnapshot serverSnapshot = waitFor(query.get(Source.SERVER));
284+
assertEquals(
285+
"Query should return exactly 2 large documents from server", 2, serverSnapshot.size());
286+
287+
waitFor(db.disableNetwork());
288+
289+
QuerySnapshot cacheSnapshot = waitFor(query.get(Source.CACHE));
290+
assertEquals(
291+
"Query should return exactly 2 large documents from cache", 2, cacheSnapshot.size());
292+
293+
for (DocumentSnapshot serverDoc : serverSnapshot.getDocuments()) {
294+
DocumentSnapshot matchingCacheDoc =
295+
cacheSnapshot.getDocuments().stream()
296+
.filter(d -> d.getId().equals(serverDoc.getId()))
297+
.findFirst()
298+
.orElse(null);
299+
assertTrue(
300+
"Document " + serverDoc.getId() + " should exist in cache snapshot",
301+
matchingCacheDoc != null);
302+
assertEquals(
303+
"Payload for " + serverDoc.getId() + " in cache should match server",
304+
serverDoc.getData(),
305+
matchingCacheDoc.getData());
306+
}
307+
308+
waitFor(db.enableNetwork());
309+
}
310+
311+
@Test(timeout = TIMEOUT_MS)
312+
public void testQueryLargeDocumentsForcesLocalScan() {
313+
FirebaseFirestore db = testFirestore();
314+
CollectionReference colRef = db.collection(seedCollection);
315+
316+
waitFor(colRef.document("doc_a").get(Source.SERVER));
317+
waitFor(colRef.document("doc_b").get(Source.SERVER));
318+
319+
waitFor(db.disableNetwork());
320+
321+
Query query =
322+
colRef.whereEqualTo("tag", "ascii_large_doc").orderBy(FieldPath.documentId()).limit(2);
323+
324+
// Execute the query offline
325+
QuerySnapshot cacheSnapshot = waitFor(query.get(Source.CACHE));
326+
327+
assertEquals(
328+
"Query should find and return exactly 2 large documents from cache",
329+
2,
330+
cacheSnapshot.size());
331+
332+
List<DocumentSnapshot> docs = cacheSnapshot.getDocuments();
333+
assertEquals("doc_a", docs.get(0).getId());
334+
assertEquals("doc_b", docs.get(1).getId());
335+
assertEquals(asciiPayload, docs.get(0).getString("chunk"));
336+
assertEquals(asciiPayload, docs.get(1).getString("chunk"));
337+
338+
waitFor(db.enableNetwork());
339+
}
340+
}

0 commit comments

Comments
 (0)