FSTLocalStoreTests.mm 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "Firestore/Source/Local/FSTLocalStore.h"
  17. #import <FirebaseFirestore/FIRTimestamp.h>
  18. #import <XCTest/XCTest.h>
  19. #import "Firestore/Source/Core/FSTQuery.h"
  20. #import "Firestore/Source/Local/FSTEagerGarbageCollector.h"
  21. #import "Firestore/Source/Local/FSTLocalWriteResult.h"
  22. #import "Firestore/Source/Local/FSTNoOpGarbageCollector.h"
  23. #import "Firestore/Source/Local/FSTPersistence.h"
  24. #import "Firestore/Source/Local/FSTQueryData.h"
  25. #import "Firestore/Source/Model/FSTDocument.h"
  26. #import "Firestore/Source/Model/FSTDocumentKey.h"
  27. #import "Firestore/Source/Model/FSTDocumentSet.h"
  28. #import "Firestore/Source/Model/FSTMutation.h"
  29. #import "Firestore/Source/Model/FSTMutationBatch.h"
  30. #import "Firestore/Source/Remote/FSTRemoteEvent.h"
  31. #import "Firestore/Source/Remote/FSTWatchChange.h"
  32. #import "Firestore/Source/Util/FSTClasses.h"
  33. #import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h"
  34. #import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h"
  35. #import "Firestore/Example/Tests/Util/FSTHelpers.h"
  36. #import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h"
  37. #import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h"
  38. #include "Firestore/core/src/firebase/firestore/auth/user.h"
  39. using firebase::firestore::auth::User;
  40. NS_ASSUME_NONNULL_BEGIN
  41. /** Creates a document version dictionary mapping the document in @a mutation to @a version. */
  42. FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation,
  43. FSTTestSnapshotVersion version) {
  44. FSTDocumentVersionDictionary *result = [FSTDocumentVersionDictionary documentVersionDictionary];
  45. result = [result dictionaryBySettingObject:FSTTestVersion(version) forKey:mutation.key];
  46. return result;
  47. }
  48. @interface FSTLocalStoreTests ()
  49. @property(nonatomic, strong, readwrite) id<FSTPersistence> localStorePersistence;
  50. @property(nonatomic, strong, readwrite) FSTLocalStore *localStore;
  51. @property(nonatomic, strong, readonly) NSMutableArray<FSTMutationBatch *> *batches;
  52. @property(nonatomic, strong, readwrite, nullable) FSTMaybeDocumentDictionary *lastChanges;
  53. @property(nonatomic, assign, readwrite) FSTTargetID lastTargetID;
  54. @end
  55. @implementation FSTLocalStoreTests
  56. - (void)setUp {
  57. [super setUp];
  58. if ([self isTestBaseClass]) {
  59. return;
  60. }
  61. id<FSTPersistence> persistence = [self persistence];
  62. self.localStorePersistence = persistence;
  63. id<FSTGarbageCollector> garbageCollector = [[FSTEagerGarbageCollector alloc] init];
  64. self.localStore = [[FSTLocalStore alloc] initWithPersistence:persistence
  65. garbageCollector:garbageCollector
  66. initialUser:User::Unauthenticated()];
  67. [self.localStore start];
  68. _batches = [NSMutableArray array];
  69. _lastChanges = nil;
  70. _lastTargetID = 0;
  71. }
  72. - (void)tearDown {
  73. [self.localStorePersistence shutdown];
  74. [super tearDown];
  75. }
  76. - (id<FSTPersistence>)persistence {
  77. @throw FSTAbstractMethodException(); // NOLINT
  78. }
  79. /**
  80. * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for
  81. * FSTLocalStoreTests since it is incomplete without the implementations supplied by its
  82. * subclasses.
  83. */
  84. - (BOOL)isTestBaseClass {
  85. return [self class] == [FSTLocalStoreTests class];
  86. }
  87. /** Restarts the local store using the FSTNoOpGarbageCollector instead of the default. */
  88. - (void)restartWithNoopGarbageCollector {
  89. id<FSTGarbageCollector> garbageCollector = [[FSTNoOpGarbageCollector alloc] init];
  90. self.localStore = [[FSTLocalStore alloc] initWithPersistence:self.localStorePersistence
  91. garbageCollector:garbageCollector
  92. initialUser:User::Unauthenticated()];
  93. [self.localStore start];
  94. }
  95. - (void)writeMutation:(FSTMutation *)mutation {
  96. [self writeMutations:@[ mutation ]];
  97. }
  98. - (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
  99. FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations];
  100. XCTAssertNotNil(result);
  101. [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batchID
  102. localWriteTime:[FIRTimestamp timestamp]
  103. mutations:mutations]];
  104. self.lastChanges = result.changes;
  105. }
  106. - (void)applyRemoteEvent:(FSTRemoteEvent *)event {
  107. self.lastChanges = [self.localStore applyRemoteEvent:event];
  108. }
  109. - (void)notifyLocalViewChanges:(FSTLocalViewChanges *)changes {
  110. [self.localStore notifyLocalViewChanges:@[ changes ]];
  111. }
  112. - (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion {
  113. FSTMutationBatch *batch = [self.batches firstObject];
  114. [self.batches removeObjectAtIndex:0];
  115. XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported.");
  116. FSTSnapshotVersion *version = FSTTestVersion(documentVersion);
  117. FSTMutationResult *mutationResult =
  118. [[FSTMutationResult alloc] initWithVersion:version transformResults:nil];
  119. FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch
  120. commitVersion:version
  121. mutationResults:@[ mutationResult ]
  122. streamToken:nil];
  123. self.lastChanges = [self.localStore acknowledgeBatchWithResult:result];
  124. }
  125. - (void)rejectMutation {
  126. FSTMutationBatch *batch = [self.batches firstObject];
  127. [self.batches removeObjectAtIndex:0];
  128. self.lastChanges = [self.localStore rejectBatchID:batch.batchID];
  129. }
  130. - (FSTTargetID)allocateQuery:(FSTQuery *)query {
  131. FSTQueryData *queryData = [self.localStore allocateQuery:query];
  132. self.lastTargetID = queryData.targetID;
  133. return queryData.targetID;
  134. }
  135. - (void)collectGarbage {
  136. [self.localStore collectGarbage];
  137. }
  138. /** Asserts that the last target ID is the given number. */
  139. #define FSTAssertTargetID(targetID) \
  140. do { \
  141. XCTAssertEqual(self.lastTargetID, targetID); \
  142. } while (0)
  143. /** Asserts that a the lastChanges contain the docs in the given array. */
  144. #define FSTAssertChanged(documents) \
  145. XCTAssertNotNil(self.lastChanges); \
  146. do { \
  147. FSTMaybeDocumentDictionary *actual = self.lastChanges; \
  148. NSArray<FSTMaybeDocument *> *expected = (documents); \
  149. XCTAssertEqual(actual.count, expected.count); \
  150. NSEnumerator<FSTMaybeDocument *> *enumerator = expected.objectEnumerator; \
  151. [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * key, FSTMaybeDocument * value, \
  152. BOOL * stop) { \
  153. XCTAssertEqualObjects(value, [enumerator nextObject]); \
  154. }]; \
  155. self.lastChanges = nil; \
  156. } while (0)
  157. /** Asserts that the given keys were removed. */
  158. #define FSTAssertRemoved(keyPaths) \
  159. XCTAssertNotNil(self.lastChanges); \
  160. do { \
  161. FSTMaybeDocumentDictionary *actual = self.lastChanges; \
  162. XCTAssertEqual(actual.count, keyPaths.count); \
  163. NSEnumerator<NSString *> *keyPathEnumerator = keyPaths.objectEnumerator; \
  164. [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * actualKey, \
  165. FSTMaybeDocument * value, BOOL * stop) { \
  166. FSTDocumentKey *expectedKey = FSTTestDocKey([keyPathEnumerator nextObject]); \
  167. XCTAssertEqualObjects(actualKey, expectedKey); \
  168. XCTAssertTrue([value isKindOfClass:[FSTDeletedDocument class]]); \
  169. }]; \
  170. self.lastChanges = nil; \
  171. } while (0)
  172. /** Asserts that the given local store contains the given document. */
  173. #define FSTAssertContains(document) \
  174. do { \
  175. FSTMaybeDocument *expected = (document); \
  176. FSTMaybeDocument *actual = [self.localStore readDocument:expected.key]; \
  177. XCTAssertEqualObjects(actual, expected); \
  178. } while (0)
  179. /** Asserts that the given local store does not contain the given document. */
  180. #define FSTAssertNotContains(keyPathString) \
  181. do { \
  182. FSTDocumentKey *key = FSTTestDocKey(keyPathString); \
  183. FSTMaybeDocument *actual = [self.localStore readDocument:key]; \
  184. XCTAssertNil(actual); \
  185. } while (0)
  186. - (void)testMutationBatchKeys {
  187. if ([self isTestBaseClass]) return;
  188. FSTMutation *set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"});
  189. FSTMutation *set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"});
  190. FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1
  191. localWriteTime:[FIRTimestamp timestamp]
  192. mutations:@[ set1, set2 ]];
  193. FSTDocumentKeySet *keys = [batch keys];
  194. XCTAssertEqual(keys.count, 2);
  195. }
  196. - (void)testHandlesSetMutation {
  197. if ([self isTestBaseClass]) return;
  198. [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
  199. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
  200. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  201. [self acknowledgeMutationWithVersion:0];
  202. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, NO) ]);
  203. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, NO));
  204. }
  205. - (void)testHandlesSetMutationThenDocument {
  206. if ([self isTestBaseClass]) return;
  207. [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
  208. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
  209. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  210. FSTQuery *query = FSTTestQuery("foo");
  211. FSTTargetID targetID = [self allocateQuery:query];
  212. [self
  213. applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO),
  214. @[ @(targetID) ], @[])];
  215. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, YES) ]);
  216. FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, YES));
  217. }
  218. - (void)testHandlesAckThenRejectThenRemoteEvent {
  219. if ([self isTestBaseClass]) return;
  220. // Start a query that requires acks to be held.
  221. FSTQuery *query = FSTTestQuery("foo");
  222. FSTTargetID targetID = [self allocateQuery:query];
  223. [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
  224. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
  225. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  226. // The last seen version is zero, so this ack must be held.
  227. [self acknowledgeMutationWithVersion:1];
  228. FSTAssertChanged(@[]);
  229. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  230. [self writeMutation:FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"})];
  231. FSTAssertChanged(@[ FSTTestDoc("bar/baz", 0, @{@"bar" : @"baz"}, YES) ]);
  232. FSTAssertContains(FSTTestDoc("bar/baz", 0, @{@"bar" : @"baz"}, YES));
  233. [self rejectMutation];
  234. FSTAssertRemoved(@[ @"bar/baz" ]);
  235. FSTAssertNotContains(@"bar/baz");
  236. [self
  237. applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO),
  238. @[ @(targetID) ], @[])];
  239. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO) ]);
  240. FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"it" : @"changed"}, NO));
  241. FSTAssertNotContains(@"bar/baz");
  242. }
  243. - (void)testHandlesDeletedDocumentThenSetMutationThenAck {
  244. if ([self isTestBaseClass]) return;
  245. FSTQuery *query = FSTTestQuery("foo");
  246. FSTTargetID targetID = [self allocateQuery:query];
  247. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @(targetID) ],
  248. @[])];
  249. FSTAssertRemoved(@[ @"foo/bar" ]);
  250. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 2));
  251. [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
  252. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
  253. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  254. // Can now remove the target, since we have a mutation pinning the document
  255. [self.localStore releaseQuery:query];
  256. [self acknowledgeMutationWithVersion:3];
  257. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, NO) ]);
  258. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, NO));
  259. }
  260. - (void)testHandlesSetMutationThenDeletedDocument {
  261. if ([self isTestBaseClass]) return;
  262. FSTQuery *query = FSTTestQuery("foo");
  263. FSTTargetID targetID = [self allocateQuery:query];
  264. [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
  265. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
  266. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @(targetID) ],
  267. @[])];
  268. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
  269. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  270. }
  271. - (void)testHandlesDocumentThenSetMutationThenAckThenDocument {
  272. if ([self isTestBaseClass]) return;
  273. // Start a query that requires acks to be held.
  274. FSTQuery *query = FSTTestQuery("foo");
  275. FSTTargetID targetID = [self allocateQuery:query];
  276. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"it" : @"base"}, NO),
  277. @[ @(targetID) ], @[])];
  278. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"it" : @"base"}, NO) ]);
  279. FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"it" : @"base"}, NO));
  280. [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
  281. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, YES) ]);
  282. FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, YES));
  283. [self acknowledgeMutationWithVersion:3];
  284. // we haven't seen the remote event yet, so the write is still held.
  285. FSTAssertChanged(@[]);
  286. FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, YES));
  287. [self
  288. applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO),
  289. @[ @(targetID) ], @[])];
  290. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO) ]);
  291. FSTAssertContains(FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO));
  292. }
  293. - (void)testHandlesPatchWithoutPriorDocument {
  294. if ([self isTestBaseClass]) return;
  295. [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
  296. FSTAssertRemoved(@[ @"foo/bar" ]);
  297. FSTAssertNotContains(@"foo/bar");
  298. [self acknowledgeMutationWithVersion:1];
  299. FSTAssertRemoved(@[ @"foo/bar" ]);
  300. FSTAssertNotContains(@"foo/bar");
  301. }
  302. - (void)testHandlesPatchMutationThenDocumentThenAck {
  303. if ([self isTestBaseClass]) return;
  304. [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
  305. FSTAssertRemoved(@[ @"foo/bar" ]);
  306. FSTAssertNotContains(@"foo/bar");
  307. FSTQuery *query = FSTTestQuery("foo");
  308. FSTTargetID targetID = [self allocateQuery:query];
  309. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO),
  310. @[ @(targetID) ], @[])];
  311. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES) ]);
  312. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES));
  313. [self acknowledgeMutationWithVersion:2];
  314. // We still haven't seen the remote events for the patch, so the local changes remain, and there
  315. // are no changes
  316. FSTAssertChanged(@[]);
  317. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES));
  318. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
  319. FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar", @"it" : @"base"}, NO), @[],
  320. @[])];
  321. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO) ]);
  322. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO));
  323. }
  324. - (void)testHandlesPatchMutationThenAckThenDocument {
  325. if ([self isTestBaseClass]) return;
  326. [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
  327. FSTAssertRemoved(@[ @"foo/bar" ]);
  328. FSTAssertNotContains(@"foo/bar");
  329. [self acknowledgeMutationWithVersion:1];
  330. FSTAssertRemoved(@[ @"foo/bar" ]);
  331. FSTAssertNotContains(@"foo/bar");
  332. FSTQuery *query = FSTTestQuery("foo");
  333. FSTTargetID targetID = [self allocateQuery:query];
  334. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO),
  335. @[ @(targetID) ], @[])];
  336. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO) ]);
  337. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO));
  338. }
  339. - (void)testHandlesDeleteMutationThenAck {
  340. if ([self isTestBaseClass]) return;
  341. [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
  342. FSTAssertRemoved(@[ @"foo/bar" ]);
  343. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  344. [self acknowledgeMutationWithVersion:1];
  345. FSTAssertRemoved(@[ @"foo/bar" ]);
  346. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  347. }
  348. - (void)testHandlesDocumentThenDeleteMutationThenAck {
  349. if ([self isTestBaseClass]) return;
  350. FSTQuery *query = FSTTestQuery("foo");
  351. FSTTargetID targetID = [self allocateQuery:query];
  352. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO),
  353. @[ @(targetID) ], @[])];
  354. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO) ]);
  355. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO));
  356. [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
  357. FSTAssertRemoved(@[ @"foo/bar" ]);
  358. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  359. // Remove the target so only the mutation is pinning the document
  360. [self.localStore releaseQuery:query];
  361. [self acknowledgeMutationWithVersion:2];
  362. FSTAssertRemoved(@[ @"foo/bar" ]);
  363. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  364. }
  365. - (void)testHandlesDeleteMutationThenDocumentThenAck {
  366. if ([self isTestBaseClass]) return;
  367. FSTQuery *query = FSTTestQuery("foo");
  368. FSTTargetID targetID = [self allocateQuery:query];
  369. [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
  370. FSTAssertRemoved(@[ @"foo/bar" ]);
  371. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  372. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO),
  373. @[ @(targetID) ], @[])];
  374. FSTAssertRemoved(@[ @"foo/bar" ]);
  375. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  376. // Don't need to keep it pinned anymore
  377. [self.localStore releaseQuery:query];
  378. [self acknowledgeMutationWithVersion:2];
  379. FSTAssertRemoved(@[ @"foo/bar" ]);
  380. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  381. }
  382. - (void)testHandlesDocumentThenDeletedDocumentThenDocument {
  383. if ([self isTestBaseClass]) return;
  384. FSTQuery *query = FSTTestQuery("foo");
  385. FSTTargetID targetID = [self allocateQuery:query];
  386. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO),
  387. @[ @(targetID) ], @[])];
  388. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO) ]);
  389. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO));
  390. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @(targetID) ],
  391. @[])];
  392. FSTAssertRemoved(@[ @"foo/bar" ]);
  393. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 2));
  394. [self
  395. applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO),
  396. @[ @(targetID) ], @[])];
  397. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO) ]);
  398. FSTAssertContains(FSTTestDoc("foo/bar", 3, @{@"it" : @"changed"}, NO));
  399. }
  400. - (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck {
  401. if ([self isTestBaseClass]) return;
  402. [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})];
  403. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"old"}, YES) ]);
  404. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"old"}, YES));
  405. [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
  406. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
  407. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  408. FSTQuery *query = FSTTestQuery("foo");
  409. FSTTargetID targetID = [self allocateQuery:query];
  410. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"it" : @"base"}, NO),
  411. @[ @(targetID) ], @[])];
  412. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, YES) ]);
  413. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, YES));
  414. [self.localStore releaseQuery:query];
  415. [self acknowledgeMutationWithVersion:2]; // delete mutation
  416. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, YES) ]);
  417. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, YES));
  418. [self acknowledgeMutationWithVersion:3]; // patch mutation
  419. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO) ]);
  420. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO));
  421. }
  422. - (void)testHandlesSetMutationAndPatchMutationTogether {
  423. if ([self isTestBaseClass]) return;
  424. [self writeMutations:@[
  425. FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}),
  426. FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})
  427. ]];
  428. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
  429. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  430. }
  431. - (void)testHandlesSetMutationThenPatchMutationThenReject {
  432. if ([self isTestBaseClass]) return;
  433. [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})];
  434. [self acknowledgeMutationWithVersion:1];
  435. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"old"}, NO));
  436. [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
  437. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  438. [self rejectMutation];
  439. FSTAssertChanged(@[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"old"}, NO) ]);
  440. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"old"}, NO));
  441. }
  442. - (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether {
  443. if ([self isTestBaseClass]) return;
  444. [self writeMutations:@[
  445. FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}),
  446. FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}),
  447. FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})
  448. ]];
  449. FSTAssertChanged((@[
  450. FSTTestDoc("bar/baz", 0, @{@"bar" : @"baz"}, YES),
  451. FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES)
  452. ]));
  453. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  454. FSTAssertContains(FSTTestDoc("bar/baz", 0, @{@"bar" : @"baz"}, YES));
  455. }
  456. - (void)testHandlesDeleteMutationThenPatchMutationThenAckThenAck {
  457. if ([self isTestBaseClass]) return;
  458. [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
  459. FSTAssertRemoved(@[ @"foo/bar" ]);
  460. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  461. [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
  462. FSTAssertRemoved(@[ @"foo/bar" ]);
  463. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  464. [self acknowledgeMutationWithVersion:2]; // delete mutation
  465. FSTAssertRemoved(@[ @"foo/bar" ]);
  466. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  467. [self acknowledgeMutationWithVersion:3]; // patch mutation
  468. FSTAssertRemoved(@[ @"foo/bar" ]);
  469. FSTAssertContains(FSTTestDeletedDoc("foo/bar", 0));
  470. }
  471. - (void)testCollectsGarbageAfterChangeBatchWithNoTargetIDs {
  472. if ([self isTestBaseClass]) return;
  473. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc("foo/bar", 2), @[ @1 ], @[])];
  474. FSTAssertRemoved(@[ @"foo/bar" ]);
  475. [self collectGarbage];
  476. FSTAssertNotContains(@"foo/bar");
  477. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO),
  478. @[ @1 ], @[])];
  479. [self collectGarbage];
  480. FSTAssertNotContains(@"foo/bar");
  481. }
  482. - (void)testCollectsGarbageAfterChangeBatch {
  483. if ([self isTestBaseClass]) return;
  484. FSTQuery *query = FSTTestQuery("foo");
  485. FSTTargetID targetID = [self allocateQuery:query];
  486. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO),
  487. @[ @(targetID) ], @[])];
  488. [self collectGarbage];
  489. FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO));
  490. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"baz"}, NO),
  491. @[], @[ @(targetID) ])];
  492. [self collectGarbage];
  493. FSTAssertNotContains(@"foo/bar");
  494. }
  495. - (void)testCollectsGarbageAfterAcknowledgedMutation {
  496. if ([self isTestBaseClass]) return;
  497. FSTQuery *query = FSTTestQuery("foo");
  498. FSTTargetID targetID = [self allocateQuery:query];
  499. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 0, @{@"foo" : @"old"}, NO),
  500. @[ @(targetID) ], @[])];
  501. [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
  502. // Release the query so that our target count goes back to 0 and we are considered up-to-date.
  503. [self.localStore releaseQuery:query];
  504. [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})];
  505. [self writeMutation:FSTTestDeleteMutation(@"foo/baz")];
  506. [self collectGarbage];
  507. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  508. FSTAssertContains(FSTTestDoc("foo/bah", 0, @{@"foo" : @"bah"}, YES));
  509. FSTAssertContains(FSTTestDeletedDoc("foo/baz", 0));
  510. [self acknowledgeMutationWithVersion:3];
  511. [self collectGarbage];
  512. FSTAssertNotContains(@"foo/bar");
  513. FSTAssertContains(FSTTestDoc("foo/bah", 0, @{@"foo" : @"bah"}, YES));
  514. FSTAssertContains(FSTTestDeletedDoc("foo/baz", 0));
  515. [self acknowledgeMutationWithVersion:4];
  516. [self collectGarbage];
  517. FSTAssertNotContains(@"foo/bar");
  518. FSTAssertNotContains(@"foo/bah");
  519. FSTAssertContains(FSTTestDeletedDoc("foo/baz", 0));
  520. [self acknowledgeMutationWithVersion:5];
  521. [self collectGarbage];
  522. FSTAssertNotContains(@"foo/bar");
  523. FSTAssertNotContains(@"foo/bah");
  524. FSTAssertNotContains(@"foo/baz");
  525. }
  526. - (void)testCollectsGarbageAfterRejectedMutation {
  527. if ([self isTestBaseClass]) return;
  528. FSTQuery *query = FSTTestQuery("foo");
  529. FSTTargetID targetID = [self allocateQuery:query];
  530. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 0, @{@"foo" : @"old"}, NO),
  531. @[ @(targetID) ], @[])];
  532. [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})];
  533. // Release the query so that our target count goes back to 0 and we are considered up-to-date.
  534. [self.localStore releaseQuery:query];
  535. [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})];
  536. [self writeMutation:FSTTestDeleteMutation(@"foo/baz")];
  537. [self collectGarbage];
  538. FSTAssertContains(FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES));
  539. FSTAssertContains(FSTTestDoc("foo/bah", 0, @{@"foo" : @"bah"}, YES));
  540. FSTAssertContains(FSTTestDeletedDoc("foo/baz", 0));
  541. [self rejectMutation]; // patch mutation
  542. [self collectGarbage];
  543. FSTAssertNotContains(@"foo/bar");
  544. FSTAssertContains(FSTTestDoc("foo/bah", 0, @{@"foo" : @"bah"}, YES));
  545. FSTAssertContains(FSTTestDeletedDoc("foo/baz", 0));
  546. [self rejectMutation]; // set mutation
  547. [self collectGarbage];
  548. FSTAssertNotContains(@"foo/bar");
  549. FSTAssertNotContains(@"foo/bah");
  550. FSTAssertContains(FSTTestDeletedDoc("foo/baz", 0));
  551. [self rejectMutation]; // delete mutation
  552. [self collectGarbage];
  553. FSTAssertNotContains(@"foo/bar");
  554. FSTAssertNotContains(@"foo/bah");
  555. FSTAssertNotContains(@"foo/baz");
  556. }
  557. - (void)testPinsDocumentsInTheLocalView {
  558. if ([self isTestBaseClass]) return;
  559. FSTQuery *query = FSTTestQuery("foo");
  560. FSTTargetID targetID = [self allocateQuery:query];
  561. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO),
  562. @[ @(targetID) ], @[])];
  563. [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})];
  564. [self collectGarbage];
  565. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO));
  566. FSTAssertContains(FSTTestDoc("foo/baz", 0, @{@"foo" : @"baz"}, YES));
  567. [self notifyLocalViewChanges:FSTTestViewChanges(query, @[ @"foo/bar", @"foo/baz" ], @[])];
  568. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO),
  569. @[], @[ @(targetID) ])];
  570. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/baz", 2, @{@"foo" : @"baz"}, NO),
  571. @[ @(targetID) ], @[])];
  572. [self acknowledgeMutationWithVersion:2];
  573. [self collectGarbage];
  574. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO));
  575. FSTAssertContains(FSTTestDoc("foo/baz", 2, @{@"foo" : @"baz"}, NO));
  576. [self notifyLocalViewChanges:FSTTestViewChanges(query, @[], @[ @"foo/bar", @"foo/baz" ])];
  577. [self.localStore releaseQuery:query];
  578. [self collectGarbage];
  579. FSTAssertNotContains(@"foo/bar");
  580. FSTAssertNotContains(@"foo/baz");
  581. }
  582. - (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately {
  583. if ([self isTestBaseClass]) return;
  584. FSTTargetID targetID = 321;
  585. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{}, NO),
  586. @[ @(targetID) ], @[])];
  587. FSTAssertContains(FSTTestDoc("foo/bar", 1, @{}, NO));
  588. [self collectGarbage];
  589. FSTAssertNotContains(@"foo/bar");
  590. }
  591. - (void)testCanExecuteDocumentQueries {
  592. if ([self isTestBaseClass]) return;
  593. [self.localStore locallyWriteMutations:@[
  594. FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}),
  595. FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}),
  596. FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"})
  597. ]];
  598. FSTQuery *query = FSTTestQuery("foo/bar");
  599. FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
  600. XCTAssertEqualObjects([docs values], @[ FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
  601. }
  602. - (void)testCanExecuteCollectionQueries {
  603. if ([self isTestBaseClass]) return;
  604. [self.localStore locallyWriteMutations:@[
  605. FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}),
  606. FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}),
  607. FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}),
  608. FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}),
  609. FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"})
  610. ]];
  611. FSTQuery *query = FSTTestQuery("foo");
  612. FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
  613. XCTAssertEqualObjects([docs values], (@[
  614. FSTTestDoc("foo/bar", 0, @{@"foo" : @"bar"}, YES),
  615. FSTTestDoc("foo/baz", 0, @{@"foo" : @"baz"}, YES)
  616. ]));
  617. }
  618. - (void)testCanExecuteMixedCollectionQueries {
  619. if ([self isTestBaseClass]) return;
  620. FSTQuery *query = FSTTestQuery("foo");
  621. [self allocateQuery:query];
  622. FSTAssertTargetID(2);
  623. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/baz", 10, @{@"a" : @"b"}, NO),
  624. @[ @2 ], @[])];
  625. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, NO),
  626. @[ @2 ], @[])];
  627. [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]];
  628. FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
  629. XCTAssertEqualObjects([docs values], (@[
  630. FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, NO),
  631. FSTTestDoc("foo/baz", 10, @{@"a" : @"b"}, NO),
  632. FSTTestDoc("foo/bonk", 0, @{@"a" : @"b"}, YES)
  633. ]));
  634. }
  635. - (void)testPersistsResumeTokens {
  636. if ([self isTestBaseClass]) return;
  637. // This test only works in the absence of the FSTEagerGarbageCollector.
  638. [self restartWithNoopGarbageCollector];
  639. FSTQuery *query = FSTTestQuery("foo/bar");
  640. FSTQueryData *queryData = [self.localStore allocateQuery:query];
  641. FSTBoxedTargetID *targetID = @(queryData.targetID);
  642. NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000);
  643. FSTWatchChange *watchChange =
  644. [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
  645. targetIDs:@[ targetID ]
  646. resumeToken:resumeToken];
  647. NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *listens =
  648. [NSMutableDictionary dictionary];
  649. listens[targetID] = queryData;
  650. NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingResponses =
  651. [NSMutableDictionary dictionary];
  652. FSTWatchChangeAggregator *aggregator =
  653. [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(1000)
  654. listenTargets:listens
  655. pendingTargetResponses:pendingResponses];
  656. [aggregator addWatchChanges:@[ watchChange ]];
  657. FSTRemoteEvent *remoteEvent = [aggregator remoteEvent];
  658. [self applyRemoteEvent:remoteEvent];
  659. // Stop listening so that the query should become inactive (but persistent)
  660. [self.localStore releaseQuery:query];
  661. // Should come back with the same resume token
  662. FSTQueryData *queryData2 = [self.localStore allocateQuery:query];
  663. XCTAssertEqualObjects(queryData2.resumeToken, resumeToken);
  664. }
  665. - (void)testRemoteDocumentKeysForTarget {
  666. if ([self isTestBaseClass]) return;
  667. [self restartWithNoopGarbageCollector];
  668. FSTQuery *query = FSTTestQuery("foo");
  669. [self allocateQuery:query];
  670. FSTAssertTargetID(2);
  671. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/baz", 10, @{@"a" : @"b"}, NO),
  672. @[ @2 ], @[])];
  673. [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, NO),
  674. @[ @2 ], @[])];
  675. [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]];
  676. FSTDocumentKeySet *keys = [self.localStore remoteDocumentKeysForTarget:2];
  677. FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ]));
  678. [self restartWithNoopGarbageCollector];
  679. keys = [self.localStore remoteDocumentKeysForTarget:2];
  680. FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ]));
  681. }
  682. @end
  683. NS_ASSUME_NONNULL_END