From afea8d5aacf474b57b4364feda08be9ca195594b Mon Sep 17 00:00:00 2001 From: Sebastian Schmidt Date: Tue, 12 Jun 2018 10:58:35 -0700 Subject: Refactor Remote Event (#1396) --- .../Example/Tests/Core/FSTQueryListenerTests.mm | 14 +- Firestore/Example/Tests/Core/FSTViewTests.mm | 46 +- .../Example/Tests/Integration/FSTDatastoreTests.mm | 5 + .../Example/Tests/Local/FSTLocalStoreTests.mm | 32 +- .../Example/Tests/Remote/FSTRemoteEventTests.mm | 906 ++++++++++------- Firestore/Example/Tests/SpecTests/FSTSpecTests.mm | 13 +- .../SpecTests/json/existence_filter_spec_test.json | 940 +++++++++++++++-- .../Tests/SpecTests/json/limbo_spec_test.json | 411 ++++++++ .../Tests/SpecTests/json/limit_spec_test.json | 20 +- .../Tests/SpecTests/json/listen_spec_test.json | 1075 ++++++++++++++++++++ Firestore/Example/Tests/Util/FSTHelpers.h | 54 +- Firestore/Example/Tests/Util/FSTHelpers.mm | 135 ++- 12 files changed, 3136 insertions(+), 515 deletions(-) (limited to 'Firestore/Example') 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 *listens = [NSMutableDictionary dictionary]; listens[targetID] = queryData; - NSMutableDictionary *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 *_noPendingResponses; + NSMutableDictionary *_noOutstandingResponses; + FSTTestTargetMetadataProvider *_targetMetadataProvider; } - (void)setUp { _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; - _noPendingResponses = [NSMutableDictionary dictionary]; + _noOutstandingResponses = [NSMutableDictionary dictionary]; + _targetMetadataProvider = [FSTTestTargetMetadataProvider new]; } -- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray *)targets - outstanding: - (NSDictionary *)outstanding - changes:(NSArray *)watchChanges { - NSMutableDictionary *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 *)queryDataForTargets: + (NSArray *)targetIDs { + NSMutableDictionary *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 *)queryDataForLimboTargets: + (NSArray *)targetIDs { + NSMutableDictionary *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 *)targetMap + outstandingResponses: + (nullable NSDictionary *)outstandingResponses + existingKeys:(DocumentKeySet)existingKeys + changes:(NSArray *)watchChanges { FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3) - listenTargets:listens - pendingTargetResponses:outstanding]; - [aggregator addWatchChanges:watchChanges]; + [[FSTWatchChangeAggregator alloc] initWithTargetMetadataProvider:_targetMetadataProvider]; + + NSMutableArray *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 *)targetMap + outstandingResponses: + (nullable NSDictionary *)outstandingResponses + existingKeys:(DocumentKeySet)existingKeys + changes:(NSArray *)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 *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 *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 *pendingResponses = @{ @1 : @2 }; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] - outstanding:pendingResponses - changes:@[ change1, change2, change3, change4 ]]; - FSTRemoteEvent *event = [aggregator remoteEvent]; + NSDictionary *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 *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 *pendingResponses = @{ @1 : @1 }; + NSDictionary *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 *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 *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 *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 *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 *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 *pendingResponses = @{ @1 : @2, @2 : @1 }; + NSDictionary *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 *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 *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 *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 *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 *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 *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 *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 *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 *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 *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 *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 *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,7 +1,699 @@ { + "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", + "itName": "Existence filter mismatch triggers re-run of query", + "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 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "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 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [ + "collection/2" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/2", + 1000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Existence filter mismatch will drop resume token": { + "describeName": "Existence Filters:", + "itName": "Existence filter mismatch will drop resume token", "tags": [], "config": { "useGarbageCollection": true @@ -62,7 +754,7 @@ [ 2 ], - "resume-token-1000" + "existence-filter-resume-token" ], "watchSnapshot": 1000, "expect": [ @@ -94,6 +786,32 @@ } ] }, + { + "watchStreamClose": { + "error": { + "code": 14, + "message": "Simulated Backend Error" + }, + "runBackoffTimer": true + }, + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "existence-filter-resume-token" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, { "watchFilter": [ [ @@ -237,9 +955,9 @@ } ] }, - "Existence filter mismatch will drop resume token": { + "Existence filter handled at global snapshot": { "describeName": "Existence Filters:", - "itName": "Existence filter mismatch will drop resume token", + "itName": "Existence filter handled at global snapshot", "tags": [], "config": { "useGarbageCollection": true @@ -281,13 +999,6 @@ { "v": 1 } - ], - [ - "collection/2", - 1000, - { - "v": 2 - } ] ], "targets": [ @@ -300,7 +1011,7 @@ [ 2 ], - "existence-filter-resume-token" + "resume-token-1000" ], "watchSnapshot": 1000, "expect": [ @@ -317,13 +1028,6 @@ { "v": 1 } - ], - [ - "collection/2", - 1000, - { - "v": 2 - } ] ], "errorCode": 0, @@ -332,39 +1036,30 @@ } ] }, - { - "watchStreamClose": { - "error": { - "code": 14, - "message": "Simulated Backend Error" - }, - "runBackoffTimer": true - }, - "stateExpect": { - "activeTargets": { - "2": { - "query": { - "path": "collection", - "filters": [], - "orderBys": [] - }, - "resumeToken": "existence-filter-resume-token" - } - } - } - }, - { - "watchAck": [ - 2 - ] - }, { "watchFilter": [ [ 2 ], - "collection/1" - ], + "collection/1", + "collection/2" + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/3", + 3000, + { + "v": 3 + } + ] + ], + "targets": [ + 2 + ] + }, "watchSnapshot": 2000, "expect": [ { @@ -373,6 +1068,15 @@ "filters": [], "orderBys": [] }, + "added": [ + [ + "collection/3", + 3000, + { + "v": 3 + } + ] + ], "errorCode": 0, "fromCache": true, "hasPendingWrites": false @@ -412,6 +1116,20 @@ { "v": 1 } + ], + [ + "collection/2", + 2000, + { + "v": 2 + } + ], + [ + "collection/3", + 3000, + { + "v": 3 + } ] ], "targets": [ @@ -424,25 +1142,55 @@ [ 2 ], - "resume-token-2000" + "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": [] + } ], - "watchSnapshot": 2000, "stateExpect": { - "limboDocs": [ - "collection/2" - ], "activeTargets": { - "1": { - "query": { - "path": "collection/2", - "filters": [], - "orderBys": [] - }, - "resumeToken": "" - }, "2": { "query": { - "path": "collection", + "path": "collection/a", "filters": [], "orderBys": [] }, @@ -453,43 +1201,75 @@ }, { "watchAck": [ - 1 + 2 ] }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, { "watchCurrent": [ [ - 1 + 2 ], - "resume-token-2000" + "resume-token-1000" ], - "watchSnapshot": 2000, - "stateExpect": { - "limboDocs": [], - "activeTargets": { - "2": { - "query": { - "path": "collection", - "filters": [], - "orderBys": [] - }, - "resumeToken": "" - } + "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", + "path": "collection/a", "filters": [], "orderBys": [] }, "removed": [ [ - "collection/2", + "collection/a", 1000, { - "v": 2 + "v": 1 } ] ], 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 -#include #include #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 + +/** + * 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 *)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 *)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 *docs); +/** Creates a remote event that inserts a new document. */ +FSTRemoteEvent *FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, NSArray *addedToTargets); + /** Creates a remote event with changes to a document. */ FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, NSArray *updatedInTargets, @@ -260,6 +299,19 @@ FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, NSArray *addedKeys, NSArray *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 #include -#include +#include #include #include @@ -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 _syncedKeys; + std::unordered_map _queryData; +} + ++ (instancetype)providerWithSingleResultForKey:(DocumentKey)documentKey + targets:(NSArray *)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 *)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 *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 *updatedInTargets, - NSArray *removedFromTargets) { + NSArray *updatedInTargets, + NSArray *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 *listens = [NSMutableDictionary dictionary]; - FSTQueryData *dummyQueryData = [FSTQueryData alloc]; - for (NSNumber *targetID in updatedInTargets) { - listens[targetID] = dummyQueryData; - } - for (NSNumber *targetID in removedFromTargets) { - listens[targetID] = dummyQueryData; - } - NSMutableDictionary *pending = [NSMutableDictionary dictionary]; - FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version - listenTargets:listens - pendingTargetResponses:pending]; - [aggregator addWatchChange:change]; - return [aggregator remoteEvent]; + NSArray *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. */ -- cgit v1.2.3