diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index 63edb9815b8..43e15242396 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -213,6 +213,7 @@ public abstract class SpecTestCase implements RemoteStoreCallback { Collections.synchronizedList(new ArrayList<>()); private final List rejectedDocs = Collections.synchronizedList(new ArrayList<>()); private List> snapshotsInSyncListeners; + private int waitForPendingWriteEvents = 0; private int snapshotsInSyncEvents = 0; /** An executor to use for test callbacks. */ @@ -534,6 +535,14 @@ private void doDelete(String key) throws Exception { doMutation(deleteMutation(key)); } + private void doWaitForPendingWrites() { + final TaskCompletionSource source = new TaskCompletionSource<>(); + source + .getTask() + .addOnSuccessListener(backgroundExecutor, result -> waitForPendingWriteEvents += 1); + syncEngine.registerPendingWritesTask(source); + } + private void doAddSnapshotsInSyncListener() { EventListener eventListener = (Void v, FirebaseFirestoreException error) -> snapshotsInSyncEvents += 1; @@ -813,6 +822,8 @@ private void doStep(JSONObject step) throws Exception { doWriteAck(step.getJSONObject("writeAck")); } else if (step.has("failWrite")) { doFailWrite(step.getJSONObject("failWrite")); + } else if (step.has("waitForPendingWrites")) { + doWaitForPendingWrites(); } else if (step.has("runTimer")) { doRunTimer(step.getString("runTimer")); } else if (step.has("enableNetwork")) { @@ -986,6 +997,11 @@ private void validateSnapshotsInSyncEvents(int expectedCount) { snapshotsInSyncEvents = 0; } + private void validateWaitForPendingWritesEvents(int expectedCount) { + assertEquals(expectedCount, waitForPendingWriteEvents); + waitForPendingWriteEvents = 0; + } + private void validateUserCallbacks(@Nullable JSONObject expected) throws JSONException { if (expected != null && expected.has("userCallbacks")) { JSONObject userCallbacks = expected.getJSONObject("userCallbacks"); @@ -1116,6 +1132,8 @@ private void runSteps(JSONArray steps, JSONObject config) throws Exception { step.remove("expectedState"); int expectedSnapshotsInSyncEvents = step.optInt("expectedSnapshotsInSyncEvents"); step.remove("expectedSnapshotsInSyncEvents"); + int expectedWaitForPendingWritesEvents = step.optInt("expectedWaitForPendingWritesEvents"); + step.remove("expectedWaitForPendingWritesEvents"); log(" Doing step " + step); doStep(step); @@ -1133,6 +1151,7 @@ private void runSteps(JSONArray steps, JSONObject config) throws Exception { } validateExpectedState(expectedState); validateSnapshotsInSyncEvents(expectedSnapshotsInSyncEvents); + validateWaitForPendingWritesEvents(expectedWaitForPendingWritesEvents); events.clear(); acknowledgedDocs.clear(); rejectedDocs.clear(); diff --git a/firebase-firestore/src/test/resources/json/listen_spec_test.json b/firebase-firestore/src/test/resources/json/listen_spec_test.json index 469b2f37981..f6c60c6957c 100644 --- a/firebase-firestore/src/test/resources/json/listen_spec_test.json +++ b/firebase-firestore/src/test/resources/json/listen_spec_test.json @@ -1152,6 +1152,341 @@ } ] }, + "Documents outside of view are cleared when listen is removed.": { + "comment": "", + "describeName": "Listens:", + "itName": "Documents outside of view are cleared when listen is removed.", + "tags": [ + "eager-gc" + ], + "config": { + "numClients": 1, + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "matches": false + } + ] + }, + { + "userListen": [ + 2, + { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "matches": true + }, + "version": 1000 + }, + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "matches": true + }, + "version": 1000 + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "targetIds": [ + ], + "version": 1000 + }, + "expectedSnapshotEvents": [ + { + "added": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "matches": true + }, + "version": 1000 + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + } + ] + }, + { + "userSet": [ + "collection/b", + { + "matches": false + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false, + "query": { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + }, + "removed": [ + { + "key": "collection/b", + "options": { + "hasCommittedMutations": false, + "hasLocalMutations": false + }, + "value": { + "matches": true + }, + "version": 1000 + } + ] + } + ] + }, + { + "userUnlisten": [ + 2, + { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "userListen": [ + 4, + { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + "4": { + "queries": [ + { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "userUnlisten": [ + 4, + { + "filters": [ + [ + "matches", + "==", + true + ] + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + }, + { + "writeAck": { + "version": 2000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "writeAck": { + "version": 3000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/b" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "userListen": [ + 6, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + "6": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + }, + { + "userUnlisten": [ + 6, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedState": { + "activeTargets": { + } + } + } + ] + }, "Does not raise event for initial document delete": { "describeName": "Listens:", "itName": "Does not raise event for initial document delete", diff --git a/firebase-firestore/src/test/resources/json/offline_spec_test.json b/firebase-firestore/src/test/resources/json/offline_spec_test.json index 0b46943841b..9584498ecd9 100644 --- a/firebase-firestore/src/test/resources/json/offline_spec_test.json +++ b/firebase-firestore/src/test/resources/json/offline_spec_test.json @@ -94,6 +94,74 @@ } ] }, + "Client stays offline during credential change": { + "describeName": "Offline:", + "itName": "Client stays offline during credential change", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": true + }, + "steps": [ + { + "enableNetwork": false, + "expectedState": { + "activeLimboDocs": [ + ], + "activeTargets": { + }, + "enqueuedLimboDocs": [ + ] + } + }, + { + "changeUser": "user1" + }, + { + "userListen": [ + 2, + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "expectedSnapshotEvents": [ + { + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false, + "query": { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + } + ], + "expectedState": { + "activeTargets": { + "2": { + "queries": [ + { + "filters": [ + ], + "orderBys": [ + ], + "path": "collection" + } + ], + "resumeToken": "" + } + } + } + } + ] + }, "Empty queries are resolved if client goes offline": { "describeName": "Offline:", "itName": "Empty queries are resolved if client goes offline", diff --git a/firebase-firestore/src/test/resources/json/write_spec_test.json b/firebase-firestore/src/test/resources/json/write_spec_test.json index f2d6dec58bb..928171a54bc 100644 --- a/firebase-firestore/src/test/resources/json/write_spec_test.json +++ b/firebase-firestore/src/test/resources/json/write_spec_test.json @@ -5945,6 +5945,441 @@ } ] }, + "Wait for pending writes resolves after write acknowledgment": { + "describeName": "Writes:", + "itName": "Wait for pending writes resolves after write acknowledgment", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "k": "a" + } + ] + }, + { + "userSet": [ + "collection/b", + { + "k": "b" + } + ] + }, + { + "waitForPendingWrites": true + }, + { + "writeAck": { + "version": 1001 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + } + }, + { + "failWrite": { + "error": { + "code": 9 + } + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + ], + "rejectedDocs": [ + "collection/b" + ] + } + }, + "expectedWaitForPendingWritesEvents": 1 + } + ] + }, + "Wait for pending writes resolves for write in secondary tab": { + "describeName": "Writes:", + "itName": "Wait for pending writes resolves for write in secondary tab", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 2, + "useGarbageCollection": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true, + "expectedState": { + "isPrimary": true + } + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userSet": [ + "collection/a", + { + "k": "a" + } + ] + }, + { + "clientIndex": 1, + "waitForPendingWrites": true + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "writeAck": { + "version": 1001 + } + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + }, + "expectedWaitForPendingWritesEvents": 1 + } + ] + }, + "Wait for pending writes resolves if another write is issued": { + "describeName": "Writes:", + "itName": "Wait for pending writes resolves if another write is issued", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "k": "a" + } + ] + }, + { + "waitForPendingWrites": true + }, + { + "userSet": [ + "collection/b", + { + "k": "b" + } + ] + }, + { + "writeAck": { + "version": 1001 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + }, + "expectedWaitForPendingWritesEvents": 1 + }, + { + "writeAck": { + "version": 1002 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/b" + ], + "rejectedDocs": [ + ] + } + } + } + ] + }, + "Wait for pending writes resolves independently for different tabs": { + "describeName": "Writes:", + "itName": "Wait for pending writes resolves independently for different tabs", + "tags": [ + "multi-client" + ], + "config": { + "numClients": 3, + "useGarbageCollection": false + }, + "steps": [ + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "userSet": [ + "collection/a", + { + "k": "a" + } + ] + }, + { + "clientIndex": 0, + "waitForPendingWrites": true + }, + { + "clientIndex": 1, + "drainQueue": true + }, + { + "clientIndex": 1, + "userSet": [ + "collection/b", + { + "k": "b" + } + ] + }, + { + "clientIndex": 1, + "waitForPendingWrites": true + }, + { + "clientIndex": 2, + "drainQueue": true + }, + { + "clientIndex": 2, + "userSet": [ + "collection/c", + { + "k": "c" + } + ] + }, + { + "clientIndex": 2, + "waitForPendingWrites": true + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "writeAck": { + "version": 1001 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + }, + "expectedWaitForPendingWritesEvents": 1 + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedWaitForPendingWritesEvents": 0 + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedWaitForPendingWritesEvents": 0 + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "writeAck": { + "version": 1002 + }, + "expectedWaitForPendingWritesEvents": 0 + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/b" + ], + "rejectedDocs": [ + ] + } + }, + "expectedWaitForPendingWritesEvents": 1 + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedWaitForPendingWritesEvents": 0 + }, + { + "clientIndex": 0, + "drainQueue": true + }, + { + "clientIndex": 0, + "writeAck": { + "version": 1003 + }, + "expectedWaitForPendingWritesEvents": 0 + }, + { + "clientIndex": 1, + "drainQueue": true, + "expectedWaitForPendingWritesEvents": 0 + }, + { + "clientIndex": 2, + "drainQueue": true, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/c" + ], + "rejectedDocs": [ + ] + } + }, + "expectedWaitForPendingWritesEvents": 1 + } + ] + }, + "Wait for pending writes resolves multiple times": { + "describeName": "Writes:", + "itName": "Wait for pending writes resolves multiple times", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "k": "a" + } + ] + }, + { + "waitForPendingWrites": true + }, + { + "waitForPendingWrites": true + }, + { + "writeAck": { + "version": 1001 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [ + ] + } + }, + "expectedWaitForPendingWritesEvents": 2 + } + ] + }, + "Wait for pending writes resolves with no writes": { + "describeName": "Writes:", + "itName": "Wait for pending writes resolves with no writes", + "tags": [ + ], + "config": { + "numClients": 1, + "useGarbageCollection": true + }, + "steps": [ + { + "waitForPendingWrites": true, + "expectedWaitForPendingWritesEvents": 1 + } + ] + }, + "Wait for pending writes waits after restart": { + "describeName": "Writes:", + "itName": "Wait for pending writes waits after restart", + "tags": [ + "durable-persistence" + ], + "config": { + "numClients": 1, + "useGarbageCollection": true + }, + "steps": [ + { + "userSet": [ + "collection/a", + { + "k": "a" + } + ] + }, + { + "restart": true, + "expectedState": { + "activeLimboDocs": [ + ], + "activeTargets": { + }, + "enqueuedLimboDocs": [ + ] + } + }, + { + "waitForPendingWrites": true + }, + { + "writeAck": { + "version": 1001 + }, + "expectedWaitForPendingWritesEvents": 1 + } + ] + }, "Write are sequenced by multiple clients": { "describeName": "Writes:", "itName": "Write are sequenced by multiple clients",