aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Example
diff options
context:
space:
mode:
authorGravatar Sebastian Schmidt <mrschmidt@google.com>2018-06-12 10:58:35 -0700
committerGravatar GitHub <noreply@github.com>2018-06-12 10:58:35 -0700
commitafea8d5aacf474b57b4364feda08be9ca195594b (patch)
treed43c39ae9f71e88d256012f4467cd2b707fc7ddd /Firestore/Example
parent0db8ef7dbe6c8c191252d33090dbb88b98735148 (diff)
Refactor Remote Event (#1396)
Diffstat (limited to 'Firestore/Example')
-rw-r--r--Firestore/Example/Tests/Core/FSTQueryListenerTests.mm14
-rw-r--r--Firestore/Example/Tests/Core/FSTViewTests.mm46
-rw-r--r--Firestore/Example/Tests/Integration/FSTDatastoreTests.mm5
-rw-r--r--Firestore/Example/Tests/Local/FSTLocalStoreTests.mm32
-rw-r--r--Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm906
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSpecTests.mm13
-rw-r--r--Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json780
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json411
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limit_spec_test.json20
-rw-r--r--Firestore/Example/Tests/SpecTests/json/listen_spec_test.json1075
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.h54
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.mm135
12 files changed, 3056 insertions, 435 deletions
diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm
index 5629075..ddd831a 100644
--- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm
+++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm
@@ -118,11 +118,7 @@ NS_ASSUME_NONNULL_BEGIN
FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}];
FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil);
-
- FSTTargetChange *ackTarget =
- [FSTTargetChange changeWithDocuments:@[]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
- FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], FSTTestTargetChangeMarkCurrent());
[listener queryDidChangeViewSnapshot:snap1];
XCTAssertEqualObjects(accum, @[]);
@@ -188,9 +184,7 @@ NS_ASSUME_NONNULL_BEGIN
FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}];
FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
- FSTTargetChange *ackTarget =
- [FSTTargetChange changeWithDocuments:@[ doc1 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
+ FSTTargetChange *ackTarget = FSTTestTargetChangeAckDocuments({doc1.key});
FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget);
FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil);
@@ -342,9 +336,7 @@ NS_ASSUME_NONNULL_BEGIN
FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil);
FSTViewSnapshot *snap3 =
- FSTTestApplyChanges(view, @[],
- [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+ FSTTestApplyChanges(view, @[], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key}));
[listener applyChangedOnlineState:FSTOnlineStateOnline]; // no event
[listener queryDidChangeViewSnapshot:snap1];
diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm
index ec62d82..8c8d8a0 100644
--- a/Firestore/Example/Tests/Core/FSTViewTests.mm
+++ b/Firestore/Example/Tests/Core/FSTViewTests.mm
@@ -56,10 +56,8 @@ NS_ASSUME_NONNULL_BEGIN
FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
FSTDocument *doc3 = FSTTestDoc("rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO);
- FSTViewSnapshot *_Nullable snapshot =
- FSTTestApplyChanges(view, @[ doc1, doc2, doc3 ],
- [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+ FSTViewSnapshot *_Nullable snapshot = FSTTestApplyChanges(
+ view, @[ doc1, doc2, doc3 ], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key, doc3.key}));
XCTAssertEqual(snapshot.query, query);
@@ -90,8 +88,7 @@ NS_ASSUME_NONNULL_BEGIN
// delete doc2, add doc3
FSTViewSnapshot *snapshot =
FSTTestApplyChanges(view, @[ FSTTestDeletedDoc("rooms/eros/messages/2", 0), doc3 ],
- [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+ FSTTestTargetChangeAckDocuments({doc1.key, doc3.key}));
XCTAssertEqual(snapshot.query, query);
@@ -216,10 +213,8 @@ NS_ASSUME_NONNULL_BEGIN
FSTTestApplyChanges(view, @[ doc1, doc3 ], nil);
// add doc2, which should push out doc3
- FSTViewSnapshot *snapshot =
- FSTTestApplyChanges(view, @[ doc2 ],
- [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(
+ view, @[ doc2 ], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key, doc3.key}));
XCTAssertEqual(snapshot.query, query);
@@ -263,9 +258,8 @@ NS_ASSUME_NONNULL_BEGIN
previousChanges:viewDocChanges];
FSTViewSnapshot *snapshot =
[view applyChangesToDocuments:viewDocChanges
- targetChange:[FSTTargetChange
- changeWithDocuments:@[ doc1, doc2, doc3, doc4 ]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]
+ targetChange:FSTTestTargetChangeAckDocuments(
+ {doc1.key, doc2.key, doc3.key, doc4.key})]
.snapshot;
XCTAssertEqual(snapshot.query, query);
@@ -294,27 +288,21 @@ NS_ASSUME_NONNULL_BEGIN
applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]];
XCTAssertEqualObjects(change.limboChanges, @[]);
- change =
- [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
- targetChange:[FSTTargetChange
- changeWithDocuments:@[]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]];
+ change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
+ targetChange:FSTTestTargetChangeMarkCurrent()];
XCTAssertEqualObjects(
change.limboChanges,
@[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]);
- change = [view
- applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
- targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ]
- currentStatusUpdate:FSTCurrentStatusUpdateNone]];
+ change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
+ targetChange:FSTTestTargetChangeAckDocuments({doc1.key})];
XCTAssertEqualObjects(
change.limboChanges,
@[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]);
- change = [view
- applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]
- targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ]
- currentStatusUpdate:FSTCurrentStatusUpdateNone]];
+ change =
+ [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]
+ targetChange:FSTTestTargetChangeAckDocuments({doc2.key})];
XCTAssertEqualObjects(change.limboChanges, @[]);
change = [view
@@ -343,11 +331,9 @@ NS_ASSUME_NONNULL_BEGIN
FSTView *view =
[[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{doc1.key, doc2.key}];
- FSTTargetChange *markCurrent =
- [FSTTargetChange changeWithDocuments:@[]
- currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])];
- FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent];
+ FSTViewChange *change =
+ [view applyChangesToDocuments:changes targetChange:FSTTestTargetChangeMarkCurrent()];
XCTAssertEqualObjects(change.limboChanges, @[]);
}
diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm
index e6e1a19..cba9017 100644
--- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm
+++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm
@@ -48,6 +48,7 @@ namespace util = firebase::firestore::util;
using firebase::firestore::auth::EmptyCredentialsProvider;
using firebase::firestore::core::DatabaseInfo;
using firebase::firestore::model::DatabaseId;
+using firebase::firestore::model::DocumentKeySet;
using firebase::firestore::model::Precondition;
using firebase::firestore::model::TargetId;
@@ -121,6 +122,10 @@ NS_ASSUME_NONNULL_BEGIN
HARD_FAIL("Not implemented");
}
+- (DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetId {
+ return DocumentKeySet{};
+}
+
- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
[self.listenEvents addObject:remoteEvent];
XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0];
diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm
index e10fb12..8760571 100644
--- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm
+++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm
@@ -609,8 +609,8 @@ NS_ASSUME_NONNULL_BEGIN
FSTQuery *query = FSTTestQuery("foo");
FSTTargetID targetID = [self allocateQuery:query];
- [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO),
- @[ @(targetID) ], @[])];
+ [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO),
+ @[ @(targetID) ])];
[self collectGarbage];
FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO));
@@ -703,8 +703,8 @@ NS_ASSUME_NONNULL_BEGIN
FSTQuery *query = FSTTestQuery("foo");
FSTTargetID targetID = [self allocateQuery:query];
- [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO),
- @[ @(targetID) ], @[])];
+ [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO),
+ @[ @(targetID) ])];
[self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})];
[self collectGarbage];
FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO));
@@ -805,21 +805,19 @@ NS_ASSUME_NONNULL_BEGIN
FSTBoxedTargetID *targetID = @(queryData.targetID);
NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000);
- FSTWatchChange *watchChange =
+ FSTWatchTargetChange *watchChange =
[FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
targetIDs:@[ targetID ]
resumeToken:resumeToken];
NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *listens =
[NSMutableDictionary dictionary];
listens[targetID] = queryData;
- NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingResponses =
- [NSMutableDictionary dictionary];
- FSTWatchChangeAggregator *aggregator =
- [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(1000)
- listenTargets:listens
- pendingTargetResponses:pendingResponses];
- [aggregator addWatchChanges:@[ watchChange ]];
- FSTRemoteEvent *remoteEvent = [aggregator remoteEvent];
+ FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc]
+ initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider
+ providerWithSingleResultForKey:testutil::Key("foo/bar")
+ targets:@[ targetID ]]];
+ [aggregator handleTargetChange:watchChange];
+ FSTRemoteEvent *remoteEvent = [aggregator remoteEventAtSnapshotVersion:testutil::Version(1000)];
[self applyRemoteEvent:remoteEvent];
// Stop listening so that the query should become inactive (but persistent)
@@ -842,10 +840,10 @@ NS_ASSUME_NONNULL_BEGIN
[self allocateQuery:query];
FSTAssertTargetID(2);
- [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/baz", 10, @{@"a" : @"b"}, NO),
- @[ @2 ], @[])];
- [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, NO),
- @[ @2 ], @[])];
+ [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/baz", 10, @{@"a" : @"b"}, NO),
+ @[ @2 ])];
+ [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, NO),
+ @[ @2 ])];
[self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]];
diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm
index 84d0fa1..c6936f7 100644
--- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm
+++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm
@@ -34,6 +34,7 @@
namespace testutil = firebase::firestore::testutil;
using firebase::firestore::model::DocumentKey;
using firebase::firestore::model::DocumentKeySet;
+using firebase::firestore::model::SnapshotVersion;
NS_ASSUME_NONNULL_BEGIN
@@ -42,86 +43,204 @@ NS_ASSUME_NONNULL_BEGIN
@implementation FSTRemoteEventTests {
NSData *_resumeToken1;
- NSMutableDictionary<NSNumber *, NSNumber *> *_noPendingResponses;
+ NSMutableDictionary<NSNumber *, NSNumber *> *_noOutstandingResponses;
+ FSTTestTargetMetadataProvider *_targetMetadataProvider;
}
- (void)setUp {
_resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding];
- _noPendingResponses = [NSMutableDictionary dictionary];
+ _noOutstandingResponses = [NSMutableDictionary dictionary];
+ _targetMetadataProvider = [FSTTestTargetMetadataProvider new];
}
-- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray<NSNumber *> *)targets
- outstanding:
- (NSDictionary<NSNumber *, NSNumber *> *)outstanding
- changes:(NSArray<FSTWatchChange *> *)watchChanges {
- NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
- FSTQueryData *dummyQueryData = [FSTQueryData alloc];
- for (NSNumber *targetID in targets) {
- listens[targetID] = dummyQueryData;
+/**
+ * Creates a map with query data for the provided target IDs. All targets are considered active
+ * and query a collection named "coll".
+ */
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)queryDataForTargets:
+ (NSArray<FSTBoxedTargetID *> *)targetIDs {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *targets =
+ [NSMutableDictionary dictionary];
+ for (FSTBoxedTargetID *targetID in targetIDs) {
+ FSTQuery *query = FSTTestQuery("coll");
+ targets[targetID] = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID.intValue
+ listenSequenceNumber:0
+ purpose:FSTQueryPurposeListen];
+ }
+ return targets;
+}
+
+/**
+ * Creates a map with query data for the provided target IDs. All targets are marked as limbo
+ * queries for the document at "coll/limbo".
+ */
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)queryDataForLimboTargets:
+ (NSArray<FSTBoxedTargetID *> *)targetIDs {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *targets =
+ [NSMutableDictionary dictionary];
+ for (FSTBoxedTargetID *targetID in targetIDs) {
+ FSTQuery *query = FSTTestQuery("coll/limbo");
+ targets[targetID] = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID.intValue
+ listenSequenceNumber:0
+ purpose:FSTQueryPurposeLimboResolution];
}
+ return targets;
+}
+
+/**
+ * Creates an aggregator initialized with the set of provided FSTWatchChanges. Tests can add further
+ * changes via `handleDocumentChange`, `handleTargetChange` and `handleExistenceFilterChange`.
+ *
+ * @param snapshotVersion The version at which to create the remote event. This corresponds to the
+ * snapshot version provided by a NO_CHANGE event.
+ * @param targetMap A map of query data for all active targets. The map must include an entry for
+ * every target referenced by any of the watch changes.
+ * @param outstandingResponses The number of outstanding ACKs a target has to receive before it is
+ * considered active, or `_noOutstandingResponses` if all targets are already active.
+ * @param existingKeys The set of documents that are considered synced with the test targets as
+ * part of a previous listen. To modify this set during test execution, invoke
+ * `[_targetMetadataProvider setSyncedKeys:forQueryData:]`.
+ * @param watchChanges The watch changes to apply before returning the aggregator. Supported
+ * changes are FSTDocumentWatchChange and FSTWatchTargetChange.
+ */
+- (FSTWatchChangeAggregator *)
+aggregatorWithTargetMap:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)targetMap
+ outstandingResponses:
+ (nullable NSDictionary<FSTBoxedTargetID *, NSNumber *> *)outstandingResponses
+ existingKeys:(DocumentKeySet)existingKeys
+ changes:(NSArray<FSTWatchChange *> *)watchChanges {
FSTWatchChangeAggregator *aggregator =
- [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3)
- listenTargets:listens
- pendingTargetResponses:outstanding];
- [aggregator addWatchChanges:watchChanges];
+ [[FSTWatchChangeAggregator alloc] initWithTargetMetadataProvider:_targetMetadataProvider];
+
+ NSMutableArray<FSTBoxedTargetID *> *targetIDs = [NSMutableArray array];
+ [targetMap enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID,
+ FSTQueryData *queryData, BOOL *stop) {
+ [targetIDs addObject:targetID];
+ [_targetMetadataProvider setSyncedKeys:existingKeys forQueryData:queryData];
+ }];
+
+ [outstandingResponses
+ enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID, NSNumber *count, BOOL *stop) {
+ for (int i = 0; i < count.intValue; ++i) {
+ [aggregator recordTargetRequest:targetID];
+ }
+ }];
+
+ for (FSTWatchChange *change in watchChanges) {
+ if ([change isKindOfClass:[FSTDocumentWatchChange class]]) {
+ [aggregator handleDocumentChange:(FSTDocumentWatchChange *)change];
+ } else if ([change isKindOfClass:[FSTWatchTargetChange class]]) {
+ [aggregator handleTargetChange:(FSTWatchTargetChange *)change];
+ } else {
+ HARD_ASSERT("Encountered unexpected type of FSTWatchChange");
+ }
+ }
+
+ [aggregator handleTargetChange:[[FSTWatchTargetChange alloc]
+ initWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:targetIDs
+ resumeToken:_resumeToken1
+ cause:nil]];
+
return aggregator;
}
+/**
+ * Creates a single remote event that includes target changes for all provided FSTWatchChanges.
+ *
+ * @param snapshotVersion The version at which to create the remote event. This corresponds to the
+ * snapshot version provided by the NO_CHANGE event.
+ * @param targetMap A map of query data for all active targets. The map must include an entry for
+ * every target referenced by any of the watch changes.
+ * @param outstandingResponses The number of outstanding ACKs a target has to receive before it is
+ * considered active, or `_noOutstandingResponses` if all targets are already active.
+ * @param existingKeys The set of documents that are considered synced with the test targets as
+ * part of a previous listen.
+ * @param watchChanges The watch changes to apply before creating the remote event. Supported
+ * changes are FSTDocumentWatchChange and FSTWatchTargetChange.
+ */
+- (FSTRemoteEvent *)
+remoteEventAtSnapshotVersion:(FSTTestSnapshotVersion)snapshotVersion
+ targetMap:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)targetMap
+ outstandingResponses:
+ (nullable NSDictionary<FSTBoxedTargetID *, NSNumber *> *)outstandingResponses
+ existingKeys:(DocumentKeySet)existingKeys
+ changes:(NSArray<FSTWatchChange *> *)watchChanges {
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:outstandingResponses
+ existingKeys:existingKeys
+ changes:watchChanges];
+ return [aggregator remoteEventAtSnapshotVersion:testutil::Version(snapshotVersion)];
+}
+
- (void)testWillAccumulateDocumentAddedAndRemovedEvents {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
+ // The target map that contains an entry for every target in this test. If a target ID is omitted,
+ // the target is considered inactive and FSTTestTargetMetadataProvider will fail on access.
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2, @3, @4, @5, @6 ]];
+ FSTDocument *existingDoc = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ]
removedTargetIDs:@[ @4, @5, @6 ]
- documentKey:doc1.key
- document:doc1];
+ documentKey:existingDoc.key
+ document:existingDoc];
+ FSTDocument *newDoc = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @4 ]
removedTargetIDs:@[ @2, @6 ]
- documentKey:doc2.key
- document:doc2];
-
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2, @3, @4, @5, @6 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ documentKey:newDoc.key
+ document:newDoc];
+
+ // Create a remote event that includes both `change1` and `change2` as well as a NO_CHANGE event
+ // with the default resume token (`_resumeToken1`).
+ // As `existingDoc` is provided as an existing key, any updates to this document will be treated
+ // as modifications rather than adds.
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{existingDoc.key}
+ changes:@[ change1, change2 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 2);
- XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
- XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
+ XCTAssertEqualObjects(event.documentUpdates.at(existingDoc.key), existingDoc);
+ XCTAssertEqualObjects(event.documentUpdates.at(newDoc.key), newDoc);
- XCTAssertEqual(event.targetChanges.count, 6);
+ // 'change1' and 'change2' affect six different targets
+ XCTAssertEqual(event.targetChanges.size(), 6);
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ FSTTargetChange *targetChange1 =
+ FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key},
+ DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
- FSTUpdateMapping *mapping2 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]];
- XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2);
+ FSTTargetChange *targetChange2 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
- FSTUpdateMapping *mapping3 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3);
+ FSTTargetChange *targetChange3 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3);
- FSTUpdateMapping *mapping4 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]];
- XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4);
+ FSTTargetChange *targetChange4 =
+ FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{},
+ DocumentKeySet{existingDoc.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(4), targetChange4);
- FSTUpdateMapping *mapping5 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]];
- XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5);
+ FSTTargetChange *targetChange5 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(5), targetChange5);
- FSTUpdateMapping *mapping6 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]];
- XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6);
+ FSTTargetChange *targetChange6 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(6), targetChange6);
}
- (void)testWillIgnoreEventsForPendingTargets {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
@@ -135,31 +254,34 @@ NS_ASSUME_NONNULL_BEGIN
targetIDs:@[ @1 ]
cause:nil];
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc2.key
document:doc2];
// We're waiting for the unwatch and watch ack
- NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2 };
-
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ]
- outstanding:pendingResponses
- changes:@[ change1, change2, change3, change4 ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ NSDictionary<NSNumber *, NSNumber *> *outstandingResponses = @{ @1 : @2 };
+
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:outstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ change1, change2, change3, change4 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
// doc1 is ignored because it was part of an inactive target, but doc2 is in the changes
// because it become active.
XCTAssertEqual(event.documentUpdates.size(), 1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
}
- (void)testWillIgnoreEventsForRemovedTargets {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
@@ -170,25 +292,25 @@ NS_ASSUME_NONNULL_BEGIN
cause:nil];
// We're waiting for the unwatch ack
- NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @1 };
+ NSDictionary<NSNumber *, NSNumber *> *outstandingResponses = @{ @1 : @1 };
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:outstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ change1, change2 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
// doc1 is ignored because it was part of an inactive target
XCTAssertEqual(event.documentUpdates.size(), 0);
// Target 1 is ignored because it was removed
- XCTAssertEqual(event.targetChanges.count, 0);
+ XCTAssertEqual(event.targetChanges.size(), 0);
}
- (void)testWillKeepResetMappingEvenWithUpdates {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
- FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
@@ -199,10 +321,13 @@ NS_ASSUME_NONNULL_BEGIN
cause:nil];
// Add doc2, doc3
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc2.key
document:doc2];
+
+ FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO);
FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc3.key
@@ -213,203 +338,208 @@ NS_ASSUME_NONNULL_BEGIN
removedTargetIDs:@[ @1 ]
documentKey:doc2.key
document:doc2];
-
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2, change3, change4, change5 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{doc1.key}
+ changes:@[ change1, change2, change3, change4, change5 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 3);
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3);
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
// Only doc3 is part of the new mapping
- FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]];
-
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping);
+ FSTTargetChange *expectedChange = FSTTestTargetChange(
+ DocumentKeySet{doc3.key}, DocumentKeySet{}, DocumentKeySet{doc1.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange);
}
- (void)testWillHandleSingleReset {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+
// Reset target
- FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
- targetIDs:@[ @1 ]
- cause:nil];
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @1 ]
+ cause:nil];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
+ [aggregator handleTargetChange:change];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 0);
-
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
// Reset mapping is empty
- FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping);
+ FSTTargetChange *expectedChange =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange);
}
- (void)testWillHandleTargetAddAndRemovalInSameBatch {
- FSTDocument *doc1a = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc1b = FSTTestDoc("docs/1", 1, @{ @"value" : @2 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2 ]];
+ FSTDocument *doc1a = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[ @2 ]
documentKey:doc1a.key
document:doc1a];
+ FSTDocument *doc1b = FSTTestDoc("docs/1", 1, @{ @"value" : @2 }, NO);
FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ]
removedTargetIDs:@[ @1 ]
documentKey:doc1b.key
document:doc1b];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2 ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{doc1a.key}
+ changes:@[ change1, change2 ]];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 1);
XCTAssertEqualObjects(event.documentUpdates.at(doc1b.key), doc1b);
- XCTAssertEqual(event.targetChanges.count, 2);
+ XCTAssertEqual(event.targetChanges.size(), 2);
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ FSTTargetChange *targetChange1 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1b.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
- FSTUpdateMapping *mapping2 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2);
+ FSTTargetChange *targetChange2 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc1b.key},
+ DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
}
- (void)testTargetCurrentChangeWillMarkTheTargetCurrent {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+
FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
targetIDs:@[ @1 ]
resumeToken:_resumeToken1];
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ change ]];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 0);
- XCTAssertEqual(event.targetChanges.count, 1);
- FSTTargetChange *targetChange = event.targetChanges[@1];
- XCTAssertEqualObjects(targetChange.mapping, [[FSTUpdateMapping alloc] init]);
- XCTAssertEqual(targetChange.currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(targetChange.resumeToken, _resumeToken1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
+
+ FSTTargetChange *targetChange =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange);
}
- (void)testTargetAddedChangeWillResetPreviousState {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @3 ]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @3 ]
removedTargetIDs:@[ @2 ]
documentKey:doc1.key
document:doc1];
+
FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
targetIDs:@[ @1, @2, @3 ]
resumeToken:_resumeToken1];
+
FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
targetIDs:@[ @1 ]
cause:nil];
+
FSTWatchChange *change4 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
targetIDs:@[ @2 ]
cause:nil];
+
FSTWatchChange *change5 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded
targetIDs:@[ @1 ]
cause:nil];
+
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[ @3 ]
documentKey:doc2.key
document:doc2];
- NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2, @2 : @1 };
+ NSDictionary<NSNumber *, NSNumber *> *outstandingResponses = @{ @1 : @2, @2 : @1 };
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1, @3 ]
- outstanding:pendingResponses
- changes:@[ change1, change2, change3, change4, change5, change6 ]];
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:outstandingResponses
+ existingKeys:DocumentKeySet{doc2.key}
+ changes:@[ change1, change2, change3, change4, change5, change6 ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 2);
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
// target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove
- XCTAssertEqual(event.targetChanges.count, 2);
+ XCTAssertEqual(event.targetChanges.size(), 2);
- // doc1 was before the remove, so it does not show up in the mapping
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
- // Current was before the remove
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone);
+ // doc1 was before the remove, so it does not show up in the mapping.
+ // Current was before the remove.
+ FSTTargetChange *targetChange1 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc2.key},
+ DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
// Doc1 was before the remove
- FSTUpdateMapping *mapping3 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]];
- XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3);
// Current was before the remove
- XCTAssertEqual(event.targetChanges[@3].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@3].resumeToken, _resumeToken1);
+ FSTTargetChange *targetChange3 = FSTTestTargetChange(
+ DocumentKeySet{doc1.key}, DocumentKeySet{}, DocumentKeySet{doc2.key}, _resumeToken1, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3);
}
- (void)testNoChangeWillStillMarkTheAffectedTargets {
- FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
- targetIDs:@[ @1 ]
- resumeToken:_resumeToken1];
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
- XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.documentUpdates.size(), 0);
- XCTAssertEqual(event.targetChanges.count, 1);
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]);
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone);
- XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
-}
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
-- (void)testExistenceFiltersWillReplacePreviousExistenceFilters {
- FSTExistenceFilter *filter1 = [FSTExistenceFilter filterWithCount:1];
- FSTExistenceFilter *filter2 = [FSTExistenceFilter filterWithCount:2];
- FSTWatchChange *change1 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:1];
- FSTWatchChange *change2 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:2];
- // replace filter1 for target 2
- FSTWatchChange *change3 = [FSTExistenceFilterWatchChange changeWithFilter:filter2 targetID:2];
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ [aggregator handleTargetChange:change];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1, @2 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2, change3 ]];
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 0);
- XCTAssertEqual(event.targetChanges.count, 0);
- XCTAssertEqual(aggregator.existenceFilters.count, 2);
- XCTAssertEqual(aggregator.existenceFilters[@1], filter1);
- XCTAssertEqual(aggregator.existenceFilters[@2], filter2);
+ XCTAssertEqual(event.targetChanges.size(), 1);
+
+ FSTTargetChange *targetChange =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange);
}
-- (void)testExistenceFilterMismatchResetsTarget {
- FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
+- (void)testExistenceFilterMismatchClearsTarget {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2 ]];
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
document:doc1];
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc2.key
@@ -420,270 +550,353 @@ NS_ASSUME_NONNULL_BEGIN
resumeToken:_resumeToken1];
FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2, change3 ]];
+ [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{doc1.key, doc2.key}
+ changes:@[ change1, change2, change3 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 2);
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 2);
+
+ FSTTargetChange *targetChange1 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, DocumentKeySet{}, _resumeToken1, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
+
+ FSTTargetChange *targetChange2 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
+
+ // The existence filter mismatch will remove the document from target 1,
+ // but not synthesize a document delete.
+ FSTExistenceFilterWatchChange *change4 =
+ [FSTExistenceFilterWatchChange changeWithFilter:[FSTExistenceFilter filterWithCount:1]
+ targetID:1];
+ [aggregator handleExistenceFilter:change4];
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
+ event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(4)];
- [event handleExistenceFilterMismatchForTargetID:@1];
+ FSTTargetChange *targetChange3 = FSTTestTargetChange(
+ DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, [NSData data], NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange3);
- // Mapping is reset
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]);
- // Reset the resume snapshot
- XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(0));
- // Target needs to be set to not current
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent);
- XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0);
+ XCTAssertEqual(event.targetChanges.size(), 1);
+ XCTAssertEqual(event.targetMismatches.size(), 1);
+ XCTAssertEqual(event.documentUpdates.size(), 0);
}
-- (void)testDocumentUpdate {
+- (void)testExistenceFilterMismatchRemovesCurrentChanges {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
+
+ FSTWatchTargetChange *markCurrent =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ [aggregator handleTargetChange:markCurrent];
+
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTDeletedDocument *deletedDoc1 =
- [FSTDeletedDocument documentWithKey:doc1.key version:testutil::Version(3)];
- FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
- FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO);
+ FSTDocumentWatchChange *addDoc = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+ [aggregator handleDocumentChange:addDoc];
+
+ // The existence filter mismatch will remove the document from target 1, but not synthesize a
+ // document delete.
+ FSTExistenceFilterWatchChange *existenceFilter =
+ [FSTExistenceFilterWatchChange changeWithFilter:[FSTExistenceFilter filterWithCount:0]
+ targetID:1];
+ [aggregator handleExistenceFilter:existenceFilter];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
+ XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
+ XCTAssertEqual(event.documentUpdates.size(), 1);
+ XCTAssertEqual(event.targetMismatches.size(), 1);
+ XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
+
+ XCTAssertEqual(event.targetChanges.size(), 1);
+
+ FSTTargetChange *targetChange1 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
+}
+
+- (void)testDocumentUpdate {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]];
+
+ FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc1.key
document:doc1];
+ FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO);
FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:doc2.key
document:doc2];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2 ]];
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ change1, change2 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
- FSTRemoteEvent *event = [aggregator remoteEvent];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 2);
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1);
XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
- // Update doc1
- [event addDocumentUpdate:deletedDoc1];
- [event addDocumentUpdate:doc3];
+ [_targetMetadataProvider setSyncedKeys:DocumentKeySet{doc1.key, doc2.key}
+ forQueryData:targetMap[@1]];
+
+ FSTDeletedDocument *deletedDoc1 =
+ [FSTDeletedDocument documentWithKey:doc1.key version:testutil::Version(3)];
+ FSTDocumentWatchChange *change3 =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1 ]
+ documentKey:deletedDoc1.key
+ document:deletedDoc1];
+ [aggregator handleDocumentChange:change3];
+
+ FSTDocument *updatedDoc2 = FSTTestDoc("docs/2", 3, @{ @"value" : @2 }, NO);
+ FSTDocumentWatchChange *change4 =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:updatedDoc2.key
+ document:updatedDoc2];
+ [aggregator handleDocumentChange:change4];
+
+ FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO);
+ FSTDocumentWatchChange *change5 =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc3.key
+ document:doc3];
+ [aggregator handleDocumentChange:change5];
+
+ event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
XCTAssertEqual(event.snapshotVersion, testutil::Version(3));
XCTAssertEqual(event.documentUpdates.size(), 3);
// doc1 is replaced
XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), deletedDoc1);
- // doc2 is untouched
- XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2);
+ // doc2 is updated
+ XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), updatedDoc2);
// doc3 is new
XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3);
// Target is unchanged
- XCTAssertEqual(event.targetChanges.count, 1);
+ XCTAssertEqual(event.targetChanges.size(), 1);
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ FSTTargetChange *targetChange =
+ FSTTestTargetChange(DocumentKeySet{doc3.key}, DocumentKeySet{updatedDoc2.key},
+ DocumentKeySet{deletedDoc1.key}, _resumeToken1, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange);
}
- (void)testResumeTokensHandledPerTarget {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2 ]];
+
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
+
+ FSTWatchTargetChange *change1 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ [aggregator handleTargetChange:change1];
+
NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding];
- FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
- targetIDs:@[ @1 ]
- resumeToken:_resumeToken1];
- FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
- targetIDs:@[ @2 ]
- resumeToken:resumeToken2];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
- XCTAssertEqual(event.targetChanges.count, 2);
-
- FSTUpdateMapping *mapping1 =
- [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
-
- XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@2].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2);
+ FSTWatchTargetChange *change2 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @2 ]
+ resumeToken:resumeToken2];
+ [aggregator handleTargetChange:change2];
+
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
+ XCTAssertEqual(event.targetChanges.size(), 2);
+
+ FSTTargetChange *targetChange1 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
+
+ FSTTargetChange *targetChange2 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
}
- (void)testLastResumeTokenWins {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForTargets:@[ @1, @2 ]];
+
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[]];
+
+ FSTWatchTargetChange *change1 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+ [aggregator handleTargetChange:change1];
+
NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding];
+ FSTWatchTargetChange *change2 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @1 ]
+ resumeToken:resumeToken2];
+ [aggregator handleTargetChange:change2];
+
NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding];
+ FSTWatchTargetChange *change3 =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @2 ]
+ resumeToken:resumeToken3];
+ [aggregator handleTargetChange:change3];
- FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
- targetIDs:@[ @1 ]
- resumeToken:_resumeToken1];
- FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
- targetIDs:@[ @1 ]
- resumeToken:resumeToken2];
- FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
- targetIDs:@[ @2 ]
- resumeToken:resumeToken3];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1, @2 ]
- outstanding:_noPendingResponses
- changes:@[ change1, change2, change3 ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
- XCTAssertEqual(event.targetChanges.count, 2);
-
- FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]];
- XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
- XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2);
-
- XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1);
- XCTAssertEqual(event.targetChanges[@2].snapshotVersion, testutil::Version(3));
- XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone);
- XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3);
+ FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)];
+ XCTAssertEqual(event.targetChanges.size(), 2);
+
+ FSTTargetChange *targetChange1 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES);
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1);
+
+ FSTTargetChange *targetChange2 =
+ FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken3, NO);
+ XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2);
}
- (void)testSynthesizeDeletes {
- FSTWatchChange *shouldSynthesize =
- [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1 ]];
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForLimboTargets:@[ @1 ]];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ shouldSynthesize ]];
+ DocumentKey limboKey = testutil::Key("coll/limbo");
- FSTRemoteEvent *event = [aggregator remoteEvent];
- DocumentKey synthesized = DocumentKey::FromPathString("docs/2");
- XCTAssertEqual(event.documentUpdates.find(synthesized), event.documentUpdates.end());
+ FSTWatchChange *resolveLimboTarget =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1 ]];
+
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ resolveLimboTarget ]];
- FSTTargetChange *limboTargetChange = event.targetChanges[@1];
- [event synthesizeDeleteForLimboTargetChange:limboTargetChange key:synthesized];
FSTDeletedDocument *expected =
- [FSTDeletedDocument documentWithKey:synthesized version:event.snapshotVersion];
- XCTAssertEqualObjects(expected, event.documentUpdates.at(synthesized));
- XCTAssertTrue(event.limboDocumentChanges.contains(synthesized));
+ [FSTDeletedDocument documentWithKey:limboKey version:event.snapshotVersion];
+ XCTAssertEqualObjects(event.documentUpdates.at(limboKey), expected);
+ XCTAssertTrue(event.limboDocumentChanges.contains(limboKey));
}
- (void)testDoesntSynthesizeDeletesForWrongState {
- FSTWatchChange *wrongState =
- [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @2 ]];
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForLimboTargets:@[ @1 ]];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @2 ] outstanding:_noPendingResponses changes:@[ wrongState ]];
+ FSTWatchChange *wrongState =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @1 ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
+ FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ wrongState ]];
- DocumentKey notSynthesized = DocumentKey::FromPathString("docs/no1");
- [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@2] key:notSynthesized];
- XCTAssertEqual(event.documentUpdates.find(notSynthesized), event.documentUpdates.end());
- XCTAssertFalse(event.limboDocumentChanges.contains(notSynthesized));
+ XCTAssertEqual(event.documentUpdates.size(), 0);
+ XCTAssertEqual(event.limboDocumentChanges.size(), 0);
}
- (void)testDoesntSynthesizeDeletesForExistingDoc {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForLimboTargets:@[ @3 ]];
+
FSTWatchChange *hasDocument =
[FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @3 ]];
- FSTDocument *doc = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO);
- FSTWatchChange *docChange = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @3 ]
- removedTargetIDs:@[]
- documentKey:doc.key
- document:doc];
- FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @3 ]
- outstanding:_noPendingResponses
- changes:@[ hasDocument, docChange ]];
-
- FSTRemoteEvent *event = [aggregator remoteEvent];
- [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@3] key:doc.key];
- FSTMaybeDocument *docData = event.documentUpdates.at(doc.key);
- XCTAssertFalse([docData isKindOfClass:[FSTDeletedDocument class]]);
- XCTAssertFalse(event.limboDocumentChanges.contains(doc.key));
+
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{FSTTestDocKey(@"coll/limbo")}
+ changes:@[ hasDocument ]];
+
+ XCTAssertEqual(event.documentUpdates.size(), 0);
+ XCTAssertEqual(event.limboDocumentChanges.size(), 0);
}
-- (void)testFilterUpdates {
+- (void)testSeparatesDocumentUpdates {
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [self queryDataForLimboTargets:@[ @1 ]];
+
FSTDocument *newDoc = FSTTestDoc("docs/new", 1, @{@"key" : @"value"}, NO);
- FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO);
FSTWatchChange *newDocChange = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:newDoc.key
document:newDoc];
+ FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO);
FSTWatchChange *existingDocChange =
[[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
removedTargetIDs:@[]
documentKey:existingDoc.key
document:existingDoc];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @1 ]
- outstanding:_noPendingResponses
- changes:@[ newDocChange, existingDocChange ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
- DocumentKeySet existingKeys = DocumentKeySet{existingDoc.key};
-
- FSTTargetChange *updateChange = event.targetChanges[@1];
- XCTAssertTrue([updateChange.mapping isKindOfClass:[FSTUpdateMapping class]]);
- FSTUpdateMapping *update = (FSTUpdateMapping *)updateChange.mapping;
- FSTDocumentKey *existingDocKey = existingDoc.key;
- FSTDocumentKey *newDocKey = newDoc.key;
- XCTAssertTrue(update.addedDocuments.contains(existingDocKey));
-
- [update filterUpdatesUsingExistingKeys:existingKeys];
- // Now it's been filtered, since it already existed.
- XCTAssertFalse(update.addedDocuments.contains(existingDocKey));
- XCTAssertTrue(update.addedDocuments.contains(newDocKey));
-}
-
-- (void)testDoesntFilterResets {
- FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO);
- const DocumentKey &existingDocKey = existingDoc.key;
- FSTWatchTargetChange *resetTargetChange =
- [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
- targetIDs:@[ @2 ]
- resumeToken:_resumeToken1];
- FSTWatchChange *existingDocChange =
- [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ]
- removedTargetIDs:@[]
- documentKey:existingDocKey
- document:existingDoc];
- FSTWatchChangeAggregator *aggregator =
- [self aggregatorWithTargets:@[ @2 ]
- outstanding:_noPendingResponses
- changes:@[ resetTargetChange, existingDocChange ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
- DocumentKeySet existingKeys = DocumentKeySet{existingDocKey};
-
- FSTTargetChange *resetChange = event.targetChanges[@2];
- XCTAssertTrue([resetChange.mapping isKindOfClass:[FSTResetMapping class]]);
- FSTResetMapping *resetMapping = (FSTResetMapping *)resetChange.mapping;
- XCTAssertTrue(resetMapping.documents.contains(existingDocKey));
-
- [resetMapping filterUpdatesUsingExistingKeys:existingKeys];
- // Document is still there, even though it already exists. Reset mappings don't get filtered.
- XCTAssertTrue(resetMapping.documents.contains(existingDocKey));
+ FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc("docs/deleted", 1);
+ FSTWatchChange *deletedDocChange =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1 ]
+ documentKey:deletedDoc.key
+ document:deletedDoc];
+
+ FSTDeletedDocument *missingDoc = FSTTestDeletedDoc("docs/missing", 1);
+ FSTWatchChange *missingDocChange =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1 ]
+ documentKey:missingDoc.key
+ document:missingDoc];
+
+ FSTRemoteEvent *event = [self
+ remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet{existingDoc.key, deletedDoc.key}
+ changes:@[
+ newDocChange, existingDocChange, deletedDocChange, missingDocChange
+ ]];
+
+ FSTTargetChange *targetChange =
+ FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key},
+ DocumentKeySet{deletedDoc.key}, _resumeToken1, NO);
+
+ XCTAssertEqualObjects(event.targetChanges.at(1), targetChange);
}
- (void)testTracksLimboDocuments {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap =
+ [NSMutableDictionary dictionary];
+ [targetMap addEntriesFromDictionary:[self queryDataForTargets:@[ @1 ]]];
+ [targetMap addEntriesFromDictionary:[self queryDataForLimboTargets:@[ @2 ]]];
+
// Add 3 docs: 1 is limbo and non-limbo, 2 is limbo-only, 3 is non-limbo
FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"key" : @"value"}, NO);
FSTDocument *doc2 = FSTTestDoc("docs/2", 1, @{@"key" : @"value"}, NO);
FSTDocument *doc3 = FSTTestDoc("docs/3", 1, @{@"key" : @"value"}, NO);
// Target 2 is a limbo target
-
FSTWatchChange *docChange1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2 ]
removedTargetIDs:@[]
documentKey:doc1.key
@@ -702,20 +915,13 @@ NS_ASSUME_NONNULL_BEGIN
FSTWatchChange *targetsChange =
[FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1, @2 ]];
- NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
- listens[@1] = [FSTQueryData alloc];
- listens[@2] = [[FSTQueryData alloc] initWithQuery:[FSTQuery alloc]
- targetID:2
- listenSequenceNumber:1000
- purpose:FSTQueryPurposeLimboResolution];
- FSTWatchChangeAggregator *aggregator =
- [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3)
- listenTargets:listens
- pendingTargetResponses:@{}];
-
- [aggregator addWatchChanges:@[ docChange1, docChange2, docChange3, targetsChange ]];
+ FSTRemoteEvent *event =
+ [self remoteEventAtSnapshotVersion:3
+ targetMap:targetMap
+ outstandingResponses:_noOutstandingResponses
+ existingKeys:DocumentKeySet {}
+ changes:@[ docChange1, docChange2, docChange3, targetsChange ]];
- FSTRemoteEvent *event = [aggregator remoteEvent];
DocumentKeySet limboDocChanges = event.limboDocumentChanges;
// Doc1 is in both limbo and non-limbo targets, therefore not tracked as limbo
XCTAssertFalse(limboDocChanges.contains(doc1.key));
diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm
index 77010e5..c131f7e 100644
--- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm
+++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm
@@ -256,12 +256,15 @@ static NSString *const kNoIOSTag = @"no-ios";
} else if (watchEntity[@"doc"]) {
NSArray *docSpec = watchEntity[@"doc"];
FSTDocumentKey *key = FSTTestDocKey(docSpec[0]);
- FSTObjectValue *value = FSTTestObjectValue(docSpec[2]);
+ FSTObjectValue *_Nullable value =
+ [docSpec[2] isKindOfClass:[NSNull class]] ? nil : FSTTestObjectValue(docSpec[2]);
SnapshotVersion version = [self parseVersion:docSpec[1]];
- FSTMaybeDocument *doc = [FSTDocument documentWithData:value
- key:key
- version:std::move(version)
- hasLocalMutations:NO];
+ FSTMaybeDocument *doc =
+ value ? [FSTDocument documentWithData:value
+ key:key
+ version:std::move(version)
+ hasLocalMutations:NO]
+ : [FSTDeletedDocument documentWithKey:key version:std::move(version)];
FSTWatchChange *change =
[[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"]
removedTargetIDs:watchEntity[@"removedTargets"]
diff --git a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
index abd2cf4..3e5d4fb 100644
--- a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
+++ b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
@@ -1,4 +1,458 @@
{
+ "Existence filter match": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter match",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1"
+ ],
+ "watchSnapshot": 2000
+ }
+ ]
+ },
+ "Existence filter match after pending update": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter match after pending update",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Existence filter with empty target": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter with empty target",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Existence filter ignored with pending target": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter ignored with pending target",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ]
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
"Existence filter mismatch triggers re-run of query": {
"describeName": "Existence Filters:",
"itName": "Existence filter mismatch triggers re-run of query",
@@ -501,6 +955,332 @@
}
]
},
+ "Existence filter handled at global snapshot": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter handled at global snapshot",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ],
+ "collection/1",
+ "collection/2"
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/3",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/3",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/1",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/2",
+ 2000,
+ {
+ "v": 2
+ }
+ ],
+ [
+ "collection/3",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-3000"
+ ],
+ "watchSnapshot": 3000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/2",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Existence filter synthesizes deletes": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter synthesizes deletes",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 2
+ ]
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
"Existence filter limbo resolution is denied": {
"describeName": "Existence Filters:",
"itName": "Existence filter limbo resolution is denied",
diff --git a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
index ee2d883..a186496 100644
--- a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
+++ b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
@@ -1146,5 +1146,416 @@
]
}
]
+ },
+ "Limbo documents handle receiving ack and then current": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents handle receiving ack and then current",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userPatch": [
+ "collection/a",
+ {
+ "include": false
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "limboDocs": [
+ "collection/b"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/b",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 1
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-3000"
+ ],
+ "watchSnapshot": 3000
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "include": true,
+ "key": "a"
+ }
+ ]
+ ],
+ "removedTargets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "include": true,
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ },
+ "watchSnapshot": 4000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 1,
+ "filters": [
+ [
+ "include",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ }
+ ]
}
}
diff --git a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
index 5a02463..6aa1daa 100644
--- a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
+++ b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
@@ -287,7 +287,15 @@
"targets": [
2
]
- },
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
"watchSnapshot": 2000,
"stateExpect": {
"limboDocs": [
@@ -1144,7 +1152,15 @@
"targets": [
2
]
- },
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
"watchSnapshot": 2000,
"stateExpect": {
"limboDocs": [
diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
index 7bfe557..e838d2f 100644
--- a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
+++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
@@ -238,6 +238,177 @@
}
]
},
+ "Doesn't raise events for empty target": {
+ "describeName": "Listens:",
+ "itName": "Doesn't raise events for empty target",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userListen": [
+ 6,
+ {
+ "path": "collection3",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "6": {
+ "query": {
+ "path": "collection3",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection2/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 6
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection2/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
"Ensure correct query results with latency-compensated deletes": {
"describeName": "Listens:",
"itName": "Ensure correct query results with latency-compensated deletes",
@@ -385,6 +556,79 @@
}
]
},
+ "Does not raise event for initial document delete": {
+ "describeName": "Listens:",
+ "itName": "Does not raise event for initial document delete",
+ "tags": [""],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ null
+ ]
+ ],
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1000
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
"Will process removals without waiting for a consistent snapshot": {
"describeName": "Listens:",
"itName": "Will process removals without waiting for a consistent snapshot",
@@ -1690,5 +1934,836 @@
]
}
]
+ },
+ "Synthesizes deletes for missing document": {
+ "describeName": "Listens:",
+ "itName": "Synthesizes deletes for missing document",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 4,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 4
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Re-opens target without existence filter": {
+ "describeName": "Listens:",
+ "itName": "Re-opens target without existence filter",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ null
+ ]
+ ],
+ "removedTargets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Ignores update from inactive target": {
+ "describeName": "Listens:",
+ "itName": "Ignores update from inactive target",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Does not synthesize deletes for previously acked documents": {
+ "describeName": "Listens:",
+ "itName": "Does not synthesize deletes for previously acked documents",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-2000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
}
}
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h
index ccc01ca..7946c06 100644
--- a/Firestore/Example/Tests/Util/FSTHelpers.h
+++ b/Firestore/Example/Tests/Util/FSTHelpers.h
@@ -16,11 +16,11 @@
#import <Foundation/Foundation.h>
-#include <map>
#include <vector>
#import "Firestore/Source/Core/FSTTypes.h"
#import "Firestore/Source/Model/FSTDocumentDictionary.h"
+#import "Firestore/Source/Remote/FSTRemoteEvent.h"
#include "Firestore/core/src/firebase/firestore/model/field_path.h"
#include "Firestore/core/src/firebase/firestore/model/field_value.h"
@@ -148,6 +148,42 @@ inline NSString *FSTRemoveExceptionPrefix(NSString *exception) {
XCTAssertTrue(didThrow, ##__VA_ARGS__); \
} while (0)
+/**
+ * An implementation of FSTTargetMetadataProvider that provides controlled access to the
+ * `FSTTargetMetadataProvider` callbacks. Any target accessed via these callbacks must be
+ * registered beforehand via the factory methods or via `setSyncedKeys:forQueryData:`.
+ */
+@interface FSTTestTargetMetadataProvider : NSObject <FSTTargetMetadataProvider>
+
+/**
+ * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for
+ * each of the given targets, where each target has previously seen query results containing just
+ * the given documentKey.
+ *
+ * Internally this means that the `remoteKeysForTarget` callback for these targets will return just
+ * the documentKey and that the provided targets will be returned as active from the
+ * `queryDataForTarget` target.
+ */
++ (instancetype)providerWithSingleResultForKey:(firebase::firestore::model::DocumentKey)documentKey
+ targets:(NSArray<FSTBoxedTargetID *> *)targets;
+
+/**
+ * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for
+ * each of the given targets, where each target has not seen any previous document.
+ *
+ * Internally this means that the `remoteKeysForTarget` callback for these targets will return an
+ * empty set of document keys and that the provided targets will be returned as active from the
+ * `queryDataForTarget` target.
+ */
++ (instancetype)providerWithEmptyResultForKey:(firebase::firestore::model::DocumentKey)documentKey
+ targets:(NSArray<FSTBoxedTargetID *> *)targets;
+
+/** Sets or replaces the local state for the provided query data. */
+- (void)setSyncedKeys:(firebase::firestore::model::DocumentKeySet)keys
+ forQueryData:(FSTQueryData *)queryData;
+
+@end
+
/** Creates a new FIRTimestamp from components. Note that year, month, and day are all one-based. */
FIRTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second);
@@ -250,6 +286,9 @@ FSTDeleteMutation *FSTTestDeleteMutation(NSString *path);
/** Converts a list of documents to a sorted map. */
FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs);
+/** Creates a remote event that inserts a new document. */
+FSTRemoteEvent *FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, NSArray<NSNumber *> *addedToTargets);
+
/** Creates a remote event with changes to a document. */
FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
NSArray<NSNumber *> *updatedInTargets,
@@ -260,6 +299,19 @@ FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query,
NSArray<NSString *> *addedKeys,
NSArray<NSString *> *removedKeys);
+/** Creates a test target change that acks all 'docs' and marks the target as CURRENT */
+FSTTargetChange *FSTTestTargetChangeAckDocuments(firebase::firestore::model::DocumentKeySet docs);
+
+/** Creates a test target change that marks the target as CURRENT */
+FSTTargetChange *FSTTestTargetChangeMarkCurrent();
+
+/** Creates a test target change. */
+FSTTargetChange *FSTTestTargetChange(firebase::firestore::model::DocumentKeySet added,
+ firebase::firestore::model::DocumentKeySet modified,
+ firebase::firestore::model::DocumentKeySet removed,
+ NSData *resumeToken,
+ BOOL current);
+
/** Creates a resume token to match the given snapshot version. */
NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot);
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm
index 5ed4fd5..8ece82f 100644
--- a/Firestore/Example/Tests/Util/FSTHelpers.mm
+++ b/Firestore/Example/Tests/Util/FSTHelpers.mm
@@ -22,7 +22,7 @@
#include <cinttypes>
#include <list>
-#include <map>
+#include <unordered_map>
#include <utility>
#include <vector>
@@ -58,6 +58,7 @@ namespace util = firebase::firestore::util;
namespace testutil = firebase::firestore::testutil;
using firebase::firestore::model::DatabaseId;
using firebase::firestore::model::DocumentKey;
+using firebase::firestore::model::DocumentKeySet;
using firebase::firestore::model::FieldMask;
using firebase::firestore::model::FieldPath;
using firebase::firestore::model::FieldTransform;
@@ -65,8 +66,8 @@ using firebase::firestore::model::FieldValue;
using firebase::firestore::model::Precondition;
using firebase::firestore::model::ResourcePath;
using firebase::firestore::model::ServerTimestampTransform;
+using firebase::firestore::model::SnapshotVersion;
using firebase::firestore::model::TransformOperation;
-using firebase::firestore::model::DocumentKeySet;
NS_ASSUME_NONNULL_BEGIN
@@ -298,29 +299,125 @@ FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view,
.snapshot;
}
+@implementation FSTTestTargetMetadataProvider {
+ std::unordered_map<FSTTargetID, DocumentKeySet> _syncedKeys;
+ std::unordered_map<FSTTargetID, FSTQueryData *> _queryData;
+}
+
++ (instancetype)providerWithSingleResultForKey:(DocumentKey)documentKey
+ targets:(NSArray<FSTBoxedTargetID *> *)targets {
+ FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new];
+ FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()];
+
+ for (FSTBoxedTargetID *targetID in targets) {
+ FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID.intValue
+ listenSequenceNumber:0
+ purpose:FSTQueryPurposeListen];
+ [metadataProvider setSyncedKeys:DocumentKeySet{documentKey} forQueryData:queryData];
+ }
+
+ return metadataProvider;
+}
+
++ (instancetype)providerWithEmptyResultForKey:(DocumentKey)documentKey
+ targets:(NSArray<FSTBoxedTargetID *> *)targets {
+ FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new];
+ FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()];
+
+ for (FSTBoxedTargetID *targetID in targets) {
+ FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID.intValue
+ listenSequenceNumber:0
+ purpose:FSTQueryPurposeListen];
+ [metadataProvider setSyncedKeys:DocumentKeySet {} forQueryData:queryData];
+ }
+
+ return metadataProvider;
+}
+
+- (void)setSyncedKeys:(DocumentKeySet)keys forQueryData:(FSTQueryData *)queryData {
+ _syncedKeys[queryData.targetID] = keys;
+ _queryData[queryData.targetID] = queryData;
+}
+
+- (DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetID {
+ auto it = _syncedKeys.find(targetID.intValue);
+ HARD_ASSERT(it != _syncedKeys.end(), "Cannot process unknown target %s", targetID.intValue);
+ return it->second;
+}
+
+- (nullable FSTQueryData *)queryDataForTarget:(FSTBoxedTargetID *)targetID {
+ auto it = _queryData.find(targetID.intValue);
+ HARD_ASSERT(it != _queryData.end(), "Cannot process unknown target %s", targetID.intValue);
+ return it->second;
+}
+
+@end
+
+FSTRemoteEvent *FSTTestAddedRemoteEvent(FSTMaybeDocument *doc,
+ NSArray<FSTBoxedTargetID *> *addedToTargets) {
+ HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations],
+ "Docs from remote updates shouldn't have local changes.");
+ FSTDocumentWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:addedToTargets
+ removedTargetIDs:{}
+ documentKey:doc.key
+ document:doc];
+ FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc]
+ initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider
+ providerWithEmptyResultForKey:doc.key
+ targets:addedToTargets]];
+ [aggregator handleDocumentChange:change];
+ return [aggregator remoteEventAtSnapshotVersion:doc.version];
+}
+
FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
- NSArray<NSNumber *> *updatedInTargets,
- NSArray<NSNumber *> *removedFromTargets) {
+ NSArray<FSTBoxedTargetID *> *updatedInTargets,
+ NSArray<FSTBoxedTargetID *> *removedFromTargets) {
+ HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations],
+ "Docs from remote updates shouldn't have local changes.");
FSTDocumentWatchChange *change =
[[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets
removedTargetIDs:removedFromTargets
documentKey:doc.key
document:doc];
- NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
- FSTQueryData *dummyQueryData = [FSTQueryData alloc];
- for (NSNumber *targetID in updatedInTargets) {
- listens[targetID] = dummyQueryData;
- }
- for (NSNumber *targetID in removedFromTargets) {
- listens[targetID] = dummyQueryData;
- }
- NSMutableDictionary<NSNumber *, NSNumber *> *pending = [NSMutableDictionary dictionary];
- FSTWatchChangeAggregator *aggregator =
- [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version
- listenTargets:listens
- pendingTargetResponses:pending];
- [aggregator addWatchChange:change];
- return [aggregator remoteEvent];
+ NSArray<FSTBoxedTargetID *> *targets =
+ [updatedInTargets arrayByAddingObjectsFromArray:removedFromTargets];
+ FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc]
+ initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider
+ providerWithSingleResultForKey:doc.key
+ targets:targets]];
+ [aggregator handleDocumentChange:change];
+ return [aggregator remoteEventAtSnapshotVersion:doc.version];
+}
+
+FSTTargetChange *FSTTestTargetChangeMarkCurrent() {
+ return [[FSTTargetChange alloc] initWithResumeToken:[NSData data]
+ current:YES
+ addedDocuments:DocumentKeySet {}
+ modifiedDocuments:DocumentKeySet {}
+ removedDocuments:DocumentKeySet{}];
+}
+
+FSTTargetChange *FSTTestTargetChangeAckDocuments(DocumentKeySet docs) {
+ return [[FSTTargetChange alloc] initWithResumeToken:[NSData data]
+ current:YES
+ addedDocuments:docs
+ modifiedDocuments:DocumentKeySet {}
+ removedDocuments:DocumentKeySet{}];
+}
+
+FSTTargetChange *FSTTestTargetChange(DocumentKeySet added,
+ DocumentKeySet modified,
+ DocumentKeySet removed,
+ NSData *resumeToken,
+ BOOL current) {
+ return [[FSTTargetChange alloc] initWithResumeToken:resumeToken
+ current:current
+ addedDocuments:added
+ modifiedDocuments:modified
+ removedDocuments:removed];
}
/** Creates a resume token to match the given snapshot version. */