aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Example
diff options
context:
space:
mode:
authorGravatar Gil <mcg@google.com>2017-10-03 08:55:22 -0700
committerGravatar GitHub <noreply@github.com>2017-10-03 08:55:22 -0700
commitbde743ed25166a0b320ae157bfb1d68064f531c9 (patch)
tree4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Example
parentbf550507ffa8beee149383a5bf1e2363bccefbb4 (diff)
Release 4.3.0 (#327)
Initial release of Firestore at 0.8.0 Bump FirebaseCommunity to 0.1.3
Diffstat (limited to 'Firestore/Example')
-rw-r--r--Firestore/Example/Firestore.xcodeproj/project.pbxproj1700
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme111
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme113
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme71
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme71
-rw-r--r--Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme91
-rw-r--r--Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard27
-rw-r--r--Firestore/Example/Firestore/Base.lproj/Main.storyboard27
-rw-r--r--Firestore/Example/Firestore/FIRAppDelegate.h23
-rw-r--r--Firestore/Example/Firestore/FIRAppDelegate.m57
-rw-r--r--Firestore/Example/Firestore/FIRViewController.h21
-rw-r--r--Firestore/Example/Firestore/FIRViewController.m35
-rw-r--r--Firestore/Example/Firestore/Firestore-Info.plist49
-rw-r--r--Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json93
-rw-r--r--Firestore/Example/Firestore/en.lproj/InfoPlist.strings2
-rw-r--r--Firestore/Example/Firestore/main.m24
-rw-r--r--Firestore/Example/Podfile22
-rw-r--r--Firestore/Example/SwiftBuildTest/main.swift284
-rw-r--r--Firestore/Example/Tests/API/FIRGeoPointTests.m67
-rw-r--r--Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m59
-rw-r--r--Firestore/Example/Tests/Core/FSTEventManagerTests.m163
-rw-r--r--Firestore/Example/Tests/Core/FSTQueryListenerTests.m487
-rw-r--r--Firestore/Example/Tests/Core/FSTQueryTests.m577
-rw-r--r--Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h32
-rw-r--r--Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m94
-rw-r--r--Firestore/Example/Tests/Core/FSTTimestampTests.m88
-rw-r--r--Firestore/Example/Tests/Core/FSTViewSnapshotTest.m141
-rw-r--r--Firestore/Example/Tests/Core/FSTViewTests.m618
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRCursorTests.m195
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m741
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRFieldsTests.m223
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m129
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRQueryTests.m197
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m183
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRTypeTests.m79
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRValidationTests.m560
-rw-r--r--Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m313
-rw-r--r--Firestore/Example/Tests/Integration/CAcert.pem0
-rw-r--r--Firestore/Example/Tests/Integration/FSTDatastoreTests.m239
-rw-r--r--Firestore/Example/Tests/Integration/FSTSmokeTests.m129
-rw-r--r--Firestore/Example/Tests/Integration/FSTTransactionTests.m541
-rw-r--r--Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m111
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm361
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m45
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm158
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m54
-rw-r--r--Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm78
-rw-r--r--Firestore/Example/Tests/Local/FSTLocalSerializerTests.m181
-rw-r--r--Firestore/Example/Tests/Local/FSTLocalStoreTests.h38
-rw-r--r--Firestore/Example/Tests/Local/FSTLocalStoreTests.m795
-rw-r--r--Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m44
-rw-r--r--Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m42
-rw-r--r--Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m54
-rw-r--r--Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m49
-rw-r--r--Firestore/Example/Tests/Local/FSTMutationQueueTests.h38
-rw-r--r--Firestore/Example/Tests/Local/FSTMutationQueueTests.m511
-rw-r--r--Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h40
-rw-r--r--Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m72
-rw-r--r--Firestore/Example/Tests/Local/FSTQueryCacheTests.h47
-rw-r--r--Firestore/Example/Tests/Local/FSTQueryCacheTests.m375
-rw-r--r--Firestore/Example/Tests/Local/FSTReferenceSetTests.m84
-rw-r--r--Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h39
-rw-r--r--Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m151
-rw-r--r--Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m113
-rw-r--r--Firestore/Example/Tests/Local/FSTWriteGroupTests.mm121
-rw-r--r--Firestore/Example/Tests/Model/FSTDatabaseIDTests.m45
-rw-r--r--Firestore/Example/Tests/Model/FSTDocumentKeyTests.m60
-rw-r--r--Firestore/Example/Tests/Model/FSTDocumentSetTests.m142
-rw-r--r--Firestore/Example/Tests/Model/FSTDocumentTests.m101
-rw-r--r--Firestore/Example/Tests/Model/FSTFieldValueTests.m576
-rw-r--r--Firestore/Example/Tests/Model/FSTMutationTests.m216
-rw-r--r--Firestore/Example/Tests/Model/FSTPathTests.m196
-rw-r--r--Firestore/Example/Tests/Remote/FSTDatastoreTests.m58
-rw-r--r--Firestore/Example/Tests/Remote/FSTRemoteEventTests.m556
-rw-r--r--Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m794
-rw-r--r--Firestore/Example/Tests/Remote/FSTStreamTests.m139
-rw-r--r--Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h40
-rw-r--r--Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m54
-rw-r--r--Firestore/Example/Tests/Remote/FSTWatchChangeTests.m66
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m43
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m42
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTMockDatastore.h68
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTMockDatastore.m344
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSpecTests.h46
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSpecTests.m642
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h248
-rw-r--r--Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m291
-rw-r--r--Firestore/Example/Tests/SpecTests/json/README.md3
-rw-r--r--Firestore/Example/Tests/SpecTests/json/collection_spec_test.json147
-rw-r--r--Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json738
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json1150
-rw-r--r--Firestore/Example/Tests/SpecTests/json/limit_spec_test.json1626
-rw-r--r--Firestore/Example/Tests/SpecTests/json/listen_spec_test.json1524
-rw-r--r--Firestore/Example/Tests/SpecTests/json/offline_spec_test.json151
-rw-r--r--Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json155
-rw-r--r--Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json858
-rw-r--r--Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json559
-rw-r--r--Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json250
-rw-r--r--Firestore/Example/Tests/SpecTests/json/write_spec_test.json5437
-rw-r--r--Firestore/Example/Tests/Tests-Info.plist22
-rw-r--r--Firestore/Example/Tests/Util/FSTAssertTests.m105
-rw-r--r--Firestore/Example/Tests/Util/FSTComparisonTests.m143
-rw-r--r--Firestore/Example/Tests/Util/FSTEventAccumulator.h41
-rw-r--r--Firestore/Example/Tests/Util/FSTEventAccumulator.m94
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.h258
-rw-r--r--Firestore/Example/Tests/Util/FSTHelpers.m348
-rw-r--r--Firestore/Example/Tests/Util/FSTIntegrationTestCase.h94
-rw-r--r--Firestore/Example/Tests/Util/FSTIntegrationTestCase.m285
-rw-r--r--Firestore/Example/Tests/Util/FSTUtilTests.m35
-rw-r--r--Firestore/Example/Tests/Util/XCTestCase+Await.h32
-rw-r--r--Firestore/Example/Tests/Util/XCTestCase+Await.m38
-rw-r--r--Firestore/Example/Tests/en.lproj/InfoPlist.strings2
112 files changed, 31139 insertions, 0 deletions
diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..979050f
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj
@@ -0,0 +1,1700 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ DE29E7F51F2174B000909613 /* AllTests */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = DE29E7F81F2174B000909613 /* Build configuration list for PBXAggregateTarget "AllTests" */;
+ buildPhases = (
+ );
+ dependencies = (
+ DE0761FA1F2FEE7E003233AF /* PBXTargetDependency */,
+ DE29E7FA1F2174DD00909613 /* PBXTargetDependency */,
+ DE29E7FC1F2174DD00909613 /* PBXTargetDependency */,
+ );
+ name = AllTests;
+ productName = AllTests;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; };
+ 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */; };
+ 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; };
+ 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */; };
+ 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */; };
+ 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */; };
+ 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */; };
+ 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */; };
+ 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */; };
+ 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */; };
+ 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; };
+ 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */; };
+ 54E928221F33952900C1953E /* FSTIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */; };
+ 54E928231F33952D00C1953E /* FSTIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */; };
+ 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; };
+ 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */; };
+ 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; };
+ 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */; };
+ 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; };
+ 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; };
+ 6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
+ 6003F598195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F596195388D20070C39A /* InfoPlist.strings */; };
+ 6003F59A195388D20070C39A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F599195388D20070C39A /* main.m */; };
+ 6003F59E195388D20070C39A /* FIRAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F59D195388D20070C39A /* FIRAppDelegate.m */; };
+ 6003F5A7195388D20070C39A /* FIRViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5A6195388D20070C39A /* FIRViewController.m */; };
+ 6003F5A9195388D20070C39A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5A8195388D20070C39A /* Images.xcassets */; };
+ 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; };
+ 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; };
+ 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
+ 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; };
+ 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */; };
+ 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; };
+ 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; };
+ AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; };
+ C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; };
+ DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */; };
+ DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; };
+ DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; };
+ DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; };
+ DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; };
+ DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; };
+ DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */; };
+ DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */; };
+ DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */; };
+ DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */; };
+ DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */; };
+ DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */; };
+ DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */; };
+ DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */; };
+ DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */; };
+ DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */; };
+ DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; };
+ DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */ = {isa = PBXBuildFile; fileRef = DE03B3621F215E1600A30B9C /* CAcert.pem */; };
+ DE0761F81F2FE68D003233AF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0761F61F2FE68D003233AF /* main.swift */; };
+ DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */; };
+ DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */; };
+ DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */; };
+ DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */; };
+ DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */; };
+ DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */; };
+ DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */; };
+ DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */; };
+ DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */; };
+ DE51B1D11F0D48CD0013853F /* FSTTargetIDGeneratorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */; };
+ DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */; };
+ DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */; };
+ DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B11F0D48AC0013853F /* FSTViewTests.m */; };
+ DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */; };
+ DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */; };
+ DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */; };
+ DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */; };
+ DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */; };
+ DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */; };
+ DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */; };
+ DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */; };
+ DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */; };
+ DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */; };
+ DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */; };
+ DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */; };
+ DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */; };
+ DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */; };
+ DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */; };
+ DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */; };
+ DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */; };
+ DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */; };
+ DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */; };
+ DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */; };
+ DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */; };
+ DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */; };
+ DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */; };
+ DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */; };
+ DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1811F0D48AC0013853F /* FSTMutationTests.m */; };
+ DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1821F0D48AC0013853F /* FSTPathTests.m */; };
+ DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */; };
+ DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */; };
+ DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */; };
+ DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */; };
+ DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */; };
+ DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */; };
+ DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */; };
+ DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */; };
+ DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */; };
+ DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1991F0D48AC0013853F /* FSTSpecTests.m */; };
+ DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */; };
+ DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1861F0D48AC0013853F /* FSTAssertTests.m */; };
+ DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */; };
+ DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B1891F0D48AC0013853F /* FSTHelpers.m */; };
+ DE51B2021F0D493E0013853F /* FSTUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */; };
+ F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 6003F5B3195388D20070C39A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6003F589195388D20070C39A;
+ remoteInfo = Firestore;
+ };
+ DE03B2961F2149D600A30B9C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6003F589195388D20070C39A;
+ remoteInfo = Firestore;
+ };
+ DE0761F91F2FEE7E003233AF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE0761E31F2FE611003233AF;
+ remoteInfo = SwiftBuildTest;
+ };
+ DE29E7F91F2174DD00909613 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 6003F5AD195388D20070C39A;
+ remoteInfo = Firestore_Tests;
+ };
+ DE29E7FB1F2174DD00909613 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6003F582195388D10070C39A /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DE03B2941F2149D600A30B9C;
+ remoteInfo = Firestore_IntegrationTests;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXFileReference section */
+ 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.release.xcconfig"; sourceTree = "<group>"; };
+ 12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; };
+ 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftBuildTest.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = remote_store_spec_test.json; sourceTree = "<group>"; };
+ 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.debug.xcconfig"; sourceTree = "<group>"; };
+ 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.release.xcconfig"; sourceTree = "<group>"; };
+ 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = collection_spec_test.json; sourceTree = "<group>"; };
+ 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = existence_filter_spec_test.json; sourceTree = "<group>"; };
+ 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = limbo_spec_test.json; sourceTree = "<group>"; };
+ 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = limit_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = listen_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = offline_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = orderby_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = persistence_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = resume_token_spec_test.json; sourceTree = "<group>"; };
+ 54DA12A51F315EE100DD57A1 /* write_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = write_spec_test.json; sourceTree = "<group>"; };
+ 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRValidationTests.m; sourceTree = "<group>"; };
+ 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTEventAccumulator.h; sourceTree = "<group>"; };
+ 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTEventAccumulator.m; sourceTree = "<group>"; };
+ 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTIntegrationTestCase.h; sourceTree = "<group>"; };
+ 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FSTIntegrationTestCase.m; sourceTree = "<group>"; };
+ 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestCase+Await.h"; sourceTree = "<group>"; };
+ 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCTestCase+Await.m"; sourceTree = "<group>"; };
+ 6003F58A195388D20070C39A /* Firestore_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firestore_Example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+ 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
+ 6003F591195388D20070C39A /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
+ 6003F595195388D20070C39A /* Firestore-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Firestore-Info.plist"; sourceTree = "<group>"; };
+ 6003F597195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ 6003F599195388D20070C39A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ 6003F59C195388D20070C39A /* FIRAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRAppDelegate.h; sourceTree = "<group>"; };
+ 6003F59D195388D20070C39A /* FIRAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRAppDelegate.m; sourceTree = "<group>"; };
+ 6003F5A5195388D20070C39A /* FIRViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FIRViewController.h; sourceTree = "<group>"; };
+ 6003F5A6195388D20070C39A /* FIRViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRViewController.m; sourceTree = "<group>"; };
+ 6003F5A8195388D20070C39A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
+ 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 6003F5AF195388D20070C39A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
+ 6003F5B7195388D20070C39A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = "<group>"; };
+ 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
+ 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+ 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+ 8E002F4AD5D9B6197C940847 /* Firestore.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = Firestore.podspec; path = ../Firestore.podspec; sourceTree = "<group>"; };
+ 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.debug.xcconfig"; sourceTree = "<group>"; };
+ 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.debug.xcconfig"; sourceTree = "<group>"; };
+ B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.debug.xcconfig"; sourceTree = "<group>"; };
+ D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
+ DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.release.xcconfig"; sourceTree = "<group>"; };
+ DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_IntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE03B3621F215E1600A30B9C /* CAcert.pem */ = {isa = PBXFileReference; lastKnownFileType = text; path = CAcert.pem; sourceTree = "<group>"; };
+ DE0761E41F2FE611003233AF /* SwiftBuildTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftBuildTest.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ DE0761F61F2FE68D003233AF /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
+ DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTArraySortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m; sourceTree = "<group>"; };
+ DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedDictionary+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h"; sourceTree = "<group>"; };
+ DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedDictionary+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m"; sourceTree = "<group>"; };
+ DE2EF0811F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedSet+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h"; sourceTree = "<group>"; };
+ DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedSet+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.m"; sourceTree = "<group>"; };
+ DE2EF0831F3D0B6E003D0CDC /* FSTLLRBValueNode+Test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTLLRBValueNode+Test.h"; path = "../../third_party/Immutable/Tests/FSTLLRBValueNode+Test.h"; sourceTree = "<group>"; };
+ DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTTreeSortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTTreeSortedDictionaryTests.m; sourceTree = "<group>"; };
+ DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEagerGarbageCollectorTests.m; sourceTree = "<group>"; };
+ DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBKeyTests.mm; sourceTree = "<group>"; };
+ DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBLocalStoreTests.m; sourceTree = "<group>"; };
+ DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBMutationQueueTests.mm; sourceTree = "<group>"; };
+ DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBQueryCacheTests.m; sourceTree = "<group>"; };
+ DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBRemoteDocumentCacheTests.mm; sourceTree = "<group>"; };
+ DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalSerializerTests.m; sourceTree = "<group>"; };
+ DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTLocalStoreTests.h; sourceTree = "<group>"; };
+ DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLocalStoreTests.m; sourceTree = "<group>"; };
+ DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryLocalStoreTests.m; sourceTree = "<group>"; };
+ DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryMutationQueueTests.m; sourceTree = "<group>"; };
+ DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryQueryCacheTests.m; sourceTree = "<group>"; };
+ DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemoryRemoteDocumentCacheTests.m; sourceTree = "<group>"; };
+ DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMutationQueueTests.h; sourceTree = "<group>"; };
+ DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationQueueTests.m; sourceTree = "<group>"; };
+ DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTPersistenceTestHelpers.h; sourceTree = "<group>"; };
+ DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPersistenceTestHelpers.m; sourceTree = "<group>"; };
+ DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTQueryCacheTests.h; sourceTree = "<group>"; };
+ DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryCacheTests.m; sourceTree = "<group>"; };
+ DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTReferenceSetTests.m; sourceTree = "<group>"; };
+ DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTRemoteDocumentCacheTests.h; sourceTree = "<group>"; };
+ DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentCacheTests.m; sourceTree = "<group>"; };
+ DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteDocumentChangeBufferTests.m; sourceTree = "<group>"; };
+ DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTWriteGroupTests.mm; sourceTree = "<group>"; };
+ DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseIDTests.m; sourceTree = "<group>"; };
+ DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentKeyTests.m; sourceTree = "<group>"; };
+ DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentSetTests.m; sourceTree = "<group>"; };
+ DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDocumentTests.m; sourceTree = "<group>"; };
+ DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTFieldValueTests.m; sourceTree = "<group>"; };
+ DE51B1811F0D48AC0013853F /* FSTMutationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMutationTests.m; sourceTree = "<group>"; };
+ DE51B1821F0D48AC0013853F /* FSTPathTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTPathTests.m; sourceTree = "<group>"; };
+ DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRGeoPointTests.m; sourceTree = "<group>"; };
+ DE51B1861F0D48AC0013853F /* FSTAssertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTAssertTests.m; sourceTree = "<group>"; };
+ DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTComparisonTests.m; sourceTree = "<group>"; };
+ DE51B1881F0D48AC0013853F /* FSTHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTHelpers.h; sourceTree = "<group>"; };
+ DE51B1891F0D48AC0013853F /* FSTHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTHelpers.m; sourceTree = "<group>"; };
+ DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTUtilTests.m; sourceTree = "<group>"; };
+ DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTLevelDBSpecTests.m; sourceTree = "<group>"; };
+ DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMemorySpecTests.m; sourceTree = "<group>"; };
+ DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTMockDatastore.h; sourceTree = "<group>"; };
+ DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTMockDatastore.m; sourceTree = "<group>"; };
+ DE51B1981F0D48AC0013853F /* FSTSpecTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSpecTests.h; sourceTree = "<group>"; };
+ DE51B1991F0D48AC0013853F /* FSTSpecTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSpecTests.m; sourceTree = "<group>"; };
+ DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSyncEngineTestDriver.h; sourceTree = "<group>"; };
+ DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSyncEngineTestDriver.m; sourceTree = "<group>"; };
+ DE51B1A71F0D48AC0013853F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
+ DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatabaseInfoTests.m; sourceTree = "<group>"; };
+ DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTEventManagerTests.m; sourceTree = "<group>"; };
+ DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryListenerTests.m; sourceTree = "<group>"; };
+ DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTQueryTests.m; sourceTree = "<group>"; };
+ DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTSyncEngine+Testing.h"; sourceTree = "<group>"; };
+ DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTargetIDGeneratorTests.m; sourceTree = "<group>"; };
+ DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTimestampTests.m; sourceTree = "<group>"; };
+ DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewSnapshotTest.m; sourceTree = "<group>"; };
+ DE51B1B11F0D48AC0013853F /* FSTViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTViewTests.m; sourceTree = "<group>"; };
+ DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = "<group>"; };
+ DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTRemoteEventTests.m; sourceTree = "<group>"; };
+ DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSerializerBetaTests.m; sourceTree = "<group>"; };
+ DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTStreamTests.m; sourceTree = "<group>"; };
+ DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FSTWatchChange+Testing.h"; sourceTree = "<group>"; };
+ DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FSTWatchChange+Testing.m"; sourceTree = "<group>"; };
+ DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTWatchChangeTests.m; sourceTree = "<group>"; };
+ DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRCursorTests.m; sourceTree = "<group>"; };
+ DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRDatabaseTests.m; sourceTree = "<group>"; };
+ DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRFieldsTests.m; sourceTree = "<group>"; };
+ DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRListenerRegistrationTests.m; sourceTree = "<group>"; };
+ DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRQueryTests.m; sourceTree = "<group>"; };
+ DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRServerTimestampTests.m; sourceTree = "<group>"; };
+ DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIRTypeTests.m; sourceTree = "<group>"; };
+ DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTDatastoreTests.m; sourceTree = "<group>"; };
+ DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTSmokeTests.m; sourceTree = "<group>"; };
+ DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FSTTransactionTests.m; sourceTree = "<group>"; };
+ DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRWriteBatchTests.m; sourceTree = "<group>"; };
+ F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.release.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 6003F587195388D20070C39A /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */,
+ 6003F592195388D20070C39A /* UIKit.framework in Frameworks */,
+ 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */,
+ 6ED54761B845349D43DB6B78 /* Pods_Firestore_Example.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6003F5AB195388D20070C39A /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */,
+ 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */,
+ 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */,
+ F104BBD69BC3F0796E3A77C1 /* Pods_Firestore_Tests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE03B2D31F2149D600A30B9C /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */,
+ DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */,
+ DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */,
+ DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */,
+ AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE0761E11F2FE611003233AF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 6003F581195388D10070C39A = {
+ isa = PBXGroup;
+ children = (
+ 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */,
+ 6003F593195388D20070C39A /* Example for Firestore */,
+ 6003F5B5195388D20070C39A /* Tests */,
+ DE0761E51F2FE611003233AF /* SwiftBuildTest */,
+ 6003F58C195388D20070C39A /* Frameworks */,
+ 6003F58B195388D20070C39A /* Products */,
+ A47A1BF74A48BCAEAFBCBF1E /* Pods */,
+ );
+ sourceTree = "<group>";
+ };
+ 6003F58B195388D20070C39A /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F58A195388D20070C39A /* Firestore_Example.app */,
+ 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */,
+ DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */,
+ DE0761E41F2FE611003233AF /* SwiftBuildTest.app */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 6003F58C195388D20070C39A /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F58D195388D20070C39A /* Foundation.framework */,
+ 6003F58F195388D20070C39A /* CoreGraphics.framework */,
+ 6003F591195388D20070C39A /* UIKit.framework */,
+ 6003F5AF195388D20070C39A /* XCTest.framework */,
+ 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */,
+ 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */,
+ B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */,
+ 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ 6003F593195388D20070C39A /* Example for Firestore */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F59C195388D20070C39A /* FIRAppDelegate.h */,
+ 6003F59D195388D20070C39A /* FIRAppDelegate.m */,
+ 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */,
+ 6003F5A5195388D20070C39A /* FIRViewController.h */,
+ 6003F5A6195388D20070C39A /* FIRViewController.m */,
+ 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */,
+ 6003F5A8195388D20070C39A /* Images.xcassets */,
+ 6003F594195388D20070C39A /* Supporting Files */,
+ );
+ name = "Example for Firestore";
+ path = Firestore;
+ sourceTree = "<group>";
+ };
+ 6003F594195388D20070C39A /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F595195388D20070C39A /* Firestore-Info.plist */,
+ 6003F596195388D20070C39A /* InfoPlist.strings */,
+ 6003F599195388D20070C39A /* main.m */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ 6003F5B5195388D20070C39A /* Tests */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1831F0D48AC0013853F /* API */,
+ DE51B1A81F0D48AC0013853F /* Core */,
+ DE2EF06E1F3D07D7003D0CDC /* Immutable */,
+ DE51B1BB1F0D48AC0013853F /* Integration */,
+ DE51B1621F0D48AC0013853F /* Local */,
+ DE51B17B1F0D48AC0013853F /* Model */,
+ DE51B1B21F0D48AC0013853F /* Remote */,
+ DE51B1931F0D48AC0013853F /* SpecTests */,
+ DE51B1851F0D48AC0013853F /* Util */,
+ 6003F5B6195388D20070C39A /* Supporting Files */,
+ );
+ path = Tests;
+ sourceTree = "<group>";
+ };
+ 6003F5B6195388D20070C39A /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ 6003F5B7195388D20070C39A /* Tests-Info.plist */,
+ 6003F5B8195388D20070C39A /* InfoPlist.strings */,
+ );
+ name = "Supporting Files";
+ sourceTree = "<group>";
+ };
+ 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */ = {
+ isa = PBXGroup;
+ children = (
+ 8E002F4AD5D9B6197C940847 /* Firestore.podspec */,
+ D3CC3DC5338DCAF43A211155 /* README.md */,
+ 12F4357299652983A615F886 /* LICENSE */,
+ );
+ name = "Podspec Metadata";
+ sourceTree = "<group>";
+ };
+ A47A1BF74A48BCAEAFBCBF1E /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */,
+ 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */,
+ 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */,
+ DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */,
+ CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */,
+ 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */,
+ 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */,
+ F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "<group>";
+ };
+ DE0761E51F2FE611003233AF /* SwiftBuildTest */ = {
+ isa = PBXGroup;
+ children = (
+ DE0761F61F2FE68D003233AF /* main.swift */,
+ );
+ path = SwiftBuildTest;
+ sourceTree = "<group>";
+ };
+ DE2EF06E1F3D07D7003D0CDC /* Immutable */ = {
+ isa = PBXGroup;
+ children = (
+ DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */,
+ DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */,
+ DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */,
+ DE2EF0811F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.h */,
+ DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */,
+ DE2EF0831F3D0B6E003D0CDC /* FSTLLRBValueNode+Test.h */,
+ DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */,
+ );
+ name = Immutable;
+ sourceTree = "<group>";
+ };
+ DE51B1621F0D48AC0013853F /* Local */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B16A1F0D48AC0013853F /* FSTLocalStoreTests.h */,
+ DE51B1701F0D48AC0013853F /* FSTMutationQueueTests.h */,
+ DE51B1721F0D48AC0013853F /* FSTPersistenceTestHelpers.h */,
+ DE51B1741F0D48AC0013853F /* FSTQueryCacheTests.h */,
+ DE51B1771F0D48AC0013853F /* FSTRemoteDocumentCacheTests.h */,
+ DE51B1631F0D48AC0013853F /* FSTEagerGarbageCollectorTests.m */,
+ DE51B1651F0D48AC0013853F /* FSTLevelDBLocalStoreTests.m */,
+ DE51B1671F0D48AC0013853F /* FSTLevelDBQueryCacheTests.m */,
+ DE51B1691F0D48AC0013853F /* FSTLocalSerializerTests.m */,
+ DE51B16B1F0D48AC0013853F /* FSTLocalStoreTests.m */,
+ DE51B16C1F0D48AC0013853F /* FSTMemoryLocalStoreTests.m */,
+ DE51B16D1F0D48AC0013853F /* FSTMemoryMutationQueueTests.m */,
+ DE51B16E1F0D48AC0013853F /* FSTMemoryQueryCacheTests.m */,
+ DE51B16F1F0D48AC0013853F /* FSTMemoryRemoteDocumentCacheTests.m */,
+ DE51B1711F0D48AC0013853F /* FSTMutationQueueTests.m */,
+ DE51B1731F0D48AC0013853F /* FSTPersistenceTestHelpers.m */,
+ DE51B1751F0D48AC0013853F /* FSTQueryCacheTests.m */,
+ DE51B1761F0D48AC0013853F /* FSTReferenceSetTests.m */,
+ DE51B1781F0D48AC0013853F /* FSTRemoteDocumentCacheTests.m */,
+ DE51B1791F0D48AC0013853F /* FSTRemoteDocumentChangeBufferTests.m */,
+ DE51B1641F0D48AC0013853F /* FSTLevelDBKeyTests.mm */,
+ DE51B1661F0D48AC0013853F /* FSTLevelDBMutationQueueTests.mm */,
+ DE51B1681F0D48AC0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm */,
+ DE51B17A1F0D48AC0013853F /* FSTWriteGroupTests.mm */,
+ );
+ path = Local;
+ sourceTree = "<group>";
+ };
+ DE51B17B1F0D48AC0013853F /* Model */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B17C1F0D48AC0013853F /* FSTDatabaseIDTests.m */,
+ DE51B17D1F0D48AC0013853F /* FSTDocumentKeyTests.m */,
+ DE51B17E1F0D48AC0013853F /* FSTDocumentSetTests.m */,
+ DE51B17F1F0D48AC0013853F /* FSTDocumentTests.m */,
+ DE51B1801F0D48AC0013853F /* FSTFieldValueTests.m */,
+ DE51B1811F0D48AC0013853F /* FSTMutationTests.m */,
+ DE51B1821F0D48AC0013853F /* FSTPathTests.m */,
+ );
+ path = Model;
+ sourceTree = "<group>";
+ };
+ DE51B1831F0D48AC0013853F /* API */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1841F0D48AC0013853F /* FIRGeoPointTests.m */,
+ );
+ path = API;
+ sourceTree = "<group>";
+ };
+ DE51B1851F0D48AC0013853F /* Util */ = {
+ isa = PBXGroup;
+ children = (
+ 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */,
+ 54E9281D1F33950B00C1953E /* FSTEventAccumulator.m */,
+ 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */,
+ 54E9281F1F33950B00C1953E /* FSTIntegrationTestCase.m */,
+ DE51B1861F0D48AC0013853F /* FSTAssertTests.m */,
+ DE51B1871F0D48AC0013853F /* FSTComparisonTests.m */,
+ DE51B1881F0D48AC0013853F /* FSTHelpers.h */,
+ DE51B1891F0D48AC0013853F /* FSTHelpers.m */,
+ DE51B18A1F0D48AC0013853F /* FSTUtilTests.m */,
+ 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */,
+ 54E9282B1F339CAD00C1953E /* XCTestCase+Await.m */,
+ );
+ path = Util;
+ sourceTree = "<group>";
+ };
+ DE51B1931F0D48AC0013853F /* SpecTests */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1961F0D48AC0013853F /* FSTMockDatastore.h */,
+ DE51B1981F0D48AC0013853F /* FSTSpecTests.h */,
+ DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */,
+ DE51B1941F0D48AC0013853F /* FSTLevelDBSpecTests.m */,
+ DE51B1951F0D48AC0013853F /* FSTMemorySpecTests.m */,
+ DE51B1971F0D48AC0013853F /* FSTMockDatastore.m */,
+ DE51B1991F0D48AC0013853F /* FSTSpecTests.m */,
+ DE51B19B1F0D48AC0013853F /* FSTSyncEngineTestDriver.m */,
+ DE51B19C1F0D48AC0013853F /* json */,
+ );
+ path = SpecTests;
+ sourceTree = "<group>";
+ };
+ DE51B19C1F0D48AC0013853F /* json */ = {
+ isa = PBXGroup;
+ children = (
+ 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */,
+ 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */,
+ 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */,
+ 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */,
+ 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */,
+ 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */,
+ 54DA12A11F315EE100DD57A1 /* offline_spec_test.json */,
+ 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */,
+ 54DA12A31F315EE100DD57A1 /* persistence_spec_test.json */,
+ 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */,
+ 54DA12A51F315EE100DD57A1 /* write_spec_test.json */,
+ DE51B1A71F0D48AC0013853F /* README.md */,
+ );
+ path = json;
+ sourceTree = "<group>";
+ };
+ DE51B1A81F0D48AC0013853F /* Core */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1AD1F0D48AC0013853F /* FSTSyncEngine+Testing.h */,
+ DE51B1A91F0D48AC0013853F /* FSTDatabaseInfoTests.m */,
+ DE51B1AA1F0D48AC0013853F /* FSTEventManagerTests.m */,
+ DE51B1AB1F0D48AC0013853F /* FSTQueryListenerTests.m */,
+ DE51B1AC1F0D48AC0013853F /* FSTQueryTests.m */,
+ DE51B1AE1F0D48AC0013853F /* FSTTargetIDGeneratorTests.m */,
+ DE51B1AF1F0D48AC0013853F /* FSTTimestampTests.m */,
+ DE51B1B01F0D48AC0013853F /* FSTViewSnapshotTest.m */,
+ DE51B1B11F0D48AC0013853F /* FSTViewTests.m */,
+ );
+ path = Core;
+ sourceTree = "<group>";
+ };
+ DE51B1B21F0D48AC0013853F /* Remote */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1B31F0D48AC0013853F /* FSTDatastoreTests.m */,
+ DE51B1B41F0D48AC0013853F /* FSTRemoteEventTests.m */,
+ DE51B1B61F0D48AC0013853F /* FSTSerializerBetaTests.m */,
+ DE51B1B71F0D48AC0013853F /* FSTStreamTests.m */,
+ DE51B1B81F0D48AC0013853F /* FSTWatchChange+Testing.h */,
+ DE51B1B91F0D48AC0013853F /* FSTWatchChange+Testing.m */,
+ DE51B1BA1F0D48AC0013853F /* FSTWatchChangeTests.m */,
+ );
+ path = Remote;
+ sourceTree = "<group>";
+ };
+ DE51B1BB1F0D48AC0013853F /* Integration */ = {
+ isa = PBXGroup;
+ children = (
+ DE03B3621F215E1600A30B9C /* CAcert.pem */,
+ DE51B1BC1F0D48AC0013853F /* API */,
+ DE51B1C41F0D48AC0013853F /* FSTDatastoreTests.m */,
+ DE51B1C51F0D48AC0013853F /* FSTSmokeTests.m */,
+ DE51B1C61F0D48AC0013853F /* FSTTransactionTests.m */,
+ DE51B1C71F0D48AC0013853F /* Util */,
+ );
+ path = Integration;
+ sourceTree = "<group>";
+ };
+ DE51B1BC1F0D48AC0013853F /* API */ = {
+ isa = PBXGroup;
+ children = (
+ DE51B1BD1F0D48AC0013853F /* FIRCursorTests.m */,
+ DE51B1BE1F0D48AC0013853F /* FIRDatabaseTests.m */,
+ DE51B1BF1F0D48AC0013853F /* FIRFieldsTests.m */,
+ DE51B1C01F0D48AC0013853F /* FIRListenerRegistrationTests.m */,
+ DE51B1C11F0D48AC0013853F /* FIRQueryTests.m */,
+ DE51B1C21F0D48AC0013853F /* FIRServerTimestampTests.m */,
+ DE51B1C31F0D48AC0013853F /* FIRTypeTests.m */,
+ 54DA12B01F315F3800DD57A1 /* FIRValidationTests.m */,
+ DEFE0F471F1F960A0071599A /* FIRWriteBatchTests.m */,
+ );
+ path = API;
+ sourceTree = "<group>";
+ };
+ DE51B1C71F0D48AC0013853F /* Util */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = Util;
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 6003F589195388D20070C39A /* Firestore_Example */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Example" */;
+ buildPhases = (
+ FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */,
+ 6003F586195388D20070C39A /* Sources */,
+ 6003F587195388D20070C39A /* Frameworks */,
+ 6003F588195388D20070C39A /* Resources */,
+ 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */,
+ DEB4B96019F51073F0553ABC /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Firestore_Example;
+ productName = Firestore;
+ productReference = 6003F58A195388D20070C39A /* Firestore_Example.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 6003F5AD195388D20070C39A /* Firestore_Tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Tests" */;
+ buildPhases = (
+ 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */,
+ 6003F5AA195388D20070C39A /* Sources */,
+ 6003F5AB195388D20070C39A /* Frameworks */,
+ 6003F5AC195388D20070C39A /* Resources */,
+ BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */,
+ AB3F19DA92555D3399DB07CE /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 6003F5B4195388D20070C39A /* PBXTargetDependency */,
+ );
+ name = Firestore_Tests;
+ productName = FirestoreTests;
+ productReference = 6003F5AE195388D20070C39A /* Firestore_Tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests" */;
+ buildPhases = (
+ DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */,
+ DE03B2981F2149D600A30B9C /* Sources */,
+ DE03B2D31F2149D600A30B9C /* Frameworks */,
+ DE03B2D81F2149D600A30B9C /* Resources */,
+ DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */,
+ DE03B2E51F2149D600A30B9C /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ DE03B2951F2149D600A30B9C /* PBXTargetDependency */,
+ );
+ name = Firestore_IntegrationTests;
+ productName = FirestoreTests;
+ productReference = DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ DE0761E31F2FE611003233AF /* SwiftBuildTest */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = DE0761F51F2FE611003233AF /* Build configuration list for PBXNativeTarget "SwiftBuildTest" */;
+ buildPhases = (
+ 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */,
+ DE0761E01F2FE611003233AF /* Sources */,
+ DE0761E11F2FE611003233AF /* Frameworks */,
+ DE0761E21F2FE611003233AF /* Resources */,
+ 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */,
+ 04C27A4B1FAE812E8153B724 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = SwiftBuildTest;
+ productName = SwiftBuildTest;
+ productReference = DE0761E41F2FE611003233AF /* SwiftBuildTest.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 6003F582195388D10070C39A /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ CLASSPREFIX = FIR;
+ LastSwiftUpdateCheck = 0830;
+ LastUpgradeCheck = 0720;
+ ORGANIZATIONNAME = Google;
+ TargetAttributes = {
+ 6003F5AD195388D20070C39A = {
+ DevelopmentTeam = EQHXZ8M8AV;
+ TestTargetID = 6003F589195388D20070C39A;
+ };
+ DE03B2941F2149D600A30B9C = {
+ DevelopmentTeam = EQHXZ8M8AV;
+ };
+ DE0761E31F2FE611003233AF = {
+ CreatedOnToolsVersion = 8.3.3;
+ DevelopmentTeam = EQHXZ8M8AV;
+ ProvisioningStyle = Automatic;
+ };
+ DE29E7F51F2174B000909613 = {
+ CreatedOnToolsVersion = 9.0;
+ DevelopmentTeam = EQHXZ8M8AV;
+ };
+ };
+ };
+ buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firestore" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 6003F581195388D10070C39A;
+ productRefGroup = 6003F58B195388D20070C39A /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 6003F589195388D20070C39A /* Firestore_Example */,
+ 6003F5AD195388D20070C39A /* Firestore_Tests */,
+ DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */,
+ DE29E7F51F2174B000909613 /* AllTests */,
+ DE0761E31F2FE611003233AF /* SwiftBuildTest */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 6003F588195388D20070C39A /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */,
+ 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */,
+ 6003F5A9195388D20070C39A /* Images.xcassets in Resources */,
+ 6003F598195388D20070C39A /* InfoPlist.strings in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6003F5AC195388D20070C39A /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */,
+ 54DA12A81F315EE100DD57A1 /* limbo_spec_test.json in Resources */,
+ 54DA12AA1F315EE100DD57A1 /* listen_spec_test.json in Resources */,
+ 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */,
+ 54DA12AE1F315EE100DD57A1 /* resume_token_spec_test.json in Resources */,
+ 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */,
+ 54DA12AF1F315EE100DD57A1 /* write_spec_test.json in Resources */,
+ 54DA12AD1F315EE100DD57A1 /* persistence_spec_test.json in Resources */,
+ 54DA12AB1F315EE100DD57A1 /* offline_spec_test.json in Resources */,
+ 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */,
+ 54DA12AC1F315EE100DD57A1 /* orderby_spec_test.json in Resources */,
+ 54DA12A91F315EE100DD57A1 /* limit_spec_test.json in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE03B2D81F2149D600A30B9C /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */,
+ DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE0761E21F2FE611003233AF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 04C27A4B1FAE812E8153B724 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseCommunity/FirebaseCommunity.framework",
+ "${BUILT_PRODUCTS_DIR}/Firestore/Firestore.framework",
+ "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
+ "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework",
+ "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework",
+ "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
+ "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCommunity.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Firestore.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseCommunity/FirebaseCommunity.framework",
+ "${BUILT_PRODUCTS_DIR}/Firestore/Firestore.framework",
+ "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
+ "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework",
+ "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework",
+ "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework",
+ "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
+ "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCommunity.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Firestore.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Firestore_Tests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-SwiftBuildTest-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ AB3F19DA92555D3399DB07CE /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework",
+ "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Firestore_IntegrationTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ DE03B2E51F2149D600A30B9C /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ DEB4B96019F51073F0553ABC /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Firestore_Example-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 6003F586195388D20070C39A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6003F59E195388D20070C39A /* FIRAppDelegate.m in Sources */,
+ 6003F5A7195388D20070C39A /* FIRViewController.m in Sources */,
+ 6003F59A195388D20070C39A /* main.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 6003F5AA195388D20070C39A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */,
+ DE51B1FD1F0D492C0013853F /* FSTSpecTests.m in Sources */,
+ DE51B2001F0D493A0013853F /* FSTComparisonTests.m in Sources */,
+ DE51B1CC1F0D48C00013853F /* FIRGeoPointTests.m in Sources */,
+ DE51B1E11F0D490D0013853F /* FSTMemoryRemoteDocumentCacheTests.m in Sources */,
+ DE51B1FF1F0D493A0013853F /* FSTAssertTests.m in Sources */,
+ DE51B1D31F0D48CD0013853F /* FSTViewSnapshotTest.m in Sources */,
+ DE51B2021F0D493E0013853F /* FSTUtilTests.m in Sources */,
+ DE51B1F91F0D491F0013853F /* FSTWatchChangeTests.m in Sources */,
+ DE51B1F81F0D491F0013853F /* FSTWatchChange+Testing.m in Sources */,
+ DE51B1EB1F0D490D0013853F /* FSTWriteGroupTests.mm in Sources */,
+ DE51B2011F0D493E0013853F /* FSTHelpers.m in Sources */,
+ DE51B1F61F0D491B0013853F /* FSTSerializerBetaTests.m in Sources */,
+ DE51B1F01F0D49140013853F /* FSTFieldValueTests.m in Sources */,
+ DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */,
+ DE51B1DE1F0D490D0013853F /* FSTMemoryLocalStoreTests.m in Sources */,
+ DE51B1EC1F0D49140013853F /* FSTDatabaseIDTests.m in Sources */,
+ 54E928221F33952900C1953E /* FSTIntegrationTestCase.m in Sources */,
+ DE51B1ED1F0D49140013853F /* FSTDocumentKeyTests.m in Sources */,
+ DE51B1D41F0D48CD0013853F /* FSTViewTests.m in Sources */,
+ DE51B1F41F0D491B0013853F /* FSTRemoteEventTests.m in Sources */,
+ 54E928241F33953300C1953E /* FSTEventAccumulator.m in Sources */,
+ DE51B1D11F0D48CD0013853F /* FSTTargetIDGeneratorTests.m in Sources */,
+ DE51B1EF1F0D49140013853F /* FSTDocumentTests.m in Sources */,
+ DE51B1DC1F0D490D0013853F /* FSTLocalSerializerTests.m in Sources */,
+ DE51B1E71F0D490D0013853F /* FSTRemoteDocumentChangeBufferTests.m in Sources */,
+ DE51B1E51F0D490D0013853F /* FSTReferenceSetTests.m in Sources */,
+ DE51B1EA1F0D490D0013853F /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */,
+ DE51B1D21F0D48CD0013853F /* FSTTimestampTests.m in Sources */,
+ DE51B1EE1F0D49140013853F /* FSTDocumentSetTests.m in Sources */,
+ DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */,
+ DE51B1F11F0D49140013853F /* FSTMutationTests.m in Sources */,
+ DE51B1FB1F0D492C0013853F /* FSTMemorySpecTests.m in Sources */,
+ DE51B1DB1F0D490D0013853F /* FSTLevelDBQueryCacheTests.m in Sources */,
+ 54E9282C1F339CAD00C1953E /* XCTestCase+Await.m in Sources */,
+ DE51B1DF1F0D490D0013853F /* FSTMemoryMutationQueueTests.m in Sources */,
+ DE51B1F31F0D491B0013853F /* FSTDatastoreTests.m in Sources */,
+ DE51B1D01F0D48CD0013853F /* FSTQueryTests.m in Sources */,
+ DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */,
+ DE51B1E01F0D490D0013853F /* FSTMemoryQueryCacheTests.m in Sources */,
+ DE51B1E91F0D490D0013853F /* FSTLevelDBMutationQueueTests.mm in Sources */,
+ DE51B1E61F0D490D0013853F /* FSTRemoteDocumentCacheTests.m in Sources */,
+ DE51B1D91F0D490D0013853F /* FSTEagerGarbageCollectorTests.m in Sources */,
+ DE51B1F71F0D491B0013853F /* FSTStreamTests.m in Sources */,
+ DE51B1E21F0D490D0013853F /* FSTMutationQueueTests.m in Sources */,
+ DE51B1E81F0D490D0013853F /* FSTLevelDBKeyTests.mm in Sources */,
+ DE51B1E31F0D490D0013853F /* FSTPersistenceTestHelpers.m in Sources */,
+ DE51B1CF1F0D48CD0013853F /* FSTQueryListenerTests.m in Sources */,
+ DE51B1DA1F0D490D0013853F /* FSTLevelDBLocalStoreTests.m in Sources */,
+ DE51B1FA1F0D492C0013853F /* FSTLevelDBSpecTests.m in Sources */,
+ DE51B1FE1F0D492C0013853F /* FSTSyncEngineTestDriver.m in Sources */,
+ DE51B1FC1F0D492C0013853F /* FSTMockDatastore.m in Sources */,
+ DE51B1CE1F0D48CD0013853F /* FSTEventManagerTests.m in Sources */,
+ DE51B1E41F0D490D0013853F /* FSTQueryCacheTests.m in Sources */,
+ DE51B1CD1F0D48CD0013853F /* FSTDatabaseInfoTests.m in Sources */,
+ DE51B1F21F0D49140013853F /* FSTPathTests.m in Sources */,
+ DE51B1DD1F0D490D0013853F /* FSTLocalStoreTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE03B2981F2149D600A30B9C /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE03B2EE1F214BAA00A30B9C /* FIRWriteBatchTests.m in Sources */,
+ DE03B2F01F214BAA00A30B9C /* FIRDatabaseTests.m in Sources */,
+ 54E928231F33952D00C1953E /* FSTIntegrationTestCase.m in Sources */,
+ DE03B2F41F214BAA00A30B9C /* FIRServerTimestampTests.m in Sources */,
+ DE03B2F11F214BAA00A30B9C /* FIRFieldsTests.m in Sources */,
+ 54E9282D1F339CAD00C1953E /* XCTestCase+Await.m in Sources */,
+ DE03B2EC1F214BA200A30B9C /* FSTDatastoreTests.m in Sources */,
+ 54E928251F33953400C1953E /* FSTEventAccumulator.m in Sources */,
+ DE03B2ED1F214BA200A30B9C /* FSTSmokeTests.m in Sources */,
+ DE03B2F31F214BAA00A30B9C /* FIRQueryTests.m in Sources */,
+ DE03B35E1F21586C00A30B9C /* FSTHelpers.m in Sources */,
+ DE03B2F51F214BAA00A30B9C /* FIRTypeTests.m in Sources */,
+ DE03B2EF1F214BAA00A30B9C /* FIRCursorTests.m in Sources */,
+ DE03B2F21F214BAA00A30B9C /* FIRListenerRegistrationTests.m in Sources */,
+ DE03B2C91F2149D600A30B9C /* FSTTransactionTests.m in Sources */,
+ 54DA12B11F315F3800DD57A1 /* FIRValidationTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ DE0761E01F2FE611003233AF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ DE0761F81F2FE68D003233AF /* main.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 6003F5B4195388D20070C39A /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6003F589195388D20070C39A /* Firestore_Example */;
+ targetProxy = 6003F5B3195388D20070C39A /* PBXContainerItemProxy */;
+ };
+ DE03B2951F2149D600A30B9C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6003F589195388D20070C39A /* Firestore_Example */;
+ targetProxy = DE03B2961F2149D600A30B9C /* PBXContainerItemProxy */;
+ };
+ DE0761FA1F2FEE7E003233AF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE0761E31F2FE611003233AF /* SwiftBuildTest */;
+ targetProxy = DE0761F91F2FEE7E003233AF /* PBXContainerItemProxy */;
+ };
+ DE29E7FA1F2174DD00909613 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 6003F5AD195388D20070C39A /* Firestore_Tests */;
+ targetProxy = DE29E7F91F2174DD00909613 /* PBXContainerItemProxy */;
+ };
+ DE29E7FC1F2174DD00909613 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests */;
+ targetProxy = DE29E7FB1F2174DD00909613 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 6003F596195388D20070C39A /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 6003F597195388D20070C39A /* en */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+ 6003F5B8195388D20070C39A /* InfoPlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 6003F5B9195388D20070C39A /* en */,
+ );
+ name = InfoPlist.strings;
+ sourceTree = "<group>";
+ };
+ 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 71719F9E1E33DC2100824A3D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "<group>";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 6003F5BD195388D20070C39A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 6003F5BE195388D20070C39A /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = YES;
+ ENABLE_NS_ASSERTIONS = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 6003F5C0195388D20070C39A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"",
+ "\"${PODS_ROOT}/leveldb-library/\"",
+ "\"${PODS_ROOT}/leveldb-library/include\"",
+ );
+ INFOPLIST_FILE = "Firestore/Firestore-Info.plist";
+ MODULE_NAME = ExampleApp;
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Debug;
+ };
+ 6003F5C1195388D20070C39A /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"",
+ "\"${PODS_ROOT}/leveldb-library/\"",
+ "\"${PODS_ROOT}/leveldb-library/include\"",
+ );
+ INFOPLIST_FILE = "Firestore/Firestore-Info.plist";
+ MODULE_NAME = ExampleApp;
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Release;
+ };
+ 6003F5C3195388D20070C39A /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "\"$(inherited)\"",
+ "\"${PODS_ROOT}/../../Source\"",
+ "\"${PODS_ROOT}/../../Source/API\"",
+ "\"${PODS_ROOT}/../../Source/Core\"",
+ "\"${PODS_ROOT}/../../Source/Remote\"",
+ "\"${PODS_ROOT}/../../Source/Model\"",
+ "\"${PODS_ROOT}/../../third_party\"",
+ "\"${PODS_ROOT}/../../third_party/Immutable\"",
+ "\"${PODS_ROOT}/../../\"",
+ "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/api\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/type\"",
+ );
+ INFOPLIST_FILE = "Tests/Tests-Info.plist";
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Debug;
+ };
+ 6003F5C4195388D20070C39A /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "\"$(inherited)\"",
+ "\"${PODS_ROOT}/../../Source\"",
+ "\"${PODS_ROOT}/../../Source/API\"",
+ "\"${PODS_ROOT}/../../Source/Core\"",
+ "\"${PODS_ROOT}/../../Source/Remote\"",
+ "\"${PODS_ROOT}/../../Source/Model\"",
+ "\"${PODS_ROOT}/../../third_party\"",
+ "\"${PODS_ROOT}/../../third_party/Immutable\"",
+ "\"${PODS_ROOT}/../../Protos/objc/firebase/datastore/clients/proto\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/api\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/type\"",
+ "\"${PODS_ROOT}/../../\"",
+ );
+ INFOPLIST_FILE = "Tests/Tests-Info.plist";
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Release;
+ };
+ DE03B2E71F2149D600A30B9C /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "\"$(inherited)\"",
+ "\"${PODS_ROOT}/../../Source\"",
+ "\"${PODS_ROOT}/../../Source/API\"",
+ "\"${PODS_ROOT}/../../Source/Core\"",
+ "\"${PODS_ROOT}/../../Source/Remote\"",
+ "\"${PODS_ROOT}/../../Source/Model\"",
+ "\"${PODS_ROOT}/../../third_party\"",
+ "\"${PODS_ROOT}/../../third_party/Immutable\"",
+ "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/api\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/type\"",
+ "\"${PODS_ROOT}/../../\"",
+ );
+ INFOPLIST_FILE = "Tests/Tests-Info.plist";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-l\"c++\"",
+ "-framework",
+ "\"OCMock\"",
+ "-framework",
+ "\"leveldb\"",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Debug;
+ };
+ DE03B2E81F2149D600A30B9C /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(SDKROOT)/Developer/Library/Frameworks",
+ "$(inherited)",
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = "";
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "COCOAPODS=1",
+ "GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1",
+ );
+ HEADER_SEARCH_PATHS = (
+ "\"$(inherited)\"",
+ "\"${PODS_ROOT}/../../Source\"",
+ "\"${PODS_ROOT}/../../Source/API\"",
+ "\"${PODS_ROOT}/../../Source/Core\"",
+ "\"${PODS_ROOT}/../../Source/Remote\"",
+ "\"${PODS_ROOT}/../../Source/Model\"",
+ "\"${PODS_ROOT}/../../third_party\"",
+ "\"${PODS_ROOT}/../../third_party/Immutable\"",
+ "\"${PODS_ROOT}/../../Protos/objc/firestore/local\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/firestore/v1beta1\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/api\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/rpc\"",
+ "\"${PODS_ROOT}/../../Protos/objc/google/type\"",
+ "\"${PODS_ROOT}/../../\"",
+ );
+ INFOPLIST_FILE = "Tests/Tests-Info.plist";
+ OTHER_LDFLAGS = (
+ "$(inherited)",
+ "-l\"c++\"",
+ "-framework",
+ "\"OCMock\"",
+ "-framework",
+ "\"leveldb\"",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example.app/Firestore_Example";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Release;
+ };
+ DE0761F31F2FE611003233AF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_NO_COMMON_BLOCKS = YES;
+ INFOPLIST_FILE = "Firestore/Firestore-Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 10.3;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBuildTest;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Debug;
+ };
+ DE0761F41F2FE611003233AF /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_NO_COMMON_BLOCKS = YES;
+ INFOPLIST_FILE = "Firestore/Firestore-Info.plist";
+ IPHONEOS_DEPLOYMENT_TARGET = 10.3;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ MTL_ENABLE_DEBUG_INFO = NO;
+ PRODUCT_BUNDLE_IDENTIFIER = com.google.SwiftBuildTest;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+ SWIFT_VERSION = 3.0;
+ };
+ name = Release;
+ };
+ DE29E7F61F2174B000909613 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ DE29E7F71F2174B000909613 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ DEVELOPMENT_TEAM = EQHXZ8M8AV;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 6003F585195388D10070C39A /* Build configuration list for PBXProject "Firestore" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6003F5BD195388D20070C39A /* Debug */,
+ 6003F5BE195388D20070C39A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Example" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6003F5C0195388D20070C39A /* Debug */,
+ 6003F5C1195388D20070C39A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 6003F5C3195388D20070C39A /* Debug */,
+ 6003F5C4195388D20070C39A /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE03B2E71F2149D600A30B9C /* Debug */,
+ DE03B2E81F2149D600A30B9C /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE0761F51F2FE611003233AF /* Build configuration list for PBXNativeTarget "SwiftBuildTest" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE0761F31F2FE611003233AF /* Debug */,
+ DE0761F41F2FE611003233AF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ DE29E7F81F2174B000909613 /* Build configuration list for PBXAggregateTarget "AllTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ DE29E7F61F2174B000909613 /* Debug */,
+ DE29E7F71F2174B000909613 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 6003F582195388D10070C39A /* Project object */;
+}
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme
new file mode 100644
index 0000000..aacb70e
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/AllTests.xcscheme
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0900"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE29E7F51F2174B000909613"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE03B2941F2149D600A30B9C"
+ BuildableName = "Firestore_IntegrationTests.xctest"
+ BlueprintName = "Firestore_IntegrationTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F5AD195388D20070C39A"
+ BuildableName = "Firestore_Tests.xctest"
+ BlueprintName = "Firestore_Tests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE29E7F51F2174B000909613"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE29E7F51F2174B000909613"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE29E7F51F2174B000909613"
+ BuildableName = "AllTests"
+ BlueprintName = "AllTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme
new file mode 100644
index 0000000..a8538f5
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore-Example.xcscheme
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0720"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F589195388D20070C39A"
+ BuildableName = "Firestore_Example.app"
+ BlueprintName = "Firestore_Example"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F5AD195388D20070C39A"
+ BuildableName = "Firestore_Tests.xctest"
+ BlueprintName = "Firestore_Tests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE03B2941F2149D600A30B9C"
+ BuildableName = "Firestore_IntegrationTests.xctest"
+ BlueprintName = "Firestore_IntegrationTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F589195388D20070C39A"
+ BuildableName = "Firestore_Example.app"
+ BlueprintName = "Firestore_Example"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F589195388D20070C39A"
+ BuildableName = "Firestore_Example.app"
+ BlueprintName = "Firestore_Example"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F589195388D20070C39A"
+ BuildableName = "Firestore_Example.app"
+ BlueprintName = "Firestore_Example"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme
new file mode 100644
index 0000000..ccd099f
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests.xcscheme
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0900"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForRunning = "YES"
+ buildForTesting = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE03B2941F2149D600A30B9C"
+ BuildableName = "Firestore_IntegrationTests.xctest"
+ BlueprintName = "Firestore_IntegrationTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE03B2941F2149D600A30B9C"
+ BuildableName = "Firestore_IntegrationTests.xctest"
+ BlueprintName = "Firestore_IntegrationTests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme
new file mode 100644
index 0000000..920e1f3
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Tests.xcscheme
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0900"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForRunning = "YES"
+ buildForTesting = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F5AD195388D20070C39A"
+ BuildableName = "Firestore_Tests.xctest"
+ BlueprintName = "Firestore_Tests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ <TestableReference
+ skipped = "NO">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "6003F5AD195388D20070C39A"
+ BuildableName = "Firestore_Tests.xctest"
+ BlueprintName = "Firestore_Tests"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </TestableReference>
+ </Testables>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ language = ""
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme
new file mode 100644
index 0000000..112a0d0
--- /dev/null
+++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/SwiftBuildTest.xcscheme
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+ LastUpgradeVersion = "0830"
+ version = "1.3">
+ <BuildAction
+ parallelizeBuildables = "YES"
+ buildImplicitDependencies = "YES">
+ <BuildActionEntries>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "YES"
+ buildForProfiling = "YES"
+ buildForArchiving = "YES"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE0761E31F2FE611003233AF"
+ BuildableName = "SwiftBuildTest.app"
+ BlueprintName = "SwiftBuildTest"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
+ </BuildActionEntries>
+ </BuildAction>
+ <TestAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ shouldUseLaunchSchemeArgsEnv = "YES">
+ <Testables>
+ </Testables>
+ <MacroExpansion>
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE0761E31F2FE611003233AF"
+ BuildableName = "SwiftBuildTest.app"
+ BlueprintName = "SwiftBuildTest"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </MacroExpansion>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </TestAction>
+ <LaunchAction
+ buildConfiguration = "Debug"
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+ launchStyle = "0"
+ useCustomWorkingDirectory = "NO"
+ ignoresPersistentStateOnLaunch = "NO"
+ debugDocumentVersioning = "YES"
+ debugServiceExtension = "internal"
+ allowLocationSimulation = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE0761E31F2FE611003233AF"
+ BuildableName = "SwiftBuildTest.app"
+ BlueprintName = "SwiftBuildTest"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ <AdditionalOptions>
+ </AdditionalOptions>
+ </LaunchAction>
+ <ProfileAction
+ buildConfiguration = "Release"
+ shouldUseLaunchSchemeArgsEnv = "YES"
+ savedToolIdentifier = ""
+ useCustomWorkingDirectory = "NO"
+ debugDocumentVersioning = "YES">
+ <BuildableProductRunnable
+ runnableDebuggingMode = "0">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "DE0761E31F2FE611003233AF"
+ BuildableName = "SwiftBuildTest.app"
+ BlueprintName = "SwiftBuildTest"
+ ReferencedContainer = "container:Firestore.xcodeproj">
+ </BuildableReference>
+ </BuildableProductRunnable>
+ </ProfileAction>
+ <AnalyzeAction
+ buildConfiguration = "Debug">
+ </AnalyzeAction>
+ <ArchiveAction
+ buildConfiguration = "Release"
+ revealArchiveInOrganizer = "YES">
+ </ArchiveAction>
+</Scheme>
diff --git a/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard b/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..66a7681
--- /dev/null
+++ b/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="16C67" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" initialViewController="01J-lp-oVM">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="EHf-IW-A2E">
+ <objects>
+ <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+ <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="53" y="375"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Firestore/Example/Firestore/Base.lproj/Main.storyboard b/Firestore/Example/Firestore/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..d164a23
--- /dev/null
+++ b/Firestore/Example/Firestore/Base.lproj/Main.storyboard
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="7706" systemVersion="14D136" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="whP-gf-Uak">
+ <dependencies>
+ <deployment identifier="iOS"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="7703"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="wQg-tq-qST">
+ <objects>
+ <viewController id="whP-gf-Uak" customClass="FIRViewController" sceneMemberID="viewController">
+ <layoutGuides>
+ <viewControllerLayoutGuide type="top" id="uEw-UM-LJ8"/>
+ <viewControllerLayoutGuide type="bottom" id="Mvr-aV-6Um"/>
+ </layoutGuides>
+ <view key="view" contentMode="scaleToFill" id="TpU-gO-2f1">
+ <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="tc2-Qw-aMS" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ <point key="canvasLocation" x="305" y="433"/>
+ </scene>
+ </scenes>
+</document>
diff --git a/Firestore/Example/Firestore/FIRAppDelegate.h b/Firestore/Example/Firestore/FIRAppDelegate.h
new file mode 100644
index 0000000..1eb5040
--- /dev/null
+++ b/Firestore/Example/Firestore/FIRAppDelegate.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import UIKit;
+
+@interface FIRAppDelegate : UIResponder <UIApplicationDelegate>
+
+@property(strong, nonatomic) UIWindow *window;
+
+@end
diff --git a/Firestore/Example/Firestore/FIRAppDelegate.m b/Firestore/Example/Firestore/FIRAppDelegate.m
new file mode 100644
index 0000000..12ca249
--- /dev/null
+++ b/Firestore/Example/Firestore/FIRAppDelegate.m
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRAppDelegate.h"
+
+@implementation FIRAppDelegate
+
+- (BOOL)application:(UIApplication *)application
+ didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+ // Override point for customization after application launch.
+ return YES;
+}
+
+- (void)applicationWillResignActive:(UIApplication *)application {
+ // Sent when the application is about to move from active to inactive state. This can occur for
+ // certain types of temporary interruptions (such as an incoming phone call or SMS message) or
+ // when the user quits the application and it begins the transition to the background state. Use
+ // this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates.
+ // Games should use this method to pause the game.
+}
+
+- (void)applicationDidEnterBackground:(UIApplication *)application {
+ // Use this method to release shared resources, save user data, invalidate timers, and store
+ // enough application state information to restore your application to its current state in case
+ // it is terminated later. If your application supports background execution, this method is
+ // called instead of applicationWillTerminate: when the user quits.
+}
+
+- (void)applicationWillEnterForeground:(UIApplication *)application {
+ // Called as part of the transition from the background to the inactive state; here you can undo
+ // many of the changes made on entering the background.
+}
+
+- (void)applicationDidBecomeActive:(UIApplication *)application {
+ // Restart any tasks that were paused (or not yet started) while the application was inactive. If
+ // the application was previously in the background, optionally refresh the user interface.
+}
+
+- (void)applicationWillTerminate:(UIApplication *)application {
+ // Called when the application is about to terminate. Save data if appropriate. See also
+ // applicationDidEnterBackground:.
+}
+
+@end
diff --git a/Firestore/Example/Firestore/FIRViewController.h b/Firestore/Example/Firestore/FIRViewController.h
new file mode 100644
index 0000000..64b4b74
--- /dev/null
+++ b/Firestore/Example/Firestore/FIRViewController.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import UIKit;
+
+@interface FIRViewController : UIViewController
+
+@end
diff --git a/Firestore/Example/Firestore/FIRViewController.m b/Firestore/Example/Firestore/FIRViewController.m
new file mode 100644
index 0000000..cdad545
--- /dev/null
+++ b/Firestore/Example/Firestore/FIRViewController.m
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRViewController.h"
+
+@interface FIRViewController ()
+
+@end
+
+@implementation FIRViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ // Do any additional setup after loading the view, typically from a nib.
+}
+
+- (void)didReceiveMemoryWarning {
+ [super didReceiveMemoryWarning];
+ // Dispose of any resources that can be recreated.
+}
+
+@end
diff --git a/Firestore/Example/Firestore/Firestore-Info.plist b/Firestore/Example/Firestore/Firestore-Info.plist
new file mode 100644
index 0000000..7576a0d
--- /dev/null
+++ b/Firestore/Example/Firestore/Firestore-Info.plist
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIMainStoryboardFile</key>
+ <string>Main</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>armv7</string>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+</dict>
+</plist>
diff --git a/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json b/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d7070bc
--- /dev/null
+++ b/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,93 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "20x20",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "29x29",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "40x40",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "iphone",
+ "size" : "60x60",
+ "scale" : "3x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "20x20",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "29x29",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "40x40",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "76x76",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "ipad",
+ "size" : "83.5x83.5",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/Firestore/Example/Firestore/en.lproj/InfoPlist.strings b/Firestore/Example/Firestore/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/Firestore/Example/Firestore/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+
diff --git a/Firestore/Example/Firestore/main.m b/Firestore/Example/Firestore/main.m
new file mode 100644
index 0000000..724fccf
--- /dev/null
+++ b/Firestore/Example/Firestore/main.m
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import UIKit;
+#import "FIRAppDelegate.h"
+
+int main(int argc, char* argv[]) {
+ @autoreleasepool {
+ return UIApplicationMain(argc, argv, nil, NSStringFromClass([FIRAppDelegate class]));
+ }
+}
diff --git a/Firestore/Example/Podfile b/Firestore/Example/Podfile
new file mode 100644
index 0000000..4008af7
--- /dev/null
+++ b/Firestore/Example/Podfile
@@ -0,0 +1,22 @@
+use_frameworks!
+platform :ios, '8.0'
+
+target 'Firestore_Example' do
+ pod 'FirebaseCommunity/Core', :path => '../../'
+ pod 'Firestore', :path => '../'
+
+ target 'Firestore_Tests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ pod 'leveldb-library'
+ end
+
+ target 'Firestore_IntegrationTests' do
+ inherit! :search_paths
+ pod 'OCMock'
+ end
+end
+
+target 'SwiftBuildTest' do
+ pod 'Firestore', :path => '../'
+end
diff --git a/Firestore/Example/SwiftBuildTest/main.swift b/Firestore/Example/SwiftBuildTest/main.swift
new file mode 100644
index 0000000..ff6c0dd
--- /dev/null
+++ b/Firestore/Example/SwiftBuildTest/main.swift
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Foundation
+
+import Firestore
+
+func main() {
+ let db = initializeDb();
+
+ let (collectionRef, documentRef) = makeRefs(database: db);
+
+ let query = makeQuery(collection: collectionRef);
+
+ writeDocument(at: documentRef);
+
+ addDocument(to: collectionRef);
+
+ readDocument(at: documentRef);
+
+ readDocuments(matching: query);
+
+ listenToDocument(at: documentRef);
+
+ listenToDocuments(matching: query);
+
+ types();
+}
+
+func initializeDb() -> Firestore {
+
+ // Initialize with ProjectID.
+ let firestore = Firestore.firestore()
+
+ // Apply settings
+ let settings = FirestoreSettings()
+ settings.host = "localhost"
+ settings.isPersistenceEnabled = true
+ firestore.settings = settings
+
+ return firestore;
+}
+
+func makeRefs(database db: Firestore) -> (CollectionReference, DocumentReference) {
+
+ var collectionRef = db.collection("my-collection")
+
+ var documentRef: DocumentReference;
+ documentRef = collectionRef.document("my-doc")
+ // or
+ documentRef = db.document("my-collection/my-doc")
+
+ // deeper collection (my-collection/my-doc/some/deep/collection)
+ collectionRef = documentRef.collection("some/deep/collection")
+
+ // parent doc (my-collection/my-doc/some/deep)
+ documentRef = collectionRef.parent!
+
+ // print paths.
+ print("Collection: \(collectionRef.path), document: \(documentRef.path)")
+
+ return (collectionRef, documentRef);
+}
+
+func makeQuery(collection collectionRef: CollectionReference) -> Query {
+
+ let query = collectionRef.whereField(FieldPath(["name"]), isEqualTo: "Fred")
+ .whereField("age", isGreaterThanOrEqualTo: 24)
+ .whereField(FieldPath.documentID(), isEqualTo: "fred")
+ .order(by: FieldPath(["age"]))
+ .order(by: "name", descending: true)
+ .limit(to: 10)
+
+ return query;
+}
+
+func writeDocument(at docRef: DocumentReference) {
+
+ let setData = [
+ "foo": 42,
+ "bar": [
+ "baz": "Hello world!"
+ ]
+ ] as [String : Any];
+
+ let updateData = [
+ "bar.baz": 42,
+ FieldPath(["foobar"]) : 42
+ ] as [AnyHashable : Any];
+
+ docRef.setData(setData)
+
+ // Completion callback (via trailing closure syntax).
+ docRef.setData(setData) { error in
+ if let error = error {
+ print("Uh oh! \(error)")
+ return
+ }
+
+ print("Set complete!")
+ }
+
+ // SetOptions
+ docRef.setData(setData, options:SetOptions.merge())
+
+ docRef.updateData(updateData)
+ docRef.delete();
+
+ docRef.delete() { error in
+ if let error = error {
+ print("Uh oh! \(error)")
+ return
+ }
+
+ print("Set complete!")
+ }
+}
+
+func addDocument(to collectionRef: CollectionReference) {
+
+ collectionRef.addDocument(data: ["foo": 42]);
+ //or
+ collectionRef.document().setData(["foo": 42]);
+}
+
+func readDocument(at docRef: DocumentReference) {
+
+ // Trailing closure syntax.
+ docRef.getDocument() { document, error in
+ if let document = document {
+ // NOTE that document is nullable.
+ let data = document.data();
+ print("Read document: \(data)")
+
+ // Fields are read via subscript notation.
+ if let foo = document["foo"] {
+ print("Field: \(foo)")
+ }
+ } else {
+ // TODO(mikelehen): There may be a better way to do this, but it at least demonstrates
+ // the swift error domain / enum codes are renamed appropriately.
+ if let errorCode = error.flatMap({
+ ($0._domain == FirestoreErrorDomain) ? FirestoreErrorCode (rawValue: $0._code) : nil
+ }) {
+ switch errorCode {
+ case .unavailable:
+ print("Can't read document due to being offline!")
+ case _:
+ print("Failed to read.")
+ }
+ } else {
+ print("Unknown error!")
+ }
+ }
+
+ }
+}
+
+func readDocuments(matching query: Query) {
+ query.getDocuments() { querySnapshot, error in
+ // TODO(mikelehen): Figure out how to make "for..in" syntax work
+ // directly on documentSet.
+ for document in querySnapshot!.documents {
+ print(document.data())
+ }
+ }
+}
+
+func listenToDocument(at docRef: DocumentReference) {
+
+ let listener = docRef.addSnapshotListener() { document, error in
+ if let error = error {
+ print("Uh oh! Listen canceled: \(error)")
+ return
+ }
+
+ if let document = document {
+ print("Current document: \(document.data())");
+ if (document.metadata.isFromCache) {
+ print("From Cache")
+ } else {
+ print("From Server")
+ }
+ }
+ }
+
+ // Unsubscribe.
+ listener.remove();
+}
+
+func listenToDocuments(matching query: Query) {
+
+ let listener = query.addSnapshotListener() { snap, error in
+ if let error = error {
+ print("Uh oh! Listen canceled: \(error)")
+ return
+ }
+
+ if let snap = snap {
+ print("NEW SNAPSHOT (empty=\(snap.isEmpty) count=\(snap.count)")
+
+ // TODO(mikelehen): Figure out how to make "for..in" syntax work
+ // directly on documentSet.
+ for document in snap.documents {
+ print("Doc: ", document.data())
+ }
+ }
+ }
+
+ // Unsubscribe
+ listener.remove();
+}
+
+func listenToQueryDiffs(onQuery query: Query) {
+
+ let listener = query.addSnapshotListener() { snap, error in
+ if let snap = snap {
+ for change in snap.documentChanges {
+ switch (change.type) {
+ case .added:
+ print("New document: \(change.document.data())")
+ case .modified:
+ print("Modified document: \(change.document.data())")
+ case .removed:
+ print("Removed document: \(change.document.data())")
+ }
+ }
+ }
+ }
+
+ // Unsubscribe
+ listener.remove();
+}
+
+func transactions() {
+ let db = Firestore.firestore()
+
+ let collectionRef = db.collection("cities")
+ let accA = collectionRef.document("accountA")
+ let accB = collectionRef.document("accountB")
+ let amount = 20.0
+
+ db.runTransaction({ (transaction, errorPointer) -> Any? in
+ do {
+ let balanceA = try transaction.getDocument(accA)["balance"] as! Double
+ let balanceB = try transaction.getDocument(accB)["balance"] as! Double
+
+ if (balanceA < amount) {
+ errorPointer?.pointee = NSError(domain: "Foo", code: 123, userInfo: nil)
+ return nil
+ }
+ transaction.updateData(["balance": balanceA - amount], forDocument:accA)
+ transaction.updateData(["balance": balanceB + amount], forDocument:accB)
+ } catch let error as NSError {
+ print("Uh oh! \(error)")
+ }
+ return 0
+ }) { (result, error) in
+ // handle result.
+ }
+}
+
+func types() {
+ // Just highlighting the types of everything, though devs can/will often omit them.
+ let _: Firestore;
+ let _: CollectionReference;
+ let _: DocumentReference;
+ let _: Query;
+ let _: DocumentSnapshot;
+ let _: QuerySnapshot;
+}
diff --git a/Firestore/Example/Tests/API/FIRGeoPointTests.m b/Firestore/Example/Tests/API/FIRGeoPointTests.m
new file mode 100644
index 0000000..ca7d24c
--- /dev/null
+++ b/Firestore/Example/Tests/API/FIRGeoPointTests.m
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Firestore/FIRGeoPoint.h"
+
+#import <XCTest/XCTest.h>
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRGeoPointTests : XCTestCase
+@end
+
+@implementation FIRGeoPointTests
+
+- (void)testEquals {
+ XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0],
+ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0]);
+ XCTAssertEqualObjects([[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56],
+ [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56]);
+ XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0],
+ [[FIRGeoPoint alloc] initWithLatitude:1 longitude:0]);
+ XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0],
+ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:1]);
+ XCTAssertNotEqualObjects([[FIRGeoPoint alloc] initWithLatitude:0 longitude:0],
+ [[NSObject alloc] init]);
+}
+
+- (void)testComparison {
+ NSArray *values = @[
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-90 longitude:180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:-89 longitude:180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:0 longitude:180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:89 longitude:180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:-180] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:0] ],
+ @[ [[FIRGeoPoint alloc] initWithLatitude:90 longitude:180] ],
+ ];
+
+ FSTAssertComparisons(values);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m
new file mode 100644
index 0000000..931a59f
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTDatabaseInfoTests.m
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTDatabaseInfo.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTDatabaseID.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDatabaseInfoTests : XCTestCase
+@end
+
+@implementation FSTDatabaseInfoTests
+
+- (void)testConstructor {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"pk"
+ host:@"h"
+ sslEnabled:YES];
+ XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p");
+ XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"d");
+ XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk");
+ XCTAssertEqualObjects(databaseInfo.host, @"h");
+ XCTAssertEqual(databaseInfo.sslEnabled, YES);
+}
+
+- (void)testDefaultDatabase {
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID];
+ FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"pk"
+ host:@"h"
+ sslEnabled:YES];
+ XCTAssertEqualObjects(databaseInfo.databaseID.projectID, @"p");
+ XCTAssertEqualObjects(databaseInfo.databaseID.databaseID, @"(default)");
+ XCTAssertEqualObjects(databaseInfo.persistenceKey, @"pk");
+ XCTAssertEqualObjects(databaseInfo.host, @"h");
+ XCTAssertEqual(databaseInfo.sslEnabled, YES);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.m b/Firestore/Example/Tests/Core/FSTEventManagerTests.m
new file mode 100644
index 0000000..85afe58
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTEventManagerTests.m
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTEventManager.h"
+
+#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTSyncEngine.h"
+#import "Model/FSTDocumentSet.h"
+#import "Util/FSTDispatchQueue.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// FSTEventManager implements this delegate privately
+@interface FSTEventManager () <FSTSyncEngineDelegate>
+@end
+
+@interface FSTEventManagerTests : XCTestCase
+@end
+
+@implementation FSTEventManagerTests {
+ FSTDispatchQueue *_testUserQueue;
+}
+
+- (void)setUp {
+ _testUserQueue = [FSTDispatchQueue queueWith:dispatch_get_main_queue()];
+}
+
+- (FSTQueryListener *)noopListenerForQuery:(FSTQuery *)query {
+ return [[FSTQueryListener alloc]
+ initWithQuery:query
+ options:[FSTListenOptions defaultOptions]
+ viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error){
+ }];
+}
+
+- (void)testHandlesManyListenersPerQuery {
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")];
+ FSTQueryListener *listener1 = [self noopListenerForQuery:query];
+ FSTQueryListener *listener2 = [self noopListenerForQuery:query];
+
+ FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]);
+ OCMExpect([syncEngineMock setDelegate:[OCMArg any]]);
+ FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock];
+
+ OCMExpect([syncEngineMock listenToQuery:query]);
+ [eventManager addListener:listener1];
+ OCMVerifyAll((id)syncEngineMock);
+
+ [eventManager addListener:listener2];
+ [eventManager removeListener:listener2];
+
+ OCMExpect([syncEngineMock stopListeningToQuery:query]);
+ [eventManager removeListener:listener1];
+ OCMVerifyAll((id)syncEngineMock);
+}
+
+- (void)testHandlesUnlistenOnUnknownListenerGracefully {
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")];
+ FSTQueryListener *listener = [self noopListenerForQuery:query];
+
+ FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]);
+ OCMExpect([syncEngineMock setDelegate:[OCMArg any]]);
+ FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock];
+
+ [eventManager removeListener:listener];
+ OCMVerifyAll((id)syncEngineMock);
+}
+
+- (FSTQueryListener *)makeMockListenerForQuery:(FSTQuery *)query
+ viewSnapshotHandler:(void (^)())handler {
+ FSTQueryListener *listener = OCMClassMock([FSTQueryListener class]);
+ OCMStub([listener query]).andReturn(query);
+ OCMStub([listener queryDidChangeViewSnapshot:[OCMArg any]]).andDo(^(NSInvocation *invocation) {
+ handler();
+ });
+ return listener;
+}
+
+- (void)testNotifiesListenersInTheRightOrder {
+ FSTQuery *query1 = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")];
+ FSTQuery *query2 = [FSTQuery queryWithPath:FSTTestPath(@"bar/baz")];
+ NSMutableArray *eventOrder = [NSMutableArray array];
+
+ FSTQueryListener *listener1 = [self makeMockListenerForQuery:query1
+ viewSnapshotHandler:^{
+ [eventOrder addObject:@"listener1"];
+ }];
+
+ FSTQueryListener *listener2 = [self makeMockListenerForQuery:query2
+ viewSnapshotHandler:^{
+ [eventOrder addObject:@"listener2"];
+ }];
+
+ FSTQueryListener *listener3 = [self makeMockListenerForQuery:query1
+ viewSnapshotHandler:^{
+ [eventOrder addObject:@"listener3"];
+ }];
+
+ FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]);
+ FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock];
+
+ [eventManager addListener:listener1];
+ [eventManager addListener:listener2];
+ [eventManager addListener:listener3];
+ OCMVerify([syncEngineMock listenToQuery:query1]);
+ OCMVerify([syncEngineMock listenToQuery:query2]);
+
+ FSTViewSnapshot *snapshot1 = OCMClassMock([FSTViewSnapshot class]);
+ OCMStub([snapshot1 query]).andReturn(query1);
+ FSTViewSnapshot *snapshot2 = OCMClassMock([FSTViewSnapshot class]);
+ OCMStub([snapshot2 query]).andReturn(query2);
+
+ [eventManager handleViewSnapshots:@[ snapshot1, snapshot2 ]];
+
+ NSArray *expected = @[ @"listener1", @"listener3", @"listener2" ];
+ XCTAssertEqualObjects(eventOrder, expected);
+}
+
+- (void)testWillForwardOnlineStateChanges {
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo/bar")];
+ FSTQueryListener *fakeListener = OCMClassMock([FSTQueryListener class]);
+ NSMutableArray *events = [NSMutableArray array];
+ OCMStub([fakeListener query]).andReturn(query);
+ OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateUnknown])
+ .andDo(^(NSInvocation *invocation) {
+ [events addObject:@(FSTOnlineStateUnknown)];
+ });
+ OCMStub([fakeListener clientDidChangeOnlineState:FSTOnlineStateHealthy])
+ .andDo(^(NSInvocation *invocation) {
+ [events addObject:@(FSTOnlineStateHealthy)];
+ });
+
+ FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]);
+ OCMExpect([syncEngineMock setDelegate:[OCMArg any]]);
+ FSTEventManager *eventManager = [FSTEventManager eventManagerWithSyncEngine:syncEngineMock];
+
+ [eventManager addListener:fakeListener];
+ XCTAssertEqualObjects(events, @[ @(FSTOnlineStateUnknown) ]);
+ [eventManager watchStreamDidChangeOnlineState:FSTOnlineStateHealthy];
+ XCTAssertEqualObjects(events, (@[ @(FSTOnlineStateUnknown), @(FSTOnlineStateHealthy) ]));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.m b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m
new file mode 100644
index 0000000..155eb00
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.m
@@ -0,0 +1,487 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTEventManager.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTView.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentSet.h"
+#import "Remote/FSTRemoteEvent.h"
+#import "Util/FSTAsyncQueryListener.h"
+#import "Util/FSTDispatchQueue.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTQueryListenerTests : XCTestCase
+@property(nonatomic, strong, readonly) FSTDispatchQueue *asyncQueue;
+@end
+
+@implementation FSTQueryListenerTests
+
+- (void)setUp {
+ _asyncQueue = [FSTDispatchQueue
+ queueWith:dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)];
+}
+
+- (void)testRaisesCollectionEvents {
+ NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array];
+ NSMutableArray<FSTViewSnapshot *> *otherAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTDocument *doc2prime =
+ FSTTestDoc(@"rooms/Hades", 3, @{@"name" : @"Hades", @"owner" : @"Jonny"}, NO);
+
+ FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum];
+ FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil);
+
+ FSTDocumentViewChange *change1 =
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change2 =
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change3 =
+ [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeModified];
+ FSTDocumentViewChange *change4 =
+ [FSTDocumentViewChange changeWithDocument:doc2prime type:FSTDocumentViewChangeTypeAdded];
+
+ [listener queryDidChangeViewSnapshot:snap1];
+ [listener queryDidChangeViewSnapshot:snap2];
+ [otherListener queryDidChangeViewSnapshot:snap2];
+
+ XCTAssertEqualObjects(accum, (@[ snap1, snap2 ]));
+ XCTAssertEqualObjects(accum[0].documentChanges, (@[ change1, change2 ]));
+ XCTAssertEqualObjects(accum[1].documentChanges, (@[ change3 ]));
+
+ FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc]
+ initWithQuery:snap2.query
+ documents:snap2.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap2.query.comparator]
+ documentChanges:@[ change1, change4 ]
+ fromCache:snap2.fromCache
+ hasPendingWrites:snap2.hasPendingWrites
+ syncStateChanged:YES];
+ XCTAssertEqualObjects(otherAccum, (@[ expectedSnap2 ]));
+}
+
+- (void)testRaisesErrorEvent {
+ NSMutableArray<NSError *> *accum = [NSMutableArray array];
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")];
+
+ FSTQueryListener *listener = [self listenToQuery:query
+ handler:^(FSTViewSnapshot *snapshot, NSError *error) {
+ [accum addObject:error];
+ }];
+
+ NSError *testError =
+ [NSError errorWithDomain:@"com.google.firestore.test" code:42 userInfo:@{@"some" : @"info"}];
+ [listener queryDidError:testError];
+
+ XCTAssertEqualObjects(accum, @[ testError ]);
+}
+
+- (void)testRaisesEventForEmptyCollectionAfterSync {
+ NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array];
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+
+ FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil);
+
+ FSTTargetChange *ackTarget =
+ [FSTTargetChange changeWithDocuments:@[]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget);
+
+ [listener queryDidChangeViewSnapshot:snap1];
+ XCTAssertEqualObjects(accum, @[]);
+
+ [listener queryDidChangeViewSnapshot:snap2];
+ XCTAssertEqualObjects(accum, @[ snap2 ]);
+}
+
+- (void)testMutingAsyncListenerPreventsAllSubsequentEvents {
+ NSMutableArray<FSTViewSnapshot *> *accum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms/Eros")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 3, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Eros", 4, @{@"name" : @"Eros2"}, NO);
+
+ __block FSTAsyncQueryListener *listener = [[FSTAsyncQueryListener alloc]
+ initWithDispatchQueue:self.asyncQueue
+ snapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) {
+ [accum addObject:snapshot];
+ [listener mute];
+ }];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
+ FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil);
+
+ FSTViewSnapshotHandler handler = listener.asyncSnapshotHandler;
+ handler(viewSnapshot1, nil);
+ handler(viewSnapshot2, nil);
+
+ // Drain queue
+ XCTestExpectation *expectation = [self expectationWithDescription:@"Queue drained"];
+ [self.asyncQueue dispatchAsync:^{
+ [expectation fulfill];
+ }];
+
+ [self waitForExpectationsWithTimeout:4.0
+ handler:^(NSError *_Nullable expectationError) {
+ if (expectationError) {
+ XCTFail(@"Error waiting for timeout: %@", expectationError);
+ }
+ }];
+
+ // We should get the first snapshot but not the second.
+ XCTAssertEqualObjects(accum, @[ viewSnapshot1 ]);
+}
+
+- (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified {
+ NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array];
+ NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+
+ FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES
+ includeDocumentMetadataChanges:NO
+ waitForSyncWhenOnline:NO];
+
+ FSTQueryListener *filteredListener =
+ [self listenToQuery:query accumulatingSnapshots:filteredAccum];
+ FSTQueryListener *fullListener =
+ [self listenToQuery:query options:options accumulatingSnapshots:fullAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
+
+ FSTTargetChange *ackTarget =
+ [FSTTargetChange changeWithDocuments:@[ doc1 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget);
+ FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil);
+
+ [filteredListener queryDidChangeViewSnapshot:snap1]; // local event
+ [filteredListener queryDidChangeViewSnapshot:snap2]; // no event
+ [filteredListener queryDidChangeViewSnapshot:snap3]; // doc2 update
+
+ [fullListener queryDidChangeViewSnapshot:snap1]; // local event
+ [fullListener queryDidChangeViewSnapshot:snap2]; // state change event
+ [fullListener queryDidChangeViewSnapshot:snap3]; // doc2 update
+
+ XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ]));
+ XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ]));
+}
+
+- (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified {
+ NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array];
+ NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO);
+
+ FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO
+ includeDocumentMetadataChanges:YES
+ waitForSyncWhenOnline:NO];
+
+ FSTQueryListener *filteredListener =
+ [self listenToQuery:query accumulatingSnapshots:filteredAccum];
+ FSTQueryListener *fullListener =
+ [self listenToQuery:query options:options accumulatingSnapshots:fullAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil);
+ FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil);
+
+ FSTDocumentViewChange *change1 =
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change2 =
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change3 =
+ [FSTDocumentViewChange changeWithDocument:doc1Prime type:FSTDocumentViewChangeTypeMetadata];
+ FSTDocumentViewChange *change4 =
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded];
+
+ [filteredListener queryDidChangeViewSnapshot:snap1];
+ [filteredListener queryDidChangeViewSnapshot:snap2];
+ [filteredListener queryDidChangeViewSnapshot:snap3];
+ [fullListener queryDidChangeViewSnapshot:snap1];
+ [fullListener queryDidChangeViewSnapshot:snap2];
+ [fullListener queryDidChangeViewSnapshot:snap3];
+
+ XCTAssertEqualObjects(filteredAccum, (@[ snap1, snap3 ]));
+ XCTAssertEqualObjects(filteredAccum[0].documentChanges, (@[ change1, change2 ]));
+ XCTAssertEqualObjects(filteredAccum[1].documentChanges, (@[ change4 ]));
+
+ XCTAssertEqualObjects(fullAccum, (@[ snap1, snap2, snap3 ]));
+ XCTAssertEqualObjects(fullAccum[0].documentChanges, (@[ change1, change2 ]));
+ XCTAssertEqualObjects(fullAccum[1].documentChanges, (@[ change3 ]));
+ XCTAssertEqualObjects(fullAccum[2].documentChanges, (@[ change4 ]));
+}
+
+- (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges {
+ NSMutableArray<FSTViewSnapshot *> *fullAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, YES);
+ FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2Prime = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO);
+
+ FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES
+ includeDocumentMetadataChanges:NO
+ waitForSyncWhenOnline:NO];
+ FSTQueryListener *fullListener =
+ [self listenToQuery:query options:options accumulatingSnapshots:fullAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil);
+ FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil);
+ FSTViewSnapshot *snap4 = FSTTestApplyChanges(view, @[ doc2Prime ], nil);
+
+ [fullListener queryDidChangeViewSnapshot:snap1];
+ [fullListener queryDidChangeViewSnapshot:snap2]; // Emits no events.
+ [fullListener queryDidChangeViewSnapshot:snap3];
+ [fullListener queryDidChangeViewSnapshot:snap4]; // Metadata change event.
+
+ FSTViewSnapshot *expectedSnap4 = [[FSTViewSnapshot alloc] initWithQuery:snap4.query
+ documents:snap4.documents
+ oldDocuments:snap3.documents
+ documentChanges:@[]
+ fromCache:snap4.fromCache
+ hasPendingWrites:NO
+ syncStateChanged:snap4.syncStateChanged];
+ XCTAssertEqualObjects(fullAccum, (@[ snap1, snap3, expectedSnap4 ]));
+}
+
+- (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadataChangesIsFalse {
+ NSMutableArray<FSTViewSnapshot *> *filteredAccum = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, YES);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTDocument *doc1Prime = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/Other", 3, @{@"name" : @"Other"}, NO);
+
+ FSTQueryListener *filteredListener =
+ [self listenToQuery:query accumulatingSnapshots:filteredAccum];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil);
+
+ FSTDocumentViewChange *change3 =
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded];
+
+ [filteredListener queryDidChangeViewSnapshot:snap1];
+ [filteredListener queryDidChangeViewSnapshot:snap2];
+
+ FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:snap2.query
+ documents:snap2.documents
+ oldDocuments:snap1.documents
+ documentChanges:@[ change3 ]
+ fromCache:snap2.isFromCache
+ hasPendingWrites:snap2.hasPendingWrites
+ syncStateChanged:snap2.syncStateChanged];
+ XCTAssertEqualObjects(filteredAccum, (@[ snap1, expectedSnap2 ]));
+}
+
+- (void)testWillWaitForSyncIfOnline {
+ NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTQueryListener *listener =
+ [self listenToQuery:query
+ options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO
+ includeDocumentMetadataChanges:NO
+ waitForSyncWhenOnline:YES]
+ accumulatingSnapshots:events];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil);
+ FSTViewSnapshot *snap3 =
+ FSTTestApplyChanges(view, @[],
+ [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+
+ [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event
+ [listener queryDidChangeViewSnapshot:snap1];
+ [listener clientDidChangeOnlineState:FSTOnlineStateUnknown];
+ [listener clientDidChangeOnlineState:FSTOnlineStateHealthy];
+ [listener queryDidChangeViewSnapshot:snap2];
+ [listener queryDidChangeViewSnapshot:snap3];
+
+ FSTDocumentViewChange *change1 =
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change2 =
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded];
+ FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc]
+ initWithQuery:snap3.query
+ documents:snap3.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap3.query.comparator]
+ documentChanges:@[ change1, change2 ]
+ fromCache:NO
+ hasPendingWrites:NO
+ syncStateChanged:YES];
+ XCTAssertEqualObjects(events, (@[ expectedSnap ]));
+}
+
+- (void)testWillRaiseInitialEventWhenGoingOffline {
+ NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/Eros", 1, @{@"name" : @"Eros"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/Hades", 2, @{@"name" : @"Hades"}, NO);
+ FSTQueryListener *listener =
+ [self listenToQuery:query
+ options:[[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO
+ includeDocumentMetadataChanges:NO
+ waitForSyncWhenOnline:YES]
+ accumulatingSnapshots:events];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil);
+ FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil);
+
+ [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event
+ [listener queryDidChangeViewSnapshot:snap1]; // no event
+ [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event
+ [listener clientDidChangeOnlineState:FSTOnlineStateUnknown]; // no event
+ [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event
+ [listener queryDidChangeViewSnapshot:snap2]; // another event
+
+ FSTDocumentViewChange *change1 =
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded];
+ FSTDocumentViewChange *change2 =
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded];
+ FSTViewSnapshot *expectedSnap1 = [[FSTViewSnapshot alloc]
+ initWithQuery:query
+ documents:snap1.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator]
+ documentChanges:@[ change1 ]
+ fromCache:YES
+ hasPendingWrites:NO
+ syncStateChanged:YES];
+ FSTViewSnapshot *expectedSnap2 = [[FSTViewSnapshot alloc] initWithQuery:query
+ documents:snap2.documents
+ oldDocuments:snap1.documents
+ documentChanges:@[ change2 ]
+ fromCache:YES
+ hasPendingWrites:NO
+ syncStateChanged:NO];
+ XCTAssertEqualObjects(events, (@[ expectedSnap1, expectedSnap2 ]));
+}
+
+- (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs {
+ NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTQueryListener *listener = [self listenToQuery:query
+ options:[FSTListenOptions defaultOptions]
+ accumulatingSnapshots:events];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil);
+
+ [listener clientDidChangeOnlineState:FSTOnlineStateHealthy]; // no event
+ [listener queryDidChangeViewSnapshot:snap1]; // no event
+ [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // event
+
+ FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc]
+ initWithQuery:query
+ documents:snap1.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator]
+ documentChanges:@[]
+ fromCache:YES
+ hasPendingWrites:NO
+ syncStateChanged:YES];
+ XCTAssertEqualObjects(events, (@[ expectedSnap ]));
+}
+
+- (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs {
+ NSMutableArray<FSTViewSnapshot *> *events = [NSMutableArray array];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"rooms")];
+ FSTQueryListener *listener = [self listenToQuery:query
+ options:[FSTListenOptions defaultOptions]
+ accumulatingSnapshots:events];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil);
+
+ [listener clientDidChangeOnlineState:FSTOnlineStateFailed]; // no event
+ [listener queryDidChangeViewSnapshot:snap1]; // event
+
+ FSTViewSnapshot *expectedSnap = [[FSTViewSnapshot alloc]
+ initWithQuery:query
+ documents:snap1.documents
+ oldDocuments:[FSTDocumentSet documentSetWithComparator:snap1.query.comparator]
+ documentChanges:@[]
+ fromCache:YES
+ hasPendingWrites:NO
+ syncStateChanged:YES];
+ XCTAssertEqualObjects(events, (@[ expectedSnap ]));
+}
+
+- (FSTQueryListener *)listenToQuery:(FSTQuery *)query handler:(FSTViewSnapshotHandler)handler {
+ return [[FSTQueryListener alloc] initWithQuery:query
+ options:[FSTListenOptions defaultOptions]
+ viewSnapshotHandler:handler];
+}
+
+- (FSTQueryListener *)listenToQuery:(FSTQuery *)query
+ options:(FSTListenOptions *)options
+ accumulatingSnapshots:(NSMutableArray<FSTViewSnapshot *> *)values {
+ return [[FSTQueryListener alloc] initWithQuery:query
+ options:options
+ viewSnapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) {
+ [values addObject:snapshot];
+ }];
+}
+
+- (FSTQueryListener *)listenToQuery:(FSTQuery *)query
+ accumulatingSnapshots:(NSMutableArray<FSTViewSnapshot *> *)values {
+ return [self listenToQuery:query
+ options:[FSTListenOptions defaultOptions]
+ accumulatingSnapshots:values];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTQueryTests.m b/Firestore/Example/Tests/Core/FSTQueryTests.m
new file mode 100644
index 0000000..051f10d
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTQueryTests.m
@@ -0,0 +1,577 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTQuery.h"
+
+#import <XCTest/XCTest.h>
+
+#import "API/FIRFirestore+Internal.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Convenience methods for building test queries. */
+@interface FSTQuery (Tests)
+- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending;
+@end
+
+@implementation FSTQuery (Tests)
+
+- (FSTQuery *)queryByAddingSortBy:(NSString *)key ascending:(BOOL)ascending {
+ return [self queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(key)
+ ascending:ascending]];
+}
+
+@end
+
+@interface FSTQueryTests : XCTestCase
+@end
+
+@implementation FSTQueryTests
+
+- (void)testConstructor {
+ FSTResourcePath *path =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages", @"0001" ]];
+ FSTQuery *query = [FSTQuery queryWithPath:path];
+ XCTAssertNotNil(query);
+
+ XCTAssertEqual(query.sortOrders.count, 1);
+ XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, kDocumentKeyPath);
+ XCTAssertEqual(query.sortOrders[0].ascending, YES);
+
+ XCTAssertEqual(query.explicitSortOrders.count, 0);
+}
+
+- (void)testOrderBy {
+ FSTResourcePath *path =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"Firestore", @"messages" ]];
+ FSTQuery *query = [FSTQuery queryWithPath:path];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"length")
+ ascending:NO]];
+
+ XCTAssertEqual(query.sortOrders.count, 2);
+ XCTAssertEqualObjects(query.sortOrders[0].field.canonicalString, @"length");
+ XCTAssertEqual(query.sortOrders[0].ascending, NO);
+ XCTAssertEqualObjects(query.sortOrders[1].field.canonicalString, kDocumentKeyPath);
+ XCTAssertEqual(query.sortOrders[1].ascending, NO);
+
+ XCTAssertEqual(query.explicitSortOrders.count, 1);
+ XCTAssertEqualObjects(query.explicitSortOrders[0].field.canonicalString, @"length");
+ XCTAssertEqual(query.explicitSortOrders[0].ascending, NO);
+}
+
+- (void)testMatchesBasedOnDocumentKey {
+ FSTResourcePath *queryKey =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages", @"1" ]];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO);
+
+ // document query
+ FSTQuery *query = [FSTQuery queryWithPath:queryKey];
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc2]);
+ XCTAssertFalse([query matchesDocument:doc3]);
+}
+
+- (void)testMatchesCorrectlyForShallowAncestorQuery {
+ FSTResourcePath *queryPath =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc1Meta = FSTTestDoc(@"rooms/eros/messages/1/meta/1", 0, @{@"meta" : @"mv"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO);
+
+ // shallow ancestor query
+ FSTQuery *query = [FSTQuery queryWithPath:queryPath];
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc1Meta]);
+ XCTAssertTrue([query matchesDocument:doc2]);
+ XCTAssertFalse([query matchesDocument:doc3]);
+}
+
+- (void)testEmptyFieldsAreAllowedForQueries {
+ FSTResourcePath *queryPath = [FSTResourcePath pathWithString:@"rooms/eros/messages"];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO);
+
+ FSTQuery *query = [[FSTQuery queryWithPath:queryPath]
+ queryByAddingFilter:FSTTestFilter(@"text", @"==", @"msg1")];
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc2]);
+}
+
+- (void)testMatchesPrimitiveValuesForFilters {
+ FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))];
+ FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))];
+
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO);
+ FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO);
+
+ XCTAssertFalse([query1 matchesDocument:doc1]);
+ XCTAssertTrue([query1 matchesDocument:doc2]);
+ XCTAssertTrue([query1 matchesDocument:doc3]);
+ XCTAssertFalse([query1 matchesDocument:doc4]);
+ XCTAssertFalse([query1 matchesDocument:doc5]);
+ XCTAssertFalse([query1 matchesDocument:doc6]);
+
+ XCTAssertTrue([query2 matchesDocument:doc1]);
+ XCTAssertTrue([query2 matchesDocument:doc2]);
+ XCTAssertFalse([query2 matchesDocument:doc3]);
+ XCTAssertFalse([query2 matchesDocument:doc4]);
+ XCTAssertFalse([query2 matchesDocument:doc5]);
+ XCTAssertFalse([query2 matchesDocument:doc6]);
+}
+
+- (void)testNullFilter {
+ FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @"==", [NSNull null])];
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{@"sort" : [NSNull null]}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO);
+
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc2]);
+ XCTAssertFalse([query matchesDocument:doc3]);
+ XCTAssertFalse([query matchesDocument:doc4]);
+ XCTAssertFalse([query matchesDocument:doc5]);
+}
+
+- (void)testNanFilter {
+ FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @"==", @(NAN))];
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @(NAN) }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @3.1 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @NO }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{@"sort" : @"string"}, NO);
+
+ XCTAssertTrue([query matchesDocument:doc1]);
+ XCTAssertFalse([query matchesDocument:doc2]);
+ XCTAssertFalse([query matchesDocument:doc3]);
+ XCTAssertFalse([query matchesDocument:doc4]);
+ XCTAssertFalse([query matchesDocument:doc5]);
+}
+
+- (void)testDoesNotMatchComplexObjectsForFilters {
+ FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @"<=", @(2))];
+ FSTQuery *query2 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingFilter:FSTTestFilter(@"sort", @">=", @(2))];
+
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO);
+ FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{ @"sort" : @{} }, NO); // no sort field
+ FSTDocument *doc7 = FSTTestDoc(@"collection/7", 0, @{ @"sort" : @[ @3, @1 ] }, NO);
+
+ XCTAssertTrue([query1 matchesDocument:doc1]);
+ XCTAssertFalse([query1 matchesDocument:doc2]);
+ XCTAssertFalse([query1 matchesDocument:doc3]);
+ XCTAssertFalse([query1 matchesDocument:doc4]);
+ XCTAssertFalse([query1 matchesDocument:doc5]);
+ XCTAssertFalse([query1 matchesDocument:doc6]);
+ XCTAssertFalse([query1 matchesDocument:doc7]);
+
+ XCTAssertTrue([query2 matchesDocument:doc1]);
+ XCTAssertFalse([query2 matchesDocument:doc2]);
+ XCTAssertFalse([query2 matchesDocument:doc3]);
+ XCTAssertFalse([query2 matchesDocument:doc4]);
+ XCTAssertFalse([query2 matchesDocument:doc5]);
+ XCTAssertFalse([query2 matchesDocument:doc6]);
+ XCTAssertFalse([query2 matchesDocument:doc7]);
+}
+
+- (void)testDoesntRemoveComplexObjectsWithOrderBy {
+ FSTQuery *query1 = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]]
+ queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort")
+ ascending:YES]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"collection/1", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"collection/2", 0, @{ @"sort" : @[] }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"collection/3", 0, @{ @"sort" : @[ @1 ] }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"collection/4", 0, @{ @"sort" : @{@"foo" : @2} }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"collection/5", 0, @{ @"sort" : @{@"foo" : @"bar"} }, NO);
+ FSTDocument *doc6 = FSTTestDoc(@"collection/6", 0, @{}, NO);
+
+ XCTAssertTrue([query1 matchesDocument:doc1]);
+ XCTAssertTrue([query1 matchesDocument:doc2]);
+ XCTAssertTrue([query1 matchesDocument:doc3]);
+ XCTAssertTrue([query1 matchesDocument:doc4]);
+ XCTAssertTrue([query1 matchesDocument:doc5]);
+ XCTAssertFalse([query1 matchesDocument:doc6]);
+}
+
+- (void)testFiltersBasedOnArrayValue {
+ FSTQuery *baseQuery =
+ [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"collection/doc", 0, @{ @"tags" : @[ @"foo", @1, @YES ] }, NO);
+
+ NSArray<id<FSTFilter>> *matchingFilters =
+ @[ FSTTestFilter(@"tags", @"==", @[ @"foo", @1, @YES ]) ];
+
+ NSArray<id<FSTFilter>> *nonMatchingFilters = @[
+ FSTTestFilter(@"tags", @"==", @"foo"),
+ FSTTestFilter(@"tags", @"==", @[ @"foo", @1 ]),
+ FSTTestFilter(@"tags", @"==", @[ @"foo", @YES, @1 ]),
+ ];
+
+ for (id<FSTFilter> filter in matchingFilters) {
+ XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]);
+ }
+
+ for (id<FSTFilter> filter in nonMatchingFilters) {
+ XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]);
+ }
+}
+
+- (void)testFiltersBasedOnObjectValue {
+ FSTQuery *baseQuery =
+ [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+
+ FSTDocument *doc1 =
+ FSTTestDoc(@"collection/doc", 0,
+ @{ @"tags" : @{@"foo" : @"foo", @"a" : @0, @"b" : @YES, @"c" : @(NAN)} }, NO);
+
+ NSArray<id<FSTFilter>> *matchingFilters = @[
+ FSTTestFilter(@"tags", @"==",
+ @{ @"foo" : @"foo",
+ @"a" : @0,
+ @"b" : @YES,
+ @"c" : @(NAN) }),
+ FSTTestFilter(@"tags", @"==",
+ @{ @"b" : @YES,
+ @"a" : @0,
+ @"foo" : @"foo",
+ @"c" : @(NAN) }),
+ FSTTestFilter(@"tags.foo", @"==", @"foo")
+ ];
+
+ NSArray<id<FSTFilter>> *nonMatchingFilters = @[
+ FSTTestFilter(@"tags", @"==", @"foo"), FSTTestFilter(@"tags", @"==", @{
+ @"foo" : @"foo",
+ @"a" : @0,
+ @"b" : @YES,
+ })
+ ];
+
+ for (id<FSTFilter> filter in matchingFilters) {
+ XCTAssertTrue([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]);
+ }
+
+ for (id<FSTFilter> filter in nonMatchingFilters) {
+ XCTAssertFalse([[baseQuery queryByAddingFilter:filter] matchesDocument:doc1]);
+ }
+}
+
+/**
+ * Checks that an ordered array of elements yields the correct pair-wise comparison result for the
+ * supplied comparator.
+ */
+- (void)assertCorrectComparisonsWithArray:(NSArray *)array comparator:(NSComparator)comp {
+ [array enumerateObjectsUsingBlock:^(id iObj, NSUInteger i, BOOL *outerStop) {
+ [array enumerateObjectsUsingBlock:^(id _Nonnull jObj, NSUInteger j, BOOL *innerStop) {
+ NSComparisonResult expected = [@(i) compare:@(j)];
+ NSComparisonResult actual = comp(iObj, jObj);
+ XCTAssertEqual(actual, expected, @"Compared %@ to %@ at (%lu, %lu).", iObj, jObj,
+ (unsigned long)i, (unsigned long)j);
+ }];
+ }];
+}
+
+- (void)testSortsDocumentsInTheCorrectOrder {
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort")
+ ascending:YES]];
+
+ // clang-format off
+ NSArray<FSTDocument *> *docs = @[
+ FSTTestDoc(@"collection/1", 0, @{@"sort": [NSNull null]}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @NO}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @YES}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @1}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort": @1}, NO), // by key
+ FSTTestDoc(@"collection/3", 0, @{@"sort": @1}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @1.9}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @2}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @2.1}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @""}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @"a"}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @"ab"}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort": @"b"}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort":
+ FSTTestRef(@"project", kDefaultDatabaseID, @"collection/id1")}, NO),
+ ];
+ // clang-format on
+
+ [self assertCorrectComparisonsWithArray:docs comparator:query.comparator];
+}
+
+- (void)testSortsDocumentsUsingMultipleFields {
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1")
+ ascending:YES]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2")
+ ascending:YES]];
+
+ // clang-format off
+ NSArray<FSTDocument *> *docs =
+ @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO),
+ ];
+ // clang-format on
+
+ [self assertCorrectComparisonsWithArray:docs comparator:query.comparator];
+}
+
+- (void)testSortsDocumentsWithDescendingToo {
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"collection" ]]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort1")
+ ascending:NO]];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"sort2")
+ ascending:NO]];
+
+ // clang-format off
+ NSArray<FSTDocument *> *docs =
+ @[FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @3}, NO),
+ FSTTestDoc(@"collection/3", 0, @{@"sort1": @2, @"sort2": @2}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @2, @"sort2": @1}, NO),
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @3}, NO),
+ FSTTestDoc(@"collection/3", 0, @{@"sort1": @1, @"sort2": @2}, NO),
+ FSTTestDoc(@"collection/2", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @2}, NO), // by key
+ FSTTestDoc(@"collection/1", 0, @{@"sort1": @1, @"sort2": @1}, NO),
+ ];
+ // clang-format on
+
+ [self assertCorrectComparisonsWithArray:docs comparator:query.comparator];
+}
+
+- (void)testEquality {
+ FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))];
+ q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))];
+ FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))];
+ q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))];
+
+ FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+
+ FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+ FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+
+ FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES];
+ q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES];
+ FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES];
+ q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES];
+ FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES];
+ q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES];
+
+ FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES];
+ q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))];
+ FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))];
+ q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES];
+ FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))];
+ q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES];
+
+ FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q61 = [q61 queryBySettingLimit:10];
+
+ // XCTAssertEqualObjects(q11, q12); // TODO(klimt): not canonical yet
+ XCTAssertNotEqualObjects(q11, q21);
+ XCTAssertNotEqualObjects(q11, q31);
+ XCTAssertNotEqualObjects(q11, q41);
+ XCTAssertNotEqualObjects(q11, q51);
+ XCTAssertNotEqualObjects(q11, q61);
+
+ XCTAssertEqualObjects(q21, q22);
+ XCTAssertNotEqualObjects(q21, q31);
+ XCTAssertNotEqualObjects(q21, q41);
+ XCTAssertNotEqualObjects(q21, q51);
+ XCTAssertNotEqualObjects(q21, q61);
+
+ XCTAssertEqualObjects(q31, q32);
+ XCTAssertNotEqualObjects(q31, q41);
+ XCTAssertNotEqualObjects(q31, q51);
+ XCTAssertNotEqualObjects(q31, q61);
+
+ XCTAssertEqualObjects(q41, q42);
+ XCTAssertNotEqualObjects(q41, q43Diff);
+ XCTAssertNotEqualObjects(q41, q51);
+ XCTAssertNotEqualObjects(q41, q61);
+
+ XCTAssertEqualObjects(q51, q52);
+ XCTAssertNotEqualObjects(q51, q53Diff);
+ XCTAssertNotEqualObjects(q51, q61);
+}
+
+- (void)testUniqueIds {
+ FSTQuery *q11 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))];
+ q11 = [q11 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))];
+ FSTQuery *q12 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i2", @"==", @(3))];
+ q12 = [q12 queryByAddingFilter:FSTTestFilter(@"i1", @"<", @(2))];
+
+ FSTQuery *q21 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ FSTQuery *q22 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+
+ FSTQuery *q31 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+ FSTQuery *q32 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+
+ FSTQuery *q41 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q41 = [q41 queryByAddingSortBy:@"foo" ascending:YES];
+ q41 = [q41 queryByAddingSortBy:@"bar" ascending:YES];
+ FSTQuery *q42 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q42 = [q42 queryByAddingSortBy:@"foo" ascending:YES];
+ q42 = [q42 queryByAddingSortBy:@"bar" ascending:YES];
+ FSTQuery *q43Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q43Diff = [q43Diff queryByAddingSortBy:@"bar" ascending:YES];
+ q43Diff = [q43Diff queryByAddingSortBy:@"foo" ascending:YES];
+
+ FSTQuery *q51 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q51 = [q51 queryByAddingSortBy:@"foo" ascending:YES];
+ q51 = [q51 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))];
+ FSTQuery *q52 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q52 = [q52 queryByAddingFilter:FSTTestFilter(@"foo", @">", @(2))];
+ q52 = [q52 queryByAddingSortBy:@"foo" ascending:YES];
+ FSTQuery *q53Diff = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q53Diff = [q53Diff queryByAddingFilter:FSTTestFilter(@"bar", @">", @(2))];
+ q53Diff = [q53Diff queryByAddingSortBy:@"bar" ascending:YES];
+
+ FSTQuery *q61 = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ q61 = [q61 queryBySettingLimit:10];
+
+ // XCTAssertEqual(q11.hash, q12.hash); // TODO(klimt): not canonical yet
+ XCTAssertNotEqual(q11.hash, q21.hash);
+ XCTAssertNotEqual(q11.hash, q31.hash);
+ XCTAssertNotEqual(q11.hash, q41.hash);
+ XCTAssertNotEqual(q11.hash, q51.hash);
+ XCTAssertNotEqual(q11.hash, q61.hash);
+
+ XCTAssertEqual(q21.hash, q22.hash);
+ XCTAssertNotEqual(q21.hash, q31.hash);
+ XCTAssertNotEqual(q21.hash, q41.hash);
+ XCTAssertNotEqual(q21.hash, q51.hash);
+ XCTAssertNotEqual(q21.hash, q61.hash);
+
+ XCTAssertEqual(q31.hash, q32.hash);
+ XCTAssertNotEqual(q31.hash, q41.hash);
+ XCTAssertNotEqual(q31.hash, q51.hash);
+ XCTAssertNotEqual(q31.hash, q61.hash);
+
+ XCTAssertEqual(q41.hash, q42.hash);
+ XCTAssertNotEqual(q41.hash, q43Diff.hash);
+ XCTAssertNotEqual(q41.hash, q51.hash);
+ XCTAssertNotEqual(q41.hash, q61.hash);
+
+ XCTAssertEqual(q51.hash, q52.hash);
+ XCTAssertNotEqual(q51.hash, q53Diff.hash);
+ XCTAssertNotEqual(q51.hash, q61.hash);
+}
+
+- (void)testImplicitOrderBy {
+ FSTQuery *baseQuery = FSTTestQuery(@"foo");
+ // Default is ascending
+ XCTAssertEqualObjects(baseQuery.sortOrders, @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]);
+
+ // Explicit key ordering is respected
+ XCTAssertEqualObjects(
+ [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")].sortOrders,
+ @[ FSTTestOrderBy(kDocumentKeyPath, @"asc") ]);
+ XCTAssertEqualObjects(
+ [baseQuery queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")].sortOrders,
+ @[ FSTTestOrderBy(kDocumentKeyPath, @"desc") ]);
+
+ XCTAssertEqualObjects(
+ [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")]
+ queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"asc")]
+ .sortOrders,
+ (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ]));
+
+ XCTAssertEqualObjects(
+ [[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")]
+ queryByAddingSortOrder:FSTTestOrderBy(kDocumentKeyPath, @"desc")]
+ .sortOrders,
+ (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ]));
+
+ // Inequality filters add order bys
+ XCTAssertEqualObjects(
+ [baseQuery queryByAddingFilter:FSTTestFilter(@"foo", @"<", @5)].sortOrders,
+ (@[ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(kDocumentKeyPath, @"asc") ]));
+
+ // Descending order by applies to implicit key ordering
+ XCTAssertEqualObjects(
+ [baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")].sortOrders,
+ (@[ FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(kDocumentKeyPath, @"desc") ]));
+ XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"asc")]
+ queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"desc")]
+ .sortOrders,
+ (@[
+ FSTTestOrderBy(@"foo", @"asc"), FSTTestOrderBy(@"bar", @"desc"),
+ FSTTestOrderBy(kDocumentKeyPath, @"desc")
+ ]));
+ XCTAssertEqualObjects([[baseQuery queryByAddingSortOrder:FSTTestOrderBy(@"foo", @"desc")]
+ queryByAddingSortOrder:FSTTestOrderBy(@"bar", @"asc")]
+ .sortOrders,
+ (@[
+ FSTTestOrderBy(@"foo", @"desc"), FSTTestOrderBy(@"bar", @"asc"),
+ FSTTestOrderBy(kDocumentKeyPath, @"asc")
+ ]));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h b/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h
new file mode 100644
index 0000000..ff98e74
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "Core/FSTSyncEngine.h"
+
+@class FSTDocumentKey;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTSyncEngine (Testing)
+
+/** Returns the current set of limbo document keys and their associated target IDs. */
+- (NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m b/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m
new file mode 100644
index 0000000..11a7f46
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTTargetIDGeneratorTests.m
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTTargetIDGenerator.h"
+
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTTargetIDGenerator ()
+- (instancetype)initWithGeneratorID:(NSInteger)generatorID startingAfterID:(FSTTargetID)after;
+@end
+
+@interface FSTTargetIDGeneratorTests : XCTestCase
+@end
+
+@implementation FSTTargetIDGeneratorTests
+
+- (void)testConstructor {
+ XCTAssertEqual([[[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:0] nextID],
+ 2);
+ XCTAssertEqual([[[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:0] nextID],
+ 1);
+
+ XCTAssertEqual([[FSTTargetIDGenerator generatorForLocalStoreStartingAfterID:0] nextID], 2);
+ XCTAssertEqual([[FSTTargetIDGenerator generatorForSyncEngineStartingAfterID:0] nextID], 1);
+}
+
+- (void)testSkipPast {
+ FSTTargetIDGenerator *gen =
+ [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:-1];
+ XCTAssertEqual([gen nextID], 1);
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:2];
+ XCTAssertEqual([gen nextID], 3);
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:4];
+ XCTAssertEqual([gen nextID], 5);
+
+ for (int i = 4; i < 12; ++i) {
+ FSTTargetIDGenerator *gen0 =
+ [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:i];
+ FSTTargetIDGenerator *gen1 =
+ [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:i];
+ XCTAssertEqual([gen0 nextID], i + 2 & ~1, @"Skip failed for index %d", i);
+ XCTAssertEqual([gen1 nextID], i + 1 | 1, @"Skip failed for index %d", i);
+ }
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:12];
+ XCTAssertEqual([gen nextID], 13);
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:22];
+ XCTAssertEqual([gen nextID], 24);
+}
+
+- (void)testIncrement {
+ FSTTargetIDGenerator *gen =
+ [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:0];
+ XCTAssertEqual([gen nextID], 2);
+ XCTAssertEqual([gen nextID], 4);
+ XCTAssertEqual([gen nextID], 6);
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:0 startingAfterID:46];
+ XCTAssertEqual([gen nextID], 48);
+ XCTAssertEqual([gen nextID], 50);
+ XCTAssertEqual([gen nextID], 52);
+ XCTAssertEqual([gen nextID], 54);
+
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:0];
+ XCTAssertEqual([gen nextID], 1);
+ XCTAssertEqual([gen nextID], 3);
+ XCTAssertEqual([gen nextID], 5);
+ gen = [[FSTTargetIDGenerator alloc] initWithGeneratorID:1 startingAfterID:46];
+ XCTAssertEqual([gen nextID], 47);
+ XCTAssertEqual([gen nextID], 49);
+ XCTAssertEqual([gen nextID], 51);
+ XCTAssertEqual([gen nextID], 53);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTTimestampTests.m b/Firestore/Example/Tests/Core/FSTTimestampTests.m
new file mode 100644
index 0000000..8538078
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTTimestampTests.m
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTTimestamp.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Util/FSTAssert.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTTimestampTests : XCTestCase
+@end
+
+@implementation FSTTimestampTests
+
+- (void)testFromDate {
+ // Very carefully construct an NSDate that won't lose precision with its milliseconds.
+ NSDate *input = [NSDate dateWithTimeIntervalSinceReferenceDate:1.5];
+
+ FSTTimestamp *actual = [FSTTimestamp timestampWithDate:input];
+ static const int64_t kSecondsFromEpochToReferenceDate = 978307200;
+ XCTAssertEqual(kSecondsFromEpochToReferenceDate + 1, actual.seconds);
+ XCTAssertEqual(500000000, actual.nanos);
+
+ FSTTimestamp *expected =
+ [[FSTTimestamp alloc] initWithSeconds:(kSecondsFromEpochToReferenceDate + 1) nanos:500000000];
+ XCTAssertEqualObjects(expected, actual);
+}
+
+- (void)testSO8601String {
+ NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0);
+ FSTTimestamp *timestamp =
+ [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:543000000];
+ XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.543000000Z");
+}
+
+- (void)testISO8601String_withLowMilliseconds {
+ NSDate *date = FSTTestDate(1912, 4, 14, 23, 40, 0);
+ FSTTimestamp *timestamp =
+ [[FSTTimestamp alloc] initWithSeconds:(int64_t)date.timeIntervalSince1970 nanos:7000000];
+ XCTAssertEqualObjects(timestamp.ISO8601String, @"1912-04-14T23:40:00.007000000Z");
+}
+
+- (void)testISO8601String_withLowNanos {
+ FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:1];
+ XCTAssertEqualObjects(timestamp.ISO8601String, @"1970-01-01T00:00:00.000000001Z");
+}
+
+- (void)testISO8601String_withNegativeSeconds {
+ FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:-1 nanos:999999999];
+ XCTAssertEqualObjects(timestamp.ISO8601String, @"1969-12-31T23:59:59.999999999Z");
+}
+
+- (void)testCompare {
+ NSArray<FSTTimestamp *> *timestamps = @[
+ [[FSTTimestamp alloc] initWithSeconds:12344 nanos:999999999],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:0],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:000000001],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:99999999],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000000],
+ [[FSTTimestamp alloc] initWithSeconds:12345 nanos:100000001],
+ [[FSTTimestamp alloc] initWithSeconds:12346 nanos:0],
+ ];
+ for (int i = 0; i < timestamps.count - 1; ++i) {
+ XCTAssertEqual(NSOrderedAscending, [timestamps[i] compare:timestamps[i + 1]]);
+ XCTAssertEqual(NSOrderedDescending, [timestamps[i + 1] compare:timestamps[i]]);
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m
new file mode 100644
index 0000000..202fae8
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.m
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTViewSnapshot.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentSet.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTViewSnapshotTests : XCTestCase
+@end
+
+@implementation FSTViewSnapshotTests
+
+- (void)testDocumentChangeConstructor {
+ FSTDocument *doc = FSTTestDoc(@"a/b", 0, @{}, NO);
+ FSTDocumentViewChangeType type = FSTDocumentViewChangeTypeModified;
+ FSTDocumentViewChange *change = [FSTDocumentViewChange changeWithDocument:doc type:type];
+ XCTAssertEqual(change.document, doc);
+ XCTAssertEqual(change.type, type);
+}
+
+- (void)testTrack {
+ FSTDocumentViewChangeSet *set = [FSTDocumentViewChangeSet changeSet];
+
+ FSTDocument *docAdded = FSTTestDoc(@"a/1", 0, @{}, NO);
+ FSTDocument *docRemoved = FSTTestDoc(@"a/2", 0, @{}, NO);
+ FSTDocument *docModified = FSTTestDoc(@"a/3", 0, @{}, NO);
+
+ FSTDocument *docAddedThenModified = FSTTestDoc(@"b/1", 0, @{}, NO);
+ FSTDocument *docAddedThenRemoved = FSTTestDoc(@"b/2", 0, @{}, NO);
+ FSTDocument *docRemovedThenAdded = FSTTestDoc(@"b/3", 0, @{}, NO);
+ FSTDocument *docModifiedThenRemoved = FSTTestDoc(@"b/4", 0, @{}, NO);
+ FSTDocument *docModifiedThenModified = FSTTestDoc(@"b/5", 0, @{}, NO);
+
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAdded
+ type:FSTDocumentViewChangeTypeAdded]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docRemoved
+ type:FSTDocumentViewChangeTypeRemoved]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModified
+ type:FSTDocumentViewChangeTypeModified]];
+
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified
+ type:FSTDocumentViewChangeTypeAdded]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenModified
+ type:FSTDocumentViewChangeTypeModified]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved
+ type:FSTDocumentViewChangeTypeAdded]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docAddedThenRemoved
+ type:FSTDocumentViewChangeTypeRemoved]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded
+ type:FSTDocumentViewChangeTypeRemoved]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docRemovedThenAdded
+ type:FSTDocumentViewChangeTypeAdded]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved
+ type:FSTDocumentViewChangeTypeModified]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenRemoved
+ type:FSTDocumentViewChangeTypeRemoved]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified
+ type:FSTDocumentViewChangeTypeModified]];
+ [set addChange:[FSTDocumentViewChange changeWithDocument:docModifiedThenModified
+ type:FSTDocumentViewChangeTypeModified]];
+
+ NSArray<FSTDocumentViewChange *> *changes = [set changes];
+ XCTAssertEqual(changes.count, 7);
+
+ XCTAssertEqual(changes[0].document, docAdded);
+ XCTAssertEqual(changes[0].type, FSTDocumentViewChangeTypeAdded);
+
+ XCTAssertEqual(changes[1].document, docRemoved);
+ XCTAssertEqual(changes[1].type, FSTDocumentViewChangeTypeRemoved);
+
+ XCTAssertEqual(changes[2].document, docModified);
+ XCTAssertEqual(changes[2].type, FSTDocumentViewChangeTypeModified);
+
+ XCTAssertEqual(changes[3].document, docAddedThenModified);
+ XCTAssertEqual(changes[3].type, FSTDocumentViewChangeTypeAdded);
+
+ XCTAssertEqual(changes[4].document, docRemovedThenAdded);
+ XCTAssertEqual(changes[4].type, FSTDocumentViewChangeTypeModified);
+
+ XCTAssertEqual(changes[5].document, docModifiedThenRemoved);
+ XCTAssertEqual(changes[5].type, FSTDocumentViewChangeTypeRemoved);
+
+ XCTAssertEqual(changes[6].document, docModifiedThenModified);
+ XCTAssertEqual(changes[6].type, FSTDocumentViewChangeTypeModified);
+}
+
+- (void)testViewSnapshotConstructor {
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"a" ]]];
+ FSTDocumentSet *documents = [FSTDocumentSet documentSetWithComparator:FSTDocumentComparatorByKey];
+ FSTDocumentSet *oldDocuments = documents;
+ documents = [documents documentSetByAddingDocument:FSTTestDoc(@"c/a", 1, @{}, NO)];
+ NSArray<FSTDocumentViewChange *> *documentChanges =
+ @[ [FSTDocumentViewChange changeWithDocument:FSTTestDoc(@"c/a", 1, @{}, NO)
+ type:FSTDocumentViewChangeTypeAdded] ];
+
+ BOOL fromCache = YES;
+ BOOL hasPendingWrites = NO;
+ BOOL syncStateChanged = YES;
+
+ FSTViewSnapshot *snapshot = [[FSTViewSnapshot alloc] initWithQuery:query
+ documents:documents
+ oldDocuments:oldDocuments
+ documentChanges:documentChanges
+ fromCache:fromCache
+ hasPendingWrites:hasPendingWrites
+ syncStateChanged:syncStateChanged];
+
+ XCTAssertEqual(snapshot.query, query);
+ XCTAssertEqual(snapshot.documents, documents);
+ XCTAssertEqual(snapshot.oldDocuments, oldDocuments);
+ XCTAssertEqual(snapshot.documentChanges, documentChanges);
+ XCTAssertEqual(snapshot.fromCache, fromCache);
+ XCTAssertEqual(snapshot.hasPendingWrites, hasPendingWrites);
+ XCTAssertEqual(snapshot.syncStateChanged, syncStateChanged);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Core/FSTViewTests.m b/Firestore/Example/Tests/Core/FSTViewTests.m
new file mode 100644
index 0000000..d939d62
--- /dev/null
+++ b/Firestore/Example/Tests/Core/FSTViewTests.m
@@ -0,0 +1,618 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Core/FSTView.h"
+
+#import <XCTest/XCTest.h>
+
+#import "API/FIRFirestore+Internal.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTViewSnapshot.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTDocumentSet.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTRemoteEvent.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTViewTests : XCTestCase
+@end
+
+@implementation FSTViewTests
+
+/** Returns a new empty query to use for testing. */
+- (FSTQuery *)queryForMessages {
+ return [FSTQuery
+ queryWithPath:[FSTResourcePath pathWithSegments:@[ @"rooms", @"eros", @"messages" ]]];
+}
+
+- (void)testAddsDocumentsBasedOnQuery {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ 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]);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded],
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertFalse(snapshot.isFromCache);
+ XCTAssertFalse(snapshot.hasPendingWrites);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testRemovesDocuments {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO);
+
+ // initial state
+ FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+
+ // delete doc2, add doc3
+ FSTViewSnapshot *snapshot =
+ FSTTestApplyChanges(view, @[ FSTTestDeletedDoc(@"rooms/eros/messages/2", 0), doc3 ],
+ [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved],
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertFalse(snapshot.isFromCache);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testReturnsNilIfThereAreNoChanges {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+
+ // initial state
+ FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+
+ // reapply same docs, no changes
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+ XCTAssertNil(snapshot);
+}
+
+- (void)testDoesNotReturnNilForFirstChanges {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil);
+ XCTAssertNotNil(snapshot);
+}
+
+- (void)testFiltersDocumentsBasedOnQueryWithFilter {
+ FSTQuery *query = [self queryForMessages];
+ FSTRelationFilter *filter =
+ [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort")
+ filterOperator:FSTRelationFilterOperatorLessThanOrEqual
+ value:[FSTDoubleValue doubleValue:2]];
+ query = [query queryByAddingFilter:filter];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO); // no sort, no match
+ FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/5", 0, @{ @"sort" : @1 }, NO);
+
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4, doc5 ], nil);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc5, doc2 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc1 type:FSTDocumentViewChangeTypeAdded],
+ [FSTDocumentViewChange changeWithDocument:doc5 type:FSTDocumentViewChangeTypeAdded],
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertTrue(snapshot.isFromCache);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testUpdatesDocumentsBasedOnQueryWithFilter {
+ FSTQuery *query = [self queryForMessages];
+ FSTRelationFilter *filter =
+ [FSTRelationFilter filterWithField:FSTTestFieldPath(@"sort")
+ filterOperator:FSTRelationFilterOperatorLessThanOrEqual
+ value:[FSTDoubleValue doubleValue:2]];
+ query = [query queryByAddingFilter:filter];
+
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"sort" : @3 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"sort" : @2 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{}, NO);
+
+ FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ doc1, doc2, doc3, doc4 ], nil);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ]));
+
+ FSTDocument *newDoc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"sort" : @2 }, NO);
+ FSTDocument *newDoc3 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"sort" : @3 }, NO);
+ FSTDocument *newDoc4 = FSTTestDoc(@"rooms/eros/messages/4", 1, @{ @"sort" : @0 }, NO);
+
+ snapshot = FSTTestApplyChanges(view, @[ newDoc2, newDoc3, newDoc4 ], nil);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ newDoc4, doc1, newDoc2 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved],
+ [FSTDocumentViewChange changeWithDocument:newDoc4 type:FSTDocumentViewChangeTypeAdded],
+ [FSTDocumentViewChange changeWithDocument:newDoc2 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertTrue(snapshot.isFromCache);
+ XCTAssertFalse(snapshot.syncStateChanged);
+}
+
+- (void)testRemovesDocumentsForQueryWithLimit {
+ FSTQuery *query = [self queryForMessages];
+ query = [query queryBySettingLimit:2];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{@"text" : @"msg3"}, NO);
+
+ // initial state
+ FSTTestApplyChanges(view, @[ doc1, doc3 ], nil);
+
+ // add doc2, which should push out doc3
+ FSTViewSnapshot *snapshot =
+ FSTTestApplyChanges(view, @[ doc2 ],
+ [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]);
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc2 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeRemoved],
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertFalse(snapshot.isFromCache);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery {
+ FSTQuery *query = [self queryForMessages];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"num")
+ ascending:YES]];
+ query = [query queryBySettingLimit:2];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"num" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"num" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"num" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"num" : @4 }, NO);
+
+ // initial state
+ FSTTestApplyChanges(view, @[ doc1, doc2 ], nil);
+
+ // change doc2 to 5, and add doc3 and doc4.
+ // doc2 will be modified + removed = removed
+ // doc3 will be added
+ // doc4 will be added + removed = nothing
+ doc2 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{ @"num" : @5 }, NO);
+ FSTViewDocumentChanges *viewDocChanges =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2, doc3, doc4 ])];
+ XCTAssertTrue(viewDocChanges.needsRefill);
+ // Verify that all the docs still match.
+ viewDocChanges = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4 ])
+ previousChanges:viewDocChanges];
+ FSTViewSnapshot *snapshot =
+ [view applyChangesToDocuments:viewDocChanges
+ targetChange:[FSTTargetChange
+ changeWithDocuments:@[ doc1, doc2, doc3, doc4 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]
+ .snapshot;
+
+ XCTAssertEqual(snapshot.query, query);
+
+ XCTAssertEqualObjects(snapshot.documents.arrayValue, (@[ doc1, doc3 ]));
+
+ XCTAssertEqualObjects(
+ snapshot.documentChanges, (@[
+ [FSTDocumentViewChange changeWithDocument:doc2 type:FSTDocumentViewChangeTypeRemoved],
+ [FSTDocumentViewChange changeWithDocument:doc3 type:FSTDocumentViewChangeTypeAdded]
+ ]));
+
+ XCTAssertFalse(snapshot.isFromCache);
+ XCTAssertTrue(snapshot.syncStateChanged);
+}
+
+- (void)testKeepsTrackOfLimboDocuments {
+ FSTQuery *query = [self queryForMessages];
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO);
+
+ FSTViewChange *change = [view
+ applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]];
+ XCTAssertEqualObjects(change.limboChanges, @[]);
+
+ change =
+ [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
+ targetChange:[FSTTargetChange
+ changeWithDocuments:@[]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]];
+ XCTAssertEqualObjects(
+ change.limboChanges,
+ @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]);
+
+ change = [view
+ applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])]
+ targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateNone]];
+ XCTAssertEqualObjects(
+ change.limboChanges,
+ @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]);
+
+ change = [view
+ applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])]
+ targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ]
+ currentStatusUpdate:FSTCurrentStatusUpdateNone]];
+ XCTAssertEqualObjects(change.limboChanges, @[]);
+
+ change = [view
+ applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]];
+ XCTAssertEqualObjects(
+ change.limboChanges,
+ @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc3.key] ]);
+
+ change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[
+ FSTTestDeletedDoc(@"rooms/eros/messages/2",
+ 1)
+ ])]]; // remove
+ XCTAssertEqualObjects(
+ change.limboChanges,
+ @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc3.key] ]);
+}
+
+- (void)testResumingQueryCreatesNoLimbos {
+ FSTQuery *query = [self queryForMessages];
+
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+
+ // Unlike other cases, here the view is initialized with a set of previously synced documents
+ // which happens when listening to a previously listened-to query.
+ FSTView *view = [[FSTView alloc] initWithQuery:query
+ remoteDocuments:FSTTestDocKeySet(@[ doc1.key, doc2.key ])];
+
+ FSTTargetChange *markCurrent =
+ [FSTTargetChange changeWithDocuments:@[]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent];
+ FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])];
+ FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent];
+ XCTAssertEqualObjects(change.limboChanges, @[]);
+}
+
+- (void)assertDocSet:(FSTDocumentSet *)docSet containsDocs:(NSArray<FSTDocument *> *)docs {
+ XCTAssertEqual(docs.count, docSet.count);
+ for (FSTDocument *doc in docs) {
+ XCTAssertTrue([docSet containsKey:doc.key]);
+ }
+}
+
+- (void)testReturnsNeedsRefillOnDeleteInLimitQuery {
+ FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Remove one of the docs.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc(
+ @"rooms/eros/messages/0", 0) ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]];
+ XCTAssertTrue(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ // Refill it with just the one doc remaining.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ]) previousChanges:changes];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testReturnsNeedsRefillOnReorderInLimitQuery {
+ FSTQuery *query = [self queryForMessages];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order")
+ ascending:YES]];
+ query = [query queryBySettingLimit:2];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Move one of the docs.
+ doc2 = FSTTestDoc(@"rooms/eros/messages/1", 1, @{ @"order" : @2000 }, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertTrue(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ // Refill it with all three current docs.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3 ])
+ previousChanges:changes];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc3 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testDoesntNeedRefillOnReorderWithinLimit {
+ FSTQuery *query = [self queryForMessages];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order")
+ ascending:YES]];
+ query = [query queryBySettingLimit:3];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(3, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Move one of the docs.
+ doc1 = FSTTestDoc(@"rooms/eros/messages/0", 1, @{ @"order" : @3 }, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc2, doc3, doc1 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testDoesntNeedRefillOnReorderAfterLimitQuery {
+ FSTQuery *query = [self queryForMessages];
+ query =
+ [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"order")
+ ascending:YES]];
+ query = [query queryBySettingLimit:3];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO);
+ FSTDocument *doc4 = FSTTestDoc(@"rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO);
+ FSTDocument *doc5 = FSTTestDoc(@"rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2, doc3, doc4, doc5 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(3, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Move one of the docs.
+ doc4 = FSTTestDoc(@"rooms/eros/messages/3", 1, @{ @"order" : @6 }, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc4 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2, doc3 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(0, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testDoesntNeedRefillForAdditionAfterTheLimit {
+ FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Add a doc that is past the limit.
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 1, @{}, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(0, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testDoesntNeedRefillForDeletionsWhenNotNearTheLimit {
+ FSTQuery *query = [[self queryForMessages] queryBySettingLimit:20];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Remove one of the docs.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc(
+ @"rooms/eros/messages/1", 0) ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(1, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testHandlesApplyingIrrelevantDocs {
+ FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(2, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+
+ // Remove a doc that isn't even in the results.
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ FSTTestDeletedDoc(
+ @"rooms/eros/messages/2", 0) ])];
+ [self assertDocSet:changes.documentSet containsDocs:@[ doc1, doc2 ]];
+ XCTAssertFalse(changes.needsRefill);
+ XCTAssertEqual(0, [changes.changeSet changes].count);
+ [view applyChangesToDocuments:changes];
+}
+
+- (void)testComputesMutatedKeys {
+ FSTQuery *query = [self queryForMessages];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[]));
+
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, YES);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc3.key ]));
+}
+
+- (void)testRemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges {
+ FSTQuery *query = [self queryForMessages];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+
+ FSTDocument *doc2Prime = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2Prime ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[]));
+}
+
+- (void)testRemembersLocalMutationsFromPreviousSnapshot {
+ FSTQuery *query = [self queryForMessages];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])];
+ [view applyChangesToDocuments:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+}
+
+- (void)testRemembersLocalMutationsFromPreviousCallToComputeChangesWithDocuments {
+ FSTQuery *query = [self queryForMessages];
+ FSTDocument *doc1 = FSTTestDoc(@"rooms/eros/messages/0", 0, @{}, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"rooms/eros/messages/1", 0, @{}, YES);
+ FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]];
+
+ // Start with a full view.
+ FSTViewDocumentChanges *changes =
+ [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+
+ FSTDocument *doc3 = FSTTestDoc(@"rooms/eros/messages/2", 0, @{}, NO);
+ changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ]) previousChanges:changes];
+ XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ]));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Integration/API/FIRCursorTests.m b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m
new file mode 100644
index 0000000..2f8babd
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRCursorTests.m
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRCursorTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRCursorTests
+
+- (void)testCanPageThroughItems {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a"},
+ @"b" : @{@"v" : @"b"},
+ @"c" : @{@"v" : @"c"},
+ @"d" : @{@"v" : @"d"},
+ @"e" : @{@"v" : @"e"},
+ @"f" : @{@"v" : @"f"}
+ }];
+
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[testCollection queryLimitedTo:2]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"v" : @"a"}, @{@"v" : @"b"} ]));
+
+ FIRDocumentSnapshot *lastDoc = snapshot.documents.lastObject;
+ snapshot = [self
+ readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot),
+ (@[ @{@"v" : @"c"}, @{@"v" : @"d"}, @{@"v" : @"e"} ]));
+
+ lastDoc = snapshot.documents.lastObject;
+ snapshot = [self
+ readDocumentSetForRef:[[testCollection queryLimitedTo:1] queryStartingAfterDocument:lastDoc]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[ @{@"v" : @"f"} ]);
+
+ lastDoc = snapshot.documents.lastObject;
+ snapshot = [self
+ readDocumentSetForRef:[[testCollection queryLimitedTo:3] queryStartingAfterDocument:lastDoc]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), @[]);
+}
+
+- (void)testCanBeCreatedFromDocuments {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a", @"sort" : @1.0},
+ @"b" : @{@"v" : @"b", @"sort" : @2.0},
+ @"c" : @{@"v" : @"c", @"sort" : @2.0},
+ @"d" : @{@"v" : @"d", @"sort" : @2.0},
+ @"e" : @{@"v" : @"e", @"sort" : @0.0},
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up
+ }];
+
+ FIRQuery *query = [testCollection queryOrderedByField:@"sort"];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"c"]];
+
+ XCTAssertTrue(snapshot.exists);
+ FIRQuerySnapshot *querySnapshot =
+ [self readDocumentSetForRef:[query queryStartingAtDocument:snapshot]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"c",
+ @"sort" : @2.0 },
+ @{ @"v" : @"d",
+ @"sort" : @2.0 }
+ ]));
+
+ querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeDocument:snapshot]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"e",
+ @"sort" : @0.0 },
+ @{ @"v" : @"a",
+ @"sort" : @1.0 },
+ @{ @"v" : @"b",
+ @"sort" : @2.0 }
+ ]));
+}
+
+- (void)testCanBeCreatedFromValues {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a", @"sort" : @1.0},
+ @"b" : @{@"v" : @"b", @"sort" : @2.0},
+ @"c" : @{@"v" : @"c", @"sort" : @2.0},
+ @"d" : @{@"v" : @"d", @"sort" : @2.0},
+ @"e" : @{@"v" : @"e", @"sort" : @0.0},
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up
+ }];
+
+ FIRQuery *query = [testCollection queryOrderedByField:@"sort"];
+ FIRQuerySnapshot *querySnapshot =
+ [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"b",
+ @"sort" : @2.0 },
+ @{ @"v" : @"c",
+ @"sort" : @2.0 },
+ @{ @"v" : @"d",
+ @"sort" : @2.0 }
+ ]));
+
+ querySnapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot), (@[
+ @{ @"v" : @"e",
+ @"sort" : @0.0 },
+ @{ @"v" : @"a",
+ @"sort" : @1.0 }
+ ]));
+}
+
+- (void)testCanBeCreatedUsingDocumentId {
+ NSDictionary *testDocs = @{
+ @"a" : @{@"k" : @"a"},
+ @"b" : @{@"k" : @"b"},
+ @"c" : @{@"k" : @"c"},
+ @"d" : @{@"k" : @"d"},
+ @"e" : @{@"k" : @"e"}
+ };
+ FIRCollectionReference *writer = [[[[self firestore] collectionWithPath:@"parent-collection"]
+ documentWithAutoID] collectionWithPath:@"sub-collection"];
+ [self writeAllDocuments:testDocs toCollection:writer];
+
+ FIRCollectionReference *reader = [[self firestore] collectionWithPath:writer.path];
+ FIRQuerySnapshot *querySnapshot =
+ [self readDocumentSetForRef:[[[reader queryOrderedByFieldPath:[FIRFieldPath documentID]]
+ queryStartingAtValues:@[ @"b" ]]
+ queryEndingBeforeValues:@[ @"d" ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(querySnapshot),
+ (@[ @{@"k" : @"b"}, @{@"k" : @"c"} ]));
+}
+
+- (void)testCanBeUsedWithReferenceValues {
+ FIRFirestore *db = [self firestore];
+
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"k" : @"1a", @"ref" : [db documentWithPath:@"1/a"]},
+ @"b" : @{@"k" : @"1b", @"ref" : [db documentWithPath:@"1/b"]},
+ @"c" : @{@"k" : @"2a", @"ref" : [db documentWithPath:@"2/a"]},
+ @"d" : @{@"k" : @"2b", @"ref" : [db documentWithPath:@"2/b"]},
+ @"e" : @{@"k" : @"3a", @"ref" : [db documentWithPath:@"3/a"]},
+ }];
+ FIRQuery *query = [testCollection queryOrderedByField:@"ref"];
+ FIRQuerySnapshot *querySnapshot = [self
+ readDocumentSetForRef:[[query queryStartingAfterValues:@[ [db documentWithPath:@"1/a"] ]]
+ queryEndingAtValues:@[ [db documentWithPath:@"2/b"] ]]];
+ NSMutableArray<NSString *> *actual = [NSMutableArray array];
+ [querySnapshot.documents enumerateObjectsUsingBlock:^(FIRDocumentSnapshot *_Nonnull doc,
+ NSUInteger idx, BOOL *_Nonnull stop) {
+ [actual addObject:doc.data[@"k"]];
+ }];
+ XCTAssertEqualObjects(actual, (@[ @"1b", @"2a", @"2b" ]));
+}
+
+- (void)testCanBeUsedInDescendingQueries {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"a" : @{@"v" : @"a", @"sort" : @1.0},
+ @"b" : @{@"v" : @"b", @"sort" : @2.0},
+ @"c" : @{@"v" : @"c", @"sort" : @2.0},
+ @"d" : @{@"v" : @"d", @"sort" : @3.0},
+ @"e" : @{@"v" : @"e", @"sort" : @0.0},
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0} // should not show up
+ }];
+ FIRQuery *query = [[testCollection queryOrderedByField:@"sort" descending:YES]
+ queryOrderedByFieldPath:[FIRFieldPath documentID]
+ descending:YES];
+
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[query queryStartingAtValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
+ @{ @"v" : @"c",
+ @"sort" : @2.0 },
+ @{ @"v" : @"b",
+ @"sort" : @2.0 },
+ @{ @"v" : @"a",
+ @"sort" : @1.0 },
+ @{ @"v" : @"e",
+ @"sort" : @0.0 }
+ ]));
+
+ snapshot = [self readDocumentSetForRef:[query queryEndingBeforeValues:@[ @2.0 ]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{ @"v" : @"d", @"sort" : @3.0 } ]));
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m
new file mode 100644
index 0000000..d5558cc
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.m
@@ -0,0 +1,741 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRDatabaseTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRDatabaseTests
+
+- (void)testCanUpdateAnExistingDocument {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+ NSDictionary<NSString *, id> *initialData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} };
+ NSDictionary<NSString *, id> *updateData =
+ @{@"desc" : @"NewDescription", @"owner.email" : @"new@xyz.com"};
+ NSDictionary<NSString *, id> *finalData =
+ @{ @"desc" : @"NewDescription",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"new@xyz.com"} };
+
+ [self writeDocumentRef:doc data:initialData];
+
+ XCTestExpectation *updateCompletion = [self expectationWithDescription:@"updateData"];
+ [doc updateData:updateData
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [updateCompletion fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertTrue(result.exists);
+ XCTAssertEqualObjects(result.data, finalData);
+}
+
+- (void)testCanDeleteAFieldWithAnUpdate {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+ NSDictionary<NSString *, id> *initialData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} };
+ NSDictionary<NSString *, id> *updateData =
+ @{@"owner.email" : [FIRFieldValue fieldValueForDelete]};
+ NSDictionary<NSString *, id> *finalData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny"} };
+
+ [self writeDocumentRef:doc data:initialData];
+ [self updateDocumentRef:doc data:updateData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertTrue(result.exists);
+ XCTAssertEqualObjects(result.data, finalData);
+}
+
+- (void)testDeleteDocument {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+ NSDictionary<NSString *, id> *data = @{@"value" : @"foo"};
+ [self writeDocumentRef:doc data:data];
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, data);
+ [self deleteDocumentRef:doc];
+ result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testCannotUpdateNonexistentDocument {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *setCompletion = [self expectationWithDescription:@"setData"];
+ [doc updateData:@{@"owner" : @"abc"}
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
+ [setCompletion fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testCanOverwriteDataAnExistingDocumentUsingSet {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData =
+ @{ @"desc" : @"Description",
+ @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} };
+ NSDictionary<NSString *, id> *udpateData = @{@"desc" : @"NewDescription"};
+
+ [self writeDocumentRef:doc data:initialData];
+ [self writeDocumentRef:doc data:udpateData];
+
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(document.data, udpateData);
+}
+
+- (void)testCanMergeDataWithAnExistingDocumentUsingSet {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{
+ @"desc" : @"Description",
+ @"owner.data" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"}
+ };
+ NSDictionary<NSString *, id> *updateData =
+ @{ @"updated" : @YES,
+ @"owner.data" : @{@"name" : @"Sebastian"} };
+ NSDictionary<NSString *, id> *finalData = @{
+ @"desc" : @"Description",
+ @"updated" : @YES,
+ @"owner.data" : @{@"name" : @"Sebastian", @"email" : @"abc@xyz.com"}
+ };
+
+ [self writeDocumentRef:doc data:initialData];
+
+ XCTestExpectation *completed =
+ [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"];
+
+ [doc setData:updateData
+ options:[FIRSetOptions merge]
+ completion:^(NSError *error) {
+ XCTAssertNil(error);
+ [completed fulfill];
+ }];
+
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(document.data, finalData);
+}
+
+- (void)testMergeReplacesArrays {
+ FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{
+ @"untouched" : @YES,
+ @"data" : @"old",
+ @"topLevel" : @[ @"old", @"old" ],
+ @"mapInArray" : @[ @{@"data" : @"old"} ]
+ };
+ NSDictionary<NSString *, id> *updateData =
+ @{ @"data" : @"new",
+ @"topLevel" : @[ @"new" ],
+ @"mapInArray" : @[ @{@"data" : @"new"} ] };
+ NSDictionary<NSString *, id> *finalData = @{
+ @"untouched" : @YES,
+ @"data" : @"new",
+ @"topLevel" : @[ @"new" ],
+ @"mapInArray" : @[ @{@"data" : @"new"} ]
+ };
+
+ [self writeDocumentRef:doc data:initialData];
+
+ XCTestExpectation *completed =
+ [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"];
+
+ [doc setData:updateData
+ options:[FIRSetOptions merge]
+ completion:^(NSError *error) {
+ XCTAssertNil(error);
+ [completed fulfill];
+ }];
+
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(document.data, finalData);
+}
+
+- (void)testAddingToACollectionYieldsTheCorrectDocumentReference {
+ FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
+ FIRDocumentReference *ref = [coll addDocumentWithData:@{ @"foo" : @1 }];
+
+ XCTestExpectation *getCompletion = [self expectationWithDescription:@"getData"];
+ [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *_Nullable document,
+ NSError *_Nullable error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(document.data, (@{ @"foo" : @1 }));
+
+ [getCompletion fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testListenCanBeCalledMultipleTimes {
+ FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
+ FIRDocumentReference *doc = [coll documentWithAutoID];
+
+ XCTestExpectation *completed = [self expectationWithDescription:@"multiple addSnapshotListeners"];
+
+ __block NSDictionary<NSString *, id> *resultingData;
+
+ // Shut the compiler up about strong references to doc.
+ FIRDocumentReference *__weak weakDoc = doc;
+
+ [doc setData:@{@"foo" : @"bar"}
+ completion:^(NSError *error1) {
+ XCTAssertNil(error1);
+ FIRDocumentReference *strongDoc = weakDoc;
+
+ [strongDoc addSnapshotListener:^(FIRDocumentSnapshot *snapshot2, NSError *error2) {
+ XCTAssertNil(error2);
+
+ FIRDocumentReference *strongDoc2 = weakDoc;
+ [strongDoc2 addSnapshotListener:^(FIRDocumentSnapshot *snapshot3, NSError *error3) {
+ XCTAssertNil(error3);
+ resultingData = snapshot3.data;
+ [completed fulfill];
+ }];
+ }];
+ }];
+
+ [self awaitExpectations];
+ XCTAssertEqualObjects(resultingData, @{@"foo" : @"bar"});
+}
+
+- (void)testDocumentSnapshotEvents_nonExistent {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *snapshotCompletion = [self expectationWithDescription:@"snapshot"];
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertNotNil(doc);
+ XCTAssertFalse(doc.exists);
+ [snapshotCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forAdd {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"];
+ __block XCTestExpectation *dataCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertNotNil(doc);
+ XCTAssertFalse(doc.exists);
+ [emptyCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 }));
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+ [dataCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ dataCompletion = [self expectationWithDescription:@"data snapshot"];
+
+ [docRef setData:@{ @"a" : @1 }];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forAddIncludingMetadata {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"];
+ __block XCTestExpectation *dataCompletion;
+ __block int callbacks = 0;
+
+ FIRDocumentListenOptions *options =
+ [[FIRDocumentListenOptions options] includeMetadataChanges:YES];
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListenerWithOptions:options
+ listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertNotNil(doc);
+ XCTAssertFalse(doc.exists);
+ [emptyCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 }));
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+
+ } else if (callbacks == 3) {
+ XCTAssertEqualObjects(doc.data, (@{ @"a" : @1 }));
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ [dataCompletion fulfill];
+
+ } else if (callbacks == 4) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ dataCompletion = [self expectationWithDescription:@"data snapshot"];
+
+ [docRef setData:@{ @"a" : @1 }];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forChange {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+ NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, changedData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef setData:changedData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forChangeIncludingMetadata {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+ NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ FIRDocumentListenOptions *options =
+ [[FIRDocumentListenOptions options] includeMetadataChanges:YES];
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListenerWithOptions:options
+ listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, YES);
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTAssertEqualObjects(doc.data, changedData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, YES);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+
+ } else if (callbacks == 4) {
+ XCTAssertEqualObjects(doc.data, changedData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 5) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef setData:changedData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forDelete {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, YES);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertFalse(doc.exists);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef deleteDocument];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testDocumentSnapshotEvents_forDeleteIncludingMetadata {
+ FIRDocumentReference *docRef = [[self.db collectionWithPath:@"rooms"] documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ FIRDocumentListenOptions *options =
+ [[FIRDocumentListenOptions options] includeMetadataChanges:YES];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [docRef addSnapshotListenerWithOptions:options
+ listener:^(FIRDocumentSnapshot *_Nullable doc, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, YES);
+
+ } else if (callbacks == 2) {
+ XCTAssertEqualObjects(doc.data, initialData);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTAssertFalse(doc.exists);
+ XCTAssertEqual(doc.metadata.hasPendingWrites, NO);
+ XCTAssertEqual(doc.metadata.isFromCache, NO);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 4) {
+ XCTFail("Should not have received this callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef deleteDocument];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testQuerySnapshotEvents_forAdd {
+ FIRCollectionReference *roomsRef = [self collectionRef];
+ FIRDocumentReference *docRef = [roomsRef documentWithAutoID];
+
+ NSDictionary<NSString *, id> *newData = @{ @"a" : @1 };
+
+ XCTestExpectation *emptyCompletion = [self expectationWithDescription:@"empty snapshot"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqual(docSet.count, 0);
+ [emptyCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, newData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received a third callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"changed snapshot"];
+
+ [docRef setData:newData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testQuerySnapshotEvents_forChange {
+ FIRCollectionReference *roomsRef = [self collectionRef];
+ FIRDocumentReference *docRef = [roomsRef documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+ NSDictionary<NSString *, id> *changedData = @{ @"b" : @2 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, initialData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, changedData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, YES);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 3) {
+ XCTFail("Should not have received a third callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef setData:changedData];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testQuerySnapshotEvents_forDelete {
+ FIRCollectionReference *roomsRef = [self collectionRef];
+ FIRDocumentReference *docRef = [roomsRef documentWithAutoID];
+
+ NSDictionary<NSString *, id> *initialData = @{ @"a" : @1 };
+
+ [self writeDocumentRef:docRef data:initialData];
+
+ XCTestExpectation *initialCompletion = [self expectationWithDescription:@"initial data"];
+ __block XCTestExpectation *changeCompletion;
+ __block int callbacks = 0;
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [roomsRef addSnapshotListener:^(FIRQuerySnapshot *_Nullable docSet, NSError *error) {
+ callbacks++;
+
+ if (callbacks == 1) {
+ XCTAssertEqual(docSet.count, 1);
+ XCTAssertEqualObjects(docSet.documents[0].data, initialData);
+ XCTAssertEqual(docSet.documents[0].metadata.hasPendingWrites, NO);
+ [initialCompletion fulfill];
+
+ } else if (callbacks == 2) {
+ XCTAssertEqual(docSet.count, 0);
+ [changeCompletion fulfill];
+
+ } else if (callbacks == 4) {
+ XCTFail("Should not have received a third callback");
+ }
+ }];
+
+ [self awaitExpectations];
+ changeCompletion = [self expectationWithDescription:@"listen for changed data"];
+
+ [docRef deleteDocument];
+ [self awaitExpectations];
+
+ [listenerRegistration remove];
+}
+
+- (void)testExposesFirestoreOnDocumentReferences {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"foo/bar"];
+ XCTAssertEqual(doc.firestore, self.db);
+}
+
+- (void)testExposesFirestoreOnQueries {
+ FIRQuery *q = [[self.db collectionWithPath:@"foo"] queryLimitedTo:5];
+ XCTAssertEqual(q.firestore, self.db);
+}
+
+- (void)testCanTraverseCollectionsAndDocuments {
+ NSString *expected = @"a/b/c/d";
+ // doc path from root Firestore.
+ XCTAssertEqualObjects([self.db documentWithPath:@"a/b/c/d"].path, expected);
+ // collection path from root Firestore.
+ XCTAssertEqualObjects([[self.db collectionWithPath:@"a/b/c"] documentWithPath:@"d"].path,
+ expected);
+ // doc path from CollectionReference.
+ XCTAssertEqualObjects([[self.db collectionWithPath:@"a"] documentWithPath:@"b/c/d"].path,
+ expected);
+ // collection path from DocumentReference.
+ XCTAssertEqualObjects([[self.db documentWithPath:@"a/b"] collectionWithPath:@"c/d/e"].path,
+ @"a/b/c/d/e");
+}
+
+- (void)testCanTraverseCollectionAndDocumentParents {
+ FIRCollectionReference *collection = [self.db collectionWithPath:@"a/b/c"];
+ XCTAssertEqualObjects(collection.path, @"a/b/c");
+
+ FIRDocumentReference *doc = collection.parent;
+ XCTAssertEqualObjects(doc.path, @"a/b");
+
+ collection = doc.parent;
+ XCTAssertEqualObjects(collection.path, @"a");
+
+ FIRDocumentReference *nilDoc = collection.parent;
+ XCTAssertNil(nilDoc);
+}
+
+- (void)testUpdateFieldsWithDots {
+ FIRDocumentReference *doc = [self documentRef];
+
+ [self writeDocumentRef:doc data:@{@"a.b" : @"old", @"c.d" : @"old"}];
+
+ [self updateDocumentRef:doc data:@{ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new" }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"];
+
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"}));
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testUpdateNestedFields {
+ FIRDocumentReference *doc = [self documentRef];
+
+ [self writeDocumentRef:doc
+ data:@{
+ @"a" : @{@"b" : @"old"},
+ @"c" : @{@"d" : @"old"},
+ @"e" : @{@"f" : @"old"}
+ }];
+
+ [self updateDocumentRef:doc
+ data:@{
+ @"a.b" : @"new",
+ [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"
+ }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"];
+
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{
+ @"a" : @{@"b" : @"new"},
+ @"c" : @{@"d" : @"new"},
+ @"e" : @{@"f" : @"old"}
+ }));
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testCollectionID {
+ XCTAssertEqualObjects([self.db collectionWithPath:@"foo"].collectionID, @"foo");
+ XCTAssertEqualObjects([self.db collectionWithPath:@"foo/bar/baz"].collectionID, @"baz");
+}
+
+- (void)testDocumentID {
+ XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar"].documentID, @"bar");
+ XCTAssertEqualObjects([self.db documentWithPath:@"foo/bar/baz/qux"].documentID, @"qux");
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m
new file mode 100644
index 0000000..d851556
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRFieldsTests.m
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTFirestoreClient.h"
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRFieldsTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRFieldsTests
+
+- (NSDictionary<NSString *, id> *)testNestedDataNumbered:(int)number {
+ return @{
+ @"name" : [NSString stringWithFormat:@"room %d", number],
+ @"metadata" : @{
+ @"createdAt" : @(number),
+ @"deep" : @{@"field" : [NSString stringWithFormat:@"deep-field-%d", number]}
+ }
+ };
+}
+
+- (void)testNestedFieldsCanBeWrittenWithSet {
+ NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, testData);
+}
+
+- (void)testNestedFieldsCanBeReadDirectly {
+ NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result[@"name"], testData[@"name"]);
+ XCTAssertEqualObjects(result[@"metadata"], testData[@"metadata"]);
+ XCTAssertEqualObjects(result[@"metadata.deep.field"], testData[@"metadata"][@"deep"][@"field"]);
+ XCTAssertNil(result[@"metadata.nofield"]);
+ XCTAssertNil(result[@"nometadata.nofield"]);
+}
+
+- (void)testNestedFieldsCanBeUpdated {
+ NSDictionary<NSString *, id> *testData = [self testNestedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+ [self updateDocumentRef:doc data:@{ @"metadata.deep.field" : @100, @"metadata.added" : @200 }];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(
+ result.data, (@{
+ @"name" : @"room 1",
+ @"metadata" : @{@"createdAt" : @1, @"deep" : @{@"field" : @100}, @"added" : @200}
+ }));
+}
+
+- (void)testNestedFieldsCanBeUsedInQueryFilters {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testNestedDataNumbered:300],
+ @"2" : [self testNestedDataNumbered:100],
+ @"3" : [self testNestedDataNumbered:200]
+ };
+
+ // inequality adds implicit sort on field
+ NSArray<NSDictionary<NSString *, id> *> *expected =
+ @[ [self testNestedDataNumbered:200], [self testNestedDataNumbered:300] ];
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ FIRQuery *q = [coll queryWhereField:@"metadata.createdAt" isGreaterThanOrEqualTo:@200];
+ FIRQuerySnapshot *results = [self readDocumentSetForRef:q];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (expected));
+}
+
+- (void)testNestedFieldsCanBeUsedInOrderBy {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testNestedDataNumbered:300],
+ @"2" : [self testNestedDataNumbered:100],
+ @"3" : [self testNestedDataNumbered:200]
+ };
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"];
+ FIRQuery *q = [coll queryOrderedByField:@"metadata.createdAt"];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[
+ [self testNestedDataNumbered:100], [self testNestedDataNumbered:200],
+ [self testNestedDataNumbered:300]
+ ]));
+ [queryCompletion fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+/**
+ * Creates test data with special characters in field names. Datastore currently prohibits mixing
+ * nested data with special characters so tests that use this data must be separate.
+ */
+- (NSDictionary<NSString *, id> *)testDottedDataNumbered:(int)number {
+ return @{
+ @"a" : [NSString stringWithFormat:@"field %d", number],
+ @"b.dot" : @(number),
+ @"c\\slash" : @(number)
+ };
+}
+
+- (void)testFieldsWithSpecialCharsCanBeWrittenWithSet {
+ NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, testData);
+}
+
+- (void)testFieldsWithSpecialCharsCanBeReadDirectly {
+ NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result[@"a"], testData[@"a"]);
+ XCTAssertEqualObjects(result[[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]],
+ testData[@"b.dot"]);
+ XCTAssertEqualObjects(result[@"c\\slash"], testData[@"c\\slash"]);
+}
+
+- (void)testFieldsWithSpecialCharsCanBeUpdated {
+ NSDictionary<NSString *, id> *testData = [self testDottedDataNumbered:1];
+
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:testData];
+ [self updateDocumentRef:doc
+ data:@{
+ [[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]] : @100,
+ @"c\\slash" : @200
+ }];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(result.data, (@{ @"a" : @"field 1", @"b.dot" : @100, @"c\\slash" : @200 }));
+}
+
+- (void)testFieldsWithSpecialCharsCanBeUsedInQueryFilters {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testDottedDataNumbered:300],
+ @"2" : [self testDottedDataNumbered:100],
+ @"3" : [self testDottedDataNumbered:200]
+ };
+
+ // inequality adds implicit sort on field
+ NSArray<NSDictionary<NSString *, id> *> *expected =
+ @[ [self testDottedDataNumbered:200], [self testDottedDataNumbered:300] ];
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ XCTestExpectation *queryCompletion = [self expectationWithDescription:@"query"];
+ FIRQuery *q = [coll queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]
+ isGreaterThanOrEqualTo:@200];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected);
+ [queryCompletion fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testFieldsWithSpecialCharsCanBeUsedInOrderBy {
+ NSDictionary<NSString *, NSDictionary<NSString *, id> *> *testDocs = @{
+ @"1" : [self testDottedDataNumbered:300],
+ @"2" : [self testDottedDataNumbered:100],
+ @"3" : [self testDottedDataNumbered:200]
+ };
+
+ NSArray<NSDictionary<NSString *, id> *> *expected = @[
+ [self testDottedDataNumbered:100], [self testDottedDataNumbered:200],
+ [self testDottedDataNumbered:300]
+ ];
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ FIRQuery *q = [coll queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"b.dot" ]]];
+ XCTestExpectation *queryDot = [self expectationWithDescription:@"query dot"];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected);
+ [queryDot fulfill];
+ }];
+ [self awaitExpectations];
+
+ XCTestExpectation *querySlash = [self expectationWithDescription:@"query slash"];
+ q = [coll queryOrderedByField:@"c\\slash"];
+ [q getDocumentsWithCompletion:^(FIRQuerySnapshot *results, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), expected);
+ [querySlash fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m
new file mode 100644
index 0000000..19771ff
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRListenerRegistrationTests.m
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRListenerRegistrationTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRListenerRegistrationTests
+
+- (void)testCanBeRemoved {
+ FIRCollectionReference *collectionRef = [self collectionRef];
+ FIRDocumentReference *docRef = [collectionRef documentWithAutoID];
+
+ __block int callbacks = 0;
+ id<FIRListenerRegistration> one = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacks++;
+ }];
+
+ id<FIRListenerRegistration> two = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacks++;
+ }];
+
+ // Wait for initial events
+ [self waitUntil:^BOOL {
+ return callbacks == 2;
+ }];
+
+ // Trigger new events
+ [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}];
+
+ // Write events should have triggered
+ XCTAssertEqual(4, callbacks);
+
+ // No more events should occur
+ [one remove];
+ [two remove];
+
+ [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}];
+
+ // Assert no further events occurred
+ XCTAssertEqual(4, callbacks);
+}
+
+- (void)testCanBeRemovedTwice {
+ FIRCollectionReference *collectionRef = [self collectionRef];
+ FIRDocumentReference *docRef = [collectionRef documentWithAutoID];
+
+ id<FIRListenerRegistration> one = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error){
+ }];
+ id<FIRListenerRegistration> two = [docRef
+ addSnapshotListener:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error){
+ }];
+
+ [one remove];
+ [one remove];
+
+ [two remove];
+ [two remove];
+}
+
+- (void)testCanBeRemovedIndependently {
+ FIRCollectionReference *collectionRef = [self collectionRef];
+ FIRDocumentReference *docRef = [collectionRef documentWithAutoID];
+
+ __block int callbacksOne = 0;
+ __block int callbacksTwo = 0;
+ id<FIRListenerRegistration> one = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacksOne++;
+ }];
+
+ id<FIRListenerRegistration> two = [collectionRef
+ addSnapshotListener:^(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ callbacksTwo++;
+ }];
+
+ // Wait for initial events
+ [self waitUntil:^BOOL {
+ return callbacksOne == 1 && callbacksTwo == 1;
+ }];
+
+ // Trigger new events
+ [self writeDocumentRef:docRef data:@{@"foo" : @"bar"}];
+
+ // Write events should have triggered
+ XCTAssertEqual(2, callbacksOne);
+ XCTAssertEqual(2, callbacksTwo);
+
+ // Should leave "two" unaffected
+ [one remove];
+
+ [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}];
+
+ // Assert only events for "two" actually occurred
+ XCTAssertEqual(2, callbacksOne);
+ XCTAssertEqual(3, callbacksTwo);
+
+ [self writeDocumentRef:docRef data:@{@"foo" : @"new-bar"}];
+
+ // No more events should occur
+ [two remove];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRQueryTests.m b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m
new file mode 100644
index 0000000..f08df33
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRQueryTests.m
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTFirestoreClient.h"
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRQueryTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRQueryTests
+
+- (void)testLimitQueries {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"k" : @"a"},
+ @"b" : @{@"k" : @"b"},
+ @"c" : @{@"k" : @"c"}
+
+ }];
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:[collRef queryLimitedTo:2]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[ @{@"k" : @"a"}, @{@"k" : @"b"} ]));
+}
+
+- (void)testLimitQueriesWithDescendingSortOrder {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"k" : @"a", @"sort" : @0},
+ @"b" : @{@"k" : @"b", @"sort" : @1},
+ @"c" : @{@"k" : @"c", @"sort" : @1},
+ @"d" : @{@"k" : @"d", @"sort" : @2},
+
+ }];
+ FIRQuerySnapshot *snapshot =
+ [self readDocumentSetForRef:[[collRef queryOrderedByField:@"sort" descending:YES]
+ queryLimitedTo:2]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(snapshot), (@[
+ @{ @"k" : @"d",
+ @"sort" : @2 },
+ @{ @"k" : @"c",
+ @"sort" : @1 }
+ ]));
+}
+
+- (void)testKeyOrderIsDescendingForDescendingInequality {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"foo" : @42},
+ @"b" : @{@"foo" : @42.0},
+ @"c" : @{@"foo" : @42},
+ @"d" : @{@"foo" : @21},
+ @"e" : @{@"foo" : @21.0},
+ @"f" : @{@"foo" : @66},
+ @"g" : @{@"foo" : @66.0},
+ }];
+ FIRQuerySnapshot *snapshot =
+ [self readDocumentSetForRef:[[collRef queryWhereField:@"foo" isGreaterThan:@21]
+ queryOrderedByField:@"foo"
+ descending:YES]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"g", @"f", @"c", @"b", @"a" ]));
+}
+
+- (void)testUnaryFilterQueries {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"null" : [NSNull null], @"nan" : @(NAN)},
+ @"b" : @{@"null" : [NSNull null], @"nan" : @0},
+ @"c" : @{@"null" : @NO, @"nan" : @(NAN)}
+ }];
+
+ FIRQuerySnapshot *results =
+ [self readDocumentSetForRef:[[collRef queryWhereField:@"null" isEqualTo:[NSNull null]]
+ queryWhereField:@"nan"
+ isEqualTo:@(NAN)]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[
+ @{ @"null" : [NSNull null],
+ @"nan" : @(NAN) }
+ ]));
+}
+
+- (void)testQueryWithFieldPaths {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"a" : @1},
+ @"b" : @{@"a" : @2},
+ @"c" : @{@"a" : @3}
+ }];
+
+ FIRQuery *query =
+ [collRef queryWhereFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]] isLessThan:@3];
+ query = [query queryOrderedByFieldPath:[[FIRFieldPath alloc] initWithFields:@[ @"a" ]]
+ descending:YES];
+
+ FIRQuerySnapshot *snapshot = [self readDocumentSetForRef:query];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(snapshot), (@[ @"b", @"a" ]));
+}
+
+- (void)testFilterOnInfinity {
+ FIRCollectionReference *collRef = [self collectionRefWithDocuments:@{
+ @"a" : @{@"inf" : @(INFINITY)},
+ @"b" : @{@"inf" : @(-INFINITY)}
+ }];
+
+ FIRQuerySnapshot *results =
+ [self readDocumentSetForRef:[collRef queryWhereField:@"inf" isEqualTo:@(INFINITY)]];
+
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(results), (@[ @{ @"inf" : @(INFINITY) } ]));
+}
+
+- (void)testCanExplicitlySortByDocumentID {
+ NSDictionary *testDocs = @{
+ @"a" : @{@"key" : @"a"},
+ @"b" : @{@"key" : @"b"},
+ @"c" : @{@"key" : @"c"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+
+ // Ideally this would be descending to validate it's different than
+ // the default, but that requires an extra index
+ FIRQuerySnapshot *docs =
+ [self readDocumentSetForRef:[collection queryOrderedByFieldPath:[FIRFieldPath documentID]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs),
+ (@[ testDocs[@"a"], testDocs[@"b"], testDocs[@"c"] ]));
+}
+
+- (void)testCanQueryByDocumentID {
+ NSDictionary *testDocs = @{
+ @"aa" : @{@"key" : @"aa"},
+ @"ab" : @{@"key" : @"ab"},
+ @"ba" : @{@"key" : @"ba"},
+ @"bb" : @{@"key" : @"bb"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *docs =
+ [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isEqualTo:@"ab"]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ]));
+}
+
+- (void)testCanQueryByDocumentIDs {
+ NSDictionary *testDocs = @{
+ @"aa" : @{@"key" : @"aa"},
+ @"ab" : @{@"key" : @"ab"},
+ @"ba" : @{@"key" : @"ba"},
+ @"bb" : @{@"key" : @"bb"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *docs =
+ [self readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isEqualTo:@"ab"]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ]));
+
+ docs = [self readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isGreaterThan:@"aa"]
+ queryWhereFieldPath:[FIRFieldPath documentID]
+ isLessThanOrEqualTo:@"ba"]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ]));
+}
+
+- (void)testCanQueryByDocumentIDsUsingRefs {
+ NSDictionary *testDocs = @{
+ @"aa" : @{@"key" : @"aa"},
+ @"ab" : @{@"key" : @"ab"},
+ @"ba" : @{@"key" : @"ba"},
+ @"bb" : @{@"key" : @"bb"},
+ };
+ FIRCollectionReference *collection = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *docs = [self
+ readDocumentSetForRef:[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isEqualTo:[collection documentWithPath:@"ab"]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"] ]));
+
+ docs = [self
+ readDocumentSetForRef:[[collection queryWhereFieldPath:[FIRFieldPath documentID]
+ isGreaterThan:[collection documentWithPath:@"aa"]]
+ queryWhereFieldPath:[FIRFieldPath documentID]
+ isLessThanOrEqualTo:[collection documentWithPath:@"ba"]]];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(docs), (@[ testDocs[@"ab"], testDocs[@"ba"] ]));
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m
new file mode 100644
index 0000000..1d77e16
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.m
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTFirestoreClient.h"
+
+#import "FSTEventAccumulator.h"
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRServerTimestampTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRServerTimestampTests {
+ // Data written in tests via set.
+ NSDictionary *_setData;
+
+ // Base and update data used for update tests.
+ NSDictionary *_initialData;
+ NSDictionary *_updateData;
+
+ // A document reference to read and write to.
+ FIRDocumentReference *_docRef;
+
+ // Accumulator used to capture events during the test.
+ FSTEventAccumulator *_accumulator;
+
+ // Listener registration for a listener maintained during the course of the test.
+ id<FIRListenerRegistration> _listenerRegistration;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ // Data written in tests via set.
+ _setData = @{
+ @"a" : @42,
+ @"when" : [FIRFieldValue fieldValueForServerTimestamp],
+ @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]}
+ };
+
+ // Base and update data used for update tests.
+ _initialData = @{ @"a" : @42 };
+ _updateData = @{
+ @"when" : [FIRFieldValue fieldValueForServerTimestamp],
+ @"deep" : @{@"when" : [FIRFieldValue fieldValueForServerTimestamp]}
+ };
+
+ _docRef = [self documentRef];
+ _accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ _listenerRegistration = [_docRef addSnapshotListener:_accumulator.handler];
+
+ // Wait for initial nil snapshot to avoid potential races.
+ FIRDocumentSnapshot *initialSnapshot = [_accumulator awaitEventWithName:@"initial event"];
+ XCTAssertFalse(initialSnapshot.exists);
+}
+
+- (void)tearDown {
+ [_listenerRegistration remove];
+
+ [super tearDown];
+}
+
+// Returns the expected data, with an arbitrary timestamp substituted in.
+- (NSDictionary *)expectedDataWithTimestamp:(id _Nullable)timestamp {
+ return @{ @"a" : @42, @"when" : timestamp, @"deep" : @{@"when" : timestamp} };
+}
+
+/** Writes _initialData and waits for the corresponding snapshot. */
+- (void)writeInitialData {
+ [self writeDocumentRef:_docRef data:_initialData];
+ FIRDocumentSnapshot *initialDataSnap = [_accumulator awaitEventWithName:@"Initial data event."];
+ XCTAssertEqualObjects(initialDataSnap.data, _initialData);
+}
+
+/** Waits for a snapshot containing _setData but with NSNull for the timestamps. */
+- (void)waitForLocalEvent {
+ FIRDocumentSnapshot *localSnap = [_accumulator awaitEventWithName:@"Local event."];
+ XCTAssertEqualObjects(localSnap.data, [self expectedDataWithTimestamp:[NSNull null]]);
+}
+
+/** Waits for a snapshot containing _setData but with resolved server timestamps. */
+- (void)waitForRemoteEvent {
+ // server event should have a resolved timestamp; verify it.
+ FIRDocumentSnapshot *remoteSnap = [_accumulator awaitEventWithName:@"Remote event"];
+ XCTAssertTrue(remoteSnap.exists);
+ NSDate *when = remoteSnap[@"when"];
+ XCTAssertTrue([when isKindOfClass:[NSDate class]]);
+ // Tolerate up to 10 seconds of clock skew between client and server.
+ XCTAssertEqualWithAccuracy(when.timeIntervalSinceNow, 0, 10);
+
+ // Validate the rest of the document.
+ XCTAssertEqualObjects(remoteSnap.data, [self expectedDataWithTimestamp:when]);
+}
+
+- (void)runTransactionBlock:(void (^)(FIRTransaction *transaction))transactionBlock {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"];
+ [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ transactionBlock(transaction);
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testServerTimestampsWorkViaSet {
+ [self writeDocumentRef:_docRef data:_setData];
+ [self waitForLocalEvent];
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsWorkViaUpdate {
+ [self writeInitialData];
+ [self updateDocumentRef:_docRef data:_updateData];
+ [self waitForLocalEvent];
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsWorkViaTransactionSet {
+ [self runTransactionBlock:^(FIRTransaction *transaction) {
+ [transaction setData:_setData forDocument:_docRef];
+ }];
+
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsWorkViaTransactionUpdate {
+ [self writeInitialData];
+ [self runTransactionBlock:^(FIRTransaction *transaction) {
+ [transaction updateData:_updateData forDocument:_docRef];
+ }];
+ [self waitForRemoteEvent];
+}
+
+- (void)testServerTimestampsFailViaUpdateOnNonexistentDocument {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"update complete"];
+ [_docRef updateData:_updateData
+ completion:^(NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testServerTimestampsFailViaTransactionUpdateOnNonexistentDocument {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction complete"];
+ [_docRef.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ [transaction updateData:_updateData forDocument:_docRef];
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ // TODO(b/35201829): This should be NotFound, but right now we retry transactions on any
+ // error and so this turns into Aborted instead.
+ // TODO(mikelehen): Actually it's FailedPrecondition, unlike Android. What do we want???
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRTypeTests.m b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m
new file mode 100644
index 0000000..f3021dd
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRTypeTests.m
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRTypeTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRTypeTests
+
+- (void)assertSuccessfulRoundtrip:(NSDictionary *)data {
+ FIRDocumentReference *doc = [self.db documentWithPath:@"rooms/eros"];
+
+ [self writeDocumentRef:doc data:data];
+ FIRDocumentSnapshot *document = [self readDocumentForRef:doc];
+ XCTAssertTrue(document.exists);
+ XCTAssertEqualObjects(document.data, data);
+}
+
+- (void)testCanReadAndWriteNullFields {
+ [self assertSuccessfulRoundtrip:@{ @"a" : @1, @"b" : [NSNull null] }];
+}
+
+- (void)testCanReadAndWriteArrayFields {
+ [self assertSuccessfulRoundtrip:@{
+ @"array" : @[ @1, @"foo", @{@"deep" : @YES}, [NSNull null] ]
+ }];
+}
+
+- (void)testCanReadAndWriteBlobFields {
+ NSData *data = [NSData dataWithBytes:"\0\1\2" length:3];
+ [self assertSuccessfulRoundtrip:@{@"blob" : data}];
+}
+
+- (void)testCanReadAndWriteGeoPointFields {
+ [self assertSuccessfulRoundtrip:@{
+ @"geoPoint" : [[FIRGeoPoint alloc] initWithLatitude:1.23 longitude:4.56]
+ }];
+}
+
+- (void)testCanReadAndWriteTimestampFields {
+ // Choose a value that can be converted losslessly between fixed point and double
+ NSDate *timestamp = [NSDate dateWithTimeIntervalSince1970:1491847082.125];
+ [self assertSuccessfulRoundtrip:@{@"timestamp" : timestamp}];
+}
+
+- (void)testCanReadAndWriteDocumentReferences {
+ // We can't use assertSuccessfulRoundtrip since FIRDocumentReference doesn't implement isEqual.
+ FIRDocumentReference *docRef = [self.db documentWithPath:@"rooms/eros"];
+ id data = @{ @"a" : @42, @"ref" : docRef };
+ [self writeDocumentRef:docRef data:data];
+
+ FIRDocumentSnapshot *readDoc = [self readDocumentForRef:docRef];
+ XCTAssertTrue(readDoc.exists);
+
+ XCTAssertEqualObjects(readDoc[@"a"], data[@"a"]);
+ FIRDocumentReference *readDocRef = readDoc[@"ref"];
+ XCTAssertTrue([readDocRef isKindOfClass:[FIRDocumentReference class]]);
+ XCTAssertEqualObjects(readDocRef.path, docRef.path);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.m b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m
new file mode 100644
index 0000000..1ba1d7a
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRValidationTests.m
@@ -0,0 +1,560 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTHelpers.h"
+#import "FSTIntegrationTestCase.h"
+
+// We have tests for passing nil when nil is not supposed to be allowed. So suppress the warnings.
+#pragma clang diagnostic ignored "-Wnonnull"
+
+@interface FIRValidationTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRValidationTests
+
+#pragma mark - FIRFirestoreSettings Validation
+
+- (void)testNilHostFails {
+ FIRFirestoreSettings *settings = self.db.settings;
+ FSTAssertThrows(settings.host = nil,
+ @"host setting may not be nil. You should generally just use the default value "
+ "(which is firestore.googleapis.com)");
+}
+
+- (void)testNilDispatchQueueFails {
+ FIRFirestoreSettings *settings = self.db.settings;
+ FSTAssertThrows(settings.dispatchQueue = nil,
+ @"dispatch queue setting may not be nil. Create a new dispatch queue with "
+ "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default "
+ "(which is the main queue, returned from dispatch_get_main_queue())");
+}
+
+- (void)testChangingSettingsAfterUseFails {
+ FIRFirestoreSettings *settings = self.db.settings;
+ [[self.db documentWithPath:@"foo/bar"] setData:@{ @"a" : @42 }];
+ settings.host = @"example.com";
+ FSTAssertThrows(self.db.settings = settings,
+ @"Firestore instance has already been started and its settings can no longer be "
+ @"changed. You can only set settings before calling any other methods on "
+ @"a Firestore instance.");
+}
+
+#pragma mark - FIRFirestore Validation
+
+- (void)testNilFIRAppFails {
+ FSTAssertThrows(
+ [FIRFirestore firestoreForApp:nil],
+ @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd like to use the "
+ "default FirebaseApp instance.");
+}
+
+// TODO(b/62410906): Test for firestoreForApp:database: with nil DatabaseID.
+
+- (void)testNilTransactionBlocksFail {
+ FSTAssertThrows([self.db runTransactionWithBlock:nil
+ completion:^(id result, NSError *error) {
+ XCTFail(@"Completion shouldn't run.");
+ }],
+ @"Transaction block cannot be nil.");
+
+ FSTAssertThrows(
+ [self.db runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ XCTFail(@"Transaction block shouldn't run.");
+ return nil;
+ }
+ completion:nil],
+ @"Transaction completion block cannot be nil.");
+}
+
+#pragma mark - Collection and Document Path Validation
+
+- (void)testNilCollectionPathsFail {
+ FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"];
+ NSString *nilError = @"Collection path cannot be nil.";
+ FSTAssertThrows([self.db collectionWithPath:nil], nilError);
+ FSTAssertThrows([baseDocRef collectionWithPath:nil], nilError);
+}
+
+- (void)testWrongLengthCollectionPathsFail {
+ FIRDocumentReference *baseDocRef = [self.db documentWithPath:@"foo/bar"];
+ NSArray *badAbsolutePaths = @[ @"foo/bar", @"foo/bar/baz/quu" ];
+ NSArray *badRelativePaths = @[ @"", @"baz/quu" ];
+ NSArray *badPathLengths = @[ @2, @4 ];
+ NSString *errorFormat =
+ @"Invalid collection reference. Collection references must have an odd "
+ @"number of segments, but %@ has %@";
+ for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) {
+ NSString *error =
+ [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]];
+ FSTAssertThrows([self.db collectionWithPath:badAbsolutePaths[i]], error);
+ FSTAssertThrows([baseDocRef collectionWithPath:badRelativePaths[i]], error);
+ }
+}
+
+- (void)testNilDocumentPathsFail {
+ FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"];
+ NSString *nilError = @"Document path cannot be nil.";
+ FSTAssertThrows([self.db documentWithPath:nil], nilError);
+ FSTAssertThrows([baseCollectionRef documentWithPath:nil], nilError);
+}
+
+- (void)testWrongLengthDocumentPathsFail {
+ FIRCollectionReference *baseCollectionRef = [self.db collectionWithPath:@"foo"];
+ NSArray *badAbsolutePaths = @[ @"foo", @"foo/bar/baz" ];
+ NSArray *badRelativePaths = @[ @"", @"bar/baz" ];
+ NSArray *badPathLengths = @[ @1, @3 ];
+ NSString *errorFormat =
+ @"Invalid document reference. Document references must have an even "
+ @"number of segments, but %@ has %@";
+ for (NSUInteger i = 0; i < badAbsolutePaths.count; i++) {
+ NSString *error =
+ [NSString stringWithFormat:errorFormat, badAbsolutePaths[i], badPathLengths[i]];
+ FSTAssertThrows([self.db documentWithPath:badAbsolutePaths[i]], error);
+ FSTAssertThrows([baseCollectionRef documentWithPath:badRelativePaths[i]], error);
+ }
+}
+
+- (void)testPathsWithEmptySegmentsFail {
+ // We're only testing using collectionWithPath since the validation happens in FSTPath which is
+ // shared by all methods that accept paths.
+
+ // leading / trailing slashes are okay.
+ [self.db collectionWithPath:@"/foo/"];
+ [self.db collectionWithPath:@"/foo"];
+ [self.db collectionWithPath:@"foo/"];
+
+ FSTAssertThrows([self.db collectionWithPath:@"foo//bar/baz"],
+ @"Invalid path (foo//bar/baz). Paths must not contain // in them.");
+ FSTAssertThrows([self.db collectionWithPath:@"//foo"],
+ @"Invalid path (//foo). Paths must not contain // in them.");
+ FSTAssertThrows([self.db collectionWithPath:@"foo//"],
+ @"Invalid path (foo//). Paths must not contain // in them.");
+}
+
+#pragma mark - Write Validation
+
+- (void)testWritesWithNonDictionaryValuesFail {
+ NSArray *badData = @[
+ @42, @"test", @[ @1 ], [NSDate date], [NSNull null], [FIRFieldValue fieldValueForDelete],
+ [FIRFieldValue fieldValueForServerTimestamp]
+ ];
+
+ for (id data in badData) {
+ [self expectWrite:data toFailWithReason:@"Data to be written must be an NSDictionary."];
+ }
+}
+
+- (void)testWritesWithNestedArraysFail {
+ [self expectWrite:@{
+ @"nested-array" : @[ @1, @[ @2 ] ]
+ }
+ toFailWithReason:@"Nested arrays are not supported"];
+}
+
+- (void)testWritesWithInvalidTypesFail {
+ [self expectWrite:@{
+ @"foo" : @{@"bar" : self}
+ }
+ toFailWithReason:@"Unsupported type: FIRValidationTests (found in field foo.bar)"];
+}
+
+- (void)testWritesWithLargeNumbersFail {
+ NSNumber *num = @((unsigned long long)LONG_MAX + 1);
+ NSString *reason =
+ [NSString stringWithFormat:@"NSNumber (%@) is too large (found in field num)", num];
+ [self expectWrite:@{@"num" : num} toFailWithReason:reason];
+}
+
+- (void)testWritesWithReferencesToADifferentDatabaseFail {
+ FIRDocumentReference *ref =
+ [[self firestoreWithProjectID:@"different-db"] documentWithPath:@"baz/quu"];
+ id data = @{@"foo" : ref};
+ [self expectWrite:data
+ toFailWithReason:
+ [NSString
+ stringWithFormat:@"Document Reference is for database different-db/(default) but "
+ "should be for database %@/(default) (found in field foo)",
+ [FSTIntegrationTestCase projectID]]];
+}
+
+- (void)testWritesWithReservedFieldsFail {
+ [self expectWrite:@{
+ @"__baz__" : @1
+ }
+ toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"];
+ [self expectWrite:@{
+ @"foo" : @{@"__baz__" : @1}
+ }
+ toFailWithReason:
+ @"Document fields cannot begin and end with __ (found in field foo.__baz__)"];
+ [self expectWrite:@{
+ @"__baz__" : @{@"foo" : @1}
+ }
+ toFailWithReason:@"Document fields cannot begin and end with __ (found in field __baz__)"];
+
+ [self expectUpdate:@{
+ @"foo.__baz__" : @1
+ }
+ toFailWithReason:
+ @"Document fields cannot begin and end with __ (found in field foo.__baz__)"];
+ [self expectUpdate:@{
+ @"__baz__.foo" : @1
+ }
+ toFailWithReason:
+ @"Document fields cannot begin and end with __ (found in field __baz__.foo)"];
+ [self expectUpdate:@{
+ @1 : @1
+ }
+ toFailWithReason:@"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."];
+}
+
+- (void)testSetsWithFieldValueDeleteFail {
+ [self expectSet:@{@"foo" : [FIRFieldValue fieldValueForDelete]}
+ toFailWithReason:@"FieldValue.delete() can only be used with updateData()."];
+}
+
+- (void)testUpdatesWithNestedFieldValueDeleteFail {
+ [self expectUpdate:@{
+ @"foo" : @{@"bar" : [FIRFieldValue fieldValueForDelete]}
+ }
+ toFailWithReason:
+ @"FieldValue.delete() can only appear at the top level of your update data "
+ "(found in field foo.bar)"];
+}
+
+- (void)testBatchWritesWithIncorrectReferencesFail {
+ FIRFirestore *db1 = [self firestore];
+ FIRFirestore *db2 = [self firestore];
+ XCTAssertNotEqual(db1, db2);
+
+ NSString *reason = @"Provided document reference is from a different Firestore instance.";
+ id data = @{ @"foo" : @1 };
+ FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"];
+ FIRWriteBatch *batch = [db1 batch];
+ FSTAssertThrows([batch setData:data forDocument:badRef], reason);
+ FSTAssertThrows([batch setData:data forDocument:badRef options:[FIRSetOptions merge]], reason);
+ FSTAssertThrows([batch updateData:data forDocument:badRef], reason);
+ FSTAssertThrows([batch deleteDocument:badRef], reason);
+}
+
+- (void)testTransactionWritesWithIncorrectReferencesFail {
+ FIRFirestore *db1 = [self firestore];
+ FIRFirestore *db2 = [self firestore];
+ XCTAssertNotEqual(db1, db2);
+
+ NSString *reason = @"Provided document reference is from a different Firestore instance.";
+ id data = @{ @"foo" : @1 };
+ FIRDocumentReference *badRef = [db2 documentWithPath:@"foo/bar"];
+
+ XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"];
+ [db1 runTransactionWithBlock:^id(FIRTransaction *txn, NSError **pError) {
+ FSTAssertThrows([txn getDocument:badRef error:nil], reason);
+ FSTAssertThrows([txn setData:data forDocument:badRef], reason);
+ FSTAssertThrows([txn setData:data forDocument:badRef options:[FIRSetOptions merge]], reason);
+ FSTAssertThrows([txn updateData:data forDocument:badRef], reason);
+ FSTAssertThrows([txn deleteDocument:badRef], reason);
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ // ends up being a no-op transaction.
+ XCTAssertNil(error);
+ [transactionDone fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+#pragma mark - Field Path validation
+// TODO(b/37244157): More validation for invalid field paths.
+
+- (void)testFieldPathsWithEmptySegmentsFail {
+ NSArray *badFieldPaths = @[ @"", @"foo..baz", @".foo", @"foo." ];
+
+ for (NSString *fieldPath in badFieldPaths) {
+ NSString *reason =
+ [NSString stringWithFormat:
+ @"Invalid field path (%@). Paths must not be empty, begin with "
+ @"'.', end with '.', or contain '..'",
+ fieldPath];
+ [self expectFieldPath:fieldPath toFailWithReason:reason];
+ }
+}
+
+- (void)testFieldPathsWithInvalidSegmentsFail {
+ NSArray *badFieldPaths = @[ @"foo~bar", @"foo*bar", @"foo/bar", @"foo[1", @"foo]1", @"foo[1]" ];
+
+ for (NSString *fieldPath in badFieldPaths) {
+ NSString *reason =
+ [NSString stringWithFormat:
+ @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'",
+ fieldPath];
+ [self expectFieldPath:fieldPath toFailWithReason:reason];
+ }
+}
+
+#pragma mark - Query Validation
+
+- (void)testQueryWithNonPositiveLimitFails {
+ FSTAssertThrows([[self collectionRef] queryLimitedTo:0],
+ @"Invalid Query. Query limit (0) is invalid. Limit must be positive.");
+ FSTAssertThrows([[self collectionRef] queryLimitedTo:-1],
+ @"Invalid Query. Query limit (-1) is invalid. Limit must be positive.");
+}
+
+- (void)testQueryInequalityOnNullOrNaNFails {
+ FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:nil],
+ @"Invalid Query. You can only perform equality comparisons on nil / NSNull.");
+ FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:[NSNull null]],
+ @"Invalid Query. You can only perform equality comparisons on nil / NSNull.");
+
+ FSTAssertThrows([[self collectionRef] queryWhereField:@"a" isGreaterThan:@(NAN)],
+ @"Invalid Query. You can only perform equality comparisons on NaN.");
+}
+
+- (void)testQueryCannotBeCreatedFromDocumentsMissingSortValues {
+ FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{
+ @"f" : @{@"v" : @"f", @"nosort" : @1.0}
+ }];
+
+ FIRQuery *query = [testCollection queryOrderedByField:@"sort"];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:[testCollection documentWithPath:@"f"]];
+ XCTAssertTrue(snapshot.exists);
+
+ NSString *reason =
+ @"Invalid query. You are trying to start or end a query using a document for "
+ "which the field 'sort' (used as the order by) does not exist.";
+ FSTAssertThrows([query queryStartingAtDocument:snapshot], reason);
+ FSTAssertThrows([query queryStartingAfterDocument:snapshot], reason);
+ FSTAssertThrows([query queryEndingBeforeDocument:snapshot], reason);
+ FSTAssertThrows([query queryEndingAtDocument:snapshot], reason);
+}
+
+- (void)testQueryBoundMustNotHaveMoreComponentsThanSortOrders {
+ FIRCollectionReference *testCollection = [self collectionRef];
+ FIRQuery *query = [testCollection queryOrderedByField:@"foo"];
+
+ NSString *reason =
+ @"Invalid query. You are trying to start or end a query using more values "
+ "than were specified in the order by.";
+ // More elements than order by
+ FSTAssertThrows(([query queryStartingAtValues:@[ @1, @2 ]]), reason);
+ FSTAssertThrows(([[query queryOrderedByField:@"bar"] queryStartingAtValues:@[ @1, @2, @3 ]]),
+ reason);
+}
+
+- (void)testQueryOrderedByKeyBoundMustBeAStringWithoutSlashes {
+ FIRCollectionReference *testCollection = [self collectionRef];
+ FIRQuery *query = [testCollection queryOrderedByFieldPath:[FIRFieldPath documentID]];
+ FSTAssertThrows([query queryStartingAtValues:@[ @1 ]],
+ @"Invalid query. Expected a string for the document ID.");
+ FSTAssertThrows([query queryStartingAtValues:@[ @"foo/bar" ]],
+ @"Invalid query. Document ID 'foo/bar' contains a slash.");
+}
+
+- (void)testQueryMustNotSpecifyStartingOrEndingPointAfterOrder {
+ FIRCollectionReference *testCollection = [self collectionRef];
+ FIRQuery *query = [testCollection queryOrderedByField:@"foo"];
+ NSString *reason =
+ @"Invalid query. You must not specify a starting point before specifying the order by.";
+ FSTAssertThrows([[query queryStartingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+ FSTAssertThrows([[query queryStartingAfterValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+ reason = @"Invalid query. You must not specify an ending point before specifying the order by.";
+ FSTAssertThrows([[query queryEndingAtValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+ FSTAssertThrows([[query queryEndingBeforeValues:@[ @1 ]] queryOrderedByField:@"bar"], reason);
+}
+
+- (void)testQueriesFilteredByDocumentIDMustUseStringsOrDocumentReferences {
+ FIRCollectionReference *collection = [self collectionRef];
+ NSString *reason =
+ @"Invalid query. When querying by document ID you must provide a valid "
+ "document ID, but it was an empty string.";
+ FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@""], reason);
+
+ reason =
+ @"Invalid query. When querying by document ID you must provide a valid document ID, "
+ "but 'foo/bar/baz' contains a '/' character.";
+ FSTAssertThrows(
+ [collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@"foo/bar/baz"], reason);
+
+ reason =
+ @"Invalid query. When querying by document ID you must provide a valid string or "
+ "DocumentReference, but it was of type: __NSCFNumber";
+ FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason);
+}
+
+- (void)testQueryInequalityFieldMustMatchFirstOrderByField {
+ FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"];
+ FIRQuery *base = [coll queryWhereField:@"x" isGreaterThanOrEqualTo:@32];
+
+ FSTAssertThrows([base queryWhereField:@"y" isLessThan:@"cat"],
+ @"Invalid Query. All where filters with an inequality (lessThan, "
+ "lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same "
+ "field. But you have inequality filters on 'x' and 'y'");
+
+ NSString *reason =
+ @"Invalid query. You have a where filter with "
+ "an inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) "
+ "on field 'x' and so you must also use 'x' as your first queryOrderedBy field, "
+ "but your first queryOrderedBy is currently on field 'y' instead.";
+ FSTAssertThrows([base queryOrderedByField:@"y"], reason);
+ FSTAssertThrows([[coll queryOrderedByField:@"y"] queryWhereField:@"x" isGreaterThan:@32], reason);
+ FSTAssertThrows([[base queryOrderedByField:@"y"] queryOrderedByField:@"x"], reason);
+ FSTAssertThrows([[[coll queryOrderedByField:@"y"] queryOrderedByField:@"x"] queryWhereField:@"x"
+ isGreaterThan:@32],
+ reason);
+
+ XCTAssertNoThrow([base queryWhereField:@"x" isLessThanOrEqualTo:@"cat"],
+ @"Same inequality fields work");
+
+ XCTAssertNoThrow([base queryWhereField:@"y" isEqualTo:@"cat"],
+ @"Inequality and equality on different fields works");
+
+ XCTAssertNoThrow([base queryOrderedByField:@"x"], @"inequality same as order by works");
+ XCTAssertNoThrow([[coll queryOrderedByField:@"x"] queryWhereField:@"x" isGreaterThan:@32],
+ @"inequality same as order by works");
+ XCTAssertNoThrow([[base queryOrderedByField:@"x"] queryOrderedByField:@"y"],
+ @"inequality same as first order by works.");
+ XCTAssertNoThrow([[[coll queryOrderedByField:@"x"] queryOrderedByField:@"y"] queryWhereField:@"x"
+ isGreaterThan:@32],
+ @"inequality same as first order by works.");
+}
+
+#pragma mark - GeoPoint Validation
+
+- (void)testInvalidGeoPointParameters {
+ [self verifyExceptionForInvalidLatitude:NAN];
+ [self verifyExceptionForInvalidLatitude:-INFINITY];
+ [self verifyExceptionForInvalidLatitude:INFINITY];
+ [self verifyExceptionForInvalidLatitude:-90.1];
+ [self verifyExceptionForInvalidLatitude:90.1];
+
+ [self verifyExceptionForInvalidLongitude:NAN];
+ [self verifyExceptionForInvalidLongitude:-INFINITY];
+ [self verifyExceptionForInvalidLongitude:INFINITY];
+ [self verifyExceptionForInvalidLongitude:-180.1];
+ [self verifyExceptionForInvalidLongitude:180.1];
+}
+
+#pragma mark - Helpers
+
+/** Performs a write using each write API and makes sure it fails with the expected reason. */
+- (void)expectWrite:(id)data toFailWithReason:(NSString *)reason {
+ [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:YES];
+}
+
+/** Performs a write using each set API and makes sure it fails with the expected reason. */
+- (void)expectSet:(id)data toFailWithReason:(NSString *)reason {
+ [self expectWrite:data toFailWithReason:reason includeSets:YES includeUpdates:NO];
+}
+
+/** Performs a write using each update API and makes sure it fails with the expected reason. */
+- (void)expectUpdate:(id)data toFailWithReason:(NSString *)reason {
+ [self expectWrite:data toFailWithReason:reason includeSets:NO includeUpdates:YES];
+}
+
+/**
+ * Performs a write using each set and/or update API and makes sure it fails with the expected
+ * reason.
+ */
+- (void)expectWrite:(id)data
+ toFailWithReason:(NSString *)reason
+ includeSets:(BOOL)includeSets
+ includeUpdates:(BOOL)includeUpdates {
+ FIRDocumentReference *ref = [self documentRef];
+ if (includeSets) {
+ FSTAssertThrows([ref setData:data], reason, @"for %@", data);
+ FSTAssertThrows([[ref.firestore batch] setData:data forDocument:ref], reason, @"for %@", data);
+ }
+
+ if (includeUpdates) {
+ FSTAssertThrows([ref updateData:data], reason, @"for %@", data);
+ FSTAssertThrows([[ref.firestore batch] updateData:data forDocument:ref], reason, @"for %@",
+ data);
+ }
+
+ XCTestExpectation *transactionDone = [self expectationWithDescription:@"transaction done"];
+ [ref.firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **pError) {
+ if (includeSets) {
+ FSTAssertThrows([transaction setData:data forDocument:ref], reason, @"for %@", data);
+ }
+ if (includeUpdates) {
+ FSTAssertThrows([transaction updateData:data forDocument:ref], reason, @"for %@", data);
+ }
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ // ends up being a no-op transaction.
+ XCTAssertNil(error);
+ [transactionDone fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testFieldNamesMustNotBeEmpty {
+ NSString *reason = @"Invalid field path. Provided names must not be empty.";
+ FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[]], reason);
+
+ reason = @"Invalid field name at index 0. Field names must not be empty.";
+ FSTAssertThrows([[FIRFieldPath alloc] initWithFields:@[ @"" ]], reason);
+
+ reason = @"Invalid field name at index 1. Field names must not be empty.";
+ FSTAssertThrows(([[FIRFieldPath alloc] initWithFields:@[ @"foo", @"" ]]), reason);
+}
+
+/**
+ * Tests a field path with all of our APIs that accept field paths and ensures they fail with the
+ * specified reason.
+ */
+- (void)expectFieldPath:(NSString *)fieldPath toFailWithReason:(NSString *)reason {
+ // Get an arbitrary snapshot we can use for testing.
+ FIRDocumentReference *docRef = [self documentRef];
+ [self writeDocumentRef:docRef data:@{ @"test" : @1 }];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:docRef];
+
+ // Update paths.
+ NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+ dict[fieldPath] = @1;
+ [self expectUpdate:dict toFailWithReason:reason];
+
+ // Snapshot fields.
+ FSTAssertThrows(snapshot[fieldPath], reason);
+
+ // Query filter / order fields.
+ FIRCollectionReference *collection = [self collectionRef];
+ FSTAssertThrows([collection queryWhereField:fieldPath isEqualTo:@1], reason);
+ // isLessThan, etc. omitted for brevity since the code path is trivially shared.
+ FSTAssertThrows([collection queryOrderedByField:fieldPath], reason);
+}
+
+- (void)verifyExceptionForInvalidLatitude:(double)latitude {
+ NSString *reason = [NSString
+ stringWithFormat:@"GeoPoint requires a latitude value in the range of [-90, 90], but was %f",
+ latitude];
+ FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:latitude longitude:0], reason);
+}
+
+- (void)verifyExceptionForInvalidLongitude:(double)longitude {
+ NSString *reason =
+ [NSString stringWithFormat:
+ @"GeoPoint requires a longitude value in the range of [-180, 180], but was %f",
+ longitude];
+ FSTAssertThrows([[FIRGeoPoint alloc] initWithLatitude:0 longitude:longitude], reason);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m
new file mode 100644
index 0000000..159cbd7
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.m
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTEventAccumulator.h"
+#import "FSTIntegrationTestCase.h"
+
+@interface FIRWriteBatchTests : FSTIntegrationTestCase
+@end
+
+@implementation FIRWriteBatchTests
+
+- (void)testSupportEmptyBatches {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ [[[self firestore] batch] commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testSetDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{@"a" : @"b"} forDocument:doc];
+ [batch setData:@{@"c" : @"d"} forDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"});
+}
+
+- (void)testSetDocumentWithMerge {
+ FIRDocumentReference *doc = [self documentRef];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc];
+ [batch setData:@{
+ @"c" : @"d",
+ @"nested" : @{@"c" : @"d"}
+ }
+ forDocument:doc
+ options:[FIRSetOptions merge]];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(
+ snapshot.data, (
+ @{ @"a" : @"b",
+ @"c" : @"d",
+ @"nested" : @{@"a" : @"b", @"c" : @"d"} }));
+}
+
+- (void)testUpdateDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch updateData:@{ @"baz" : @42 } forDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(snapshot.data, (@{ @"foo" : @"bar", @"baz" : @42 }));
+}
+
+- (void)testCannotUpdateNonexistentDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch updateData:@{ @"baz" : @42 } forDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNotNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testDeleteDocuments {
+ FIRDocumentReference *doc = [self documentRef];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+
+ XCTAssertTrue(snapshot.exists);
+ XCTestExpectation *batchExpectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch deleteDocument:doc];
+ [batch commitWithCompletion:^(NSError *error) {
+ XCTAssertNil(error);
+ [batchExpectation fulfill];
+ }];
+ [self awaitExpectations];
+ snapshot = [self readDocumentForRef:doc];
+ XCTAssertFalse(snapshot.exists);
+}
+
+- (void)testBatchesCommitAtomicallyRaisingCorrectEvents {
+ FIRCollectionReference *collection = [self collectionRef];
+ FIRDocumentReference *docA = [collection documentWithPath:@"a"];
+ FIRDocumentReference *docB = [collection documentWithPath:@"b"];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options]
+ includeQueryMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertEqual(initialSnap.count, 0);
+
+ // Atomically write two documents.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [collection.firestore batch];
+ [batch setData:@{ @"a" : @1 } forDocument:docA];
+ [batch setData:@{ @"b" : @2 } forDocument:docB];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ]));
+
+ FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap), (@[ @{ @"a" : @1 }, @{ @"b" : @2 } ]));
+}
+
+- (void)testBatchesFailAtomicallyRaisingCorrectEvents {
+ FIRCollectionReference *collection = [self collectionRef];
+ FIRDocumentReference *docA = [collection documentWithPath:@"a"];
+ FIRDocumentReference *docB = [collection documentWithPath:@"b"];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options]
+ includeQueryMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertEqual(initialSnap.count, 0);
+
+ // Atomically write 1 document and update a nonexistent document.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch failed"];
+ FIRWriteBatch *batch = [collection.firestore batch];
+ [batch setData:@{ @"a" : @1 } forDocument:docA];
+ [batch updateData:@{ @"b" : @2 } forDocument:docB];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeNotFound);
+ [expectation fulfill];
+ }];
+
+ // Local event with the set document.
+ FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap), (@[ @{ @"a" : @1 } ]));
+
+ // Server event with the set reverted.
+ FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ XCTAssertEqual(serverSnap.count, 0);
+}
+
+- (void)testWriteTheSameServerTimestampAcrossWrites {
+ FIRCollectionReference *collection = [self collectionRef];
+ FIRDocumentReference *docA = [collection documentWithPath:@"a"];
+ FIRDocumentReference *docB = [collection documentWithPath:@"b"];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [collection addSnapshotListenerWithOptions:[[FIRQueryListenOptions options]
+ includeQueryMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRQuerySnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertEqual(initialSnap.count, 0);
+
+ // Atomically write 2 documents with server timestamps.
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [collection.firestore batch];
+ [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docA];
+ [batch setData:@{@"when" : [FIRFieldValue fieldValueForServerTimestamp]} forDocument:docB];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ FIRQuerySnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(localSnap),
+ (@[ @{@"when" : [NSNull null]}, @{@"when" : [NSNull null]} ]));
+
+ FIRQuerySnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ XCTAssertEqual(serverSnap.count, 2);
+ NSDate *when = serverSnap.documents[0][@"when"];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(serverSnap),
+ (@[ @{@"when" : when}, @{@"when" : when} ]));
+}
+
+- (void)testCanWriteTheSameDocumentMultipleTimes {
+ FIRDocumentReference *doc = [self documentRef];
+ FSTEventAccumulator *accumulator = [FSTEventAccumulator accumulatorForTest:self];
+ [doc
+ addSnapshotListenerWithOptions:[[FIRDocumentListenOptions options] includeMetadataChanges:YES]
+ listener:accumulator.handler];
+ FIRDocumentSnapshot *initialSnap = [accumulator awaitEventWithName:@"initial event"];
+ XCTAssertFalse(initialSnap.exists);
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"batch written"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch deleteDocument:doc];
+ [batch setData:@{ @"a" : @1, @"b" : @1, @"when" : @"when" } forDocument:doc];
+ [batch updateData:@{
+ @"b" : @2,
+ @"when" : [FIRFieldValue fieldValueForServerTimestamp]
+ }
+ forDocument:doc];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+
+ FIRDocumentSnapshot *localSnap = [accumulator awaitEventWithName:@"local event"];
+ XCTAssertTrue(localSnap.metadata.hasPendingWrites);
+ XCTAssertEqualObjects(localSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : [NSNull null] }));
+
+ FIRDocumentSnapshot *serverSnap = [accumulator awaitEventWithName:@"server event"];
+ XCTAssertFalse(serverSnap.metadata.hasPendingWrites);
+ NSDate *when = serverSnap[@"when"];
+ XCTAssertEqualObjects(serverSnap.data, (@{ @"a" : @1, @"b" : @2, @"when" : when }));
+}
+
+- (void)testUpdateFieldsWithDots {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateFieldsWithDots"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc];
+ [batch updateData:@{
+ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"
+ }
+ forDocument:doc];
+
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"}));
+ }];
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testUpdateNestedFields {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"testUpdateNestedFields"];
+ FIRWriteBatch *batch = [doc.firestore batch];
+ [batch setData:@{
+ @"a" : @{@"b" : @"old"},
+ @"c" : @{@"d" : @"old"},
+ @"e" : @{@"f" : @"old"}
+ }
+ forDocument:doc];
+ [batch updateData:@{
+ @"a.b" : @"new",
+ [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"
+ }
+ forDocument:doc];
+ [batch commitWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{
+ @"a" : @{@"b" : @"new"},
+ @"c" : @{@"d" : @"new"},
+ @"e" : @{@"f" : @"old"}
+ }));
+ }];
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/CAcert.pem b/Firestore/Example/Tests/Integration/CAcert.pem
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/CAcert.pem
diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.m b/Firestore/Example/Tests/Integration/FSTDatastoreTests.m
new file mode 100644
index 0000000..bab8f44
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.m
@@ -0,0 +1,239 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <GRPCClient/GRPCCall+ChannelCredentials.h>
+#import <GRPCClient/GRPCCall+Tests.h>
+#import <XCTest/XCTest.h>
+
+#import "API/FIRDocumentReference+Internal.h"
+#import "API/FSTUserDataConverter.h"
+#import "Auth/FSTEmptyCredentialsProvider.h"
+#import "Core/FSTDatabaseInfo.h"
+#import "Core/FSTFirestoreClient.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTTimestamp.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTDatastore.h"
+#import "Remote/FSTRemoteEvent.h"
+#import "Remote/FSTRemoteStore.h"
+#import "Util/FSTAssert.h"
+#import "Util/FSTDispatchQueue.h"
+#import "Util/FSTUtil.h"
+
+#import "FSTIntegrationTestCase.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTRemoteStore (Tests)
+- (void)commitBatch:(FSTMutationBatch *)batch;
+@end
+
+#pragma mark - FSTRemoteStoreEventCapture
+
+@interface FSTRemoteStoreEventCapture : NSObject <FSTRemoteSyncer>
+
+- (instancetype)init __attribute__((unavailable("Use initWithTestCase:")));
+
+- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase NS_DESIGNATED_INITIALIZER;
+
+- (void)expectWriteEventWithDescription:(NSString *)description;
+- (void)expectListenEventWithDescription:(NSString *)description;
+
+@property(nonatomic, weak, nullable) XCTestCase *testCase;
+@property(nonatomic, strong) NSMutableArray<NSObject *> *writeEvents;
+@property(nonatomic, strong) NSMutableArray<NSObject *> *listenEvents;
+@property(nonatomic, strong) NSMutableArray<XCTestExpectation *> *writeEventExpectations;
+@property(nonatomic, strong) NSMutableArray<XCTestExpectation *> *listenEventExpectations;
+@end
+
+@implementation FSTRemoteStoreEventCapture
+
+- (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase {
+ if (self = [super init]) {
+ _writeEvents = [NSMutableArray array];
+ _listenEvents = [NSMutableArray array];
+ _testCase = testCase;
+ _writeEventExpectations = [NSMutableArray array];
+ _listenEventExpectations = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (void)expectWriteEventWithDescription:(NSString *)description {
+ [self.writeEventExpectations
+ addObject:[self.testCase
+ expectationWithDescription:[NSString
+ stringWithFormat:@"write event %lu: %@",
+ (unsigned long)
+ self.writeEventExpectations
+ .count,
+ description]]];
+}
+
+- (void)expectListenEventWithDescription:(NSString *)description {
+ [self.listenEventExpectations
+ addObject:[self.testCase
+ expectationWithDescription:[NSString
+ stringWithFormat:@"listen event %lu: %@",
+ (unsigned long)
+ self.listenEventExpectations
+ .count,
+ description]]];
+}
+
+- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult {
+ [self.writeEvents addObject:batchResult];
+ XCTestExpectation *expectation = [self.writeEventExpectations objectAtIndex:0];
+ [self.writeEventExpectations removeObjectAtIndex:0];
+ [expectation fulfill];
+}
+
+- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error {
+ FSTFail(@"Not implemented");
+}
+
+- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
+ [self.listenEvents addObject:remoteEvent];
+ XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0];
+ [self.listenEventExpectations removeObjectAtIndex:0];
+ [expectation fulfill];
+}
+
+- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error {
+ FSTFail(@"Not implemented");
+}
+
+@end
+
+#pragma mark - FSTDatastoreTests
+
+@interface FSTDatastoreTests : XCTestCase
+
+@end
+
+@implementation FSTDatastoreTests {
+ FSTDispatchQueue *_testWorkerQueue;
+ FSTLocalStore *_localStore;
+ id<FSTCredentialsProvider> _credentials;
+
+ FSTDatastore *_datastore;
+ FSTRemoteStore *_remoteStore;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ NSString *projectID = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"];
+ if (!projectID) {
+ projectID = @"test-db";
+ }
+
+ FIRFirestoreSettings *settings = [FSTIntegrationTestCase settings];
+ if (!settings.sslEnabled) {
+ [GRPCCall useInsecureConnectionsForHost:settings.host];
+ }
+
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:projectID database:kDefaultDatabaseID];
+
+ FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"test-key"
+ host:settings.host
+ sslEnabled:settings.sslEnabled];
+
+ _testWorkerQueue = [FSTDispatchQueue
+ queueWith:dispatch_queue_create("com.google.firestore.FSTDatastoreTestsWorkerQueue",
+ DISPATCH_QUEUE_SERIAL)];
+
+ _credentials = [[FSTEmptyCredentialsProvider alloc] init];
+
+ _datastore = [FSTDatastore datastoreWithDatabase:databaseInfo
+ workerDispatchQueue:_testWorkerQueue
+ credentials:_credentials];
+
+ _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore];
+ [_remoteStore start];
+}
+
+- (void)tearDown {
+ XCTestExpectation *completion = [self expectationWithDescription:@"shutdown"];
+ [_testWorkerQueue dispatchAsync:^{
+ [_remoteStore shutdown];
+ [completion fulfill];
+ }];
+ [self awaitExpectations];
+
+ [super tearDown];
+}
+
+- (void)testCommit {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"commitWithCompletion"];
+
+ [_datastore commitMutations:@[]
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error, @"Failed to commit");
+ [expectation fulfill];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)testStreamingWrite {
+ FSTRemoteStoreEventCapture *capture = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:self];
+ [capture expectWriteEventWithDescription:@"write mutations"];
+
+ _remoteStore.syncEngine = capture;
+
+ FSTSetMutation *mutation = [self setMutation];
+ FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23
+ localWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ mutation ]];
+ [_testWorkerQueue dispatchAsync:^{
+ [_remoteStore commitBatch:batch];
+ }];
+
+ [self awaitExpectations];
+}
+
+- (void)awaitExpectations {
+ [self waitForExpectationsWithTimeout:4.0
+ handler:^(NSError *_Nullable expectationError) {
+ if (expectationError) {
+ XCTFail(@"Error waiting for timeout: %@", expectationError);
+ }
+ }];
+}
+
+- (FSTSetMutation *)setMutation {
+ return [[FSTSetMutation alloc]
+ initWithKey:[FSTDocumentKey keyWithPathString:@"rooms/eros"]
+ value:[[FSTObjectValue alloc]
+ initWithDictionary:@{@"name" : [FSTStringValue stringValue:@"Eros"]}]
+ precondition:[FSTPrecondition none]];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Integration/FSTSmokeTests.m b/Firestore/Example/Tests/Integration/FSTSmokeTests.m
new file mode 100644
index 0000000..e0ad06c
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/FSTSmokeTests.m
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+
+#import "FSTEventAccumulator.h"
+#import "FSTIntegrationTestCase.h"
+
+@interface FSTSmokeTests : FSTIntegrationTestCase
+@end
+
+@implementation FSTSmokeTests
+
+- (void)testCanWriteASingleDocument {
+ FIRDocumentReference *ref = [self documentRef];
+ [self writeDocumentRef:ref data:[self chatMessage]];
+}
+
+- (void)testCanReadAWrittenDocument {
+ NSDictionary<NSString *, id> *data = [self chatMessage];
+
+ FIRDocumentReference *ref = [self documentRef];
+ [self writeDocumentRef:ref data:data];
+
+ FIRDocumentSnapshot *doc = [self readDocumentForRef:ref];
+ XCTAssertEqualObjects(doc.data, data);
+}
+
+- (void)testObservesExistingDocument {
+ [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef) {
+ NSDictionary<NSString *, id> *data = [self chatMessage];
+ [self writeDocumentRef:writerRef data:data];
+
+ id<FIRListenerRegistration> listenerRegistration =
+ [readerRef addSnapshotListener:self.eventAccumulator.handler];
+
+ FIRDocumentSnapshot *doc = [self.eventAccumulator awaitEventWithName:@"snapshot"];
+ XCTAssertEqual([doc class], [FIRDocumentSnapshot class]);
+ XCTAssertEqualObjects(doc.data, data);
+
+ [listenerRegistration remove];
+ }];
+}
+
+- (void)testObservesNewDocument {
+ [self readerAndWriterOnDocumentRef:^(NSString *path, FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef) {
+ id<FIRListenerRegistration> listenerRegistration =
+ [readerRef addSnapshotListener:self.eventAccumulator.handler];
+
+ FIRDocumentSnapshot *doc1 = [self.eventAccumulator awaitEventWithName:@"null snapshot"];
+ XCTAssertFalse(doc1.exists);
+ // TODO(b/36366944): add tests for doc1.path)
+
+ NSDictionary<NSString *, id> *data = [self chatMessage];
+ [self writeDocumentRef:writerRef data:data];
+
+ FIRDocumentSnapshot *doc2 = [self.eventAccumulator awaitEventWithName:@"full snapshot"];
+ XCTAssertEqual([doc2 class], [FIRDocumentSnapshot class]);
+ XCTAssertEqualObjects(doc2.data, data);
+
+ [listenerRegistration remove];
+ }];
+}
+
+- (void)testWillFireValueEventsForEmptyCollections {
+ FIRCollectionReference *collection = [self.db collectionWithPath:@"empty-collection"];
+ id<FIRListenerRegistration> listenerRegistration =
+ [collection addSnapshotListener:self.eventAccumulator.handler];
+
+ FIRQuerySnapshot *snap = [self.eventAccumulator awaitEventWithName:@"empty query snapshot"];
+ XCTAssertEqual([snap class], [FIRQuerySnapshot class]);
+ XCTAssertEqual(snap.count, 0);
+
+ [listenerRegistration remove];
+}
+
+- (void)testGetCollectionQuery {
+ NSDictionary<NSString *, id> *testDocs = @{
+ @"1" : @{@"name" : @"Patryk", @"message" : @"Real data, yo!"},
+ @"2" : @{@"name" : @"Gil", @"message" : @"Yep!"},
+ @"3" : @{@"name" : @"Jonny", @"message" : @"Back to work!"},
+ };
+
+ FIRCollectionReference *docs = [self collectionRefWithDocuments:testDocs];
+ FIRQuerySnapshot *result = [self readDocumentSetForRef:docs];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(result),
+ (@[ testDocs[@"1"], testDocs[@"2"], testDocs[@"3"] ]));
+}
+
+// TODO(klimt): This test is disabled because we can't create compound indexes programmatically.
+- (void)xtestQueryByFieldAndUseOrderBy {
+ NSDictionary<NSString *, id> *testDocs = @{
+ @"1" : @{@"sort" : @1, @"filter" : @YES, @"key" : @"1"},
+ @"2" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"2"},
+ @"3" : @{@"sort" : @2, @"filter" : @YES, @"key" : @"3"},
+ @"4" : @{@"sort" : @3, @"filter" : @NO, @"key" : @"4"}
+ };
+
+ FIRCollectionReference *coll = [self collectionRefWithDocuments:testDocs];
+
+ FIRQuery *query =
+ [[coll queryWhereField:@"filter" isEqualTo:@YES] queryOrderedByField:@"sort" descending:YES];
+ FIRQuerySnapshot *result = [self readDocumentSetForRef:query];
+ XCTAssertEqualObjects(FIRQuerySnapshotGetData(result),
+ (@[ testDocs[@"2"], testDocs[@"3"], testDocs[@"1"] ]));
+}
+
+- (NSDictionary<NSString *, id> *)chatMessage {
+ return @{@"name" : @"Patryk", @"message" : @"We are actually writing data!"};
+}
+
+@end
diff --git a/Firestore/Example/Tests/Integration/FSTTransactionTests.m b/Firestore/Example/Tests/Integration/FSTTransactionTests.m
new file mode 100644
index 0000000..a0b5bbe
--- /dev/null
+++ b/Firestore/Example/Tests/Integration/FSTTransactionTests.m
@@ -0,0 +1,541 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import <XCTest/XCTest.h>
+#include <libkern/OSAtomic.h>
+
+#import "FSTIntegrationTestCase.h"
+
+@interface FSTTransactionTests : FSTIntegrationTestCase
+@end
+
+@implementation FSTTransactionTests
+
+// We currently require every document read to also be written.
+// TODO(b/34879758): Re-enable this test once we fix it.
+- (void)xtestGetDocuments {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"spaces"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{ @"foo" : @1, @"desc" : @"Stuff", @"owner" : @"Jonny" }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ // We currently require every document read to also be written.
+ // TODO(b/34879758): Fix this check once we drop that requirement.
+ XCTAssertNotNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testDeleteDocument {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(@"bar", snapshot[@"foo"]);
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ [transaction deleteDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@YES, result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ snapshot = [self readDocumentForRef:doc];
+ XCTAssertFalse(snapshot.exists);
+}
+
+- (void)testGetNonexistentDocumentThenCreate {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertFalse(snapshot.exists);
+ [transaction setData:@{@"foo" : @"bar"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@YES, result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertEqualObjects(@"bar", snapshot[@"foo"]);
+}
+
+- (void)testGetNonexistentDocumentThenFailPatch {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertFalse(snapshot.exists);
+ [transaction updateData:@{@"foo" : @"bar"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ // TODO(dimond): This is probably the wrong error code, but it's what we use today. We
+ // should update the code once the underlying error was fixed.
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testDeleteDocumentAndPatch {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertTrue(snapshot.exists);
+ [transaction deleteDocument:doc];
+ // Since we deleted the doc, the update will fail
+ [transaction updateData:@{@"foo" : @"bar"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ // TODO(dimond): This is probably the wrong error code, but it's what we use today. We
+ // should update the code once the underlying error was fixed.
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testDeleteDocumentAndSet {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertTrue(snapshot.exists);
+ [transaction deleteDocument:doc];
+ // TODO(dimond): In theory this should work, but it's complex to make it work, so instead we
+ // just let the transaction fail and verify it's unsupported for now
+ [transaction setData:@{@"foo" : @"new-bar"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ XCTAssertNotNil(error);
+ XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain);
+ // TODO(dimond): This is probably the wrong error code, but it's what we use today. We
+ // should update the code once the underlying error was fixed.
+ XCTAssertEqual(error.code, FIRFirestoreErrorCodeFailedPrecondition);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testWriteDocumentTwice {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id(FIRTransaction *transaction, NSError **error) {
+ [transaction setData:@{@"a" : @"b"} forDocument:doc];
+ [transaction setData:@{@"c" : @"d"} forDocument:doc];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@YES, result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(snapshot.data, @{@"c" : @"d"});
+}
+
+- (void)testSetDocumentWithMerge {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ [transaction setData:@{ @"a" : @"b", @"nested" : @{@"a" : @"b"} } forDocument:doc];
+ [transaction setData:@{
+ @"c" : @"d",
+ @"nested" : @{@"c" : @"d"}
+ }
+ forDocument:doc
+ options:[FIRSetOptions merge]];
+ return @YES;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@YES, result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(
+ snapshot.data, (
+ @{ @"a" : @"b",
+ @"c" : @"d",
+ @"nested" : @{@"a" : @"b", @"c" : @"d"} }));
+}
+
+- (void)testCannotUpdateNonExistentDocument {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ [transaction updateData:@{@"foo" : @"bar"} forDocument:doc];
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *result = [self readDocumentForRef:doc];
+ XCTAssertFalse(result.exists);
+}
+
+- (void)testIncrementTransactionally {
+ // A barrier to make sure every transaction reaches the same spot.
+ dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0);
+ __block volatile int32_t started = 0;
+
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{ @"count" : @(5.0) }];
+
+ // Make 3 transactions that will all increment.
+ int total = 3;
+ for (int i = 0; i < total; i++) {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ int32_t nowStarted = OSAtomicIncrement32(&started);
+ // Once all of the transactions have read, allow the first write.
+ if (nowStarted == total) {
+ dispatch_semaphore_signal(writeBarrier);
+ }
+
+ dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER);
+ // Refill the barrier so that the other transactions and retries succeed.
+ dispatch_semaphore_signal(writeBarrier);
+
+ double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0;
+ [transaction setData:@{ @"count" : @(newCount) } forDocument:doc];
+ return @YES;
+
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+ }
+
+ [self awaitExpectations];
+ // Now all transaction should be completed, so check the result.
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]);
+}
+
+- (void)testUpdateTransactionally {
+ // A barrier to make sure every transaction reaches the same spot.
+ dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0);
+ __block volatile int32_t started = 0;
+
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{ @"count" : @(5.0), @"other" : @"yes" }];
+
+ // Make 3 transactions that will all increment.
+ int total = 3;
+ for (int i = 0; i < total; i++) {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ int32_t nowStarted = OSAtomicIncrement32(&started);
+ // Once all of the transactions have read, allow the first write.
+ if (nowStarted == total) {
+ dispatch_semaphore_signal(writeBarrier);
+ }
+
+ dispatch_semaphore_wait(writeBarrier, DISPATCH_TIME_FOREVER);
+ // Refill the barrier so that the other transactions and retries succeed.
+ dispatch_semaphore_signal(writeBarrier);
+
+ double newCount = ((NSNumber *)snapshot[@"count"]).doubleValue + 1.0;
+ [transaction updateData:@{ @"count" : @(newCount) } forDocument:doc];
+ return @YES;
+
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+ }
+
+ [self awaitExpectations];
+ // Now all transaction should be completed, so check the result.
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]);
+ XCTAssertEqualObjects(@"yes", snapshot[@"other"]);
+}
+
+// We currently require every document read to also be written.
+// TODO(b/34879758): Re-enable this test once we fix it.
+- (void)xtestHandleReadingOneDocAndWritingAnother {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc1 = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+ FIRDocumentReference *doc2 = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+
+ [self writeDocumentRef:doc1 data:@{ @"count" : @(15.0) }];
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ // Get the first doc.
+ [transaction getDocument:doc1 error:error];
+ XCTAssertNil(*error);
+ // Do a write outside of the transaction. The first time the
+ // transaction is tried, this will bump the version, which
+ // will cause the write to doc2 to fail. The second time, it
+ // will be a no-op and not bump the version.
+ dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0);
+ [doc1 setData:@{
+ @"count" : @(1234)
+ }
+ completion:^(NSError *_Nullable error) {
+ dispatch_semaphore_signal(writeSemaphore);
+ }];
+ // We can block on it, because transactions run on a background queue.
+ dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER);
+ // Now try to update the other doc from within the transaction.
+ // This should fail once, because we read 15 earlier.
+ [transaction setData:@{ @"count" : @(16) } forDocument:doc2];
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ // We currently require every document read to also be written.
+ // TODO(b/34879758): Add this check back once we drop that.
+ // NSError *error = nil;
+ // FIRDocument *snapshot = [transaction getDocument:doc1 error:&error];
+ // XCTAssertNil(error);
+ // XCTAssertEquals(0, tries);
+ // XCTAssertEqualObjects(@(1234), snapshot[@"count"]);
+ // snapshot = [transaction getDocument:doc2 error:&error];
+ // XCTAssertNil(error);
+ // XCTAssertEqualObjects(@(16), snapshot[@"count"]);
+ XCTAssertNotNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testReadingADocTwiceWithDifferentVersions {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{ @"count" : @(15.0) }];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ // Get the doc once.
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertNil(*error);
+ XCTAssertEqualObjects(@(15), snapshot[@"count"]);
+ // Do a write outside of the transaction.
+ dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0);
+ [doc setData:@{
+ @"count" : @(1234)
+ }
+ completion:^(NSError *_Nullable error) {
+ dispatch_semaphore_signal(writeSemaphore);
+ }];
+ // We can block on it, because transactions run on a background queue.
+ dispatch_semaphore_wait(writeSemaphore, DISPATCH_TIME_FOREVER);
+ // Get the doc again in the transaction with the new version.
+ snapshot = [transaction getDocument:doc error:error];
+ // The get itself will fail, because we already read an earlier version of this document.
+ // TODO(klimt): Perhaps we shouldn't fail reads for this, but should wait and fail the
+ // whole transaction? It's an edge-case anyway, as developers shouldn't be reading the same
+ // do multiple times. But they need to handle read errors anyway.
+ XCTAssertNotNil(*error);
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertEqualObjects(@(1234.0), snapshot[@"count"]);
+}
+
+// We currently require every document read to also be written.
+// TODO(b/34879758): Add this test back once we fix that.
+- (void)xtestCannotHaveAGetWithoutMutations {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"foo"] documentWithAutoID];
+ [self writeDocumentRef:doc data:@{@"foo" : @"bar"}];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error];
+ XCTAssertTrue(snapshot.exists);
+ XCTAssertNil(*error);
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNotNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testSuccessWithNoTransactionOperations {
+ FIRFirestore *firestore = [self firestore];
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ return @"yes";
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertEqualObjects(@"yes", result);
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testCancellationOnError {
+ FIRFirestore *firestore = [self firestore];
+ FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID];
+ __block volatile int32_t count = 0;
+ XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"];
+ [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ OSAtomicIncrement32(&count);
+ [transaction setData:@{@"foo" : @"bar"} forDocument:doc];
+ *error = [NSError errorWithDomain:NSCocoaErrorDomain code:35 userInfo:@{}];
+ return nil;
+ }
+ completion:^(id _Nullable result, NSError *_Nullable error) {
+ XCTAssertNil(result);
+ XCTAssertNotNil(error);
+ XCTAssertEqual(35, error.code);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+ XCTAssertEqual(1, (int)count);
+ FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc];
+ XCTAssertFalse(snapshot.exists);
+}
+
+- (void)testUpdateFieldsWithDotsTransactionally {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUpdateFieldsWithDotsTransactionally"];
+
+ [doc.firestore
+ runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ XCTAssertNil(*error);
+ [transaction setData:@{@"a.b" : @"old", @"c.d" : @"old"} forDocument:doc];
+ [transaction updateData:@{
+ [[FIRFieldPath alloc] initWithFields:@[ @"a.b" ]] : @"new"
+ }
+ forDocument:doc];
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{@"a.b" : @"new", @"c.d" : @"old"}));
+ }];
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)testUpdateNestedFieldsTransactionally {
+ FIRDocumentReference *doc = [self documentRef];
+
+ XCTestExpectation *expectation =
+ [self expectationWithDescription:@"testUpdateNestedFieldsTransactionally"];
+
+ [doc.firestore
+ runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) {
+ XCTAssertNil(*error);
+ [transaction setData:@{
+ @"a" : @{@"b" : @"old"},
+ @"c" : @{@"d" : @"old"},
+ @"e" : @{@"f" : @"old"}
+ }
+ forDocument:doc];
+ [transaction updateData:@{
+ @"a.b" : @"new",
+ [[FIRFieldPath alloc] initWithFields:@[ @"c", @"d" ]] : @"new"
+ }
+ forDocument:doc];
+ return nil;
+ }
+ completion:^(id result, NSError *error) {
+ XCTAssertNil(error);
+ [doc getDocumentWithCompletion:^(FIRDocumentSnapshot *snapshot, NSError *error) {
+ XCTAssertNil(error);
+ XCTAssertEqualObjects(snapshot.data, (@{
+ @"a" : @{@"b" : @"new"},
+ @"c" : @{@"d" : @"new"},
+ @"e" : @{@"f" : @"old"}
+ }));
+ }];
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m
new file mode 100644
index 0000000..34c0685
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTEagerGarbageCollectorTests.m
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTEagerGarbageCollector.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Local/FSTReferenceSet.h"
+#import "Model/FSTDocumentKey.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTEagerGarbageCollectorTests : XCTestCase
+@end
+
+@implementation FSTEagerGarbageCollectorTests
+
+- (void)testAddOrRemoveReferences {
+ FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init];
+ FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init];
+ [gc addGarbageSource:referenceSet];
+
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ [referenceSet addReferenceToKey:key forID:1];
+ FSTAssertEqualSets([gc collectGarbage], @[]);
+ XCTAssertFalse([referenceSet isEmpty]);
+
+ [referenceSet removeReferenceToKey:key forID:1];
+ FSTAssertEqualSets([gc collectGarbage], @[ key ]);
+ XCTAssertTrue([referenceSet isEmpty]);
+}
+
+- (void)testRemoveAllReferencesForID {
+ FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init];
+ FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init];
+ [gc addGarbageSource:referenceSet];
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"];
+ [referenceSet addReferenceToKey:key1 forID:1];
+ [referenceSet addReferenceToKey:key2 forID:1];
+ [referenceSet addReferenceToKey:key3 forID:2];
+ XCTAssertFalse([referenceSet isEmpty]);
+
+ [referenceSet removeReferencesForID:1];
+ FSTAssertEqualSets([gc collectGarbage], (@[ key1, key2 ]));
+ XCTAssertFalse([referenceSet isEmpty]);
+
+ [referenceSet removeReferencesForID:2];
+ FSTAssertEqualSets([gc collectGarbage], @[ key3 ]);
+ XCTAssertTrue([referenceSet isEmpty]);
+}
+
+- (void)testTwoReferenceSetsAtTheSameTime {
+ FSTReferenceSet *remoteTargets = [[FSTReferenceSet alloc] init];
+ FSTReferenceSet *localViews = [[FSTReferenceSet alloc] init];
+ FSTReferenceSet *mutations = [[FSTReferenceSet alloc] init];
+
+ FSTEagerGarbageCollector *gc = [[FSTEagerGarbageCollector alloc] init];
+ [gc addGarbageSource:remoteTargets];
+ [gc addGarbageSource:localViews];
+ [gc addGarbageSource:mutations];
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ [remoteTargets addReferenceToKey:key1 forID:1];
+ [localViews addReferenceToKey:key1 forID:1];
+ [mutations addReferenceToKey:key1 forID:10];
+
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ [mutations addReferenceToKey:key2 forID:10];
+
+ XCTAssertFalse([remoteTargets isEmpty]);
+ XCTAssertFalse([localViews isEmpty]);
+ XCTAssertFalse([mutations isEmpty]);
+
+ [localViews removeReferencesForID:1];
+ FSTAssertEqualSets([gc collectGarbage], @[]);
+
+ [remoteTargets removeReferencesForID:1];
+ FSTAssertEqualSets([gc collectGarbage], @[]);
+
+ [mutations removeReferenceToKey:key1 forID:10];
+ FSTAssertEqualSets([gc collectGarbage], @[ key1 ]);
+
+ [mutations removeReferenceToKey:key2 forID:10];
+ FSTAssertEqualSets([gc collectGarbage], @[ key2 ]);
+
+ XCTAssertTrue([remoteTargets isEmpty]);
+ XCTAssertTrue([localViews isEmpty]);
+ XCTAssertTrue([mutations isEmpty]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm
new file mode 100644
index 0000000..3374fcf
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBKeyTests.mm
@@ -0,0 +1,361 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLevelDBKey.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLevelDBKeyTests : XCTestCase
+@end
+
+// I can't believe I have to write this...
+bool StartsWith(const std::string &value, const std::string &prefix) {
+ return prefix.size() <= value.size() && std::equal(prefix.begin(), prefix.end(), value.begin());
+}
+
+static std::string RemoteDocKey(NSString *pathString) {
+ return [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:FSTTestDocKey(pathString)];
+}
+
+static std::string RemoteDocKeyPrefix(NSString *pathString) {
+ return [FSTLevelDBRemoteDocumentKey keyPrefixWithResourcePath:FSTTestPath(pathString)];
+}
+
+static std::string DocMutationKey(NSString *userID, NSString *key, FSTBatchID batchID) {
+ return [FSTLevelDBDocumentMutationKey keyWithUserID:userID
+ documentKey:FSTTestDocKey(key)
+ batchID:batchID];
+}
+
+static std::string TargetDocKey(FSTTargetID targetID, NSString *key) {
+ return [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:FSTTestDocKey(key)];
+}
+
+static std::string DocTargetKey(NSString *key, FSTTargetID targetID) {
+ return [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(key) targetID:targetID];
+}
+
+/**
+ * Asserts that the description for given key is equal to the expected description.
+ *
+ * @param key A StringView of a textual key
+ * @param key An NSString that [FSTLevelDBKey descriptionForKey:] is expected to produce.
+ */
+#define FSTAssertExpectedKeyDescription(key, expectedDescription) \
+ XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:(key)], (expectedDescription))
+
+#define FSTAssertKeyLessThan(left, right) \
+ do { \
+ std::string leftKey = (left); \
+ std::string rightKey = (right); \
+ XCTAssertLessThan(leftKey.compare(right), 0, @"Expected %@ to be less than %@", \
+ [FSTLevelDBKey descriptionForKey:leftKey], \
+ [FSTLevelDBKey descriptionForKey:rightKey]); \
+ } while (0)
+
+@implementation FSTLevelDBKeyTests
+
+- (void)testMutationKeyPrefixing {
+ auto tableKey = [FSTLevelDBMutationKey keyPrefix];
+ auto emptyUserKey = [FSTLevelDBMutationKey keyPrefixWithUserID:""];
+ auto fooUserKey = [FSTLevelDBMutationKey keyPrefixWithUserID:"foo"];
+
+ auto foo2Key = [FSTLevelDBMutationKey keyWithUserID:"foo" batchID:2];
+
+ XCTAssertTrue(StartsWith(emptyUserKey, tableKey));
+
+ // This is critical: prefixes of the a value don't convert into prefixes of the key.
+ XCTAssertTrue(StartsWith(fooUserKey, tableKey));
+ XCTAssertFalse(StartsWith(fooUserKey, emptyUserKey));
+
+ // However whole segments in common are prefixes.
+ XCTAssertTrue(StartsWith(foo2Key, tableKey));
+ XCTAssertTrue(StartsWith(foo2Key, fooUserKey));
+}
+
+- (void)testMutationKeyEncodeDecodeCycle {
+ FSTLevelDBMutationKey *key = [[FSTLevelDBMutationKey alloc] init];
+ std::string user("foo");
+
+ NSArray<NSNumber *> *batchIds = @[ @0, @1, @100, @(INT_MAX - 1), @(INT_MAX) ];
+ for (NSNumber *batchIDNumber in batchIds) {
+ FSTBatchID batchID = [batchIDNumber intValue];
+ auto encoded = [FSTLevelDBMutationKey keyWithUserID:user batchID:batchID];
+
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.userID, user);
+ XCTAssertEqual(key.batchID, batchID);
+ }
+}
+
+- (void)testMutationKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBMutationKey keyPrefix], @"[mutation: incomplete key]");
+
+ FSTAssertExpectedKeyDescription([FSTLevelDBMutationKey keyPrefixWithUserID:@"user1"],
+ @"[mutation: userID=user1 incomplete key]");
+
+ auto key = [FSTLevelDBMutationKey keyWithUserID:@"user1" batchID:42];
+ FSTAssertExpectedKeyDescription(key, @"[mutation: userID=user1 batchID=42]");
+
+ FSTAssertExpectedKeyDescription(key + " extra",
+ @"[mutation: userID=user1 batchID=42 invalid "
+ @"key=<hW11dGF0aW9uAAGNdXNlcjEAAYqqgCBleHRyYQ==>]");
+
+ // Truncate the key so that it's missing its terminator.
+ key.resize(key.size() - 1);
+ FSTAssertExpectedKeyDescription(key, @"[mutation: userID=user1 batchID=42 incomplete key]");
+}
+
+- (void)testDocumentMutationKeyPrefixing {
+ auto tableKey = [FSTLevelDBDocumentMutationKey keyPrefix];
+ auto emptyUserKey = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:""];
+ auto fooUserKey = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:"foo"];
+
+ FSTDocumentKey *documentKey = FSTTestDocKey(@"foo/bar");
+ auto foo2Key =
+ [FSTLevelDBDocumentMutationKey keyWithUserID:"foo" documentKey:documentKey batchID:2];
+
+ XCTAssertTrue(StartsWith(emptyUserKey, tableKey));
+
+ // While we want a key with whole segments in common be considered a prefix it's vital that
+ // partial segments in common not be prefixes.
+ XCTAssertTrue(StartsWith(fooUserKey, tableKey));
+
+ // Here even though "" is a prefix of "foo" that prefix is within a segment so keys derived from
+ // those segments cannot be prefixes of each other.
+ XCTAssertFalse(StartsWith(fooUserKey, emptyUserKey));
+ XCTAssertFalse(StartsWith(emptyUserKey, fooUserKey));
+
+ // However whole segments in common are prefixes.
+ XCTAssertTrue(StartsWith(foo2Key, tableKey));
+ XCTAssertTrue(StartsWith(foo2Key, fooUserKey));
+}
+
+- (void)testDocumentMutationKeyEncodeDecodeCycle {
+ FSTLevelDBDocumentMutationKey *key = [[FSTLevelDBDocumentMutationKey alloc] init];
+ std::string user("foo");
+
+ NSArray<FSTDocumentKey *> *documentKeys = @[ FSTTestDocKey(@"a/b"), FSTTestDocKey(@"a/b/c/d") ];
+
+ NSArray<NSNumber *> *batchIds = @[ @0, @1, @100, @(INT_MAX - 1), @(INT_MAX) ];
+ for (NSNumber *batchIDNumber in batchIds) {
+ for (FSTDocumentKey *documentKey in documentKeys) {
+ FSTBatchID batchID = [batchIDNumber intValue];
+ auto encoded = [FSTLevelDBDocumentMutationKey keyWithUserID:user
+ documentKey:documentKey
+ batchID:batchID];
+
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.userID, user);
+ XCTAssertEqualObjects(key.documentKey, documentKey);
+ XCTAssertEqual(key.batchID, batchID);
+ }
+ }
+}
+
+- (void)testDocumentMutationKeyOrdering {
+ // Different user:
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"10", @"foo/bar", 0));
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"2", @"foo/bar", 0));
+
+ // Different paths:
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/baz", 0));
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/bar2", 0));
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0),
+ DocMutationKey(@"1", @"foo/bar/suffix/key", 0));
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar/suffix/key", 0),
+ DocMutationKey(@"1", @"foo/bar2", 0));
+
+ // Different batchID:
+ FSTAssertKeyLessThan(DocMutationKey(@"1", @"foo/bar", 0), DocMutationKey(@"1", @"foo/bar", 1));
+}
+
+- (void)testDocumentMutationKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBDocumentMutationKey keyPrefix],
+ @"[document_mutation: incomplete key]");
+
+ FSTAssertExpectedKeyDescription([FSTLevelDBDocumentMutationKey keyPrefixWithUserID:@"user1"],
+ @"[document_mutation: userID=user1 incomplete key]");
+
+ auto key = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:@"user1"
+ resourcePath:FSTTestPath(@"foo/bar")];
+ FSTAssertExpectedKeyDescription(key,
+ @"[document_mutation: userID=user1 key=foo/bar incomplete key]");
+
+ key = [FSTLevelDBDocumentMutationKey keyWithUserID:@"user1"
+ documentKey:FSTTestDocKey(@"foo/bar")
+ batchID:42];
+ FSTAssertExpectedKeyDescription(key, @"[document_mutation: userID=user1 key=foo/bar batchID=42]");
+}
+
+- (void)testTargetGlobalKeyEncodeDecodeCycle {
+ FSTLevelDBTargetGlobalKey *key = [[FSTLevelDBTargetGlobalKey alloc] init];
+
+ auto encoded = [FSTLevelDBTargetGlobalKey key];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+}
+
+- (void)testTargetGlobalKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBTargetGlobalKey key], @"[target_global:]");
+}
+
+- (void)testTargetKeyEncodeDecodeCycle {
+ FSTLevelDBTargetKey *key = [[FSTLevelDBTargetKey alloc] init];
+ FSTTargetID targetID = 42;
+
+ auto encoded = [FSTLevelDBTargetKey keyWithTargetID:42];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.targetID, targetID);
+}
+
+- (void)testTargetKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBTargetKey keyWithTargetID:42],
+ @"[target: targetID=42]");
+}
+
+- (void)testQueryTargetKeyEncodeDecodeCycle {
+ FSTLevelDBQueryTargetKey *key = [[FSTLevelDBQueryTargetKey alloc] init];
+ std::string canonicalID("foo");
+ FSTTargetID targetID = 42;
+
+ auto encoded = [FSTLevelDBQueryTargetKey keyWithCanonicalID:canonicalID targetID:42];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.canonicalID, canonicalID);
+ XCTAssertEqual(key.targetID, targetID);
+}
+
+- (void)testQueryKeyDescription {
+ FSTAssertExpectedKeyDescription([FSTLevelDBQueryTargetKey keyWithCanonicalID:"foo" targetID:42],
+ @"[query_target: canonicalID=foo targetID=42]");
+}
+
+- (void)testTargetDocumentKeyEncodeDecodeCycle {
+ FSTLevelDBTargetDocumentKey *key = [[FSTLevelDBTargetDocumentKey alloc] init];
+
+ auto encoded =
+ [FSTLevelDBTargetDocumentKey keyWithTargetID:42 documentKey:FSTTestDocKey(@"foo/bar")];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqual(key.targetID, 42);
+ XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(@"foo/bar"));
+}
+
+- (void)testTargetDocumentKeyOrdering {
+ // Different targetID:
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(2, @"foo/bar"));
+ FSTAssertKeyLessThan(TargetDocKey(2, @"foo/bar"), TargetDocKey(10, @"foo/bar"));
+ FSTAssertKeyLessThan(TargetDocKey(10, @"foo/bar"), TargetDocKey(100, @"foo/bar"));
+ FSTAssertKeyLessThan(TargetDocKey(42, @"foo/bar"), TargetDocKey(100, @"foo/bar"));
+
+ // Different paths:
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/baz"));
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/bar2"));
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar"), TargetDocKey(1, @"foo/bar/suffix/key"));
+ FSTAssertKeyLessThan(TargetDocKey(1, @"foo/bar/suffix/key"), TargetDocKey(1, @"foo/bar2"));
+}
+
+- (void)testTargetDocumentKeyDescription {
+ auto key = [FSTLevelDBTargetDocumentKey keyWithTargetID:42 documentKey:FSTTestDocKey(@"foo/bar")];
+ XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:key],
+ @"[target_document: targetID=42 key=foo/bar]");
+}
+
+- (void)testDocumentTargetKeyEncodeDecodeCycle {
+ FSTLevelDBDocumentTargetKey *key = [[FSTLevelDBDocumentTargetKey alloc] init];
+
+ auto encoded =
+ [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar") targetID:42];
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(@"foo/bar"));
+ XCTAssertEqual(key.targetID, 42);
+}
+
+- (void)testDocumentTargetKeyDescription {
+ auto key = [FSTLevelDBDocumentTargetKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar") targetID:42];
+ XCTAssertEqualObjects([FSTLevelDBKey descriptionForKey:key],
+ @"[document_target: key=foo/bar targetID=42]");
+}
+
+- (void)testDocumentTargetKeyOrdering {
+ // Different paths:
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/baz", 1));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar2", 1));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar/suffix/key", 1));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar/suffix/key", 1), DocTargetKey(@"foo/bar2", 1));
+
+ // Different targetID:
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 1), DocTargetKey(@"foo/bar", 2));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 2), DocTargetKey(@"foo/bar", 10));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 10), DocTargetKey(@"foo/bar", 100));
+ FSTAssertKeyLessThan(DocTargetKey(@"foo/bar", 42), DocTargetKey(@"foo/bar", 100));
+}
+
+- (void)testRemoteDocumentKeyPrefixing {
+ auto tableKey = [FSTLevelDBRemoteDocumentKey keyPrefix];
+
+ XCTAssertTrue(StartsWith(RemoteDocKey(@"foo/bar"), tableKey));
+
+ // This is critical: foo/bar2 should not contain foo/bar.
+ XCTAssertFalse(StartsWith(RemoteDocKey(@"foo/bar2"), RemoteDocKey(@"foo/bar")));
+
+ // Prefixes must be encoded specially
+ XCTAssertFalse(StartsWith(RemoteDocKey(@"foo/bar/baz/quu"), RemoteDocKey(@"foo/bar")));
+ XCTAssertTrue(StartsWith(RemoteDocKey(@"foo/bar/baz/quu"), RemoteDocKeyPrefix(@"foo/bar")));
+ XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar/baz/quu"), RemoteDocKeyPrefix(@"foo/bar")));
+ XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar/baz"), RemoteDocKeyPrefix(@"foo/bar")));
+ XCTAssertTrue(StartsWith(RemoteDocKeyPrefix(@"foo/bar"), RemoteDocKeyPrefix(@"foo")));
+}
+
+- (void)testRemoteDocumentKeyOrdering {
+ FSTAssertKeyLessThan(RemoteDocKey(@"foo/bar"), RemoteDocKey(@"foo/bar2"));
+ FSTAssertKeyLessThan(RemoteDocKey(@"foo/bar"), RemoteDocKey(@"foo/bar/suffix/key"));
+}
+
+- (void)testRemoteDocumentKeyEncodeDecodeCycle {
+ FSTLevelDBRemoteDocumentKey *key = [[FSTLevelDBRemoteDocumentKey alloc] init];
+
+ NSArray<NSString *> *paths = @[ @"foo/bar", @"foo/bar2", @"foo/bar/baz/quux" ];
+ for (NSString *path in paths) {
+ auto encoded = RemoteDocKey(path);
+ BOOL ok = [key decodeKey:encoded];
+ XCTAssertTrue(ok);
+ XCTAssertEqualObjects(key.documentKey, FSTTestDocKey(path));
+ }
+}
+
+- (void)testRemoteDocumentKeyDescription {
+ FSTAssertExpectedKeyDescription(
+ [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:FSTTestDocKey(@"foo/bar/baz/quux")],
+ @"[remote_document: key=foo/bar/baz/quux]");
+}
+
+@end
+
+#undef FSTAssertExpectedKeyDescription
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m
new file mode 100644
index 0000000..d426149
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.m
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLocalStore.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Auth/FSTUser.h"
+#import "Local/FSTLevelDB.h"
+
+#import "FSTLocalStoreTests.h"
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * The tests for FSTLevelDBLocalStore are performed on the FSTLocalStore protocol in
+ * FSTLocalStoreTests. This class is merely responsible for creating a new FSTPersistence
+ * implementation on demand.
+ */
+@interface FSTLevelDBLocalStoreTests : FSTLocalStoreTests
+@end
+
+@implementation FSTLevelDBLocalStoreTests
+
+- (id<FSTPersistence>)persistence {
+ return [FSTPersistenceTestHelpers levelDBPersistence];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm
new file mode 100644
index 0000000..1e66aac
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLevelDBMutationQueue.h"
+
+#import <XCTest/XCTest.h>
+#include <leveldb/db.h>
+
+#import "Protos/objc/firestore/local/Mutation.pbobjc.h"
+#import "Auth/FSTUser.h"
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTLevelDBKey.h"
+#import "Local/FSTWriteGroup.h"
+#include "Port/ordered_code.h"
+
+#import "FSTMutationQueueTests.h"
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+using leveldb::DB;
+using leveldb::Slice;
+using leveldb::Status;
+using leveldb::WriteOptions;
+using Firestore::StringView;
+using Firestore::OrderedCode;
+
+// A dummy mutation value, useful for testing code that's known to examine only mutation keys.
+static const char *kDummy = "1";
+
+/**
+ * Most of the tests for FSTLevelDBMutationQueue are performed on the FSTMutationQueue protocol in
+ * FSTMutationQueueTests. This class is responsible for setting up the @a mutationQueue plus any
+ * additional LevelDB-specific tests.
+ */
+@interface FSTLevelDBMutationQueueTests : FSTMutationQueueTests
+@end
+
+/**
+ * Creates a key that's structurally the same as FSTLevelDBMutationKey except it allows for
+ * nonstandard table names.
+ */
+std::string MutationLikeKey(StringView table, StringView userID, FSTBatchID batchID) {
+ std::string key;
+ OrderedCode::WriteString(&key, table);
+ OrderedCode::WriteString(&key, userID);
+ OrderedCode::WriteSignedNumIncreasing(&key, batchID);
+ return key;
+}
+
+@implementation FSTLevelDBMutationQueueTests {
+ FSTLevelDB *_db;
+}
+
+- (void)setUp {
+ [super setUp];
+ _db = [FSTPersistenceTestHelpers levelDBPersistence];
+ self.mutationQueue = [_db mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]];
+ self.persistence = _db;
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"];
+ [self.mutationQueue startWithGroup:group];
+ [self.persistence commitGroup:group];
+}
+
+- (void)testLoadNextBatchID_zeroWhenTotallyEmpty {
+ // Initial seek is invalid
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0);
+}
+
+- (void)testLoadNextBatchID_zeroWhenNoMutations {
+ // Initial seek finds no mutations
+ [self setDummyValueForKey:MutationLikeKey("mutationr", "foo", 20)];
+ [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)];
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 0);
+}
+
+- (void)testLoadNextBatchID_findsSingleRow {
+ // Seeks off the end of the table altogether
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]];
+
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7);
+}
+
+- (void)testLoadNextBatchID_findsSingleRowAmongNonMutations {
+ // Seeks into table following mutations.
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]];
+ [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)];
+
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7);
+}
+
+- (void)testLoadNextBatchID_findsMaxAcrossUsers {
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"fo" batchID:5]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"food" batchID:3]];
+
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:6]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:2]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:1]];
+
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 7);
+}
+
+- (void)testLoadNextBatchID_onlyFindsMutations {
+ // Write higher-valued batchIDs in nearby "tables"
+ auto tables = @[ @"mutatio", @"mutationsa", @"bears", @"zombies" ];
+ FSTBatchID highBatchID = 5;
+ for (NSString *table in tables) {
+ [self setDummyValueForKey:MutationLikeKey(table, "", highBatchID++)];
+ }
+
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"bar" batchID:3]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"bar" batchID:2]];
+ [self setDummyValueForKey:[FSTLevelDBMutationKey keyWithUserID:@"foo" batchID:1]];
+
+ // None of the higher tables should match -- this is the only entry that's in the mutations
+ // table
+ XCTAssertEqual([FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db.ptr], 4);
+}
+
+- (void)testEmptyProtoCanBeUpgraded {
+ // An empty protocol buffer serializes to a zero-length byte buffer.
+ GPBEmpty *empty = [GPBEmpty message];
+ NSData *emptyData = [empty data];
+ XCTAssertEqual(emptyData.length, 0);
+
+ // Choose some other (arbitrary) proto and parse it from the empty message and it should all be
+ // defaults. This shows that empty proto values within the index row value don't pose any future
+ // liability.
+ NSError *error;
+ FSTPBMutationQueue *parsedMessage = [FSTPBMutationQueue parseFromData:emptyData error:&error];
+ XCTAssertNil(error);
+
+ FSTPBMutationQueue *defaultMessage = [FSTPBMutationQueue message];
+ XCTAssertEqual(parsedMessage.lastAcknowledgedBatchId, defaultMessage.lastAcknowledgedBatchId);
+ XCTAssertEqualObjects(parsedMessage.lastStreamToken, defaultMessage.lastStreamToken);
+}
+
+- (void)setDummyValueForKey:(const std::string &)key {
+ _db.ptr->Put(WriteOptions(), key, kDummy);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m
new file mode 100644
index 0000000..8149397
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.m
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLevelDBQueryCache.h"
+
+#import "Local/FSTLevelDB.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTQueryCacheTests.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTLevelDBQueryCacheTests : FSTQueryCacheTests
+@end
+
+/**
+ * The tests for FSTLevelDBQueryCache are performed on the FSTQueryCache protocol in
+ * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the
+ * @a queryCache.
+ */
+@implementation FSTLevelDBQueryCacheTests
+
+- (void)setUp {
+ [super setUp];
+
+ self.persistence = [FSTPersistenceTestHelpers levelDBPersistence];
+ self.queryCache = [self.persistence queryCache];
+ [self.queryCache start];
+}
+
+- (void)tearDown {
+ [self.queryCache shutdown];
+ self.persistence = nil;
+ self.queryCache = nil;
+
+ [super tearDown];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm
new file mode 100644
index 0000000..a6af103
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTRemoteDocumentCacheTests.h"
+
+#include <leveldb/db.h>
+
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTLevelDBKey.h"
+#include "Port/ordered_code.h"
+
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+using leveldb::WriteOptions;
+using Firestore::OrderedCode;
+
+// A dummy document value, useful for testing code that's known to examine only document keys.
+static const char *kDummy = "1";
+
+/**
+ * The tests for FSTLevelDBRemoteDocumentCache are performed on the FSTRemoteDocumentCache
+ * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and
+ * tearing down the @a remoteDocumentCache.
+ */
+@interface FSTLevelDBRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests
+@end
+
+@implementation FSTLevelDBRemoteDocumentCacheTests {
+ FSTLevelDB *_db;
+}
+
+- (void)setUp {
+ [super setUp];
+ _db = [FSTPersistenceTestHelpers levelDBPersistence];
+ self.persistence = _db;
+ self.remoteDocumentCache = [self.persistence remoteDocumentCache];
+
+ // Write a couple dummy rows that should appear before/after the remote_documents table to make
+ // sure the tests are unaffected.
+ [self writeDummyRowWithSegments:@[ @"remote_documentr", @"foo", @"bar" ]];
+ [self writeDummyRowWithSegments:@[ @"remote_documentsa", @"foo", @"bar" ]];
+}
+
+- (void)tearDown {
+ [self.remoteDocumentCache shutdown];
+ self.remoteDocumentCache = nil;
+ self.persistence = nil;
+ _db = nil;
+ [super tearDown];
+}
+
+- (void)writeDummyRowWithSegments:(NSArray<NSString *> *)segments {
+ std::string key;
+ for (NSString *segment in segments) {
+ OrderedCode::WriteString(&key, segment.UTF8String);
+ }
+
+ _db.ptr->Put(WriteOptions(), key, kDummy);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m
new file mode 100644
index 0000000..0080f89
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.m
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLocalSerializer.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTTimestamp.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+#import "Model/FSTPath.h"
+#import "Protos/objc/firestore/local/MaybeDocument.pbobjc.h"
+#import "Protos/objc/firestore/local/Mutation.pbobjc.h"
+#import "Protos/objc/firestore/local/Target.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Common.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Document.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Query.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Write.pbobjc.h"
+#import "Protos/objc/google/type/Latlng.pbobjc.h"
+#import "Remote/FSTSerializerBeta.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTSerializerBeta (Test)
+- (GCFSValue *)encodedNull;
+- (GCFSValue *)encodedBool:(BOOL)value;
+- (GCFSValue *)encodedDouble:(double)value;
+- (GCFSValue *)encodedInteger:(int64_t)value;
+- (GCFSValue *)encodedString:(NSString *)value;
+@end
+
+@interface FSTLocalSerializerTests : XCTestCase
+
+@property(nonatomic, strong) FSTLocalSerializer *serializer;
+@property(nonatomic, strong) FSTSerializerBeta *remoteSerializer;
+
+@end
+
+@implementation FSTLocalSerializerTests
+
+- (void)setUp {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ self.remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID];
+ self.serializer = [[FSTLocalSerializer alloc] initWithRemoteSerializer:self.remoteSerializer];
+}
+
+- (void)testEncodesMutationBatch {
+ FSTMutation *set = FSTTestSetMutation(@"foo/bar", @{ @"a" : @"b", @"num" : @1 });
+ FSTMutation *patch = [[FSTPatchMutation alloc]
+ initWithKey:[FSTDocumentKey keyWithPathString:@"bar/baz"]
+ fieldMask:[[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"a") ]]
+ value:FSTTestObjectValue(
+ @{ @"a" : @"b",
+ @"num" : @1 })
+ precondition:[FSTPrecondition preconditionWithExists:YES]];
+ FSTMutation *del = FSTTestDeleteMutation(@"baz/quux");
+ FSTTimestamp *writeTime = [FSTTimestamp timestamp];
+ FSTMutationBatch *model = [[FSTMutationBatch alloc] initWithBatchID:42
+ localWriteTime:writeTime
+ mutations:@[ set, patch, del ]];
+
+ GCFSWrite *setProto = [GCFSWrite message];
+ setProto.update.name = @"projects/p/databases/d/documents/foo/bar";
+ [setProto.update.fields addEntriesFromDictionary:@{
+ @"a" : [self.remoteSerializer encodedString:@"b"],
+ @"num" : [self.remoteSerializer encodedInteger:1]
+ }];
+
+ GCFSWrite *patchProto = [GCFSWrite message];
+ patchProto.update.name = @"projects/p/databases/d/documents/bar/baz";
+ [patchProto.update.fields addEntriesFromDictionary:@{
+ @"a" : [self.remoteSerializer encodedString:@"b"],
+ @"num" : [self.remoteSerializer encodedInteger:1]
+ }];
+ [patchProto.updateMask.fieldPathsArray addObjectsFromArray:@[ @"a" ]];
+ patchProto.currentDocument.exists = YES;
+
+ GCFSWrite *delProto = [GCFSWrite message];
+ delProto.delete_p = @"projects/p/databases/d/documents/baz/quux";
+
+ GPBTimestamp *writeTimeProto = [GPBTimestamp message];
+ writeTimeProto.seconds = writeTime.seconds;
+ writeTimeProto.nanos = writeTime.nanos;
+
+ FSTPBWriteBatch *batchProto = [FSTPBWriteBatch message];
+ batchProto.batchId = 42;
+ [batchProto.writesArray addObjectsFromArray:@[ setProto, patchProto, delProto ]];
+ batchProto.localWriteTime = writeTimeProto;
+
+ XCTAssertEqualObjects([self.serializer encodedMutationBatch:model], batchProto);
+ FSTMutationBatch *decoded = [self.serializer decodedMutationBatch:batchProto];
+ XCTAssertEqual(decoded.batchID, model.batchID);
+ XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime);
+ XCTAssertEqualObjects(decoded.mutations, model.mutations);
+ XCTAssertEqualObjects([decoded keys], [model keys]);
+}
+
+- (void)testEncodesDocumentAsMaybeDocument {
+ FSTDocument *doc = FSTTestDoc(@"some/path", 42, @{@"foo" : @"bar"}, NO);
+
+ FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message];
+ maybeDocProto.document = [GCFSDocument message];
+ maybeDocProto.document.name = @"projects/p/databases/d/documents/some/path";
+ [maybeDocProto.document.fields addEntriesFromDictionary:@{
+ @"foo" : [self.remoteSerializer encodedString:@"bar"],
+ }];
+ maybeDocProto.document.updateTime.seconds = 0;
+ maybeDocProto.document.updateTime.nanos = 42000;
+
+ XCTAssertEqualObjects([self.serializer encodedMaybeDocument:doc], maybeDocProto);
+ FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto];
+ XCTAssertEqualObjects(decoded, doc);
+}
+
+- (void)testEncodesDeletedDocumentAsMaybeDocument {
+ FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(@"some/path", 42);
+
+ FSTPBMaybeDocument *maybeDocProto = [FSTPBMaybeDocument message];
+ maybeDocProto.noDocument = [FSTPBNoDocument message];
+ maybeDocProto.noDocument.name = @"projects/p/databases/d/documents/some/path";
+ maybeDocProto.noDocument.readTime.seconds = 0;
+ maybeDocProto.noDocument.readTime.nanos = 42000;
+
+ XCTAssertEqualObjects([self.serializer encodedMaybeDocument:deletedDoc], maybeDocProto);
+ FSTMaybeDocument *decoded = [self.serializer decodedMaybeDocument:maybeDocProto];
+ XCTAssertEqualObjects(decoded, deletedDoc);
+}
+
+- (void)testEncodesQueryData {
+ FSTQuery *query = FSTTestQuery(@"room");
+ FSTTargetID targetID = 42;
+ FSTSnapshotVersion *version = FSTTestVersion(1039);
+ NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1039);
+
+ FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:version
+ resumeToken:resumeToken];
+
+ // Let the RPC serializer test various permutations of query serialization.
+ GCFSTarget_QueryTarget *queryTarget = [self.remoteSerializer encodedQueryTarget:query];
+
+ FSTPBTarget *expected = [FSTPBTarget message];
+ expected.targetId = targetID;
+ expected.snapshotVersion.nanos = 1039000;
+ expected.resumeToken = [resumeToken copy];
+ expected.query.parent = queryTarget.parent;
+ expected.query.structuredQuery = queryTarget.structuredQuery;
+
+ XCTAssertEqualObjects([self.serializer encodedQueryData:queryData], expected);
+ FSTQueryData *decoded = [self.serializer decodedQueryData:expected];
+ XCTAssertEqualObjects(decoded, queryData);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.h b/Firestore/Example/Tests/Local/FSTLocalStoreTests.h
new file mode 100644
index 0000000..8e06d82
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <XCTest/XCTest.h>
+
+@class FSTLocalStore;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * These are tests for any implementation of the FSTLocalStore protocol.
+ *
+ * To test a specific implementation of FSTLocalStore:
+ *
+ * + Subclass FSTLocalStoreTests
+ * + override -persistence, creating a new instance of FSTPersistence.
+ */
+@interface FSTLocalStoreTests : XCTestCase
+
+/** Creates and returns an appropriate id<FSTPersistence> implementation. */
+- (id<FSTPersistence>)persistence;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m
new file mode 100644
index 0000000..ab492a7
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.m
@@ -0,0 +1,795 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLocalStore.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Auth/FSTUser.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTTimestamp.h"
+#import "Local/FSTEagerGarbageCollector.h"
+#import "Local/FSTLocalWriteResult.h"
+#import "Local/FSTNoOpGarbageCollector.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTDocumentSet.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTRemoteEvent.h"
+#import "Remote/FSTWatchChange.h"
+#import "Util/FSTClasses.h"
+
+#import "FSTHelpers.h"
+#import "FSTImmutableSortedDictionary+Testing.h"
+#import "FSTImmutableSortedSet+Testing.h"
+#import "FSTLocalStoreTests.h"
+#import "FSTWatchChange+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Creates a document version dictionary mapping the document in @a mutation to @a version. */
+FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation,
+ FSTTestSnapshotVersion version) {
+ FSTDocumentVersionDictionary *result = [FSTDocumentVersionDictionary documentVersionDictionary];
+ result = [result dictionaryBySettingObject:FSTTestVersion(version) forKey:mutation.key];
+ return result;
+}
+
+@interface FSTLocalStoreTests ()
+
+@property(nonatomic, strong, readwrite) id<FSTPersistence> localStorePersistence;
+@property(nonatomic, strong, readwrite) FSTLocalStore *localStore;
+
+@property(nonatomic, strong, readonly) NSMutableArray<FSTMutationBatch *> *batches;
+@property(nonatomic, strong, readwrite, nullable) FSTMaybeDocumentDictionary *lastChanges;
+@property(nonatomic, assign, readwrite) FSTTargetID lastTargetID;
+
+@end
+
+@implementation FSTLocalStoreTests
+
+- (void)setUp {
+ [super setUp];
+
+ if ([self isTestBaseClass]) {
+ return;
+ }
+
+ id<FSTPersistence> persistence = [self persistence];
+ self.localStorePersistence = persistence;
+ id<FSTGarbageCollector> garbageCollector = [[FSTEagerGarbageCollector alloc] init];
+ self.localStore = [[FSTLocalStore alloc] initWithPersistence:persistence
+ garbageCollector:garbageCollector
+ initialUser:[FSTUser unauthenticatedUser]];
+ [self.localStore start];
+
+ _batches = [NSMutableArray array];
+ _lastChanges = nil;
+ _lastTargetID = 0;
+}
+
+- (void)tearDown {
+ [self.localStore shutdown];
+ [self.localStorePersistence shutdown];
+
+ [super tearDown];
+}
+
+- (id<FSTPersistence>)persistence {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+/**
+ * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for
+ * FSTLocalStoreTests since it is incomplete without the implementations supplied by its
+ * subclasses.
+ */
+- (BOOL)isTestBaseClass {
+ return [self class] == [FSTLocalStoreTests class];
+}
+
+/** Restarts the local store using the FSTNoOpGarbageCollector instead of the default. */
+- (void)restartWithNoopGarbageCollector {
+ [self.localStore shutdown];
+
+ id<FSTGarbageCollector> garbageCollector = [[FSTNoOpGarbageCollector alloc] init];
+ self.localStore = [[FSTLocalStore alloc] initWithPersistence:self.localStorePersistence
+ garbageCollector:garbageCollector
+ initialUser:[FSTUser unauthenticatedUser]];
+ [self.localStore start];
+}
+
+- (void)writeMutation:(FSTMutation *)mutation {
+ [self writeMutations:@[ mutation ]];
+}
+
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
+ FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations];
+ XCTAssertNotNil(result);
+ [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batchID
+ localWriteTime:[FSTTimestamp timestamp]
+ mutations:mutations]];
+ self.lastChanges = result.changes;
+}
+
+- (void)applyRemoteEvent:(FSTRemoteEvent *)event {
+ self.lastChanges = [self.localStore applyRemoteEvent:event];
+}
+
+- (void)notifyLocalViewChanges:(FSTLocalViewChanges *)changes {
+ [self.localStore notifyLocalViewChanges:@[ changes ]];
+}
+
+- (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion {
+ FSTMutationBatch *batch = [self.batches firstObject];
+ [self.batches removeObjectAtIndex:0];
+ XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported.");
+ FSTSnapshotVersion *version = FSTTestVersion(documentVersion);
+ FSTMutationResult *mutationResult =
+ [[FSTMutationResult alloc] initWithVersion:version transformResults:nil];
+ FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch
+ commitVersion:version
+ mutationResults:@[ mutationResult ]
+ streamToken:nil];
+ self.lastChanges = [self.localStore acknowledgeBatchWithResult:result];
+}
+
+- (void)rejectMutation {
+ FSTMutationBatch *batch = [self.batches firstObject];
+ [self.batches removeObjectAtIndex:0];
+ self.lastChanges = [self.localStore rejectBatchID:batch.batchID];
+}
+
+- (void)allocateQuery:(FSTQuery *)query {
+ FSTQueryData *queryData = [self.localStore allocateQuery:query];
+ self.lastTargetID = queryData.targetID;
+}
+
+- (void)collectGarbage {
+ [self.localStore collectGarbage];
+}
+
+/** Asserts that the last target ID is the given number. */
+#define FSTAssertTargetID(targetID) \
+ do { \
+ XCTAssertEqual(self.lastTargetID, targetID); \
+ } while (0)
+
+/** Asserts that a the lastChanges contain the docs in the given array. */
+#define FSTAssertChanged(documents) \
+ XCTAssertNotNil(self.lastChanges); \
+ do { \
+ FSTMaybeDocumentDictionary *actual = self.lastChanges; \
+ NSArray<FSTMaybeDocument *> *expected = (documents); \
+ XCTAssertEqual(actual.count, expected.count); \
+ NSEnumerator<FSTMaybeDocument *> *enumerator = expected.objectEnumerator; \
+ [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * key, FSTMaybeDocument * value, \
+ BOOL * stop) { \
+ XCTAssertEqualObjects(value, [enumerator nextObject]); \
+ }]; \
+ self.lastChanges = nil; \
+ } while (0)
+
+/** Asserts that the given keys were removed. */
+#define FSTAssertRemoved(keyPaths) \
+ XCTAssertNotNil(self.lastChanges); \
+ do { \
+ FSTMaybeDocumentDictionary *actual = self.lastChanges; \
+ XCTAssertEqual(actual.count, keyPaths.count); \
+ NSEnumerator<NSString *> *keyPathEnumerator = keyPaths.objectEnumerator; \
+ [actual enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey * actualKey, \
+ FSTMaybeDocument * value, BOOL * stop) { \
+ FSTDocumentKey *expectedKey = \
+ [FSTDocumentKey keyWithPathString:[keyPathEnumerator nextObject]]; \
+ XCTAssertEqualObjects(actualKey, expectedKey); \
+ XCTAssertTrue([value isKindOfClass:[FSTDeletedDocument class]]); \
+ }]; \
+ self.lastChanges = nil; \
+ } while (0)
+
+/** Asserts that the given local store contains the given document. */
+#define FSTAssertContains(document) \
+ do { \
+ FSTMaybeDocument *expected = (document); \
+ FSTMaybeDocument *actual = [self.localStore readDocument:expected.key]; \
+ XCTAssertEqualObjects(actual, expected); \
+ } while (0)
+
+/** Asserts that the given local store does not contain the given document. */
+#define FSTAssertNotContains(keyPathString) \
+ do { \
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPathString]; \
+ FSTMaybeDocument *actual = [self.localStore readDocument:key]; \
+ XCTAssertNil(actual); \
+ } while (0)
+
+- (void)testMutationBatchKeys {
+ if ([self isTestBaseClass]) return;
+
+ FSTMutation *set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"});
+ FSTMutation *set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"});
+ FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1
+ localWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ set1, set2 ]];
+ FSTDocumentKeySet *keys = [batch keys];
+ XCTAssertEqual(keys.count, 2);
+}
+
+- (void)testHandlesSetMutation {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:0];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO));
+}
+
+- (void)testHandlesSetMutationThenDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
+ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES));
+}
+
+- (void)testHandlesAckThenRejectThenRemoteEvent {
+ if ([self isTestBaseClass]) return;
+
+ // Start a query that requires acks to be held.
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [self allocateQuery:query];
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ // The last seen version is zero, so this ack must be held.
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertChanged(@[]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self writeMutation:FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES));
+
+ [self rejectMutation];
+ FSTAssertRemoved(@[ @"bar/baz" ]);
+ FSTAssertNotContains(@"bar/baz");
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
+ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"changed"}, NO));
+ FSTAssertNotContains(@"bar/baz");
+}
+
+- (void)testHandlesDeletedDocumentThenSetMutationThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2));
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:3];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, NO));
+}
+
+- (void)testHandlesSetMutationThenDeletedDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+}
+
+- (void)testHandlesDocumentThenSetMutationThenAckThenDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"it" : @"base"}, NO));
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:3];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
+ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO));
+}
+
+- (void)testHandlesPatchWithoutPriorDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+}
+
+- (void)testHandlesPatchMutationThenDocumentThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, YES));
+
+ [self acknowledgeMutationWithVersion:2];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar", @"it" : @"base"}, NO));
+}
+
+- (void)testHandlesPatchMutationThenAckThenDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertNotContains(@"foo/bar");
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO));
+}
+
+- (void)testHandlesDeleteMutationThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+}
+
+- (void)testHandlesDocumentThenDeleteMutationThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO));
+
+ [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:2];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+}
+
+- (void)testHandlesDeleteMutationThenDocumentThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:2];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+}
+
+- (void)testHandlesDocumentThenDeletedDocumentThenDocument {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 2));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(
+ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO), @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 3, @{@"it" : @"changed"}, NO));
+}
+
+- (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, YES));
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"it" : @"base"}, NO),
+ @[ @1 ], @[])];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:2]; // delete mutation
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, YES));
+
+ [self acknowledgeMutationWithVersion:3]; // patch mutation
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO));
+}
+
+- (void)testHandlesSetMutationAndPatchMutationTogether {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutations:@[
+ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}),
+ FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)
+ ]];
+
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+}
+
+- (void)testHandlesSetMutationThenPatchMutationThenReject {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"})];
+ [self acknowledgeMutationWithVersion:1];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO));
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+
+ [self rejectMutation];
+ FSTAssertChanged(@[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO) ]);
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO));
+}
+
+- (void)testHandlesSetMutationsAndPatchMutationOfJustOneTogether {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutations:@[
+ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"old"}),
+ FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}),
+ FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)
+ ]];
+
+ FSTAssertChanged((@[
+ FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES),
+ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES)
+ ]));
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+ FSTAssertContains(FSTTestDoc(@"bar/baz", 0, @{@"bar" : @"baz"}, YES));
+}
+
+- (void)testHandlesDeleteMutationThenPatchMutationThenAckThenAck {
+ if ([self isTestBaseClass]) return;
+
+ [self writeMutation:FSTTestDeleteMutation(@"foo/bar")];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:2]; // delete mutation
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+
+ [self acknowledgeMutationWithVersion:3]; // patch mutation
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/bar", 0));
+}
+
+- (void)testCollectsGarbageAfterChangeBatchWithNoTargetIDs {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDeletedDoc(@"foo/bar", 2), @[ @1 ], @[])];
+ FSTAssertRemoved(@[ @"foo/bar" ]);
+
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO),
+ @[ @1 ], @[])];
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+}
+
+- (void)testCollectsGarbageAfterChangeBatch {
+ if ([self isTestBaseClass]) return;
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [self allocateQuery:query];
+ FSTAssertTargetID(2);
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO),
+ @[ @2 ], @[])];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"bar"}, NO));
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 2, @{@"foo" : @"baz"}, NO),
+ @[], @[ @2 ])];
+ [self collectGarbage];
+
+ FSTAssertNotContains(@"foo/bar");
+}
+
+- (void)testCollectsGarbageAfterAcknowledgedMutation {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO),
+ @[ @1 ], @[])];
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})];
+ [self writeMutation:FSTTestDeleteMutation(@"foo/baz")];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+ FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES));
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self acknowledgeMutationWithVersion:3];
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES));
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self acknowledgeMutationWithVersion:4];
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/bah");
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self acknowledgeMutationWithVersion:5];
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/bah");
+ FSTAssertNotContains(@"foo/baz");
+}
+
+- (void)testCollectsGarbageAfterRejectedMutation {
+ if ([self isTestBaseClass]) return;
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"old"}, NO),
+ @[ @1 ], @[])];
+ [self writeMutation:FSTTestPatchMutation(@"foo/bar", @{@"foo" : @"bar"}, nil)];
+ [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})];
+ [self writeMutation:FSTTestDeleteMutation(@"foo/baz")];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES));
+ FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES));
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self rejectMutation]; // patch mutation
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertContains(FSTTestDoc(@"foo/bah", 0, @{@"foo" : @"bah"}, YES));
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self rejectMutation]; // set mutation
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/bah");
+ FSTAssertContains(FSTTestDeletedDoc(@"foo/baz", 0));
+
+ [self rejectMutation]; // delete mutation
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/bah");
+ FSTAssertNotContains(@"foo/baz");
+}
+
+- (void)testPinsDocumentsInTheLocalView {
+ if ([self isTestBaseClass]) return;
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [self allocateQuery:query];
+ FSTAssertTargetID(2);
+
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO),
+ @[ @2 ], @[])];
+ [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO));
+ FSTAssertContains(FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES));
+
+ [self notifyLocalViewChanges:FSTTestViewChanges(query, @[ @"foo/bar", @"foo/baz" ], @[])];
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO),
+ @[], @[ @2 ])];
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO),
+ @[ @1 ], @[])];
+ [self acknowledgeMutationWithVersion:2];
+ [self collectGarbage];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{@"foo" : @"bar"}, NO));
+ FSTAssertContains(FSTTestDoc(@"foo/baz", 2, @{@"foo" : @"baz"}, NO));
+
+ [self notifyLocalViewChanges:FSTTestViewChanges(query, @[], @[ @"foo/bar", @"foo/baz" ])];
+ [self collectGarbage];
+
+ FSTAssertNotContains(@"foo/bar");
+ FSTAssertNotContains(@"foo/baz");
+}
+
+- (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately {
+ if ([self isTestBaseClass]) return;
+
+ FSTTargetID targetID = 321;
+ [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc(@"foo/bar", 1, @{}, NO),
+ @[ @(targetID) ], @[])];
+ FSTAssertContains(FSTTestDoc(@"foo/bar", 1, @{}, NO));
+
+ [self collectGarbage];
+ FSTAssertNotContains(@"foo/bar");
+}
+
+- (void)testCanExecuteDocumentQueries {
+ if ([self isTestBaseClass]) return;
+
+ [self.localStore locallyWriteMutations:@[
+ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}),
+ FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}),
+ FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"})
+ ]];
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+ FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
+ XCTAssertEqualObjects([docs values], @[ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES) ]);
+}
+
+- (void)testCanExecuteCollectionQueries {
+ if ([self isTestBaseClass]) return;
+
+ [self.localStore locallyWriteMutations:@[
+ FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}),
+ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}),
+ FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}),
+ FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}),
+ FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"})
+ ]];
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
+ XCTAssertEqualObjects([docs values], (@[
+ FSTTestDoc(@"foo/bar", 0, @{@"foo" : @"bar"}, YES),
+ FSTTestDoc(@"foo/baz", 0, @{@"foo" : @"baz"}, YES)
+ ]));
+}
+
+- (void)testCanExecuteMixedCollectionQueries {
+ if ([self isTestBaseClass]) return;
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [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.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]];
+
+ FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
+ XCTAssertEqualObjects([docs values], (@[
+ FSTTestDoc(@"foo/bar", 20, @{@"a" : @"b"}, NO),
+ FSTTestDoc(@"foo/baz", 10, @{@"a" : @"b"}, NO),
+ FSTTestDoc(@"foo/bonk", 0, @{@"a" : @"b"}, YES)
+ ]));
+}
+
+- (void)testPersistsResumeTokens {
+ if ([self isTestBaseClass]) return;
+
+ // This test only works in the absence of the FSTEagerGarbageCollector.
+ [self restartWithNoopGarbageCollector];
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo", @"bar" ]]];
+ FSTQueryData *queryData = [self.localStore allocateQuery:query];
+ FSTBoxedTargetID *targetID = @(queryData.targetID);
+ NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000);
+
+ FSTWatchChange *watchChange =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ targetID ]
+ resumeToken:resumeToken];
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *listens =
+ [NSMutableDictionary dictionary];
+ listens[targetID] = queryData;
+ NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingResponses =
+ [NSMutableDictionary dictionary];
+ FSTWatchChangeAggregator *aggregator =
+ [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(1000)
+ listenTargets:listens
+ pendingTargetResponses:pendingResponses];
+ [aggregator addWatchChanges:@[ watchChange ]];
+ FSTRemoteEvent *remoteEvent = [aggregator remoteEvent];
+ [self applyRemoteEvent:remoteEvent];
+
+ // Stop listening so that the query should become inactive (but persistent)
+ [self.localStore releaseQuery:query];
+
+ // Should come back with the same resume token
+ FSTQueryData *queryData2 = [self.localStore allocateQuery:query];
+ XCTAssertEqualObjects(queryData2.resumeToken, resumeToken);
+}
+
+- (void)testRemoteDocumentKeysForTarget {
+ if ([self isTestBaseClass]) return;
+ [self restartWithNoopGarbageCollector];
+
+ FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"foo" ]]];
+ [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.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]];
+
+ FSTDocumentKeySet *keys = [self.localStore remoteDocumentKeysForTarget:2];
+ FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ]));
+
+ [self restartWithNoopGarbageCollector];
+
+ keys = [self.localStore remoteDocumentKeysForTarget:2];
+ FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ]));
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m
new file mode 100644
index 0000000..e7486d0
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.m
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTLocalStore.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTLocalStoreTests.h"
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * This tests the FSTLocalStore with an FSTMemoryPersistence persistence implementation. The tests
+ * are in FSTLocalStoreTests and this class is merely responsible for creating a new FSTPersistence
+ * implementation on demand.
+ */
+@interface FSTMemoryLocalStoreTests : FSTLocalStoreTests
+@end
+
+@implementation FSTMemoryLocalStoreTests
+
+- (id<FSTPersistence>)persistence {
+ return [FSTPersistenceTestHelpers memoryPersistence];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m
new file mode 100644
index 0000000..4d76393
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.m
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTMemoryMutationQueue.h"
+
+#import "Auth/FSTUser.h"
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTMutationQueueTests.h"
+#import "FSTPersistenceTestHelpers.h"
+
+@interface FSTMemoryMutationQueueTests : FSTMutationQueueTests
+@end
+
+/**
+ * The tests for FSTMemoryMutationQueue are performed on the FSTMutationQueue protocol in
+ * FSTMutationQueueTests. This class is merely responsible for setting up the @a mutationQueue.
+ */
+@implementation FSTMemoryMutationQueueTests
+
+- (void)setUp {
+ [super setUp];
+
+ self.persistence = [FSTPersistenceTestHelpers memoryPersistence];
+ self.mutationQueue =
+ [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m
new file mode 100644
index 0000000..6574647
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.m
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTMemoryQueryCache.h"
+
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTQueryCacheTests.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMemoryQueryCacheTests : FSTQueryCacheTests
+@end
+
+/**
+ * The tests for FSTMemoryQueryCache are performed on the FSTQueryCache protocol in
+ * FSTQueryCacheTests. This class is merely responsible for setting up and tearing down the
+ * @a queryCache.
+ */
+@implementation FSTMemoryQueryCacheTests
+
+- (void)setUp {
+ [super setUp];
+
+ self.persistence = [FSTPersistenceTestHelpers memoryPersistence];
+ self.queryCache = [self.persistence queryCache];
+ [self.queryCache start];
+}
+
+- (void)tearDown {
+ [self.queryCache shutdown];
+ self.persistence = nil;
+ self.queryCache = nil;
+
+ [super tearDown];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m
new file mode 100644
index 0000000..7602134
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.m
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTMemoryRemoteDocumentCache.h"
+
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTRemoteDocumentCacheTests.h"
+
+@interface FSTMemoryRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests
+@end
+
+/**
+ * The tests for FSTMemoryRemoteDocumentCache are performed on the FSTRemoteDocumentCache
+ * protocol in FSTRemoteDocumentCacheTests. This class is merely responsible for setting up and
+ * tearing down the @a remoteDocumentCache.
+ */
+@implementation FSTMemoryRemoteDocumentCacheTests
+
+- (void)setUp {
+ [super setUp];
+
+ self.persistence = [FSTPersistenceTestHelpers memoryPersistence];
+ self.remoteDocumentCache = [self.persistence remoteDocumentCache];
+}
+
+- (void)tearDown {
+ [self.remoteDocumentCache shutdown];
+ self.persistence = nil;
+ self.remoteDocumentCache = nil;
+
+ [super tearDown];
+}
+
+@end
diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.h b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h
new file mode 100644
index 0000000..0193c36
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <XCTest/XCTest.h>
+
+@protocol FSTMutationQueue;
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * These are tests for any implementation of the FSTMutationQueue protocol.
+ *
+ * To test a specific implementation of FSTMutationQueue:
+ *
+ * + Subclass FSTMutationQueueTests
+ * + override -setUp, assigning to mutationQueue and persistence
+ * + override -tearDown, cleaning up mutationQueue and persistence
+ */
+@interface FSTMutationQueueTests : XCTestCase
+@property(nonatomic, strong, nullable) id<FSTMutationQueue> mutationQueue;
+@property(nonatomic, strong, nullable) id<FSTPersistence> persistence;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.m b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m
new file mode 100644
index 0000000..42ba0b3
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.m
@@ -0,0 +1,511 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTMutationQueueTests.h"
+
+#import "Auth/FSTUser.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTTimestamp.h"
+#import "Local/FSTEagerGarbageCollector.h"
+#import "Local/FSTMutationQueue.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTWriteGroup.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTMutationQueueTests
+
+- (void)tearDown {
+ [self.mutationQueue shutdown];
+ [self.persistence shutdown];
+ [super tearDown];
+}
+
+/**
+ * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for
+ * FSTMutationQueueTests since it is incomplete without the implementations supplied by its
+ * subclasses.
+ */
+- (BOOL)isTestBaseClass {
+ return [self class] == [FSTMutationQueueTests class];
+}
+
+- (void)testCountBatches {
+ if ([self isTestBaseClass]) return;
+
+ XCTAssertEqual(0, [self batchCount]);
+ XCTAssertTrue([self.mutationQueue isEmpty]);
+
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+ XCTAssertEqual(1, [self batchCount]);
+ XCTAssertFalse([self.mutationQueue isEmpty]);
+
+ FSTMutationBatch *batch2 = [self addMutationBatch];
+ XCTAssertEqual(2, [self batchCount]);
+
+ [self removeMutationBatches:@[ batch2 ]];
+ XCTAssertEqual(1, [self batchCount]);
+
+ [self removeMutationBatches:@[ batch1 ]];
+ XCTAssertEqual(0, [self batchCount]);
+ XCTAssertTrue([self.mutationQueue isEmpty]);
+}
+
+- (void)testAcknowledgeBatchID {
+ if ([self isTestBaseClass]) return;
+
+ // Initial state of an empty queue
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown);
+
+ // Adding mutation batches should not change the highest acked batchID.
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+ FSTMutationBatch *batch2 = [self addMutationBatch];
+ FSTMutationBatch *batch3 = [self addMutationBatch];
+ XCTAssertGreaterThan(batch1.batchID, kFSTBatchIDUnknown);
+ XCTAssertGreaterThan(batch2.batchID, batch1.batchID);
+ XCTAssertGreaterThan(batch3.batchID, batch2.batchID);
+
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown);
+
+ [self acknowledgeBatch:batch1];
+ [self acknowledgeBatch:batch2];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ [self removeMutationBatches:@[ batch1 ]];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ [self removeMutationBatches:@[ batch2 ]];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ // Batch 3 never acknowledged.
+ [self removeMutationBatches:@[ batch3 ]];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+}
+
+- (void)testAcknowledgeThenRemove {
+ if ([self isTestBaseClass]) return;
+
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:NSStringFromSelector(_cmd)];
+ [self.mutationQueue acknowledgeBatch:batch1 streamToken:nil group:group];
+ [self.mutationQueue removeMutationBatches:@[ batch1 ] group:group];
+ [self.persistence commitGroup:group];
+
+ XCTAssertEqual([self batchCount], 0);
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch1.batchID);
+}
+
+- (void)testHighestAcknowledgedBatchIDNeverExceedsNextBatchID {
+ if ([self isTestBaseClass]) return;
+
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+ FSTMutationBatch *batch2 = [self addMutationBatch];
+ [self acknowledgeBatch:batch1];
+ [self acknowledgeBatch:batch2];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ [self removeMutationBatches:@[ batch1, batch2 ]];
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], batch2.batchID);
+
+ // Restart the queue so that nextBatchID will be reset.
+ [self.mutationQueue shutdown];
+ self.mutationQueue =
+ [self.persistence mutationQueueForUser:[[FSTUser alloc] initWithUID:@"user"]];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"];
+ [self.mutationQueue startWithGroup:group];
+ [self.persistence commitGroup:group];
+
+ // Verify that on restart with an empty queue, nextBatchID falls to a lower value.
+ XCTAssertLessThan(self.mutationQueue.nextBatchID, batch2.batchID);
+
+ // As a result highestAcknowledgedBatchID must also reset lower.
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown);
+
+ // The mutation queue will reset the next batchID after all mutations are removed so adding
+ // another mutation will cause a collision.
+ FSTMutationBatch *newBatch = [self addMutationBatch];
+ XCTAssertEqual(newBatch.batchID, batch1.batchID);
+
+ // Restart the queue with one unacknowledged batch in it.
+ group = [self.persistence startGroupWithAction:@"Start MutationQueue"];
+ [self.mutationQueue startWithGroup:group];
+ [self.persistence commitGroup:group];
+
+ XCTAssertEqual([self.mutationQueue nextBatchID], newBatch.batchID + 1);
+
+ // highestAcknowledgedBatchID must still be kFSTBatchIDUnknown.
+ XCTAssertEqual([self.mutationQueue highestAcknowledgedBatchID], kFSTBatchIDUnknown);
+}
+
+- (void)testLookupMutationBatch {
+ if ([self isTestBaseClass]) return;
+
+ // Searching on an empty queue should not find a non-existent batch
+ FSTMutationBatch *notFound = [self.mutationQueue lookupMutationBatch:42];
+ XCTAssertNil(notFound);
+
+ NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10];
+ NSArray<FSTMutationBatch *> *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches];
+
+ // After removing, a batch should not be found
+ for (NSUInteger i = 0; i < removed.count; i++) {
+ notFound = [self.mutationQueue lookupMutationBatch:removed[i].batchID];
+ XCTAssertNil(notFound);
+ }
+
+ // Remaining entries should still be found
+ for (FSTMutationBatch *batch in batches) {
+ FSTMutationBatch *found = [self.mutationQueue lookupMutationBatch:batch.batchID];
+ XCTAssertEqual(found.batchID, batch.batchID);
+ }
+
+ // Even on a nonempty queue searching should not find a non-existent batch
+ notFound = [self.mutationQueue lookupMutationBatch:42];
+ XCTAssertNil(notFound);
+}
+
+- (void)testNextMutationBatchAfterBatchID {
+ if ([self isTestBaseClass]) return;
+
+ NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10];
+
+ // This is an array of successors assuming the removals below will happen:
+ NSArray<FSTMutationBatch *> *afters = @[ batches[3], batches[8], batches[8] ];
+ NSArray<FSTMutationBatch *> *removed = [self makeHoles:@[ @2, @6, @7 ] inBatches:batches];
+
+ for (NSUInteger i = 0; i < batches.count - 1; i++) {
+ FSTMutationBatch *current = batches[i];
+ FSTMutationBatch *next = batches[i + 1];
+ FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID];
+ XCTAssertEqual(found.batchID, next.batchID);
+ }
+
+ for (NSUInteger i = 0; i < removed.count; i++) {
+ FSTMutationBatch *current = removed[i];
+ FSTMutationBatch *next = afters[i];
+ FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:current.batchID];
+ XCTAssertEqual(found.batchID, next.batchID);
+ }
+
+ FSTMutationBatch *first = batches[0];
+ FSTMutationBatch *found = [self.mutationQueue nextMutationBatchAfterBatchID:first.batchID - 42];
+ XCTAssertEqual(found.batchID, first.batchID);
+
+ FSTMutationBatch *last = batches[batches.count - 1];
+ FSTMutationBatch *notFound = [self.mutationQueue nextMutationBatchAfterBatchID:last.batchID];
+ XCTAssertNil(notFound);
+}
+
+- (void)testAllMutationBatchesThroughBatchID {
+ if ([self isTestBaseClass]) return;
+
+ NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10];
+ [self makeHoles:@[ @2, @6, @7 ] inBatches:batches];
+
+ NSArray<FSTMutationBatch *> *found, *expected;
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[0].batchID - 1];
+ XCTAssertEqualObjects(found, (@[]));
+
+ for (NSUInteger i = 0; i < batches.count; i++) {
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:batches[i].batchID];
+ expected = [batches subarrayWithRange:NSMakeRange(0, i + 1)];
+ XCTAssertEqualObjects(found, expected, @"for index %lu", (unsigned long)i);
+ }
+}
+
+- (void)testAllMutationBatchesAffectingDocumentKey {
+ if ([self isTestBaseClass]) return;
+
+ NSArray<FSTMutation *> *mutations = @[
+ FSTTestSetMutation(@"fob/bar",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"foo/bar",
+ @{ @"a" : @1 }),
+ FSTTestPatchMutation(@"foo/bar",
+ @{ @"b" : @1 }, nil),
+ FSTTestSetMutation(@"foo/bar/suffix/key",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"foo/baz",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"food/bar",
+ @{ @"a" : @1 })
+ ];
+
+ // Store all the mutations.
+ NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"];
+ for (FSTMutation *mutation in mutations) {
+ FSTMutationBatch *batch =
+ [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ mutation ]
+ group:group];
+ [batches addObject:batch];
+ }
+ [self.persistence commitGroup:group];
+
+ NSArray<FSTMutationBatch *> *expected = @[ batches[1], batches[2] ];
+ NSArray<FSTMutationBatch *> *matches =
+ [self.mutationQueue allMutationBatchesAffectingDocumentKey:FSTTestDocKey(@"foo/bar")];
+
+ XCTAssertEqualObjects(matches, expected);
+}
+
+- (void)testAllMutationBatchesAffectingQuery {
+ if ([self isTestBaseClass]) return;
+
+ NSArray<FSTMutation *> *mutations = @[
+ FSTTestSetMutation(@"fob/bar",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"foo/bar",
+ @{ @"a" : @1 }),
+ FSTTestPatchMutation(@"foo/bar",
+ @{ @"b" : @1 }, nil),
+ FSTTestSetMutation(@"foo/bar/suffix/key",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"foo/baz",
+ @{ @"a" : @1 }),
+ FSTTestSetMutation(@"food/bar",
+ @{ @"a" : @1 })
+ ];
+
+ // Store all the mutations.
+ NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"];
+ for (FSTMutation *mutation in mutations) {
+ FSTMutationBatch *batch =
+ [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ mutation ]
+ group:group];
+ [batches addObject:batch];
+ }
+ [self.persistence commitGroup:group];
+
+ NSArray<FSTMutationBatch *> *expected = @[ batches[1], batches[2], batches[4] ];
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"foo")];
+ NSArray<FSTMutationBatch *> *matches =
+ [self.mutationQueue allMutationBatchesAffectingQuery:query];
+
+ XCTAssertEqualObjects(matches, expected);
+}
+
+- (void)testRemoveMutationBatches {
+ if ([self isTestBaseClass]) return;
+
+ NSMutableArray<FSTMutationBatch *> *batches = [self createBatches:10];
+ FSTMutationBatch *last = batches[batches.count - 1];
+
+ [self removeMutationBatches:@[ batches[0] ]];
+ [batches removeObjectAtIndex:0];
+ XCTAssertEqual([self batchCount], 9);
+
+ NSArray<FSTMutationBatch *> *found;
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, batches);
+ XCTAssertEqual(found.count, 9);
+
+ [self removeMutationBatches:@[ batches[0], batches[1], batches[2] ]];
+ [batches removeObjectsInRange:NSMakeRange(0, 3)];
+ XCTAssertEqual([self batchCount], 6);
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, batches);
+ XCTAssertEqual(found.count, 6);
+
+ [self removeMutationBatches:@[ batches[batches.count - 1] ]];
+ [batches removeObjectAtIndex:batches.count - 1];
+ XCTAssertEqual([self batchCount], 5);
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, batches);
+ XCTAssertEqual(found.count, 5);
+
+ [self removeMutationBatches:@[ batches[3] ]];
+ [batches removeObjectAtIndex:3];
+ XCTAssertEqual([self batchCount], 4);
+
+ [self removeMutationBatches:@[ batches[1] ]];
+ [batches removeObjectAtIndex:1];
+ XCTAssertEqual([self batchCount], 3);
+
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, batches);
+ XCTAssertEqual(found.count, 3);
+ XCTAssertFalse([self.mutationQueue isEmpty]);
+
+ [self removeMutationBatches:batches];
+ found = [self.mutationQueue allMutationBatchesThroughBatchID:last.batchID];
+ XCTAssertEqualObjects(found, @[]);
+ XCTAssertEqual(found.count, 0);
+ XCTAssertTrue([self.mutationQueue isEmpty]);
+}
+
+- (void)testRemoveMutationBatchesEmitsGarbageEvents {
+ if ([self isTestBaseClass]) return;
+
+ FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init];
+ [garbageCollector addGarbageSource:self.mutationQueue];
+
+ NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
+ [batches addObjectsFromArray:@[
+ [self addMutationBatchWithKey:@"foo/bar"],
+ [self addMutationBatchWithKey:@"foo/ba"],
+ [self addMutationBatchWithKey:@"foo/bar2"],
+ [self addMutationBatchWithKey:@"foo/bar"],
+ [self addMutationBatchWithKey:@"foo/bar/suffix/baz"],
+ [self addMutationBatchWithKey:@"bar/baz"],
+ ]];
+
+ [self removeMutationBatches:@[ batches[0] ]];
+ NSSet<FSTDocumentKey *> *garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[]);
+
+ [self removeMutationBatches:@[ batches[1] ]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/ba") ]);
+
+ [self removeMutationBatches:@[ batches[5] ]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"bar/baz") ]);
+
+ [self removeMutationBatches:@[ batches[2], batches[3] ]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/bar2") ]));
+
+ [batches addObject:[self addMutationBatchWithKey:@"foo/bar/suffix/baz"]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[]);
+
+ [self removeMutationBatches:@[ batches[4], batches[6] ]];
+ garbage = [garbageCollector collectGarbage];
+ FSTAssertEqualSets(garbage, @[ FSTTestDocKey(@"foo/bar/suffix/baz") ]);
+}
+
+- (void)testStreamToken {
+ if ([self isTestBaseClass]) return;
+
+ NSData *streamToken1 = [@"token1" dataUsingEncoding:NSUTF8StringEncoding];
+ NSData *streamToken2 = [@"token2" dataUsingEncoding:NSUTF8StringEncoding];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"initial stream token"];
+ [self.mutationQueue setLastStreamToken:streamToken1 group:group];
+ [self.persistence commitGroup:group];
+
+ FSTMutationBatch *batch1 = [self addMutationBatch];
+ [self addMutationBatch];
+
+ XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken1);
+
+ group = [self.persistence startGroupWithAction:@"acknowledgeBatchID"];
+ [self.mutationQueue acknowledgeBatch:batch1 streamToken:streamToken2 group:group];
+ [self.persistence commitGroup:group];
+
+ XCTAssertEqual(self.mutationQueue.highestAcknowledgedBatchID, batch1.batchID);
+ XCTAssertEqualObjects([self.mutationQueue lastStreamToken], streamToken2);
+}
+
+/** Creates a new FSTMutationBatch with the next batch ID and a set of dummy mutations. */
+- (FSTMutationBatch *)addMutationBatch {
+ return [self addMutationBatchWithKey:@"foo/bar"];
+}
+
+/**
+ * Creates a new FSTMutationBatch with the given key, the next batch ID and a set of dummy
+ * mutations.
+ */
+- (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key {
+ FSTSetMutation *mutation = FSTTestSetMutation(key, @{ @"a" : @1 });
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"New mutation batch"];
+ FSTMutationBatch *batch =
+ [self.mutationQueue addMutationBatchWithWriteTime:[FSTTimestamp timestamp]
+ mutations:@[ mutation ]
+ group:group];
+ [self.persistence commitGroup:group];
+ return batch;
+}
+
+/**
+ * Creates an array of batches containing @a number dummy FSTMutationBatches. Each has a different
+ * batchID.
+ */
+- (NSMutableArray<FSTMutationBatch *> *)createBatches:(int)number {
+ NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
+
+ for (int i = 0; i < number; i++) {
+ FSTMutationBatch *batch = [self addMutationBatch];
+ [batches addObject:batch];
+ }
+
+ return batches;
+}
+
+/**
+ * Calls -acknowledgeBatch:streamToken:group: on the mutation queue in a new group and commits the
+ * the group.
+ */
+- (void)acknowledgeBatch:(FSTMutationBatch *)batch {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Ack batchID"];
+ [self.mutationQueue acknowledgeBatch:batch streamToken:nil group:group];
+ [self.persistence commitGroup:group];
+}
+
+/**
+ * Calls -removeMutationBatches:group: on the mutation queue in a new group and commits the group.
+ */
+- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Remove mutation batch"];
+ [self.mutationQueue removeMutationBatches:batches group:group];
+ [self.persistence commitGroup:group];
+}
+
+/** Returns the number of mutation batches in the mutation queue. */
+- (NSUInteger)batchCount {
+ return [self.mutationQueue allMutationBatches].count;
+}
+
+/**
+ * Removes entries from from the given @a batches and returns them.
+ *
+ * @param holes An array of indexes in the batches array; in increasing order. Indexes are relative
+ * to the original state of the batches array, not any intermediate state that might occur.
+ * @param batches The array to mutate, removing entries from it.
+ * @return A new array containing all the entries that were removed from @a batches.
+ */
+- (NSArray<FSTMutationBatch *> *)makeHoles:(NSArray<NSNumber *> *)holes
+ inBatches:(NSMutableArray<FSTMutationBatch *> *)batches {
+ NSMutableArray<FSTMutationBatch *> *removed = [NSMutableArray array];
+ for (NSUInteger i = 0; i < holes.count; i++) {
+ NSUInteger index = holes[i].unsignedIntegerValue - i;
+ FSTMutationBatch *batch = batches[index];
+ [self removeMutationBatches:@[ batch ]];
+
+ [batches removeObjectAtIndex:index];
+ [removed addObject:batch];
+ }
+ return removed;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h
new file mode 100644
index 0000000..936bacf
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FSTLevelDB;
+@class FSTMemoryPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTPersistenceTestHelpers : NSObject
+
+/**
+ * Creates and starts a new FSTLevelDB instance for testing, destroying any previous contents
+ * if they existed.
+ *
+ * Note that in order to avoid generating a bunch of garbage on the filesystem, the path of the
+ * database is reused. This prevents concurrent running of tests using this database. We may
+ * need to revisit this if we want to parallelize the tests.
+ */
++ (FSTLevelDB *)levelDBPersistence;
+
+/** Creates and starts a new FSTMemoryPersistence instance for testing. */
++ (FSTMemoryPersistence *)memoryPersistence;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m
new file mode 100644
index 0000000..f3d7914
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.m
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTPersistenceTestHelpers.h"
+
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTLocalSerializer.h"
+#import "Local/FSTMemoryPersistence.h"
+#import "Model/FSTDatabaseID.h"
+#import "Remote/FSTSerializerBeta.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTPersistenceTestHelpers
+
++ (FSTLevelDB *)levelDBPersistence {
+ NSError *error;
+ NSFileManager *files = [NSFileManager defaultManager];
+
+ NSString *dir =
+ [NSTemporaryDirectory() stringByAppendingPathComponent:@"FSTPersistenceTestHelpers"];
+ if ([files fileExistsAtPath:dir]) {
+ // Delete the directory first to ensure isolation between runs.
+ BOOL success = [files removeItemAtPath:dir error:&error];
+ if (!success) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"Failed to clean up leveldb path %@: %@", dir, error];
+ }
+ }
+
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ FSTSerializerBeta *remoteSerializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID];
+ FSTLocalSerializer *serializer =
+ [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer];
+ FSTLevelDB *db = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer];
+ BOOL success = [db start:&error];
+ if (!success) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"Failed to create leveldb path %@: %@", dir, error];
+ }
+
+ return db;
+}
+
++ (FSTMemoryPersistence *)memoryPersistence {
+ NSError *error;
+ FSTMemoryPersistence *persistence = [FSTMemoryPersistence persistence];
+ BOOL success = [persistence start:&error];
+ if (!success) {
+ [NSException raise:NSInternalInconsistencyException
+ format:@"Failed to start memory persistence: %@", error];
+ }
+
+ return persistence;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.h b/Firestore/Example/Tests/Local/FSTQueryCacheTests.h
new file mode 100644
index 0000000..a615372
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTQueryCache.h"
+
+#import <XCTest/XCTest.h>
+
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * These are tests for any implementation of the FSTQueryCache protocol.
+ *
+ * To test a specific implementation of FSTQueryCache:
+ *
+ * + Subclass FSTQueryCacheTests
+ * + override -setUp, assigning to queryCache and persistence
+ * + override -tearDown, cleaning up queryCache and persistence
+ */
+@interface FSTQueryCacheTests : XCTestCase
+
+/** The implementation of the query cache to test. */
+@property(nonatomic, strong, nullable) id<FSTQueryCache> queryCache;
+
+/**
+ * The persistence implementation to use while testing the queryCache (e.g. for committing write
+ * groups).
+ */
+@property(nonatomic, strong, nullable) id<FSTPersistence> persistence;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.m b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m
new file mode 100644
index 0000000..ed409b4
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.m
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTQueryCacheTests.h"
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Local/FSTEagerGarbageCollector.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTQueryData.h"
+#import "Local/FSTWriteGroup.h"
+#import "Model/FSTDocumentKey.h"
+
+#import "FSTHelpers.h"
+#import "FSTImmutableSortedSet+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTQueryCacheTests {
+ FSTQuery *_queryRooms;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ _queryRooms = FSTTestQuery(@"rooms");
+}
+
+/**
+ * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for
+ * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses.
+ */
+- (BOOL)isTestBaseClass {
+ return [self class] == [FSTQueryCacheTests class];
+}
+
+- (void)testReadQueryNotInCache {
+ if ([self isTestBaseClass]) return;
+
+ XCTAssertNil([self.queryCache queryDataForQuery:_queryRooms]);
+}
+
+- (void)testSetAndReadAQuery {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+ [self addQueryData:queryData];
+
+ FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms];
+ XCTAssertEqualObjects(result.query, queryData.query);
+ XCTAssertEqual(result.targetID, queryData.targetID);
+ XCTAssertEqualObjects(result.resumeToken, queryData.resumeToken);
+}
+
+- (void)testCanonicalIDCollision {
+ if ([self isTestBaseClass]) return;
+
+ // Type information is currently lost in our canonicalID implementations so this currently an
+ // easy way to force colliding canonicalIDs
+ FSTQuery *q1 = [[FSTQuery queryWithPath:FSTTestPath(@"a")]
+ queryByAddingFilter:FSTTestFilter(@"foo", @"==", @(1))];
+ FSTQuery *q2 = [[FSTQuery queryWithPath:FSTTestPath(@"a")]
+ queryByAddingFilter:FSTTestFilter(@"foo", @"==", @"1")];
+ XCTAssertEqualObjects(q1.canonicalID, q2.canonicalID);
+
+ FSTQueryData *data1 = [self queryDataWithQuery:q1 targetID:1 version:1];
+ [self addQueryData:data1];
+
+ // Using the other query should not return the query cache entry despite equal canonicalIDs.
+ XCTAssertNil([self.queryCache queryDataForQuery:q2]);
+ XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1);
+
+ FSTQueryData *data2 = [self queryDataWithQuery:q2 targetID:2 version:1];
+ [self addQueryData:data2];
+
+ XCTAssertEqualObjects([self.queryCache queryDataForQuery:q1], data1);
+ XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2);
+
+ [self removeQueryData:data1];
+ XCTAssertNil([self.queryCache queryDataForQuery:q1]);
+ XCTAssertEqualObjects([self.queryCache queryDataForQuery:q2], data2);
+
+ [self removeQueryData:data2];
+ XCTAssertNil([self.queryCache queryDataForQuery:q1]);
+ XCTAssertNil([self.queryCache queryDataForQuery:q2]);
+}
+
+- (void)testSetQueryToNewValue {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+ [self addQueryData:queryData1];
+
+ FSTQueryData *queryData2 = [self queryDataWithQuery:_queryRooms targetID:1 version:2];
+ [self addQueryData:queryData2];
+
+ FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms];
+ XCTAssertNotEqualObjects(queryData2.resumeToken, queryData1.resumeToken);
+ XCTAssertNotEqualObjects(queryData2.snapshotVersion, queryData1.snapshotVersion);
+ XCTAssertEqualObjects(result.resumeToken, queryData2.resumeToken);
+ XCTAssertEqualObjects(result.snapshotVersion, queryData2.snapshotVersion);
+}
+
+- (void)testRemoveQuery {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+ [self addQueryData:queryData1];
+
+ [self removeQueryData:queryData1];
+
+ FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms];
+ XCTAssertNil(result);
+}
+
+- (void)testRemoveNonExistentQuery {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+
+ // no-op, but make sure it doesn't throw.
+ XCTAssertNoThrow([self removeQueryData:queryData]);
+}
+
+- (void)testRemoveQueryRemovesMatchingKeysToo {
+ if ([self isTestBaseClass]) return;
+
+ FSTQueryData *rooms = [self queryDataWithQuery:_queryRooms targetID:1 version:1];
+ [self addQueryData:rooms];
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/foo"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/bar"];
+ [self addMatchingKey:key1 forTargetID:rooms.targetID];
+ [self addMatchingKey:key2 forTargetID:rooms.targetID];
+
+ XCTAssertTrue([self.queryCache containsKey:key1]);
+ XCTAssertTrue([self.queryCache containsKey:key2]);
+
+ [self removeQueryData:rooms];
+ XCTAssertFalse([self.queryCache containsKey:key1]);
+ XCTAssertFalse([self.queryCache containsKey:key2]);
+}
+
+- (void)testAddOrRemoveMatchingKeys {
+ if ([self isTestBaseClass]) return;
+
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+
+ XCTAssertFalse([self.queryCache containsKey:key]);
+
+ [self addMatchingKey:key forTargetID:1];
+ XCTAssertTrue([self.queryCache containsKey:key]);
+
+ [self addMatchingKey:key forTargetID:2];
+ XCTAssertTrue([self.queryCache containsKey:key]);
+
+ [self removeMatchingKey:key forTargetID:1];
+ XCTAssertTrue([self.queryCache containsKey:key]);
+
+ [self removeMatchingKey:key forTargetID:2];
+ XCTAssertFalse([self.queryCache containsKey:key]);
+}
+
+- (void)testRemoveMatchingKeysForTargetID {
+ if ([self isTestBaseClass]) return;
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"];
+
+ [self addMatchingKey:key1 forTargetID:1];
+ [self addMatchingKey:key2 forTargetID:1];
+ [self addMatchingKey:key3 forTargetID:2];
+ XCTAssertTrue([self.queryCache containsKey:key1]);
+ XCTAssertTrue([self.queryCache containsKey:key2]);
+ XCTAssertTrue([self.queryCache containsKey:key3]);
+
+ [self removeMatchingKeysForTargetID:1];
+ XCTAssertFalse([self.queryCache containsKey:key1]);
+ XCTAssertFalse([self.queryCache containsKey:key2]);
+ XCTAssertTrue([self.queryCache containsKey:key3]);
+
+ [self removeMatchingKeysForTargetID:2];
+ XCTAssertFalse([self.queryCache containsKey:key1]);
+ XCTAssertFalse([self.queryCache containsKey:key2]);
+ XCTAssertFalse([self.queryCache containsKey:key3]);
+}
+
+- (void)testRemoveEmitsGarbageEvents {
+ if ([self isTestBaseClass]) return;
+
+ FSTEagerGarbageCollector *garbageCollector = [[FSTEagerGarbageCollector alloc] init];
+ [garbageCollector addGarbageSource:self.queryCache];
+ FSTAssertEqualSets([garbageCollector collectGarbage], @[]);
+
+ FSTQueryData *rooms = [self queryDataWithQuery:FSTTestQuery(@"rooms") targetID:1 version:1];
+ FSTDocumentKey *room1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"];
+ FSTDocumentKey *room2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"];
+ [self addQueryData:rooms];
+ [self addMatchingKey:room1 forTargetID:rooms.targetID];
+ [self addMatchingKey:room2 forTargetID:rooms.targetID];
+
+ FSTQueryData *halls = [self queryDataWithQuery:FSTTestQuery(@"halls") targetID:2 version:1];
+ FSTDocumentKey *hall1 = [FSTDocumentKey keyWithPathString:@"halls/bar"];
+ FSTDocumentKey *hall2 = [FSTDocumentKey keyWithPathString:@"halls/foo"];
+ [self addQueryData:halls];
+ [self addMatchingKey:hall1 forTargetID:halls.targetID];
+ [self addMatchingKey:hall2 forTargetID:halls.targetID];
+
+ FSTAssertEqualSets([garbageCollector collectGarbage], @[]);
+
+ [self removeMatchingKey:room1 forTargetID:rooms.targetID];
+ FSTAssertEqualSets([garbageCollector collectGarbage], @[ room1 ]);
+
+ [self removeQueryData:rooms];
+ FSTAssertEqualSets([garbageCollector collectGarbage], @[ room2 ]);
+
+ [self removeMatchingKeysForTargetID:halls.targetID];
+ FSTAssertEqualSets([garbageCollector collectGarbage], (@[ hall1, hall2 ]));
+}
+
+- (void)testMatchingKeysForTargetID {
+ if ([self isTestBaseClass]) return;
+
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"];
+
+ [self addMatchingKey:key1 forTargetID:1];
+ [self addMatchingKey:key2 forTargetID:1];
+ [self addMatchingKey:key3 forTargetID:2];
+
+ FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ]));
+ FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], @[ key3 ]);
+
+ [self addMatchingKey:key1 forTargetID:2];
+ FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ]));
+ FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], (@[ key1, key3 ]));
+}
+
+- (void)testHighestTargetID {
+ if ([self isTestBaseClass]) return;
+
+ XCTAssertEqual([self.queryCache highestTargetID], 0);
+
+ FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"rooms")
+ targetID:1
+ purpose:FSTQueryPurposeListen];
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"rooms/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"rooms/foo"];
+ [self addQueryData:query1];
+ [self addMatchingKey:key1 forTargetID:1];
+ [self addMatchingKey:key2 forTargetID:1];
+
+ FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"halls")
+ targetID:2
+ purpose:FSTQueryPurposeListen];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"halls/foo"];
+ [self addQueryData:query2];
+ [self addMatchingKey:key3 forTargetID:2];
+ XCTAssertEqual([self.queryCache highestTargetID], 2);
+
+ // TargetIDs never come down.
+ [self removeQueryData:query2];
+ XCTAssertEqual([self.queryCache highestTargetID], 2);
+
+ // A query with an empty result set still counts.
+ FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:FSTTestQuery(@"garages")
+ targetID:42
+ purpose:FSTQueryPurposeListen];
+ [self addQueryData:query3];
+ XCTAssertEqual([self.queryCache highestTargetID], 42);
+
+ [self removeQueryData:query1];
+ XCTAssertEqual([self.queryCache highestTargetID], 42);
+
+ [self removeQueryData:query3];
+ XCTAssertEqual([self.queryCache highestTargetID], 42);
+
+ // Verify that the highestTargetID even survives restarts.
+ [self.queryCache shutdown];
+ self.queryCache = [self.persistence queryCache];
+ [self.queryCache start];
+ XCTAssertEqual([self.queryCache highestTargetID], 42);
+}
+
+- (void)testLastRemoteSnapshotVersion {
+ if ([self isTestBaseClass]) return;
+
+ XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion],
+ [FSTSnapshotVersion noVersion]);
+
+ // Can set the snapshot version.
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"setLastRemoteSnapshotVersion"];
+ [self.queryCache setLastRemoteSnapshotVersion:FSTTestVersion(42) group:group];
+ [self.persistence commitGroup:group];
+ XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42));
+
+ // Snapshot version persists restarts.
+ self.queryCache = [self.persistence queryCache];
+ [self.queryCache start];
+ XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42));
+}
+
+#pragma mark - Helpers
+
+/**
+ * Creates a new FSTQueryData object from the given parameters, synthesizing a resume token from
+ * the snapshot version.
+ */
+- (FSTQueryData *)queryDataWithQuery:(FSTQuery *)query
+ targetID:(FSTTargetID)targetID
+ version:(FSTTestSnapshotVersion)version {
+ NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(version);
+ return [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:FSTTestVersion(version)
+ resumeToken:resumeToken];
+}
+
+/** Adds the given query data to the queryCache under test, committing immediately. */
+- (void)addQueryData:(FSTQueryData *)queryData {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addQueryData"];
+ [self.queryCache addQueryData:queryData group:group];
+ [self.persistence commitGroup:group];
+}
+
+/** Removes the given query data from the queryCache under test, committing immediately. */
+- (void)removeQueryData:(FSTQueryData *)queryData {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeQueryData"];
+ [self.queryCache removeQueryData:queryData group:group];
+ [self.persistence commitGroup:group];
+}
+
+- (void)addMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID {
+ FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet];
+ keys = [keys setByAddingObject:key];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addMatchingKeys"];
+ [self.queryCache addMatchingKeys:keys forTargetID:targetID group:group];
+ [self.persistence commitGroup:group];
+}
+
+- (void)removeMatchingKey:(FSTDocumentKey *)key forTargetID:(FSTTargetID)targetID {
+ FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet];
+ keys = [keys setByAddingObject:key];
+
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeys"];
+ [self.queryCache removeMatchingKeys:keys forTargetID:targetID group:group];
+ [self.persistence commitGroup:group];
+}
+
+- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeMatchingKeysForTargetID"];
+ [self.queryCache removeMatchingKeysForTargetID:targetID group:group];
+ [self.persistence commitGroup:group];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTReferenceSetTests.m b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m
new file mode 100644
index 0000000..a8c783a
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTReferenceSetTests.m
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTReferenceSet.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTDocumentKey.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTReferenceSetTests : XCTestCase
+@end
+
+@implementation FSTReferenceSetTests
+
+- (void)testAddOrRemoveReferences {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+
+ FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init];
+ XCTAssertTrue([referenceSet isEmpty]);
+ XCTAssertFalse([referenceSet containsKey:key]);
+
+ [referenceSet addReferenceToKey:key forID:1];
+ XCTAssertTrue([referenceSet containsKey:key]);
+ XCTAssertFalse([referenceSet isEmpty]);
+
+ [referenceSet addReferenceToKey:key forID:2];
+ XCTAssertTrue([referenceSet containsKey:key]);
+
+ [referenceSet removeReferenceToKey:key forID:1];
+ XCTAssertTrue([referenceSet containsKey:key]);
+
+ [referenceSet removeReferenceToKey:key forID:3];
+ XCTAssertTrue([referenceSet containsKey:key]);
+
+ [referenceSet removeReferenceToKey:key forID:2];
+ XCTAssertFalse([referenceSet containsKey:key]);
+ XCTAssertTrue([referenceSet isEmpty]);
+}
+
+- (void)testRemoveAllReferencesForTargetID {
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"foo/bar"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"foo/baz"];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithPathString:@"foo/blah"];
+ FSTReferenceSet *referenceSet = [[FSTReferenceSet alloc] init];
+
+ [referenceSet addReferenceToKey:key1 forID:1];
+ [referenceSet addReferenceToKey:key2 forID:1];
+ [referenceSet addReferenceToKey:key3 forID:2];
+ XCTAssertFalse([referenceSet isEmpty]);
+ XCTAssertTrue([referenceSet containsKey:key1]);
+ XCTAssertTrue([referenceSet containsKey:key2]);
+ XCTAssertTrue([referenceSet containsKey:key3]);
+
+ [referenceSet removeReferencesForID:1];
+ XCTAssertFalse([referenceSet isEmpty]);
+ XCTAssertFalse([referenceSet containsKey:key1]);
+ XCTAssertFalse([referenceSet containsKey:key2]);
+ XCTAssertTrue([referenceSet containsKey:key3]);
+
+ [referenceSet removeReferencesForID:2];
+ XCTAssertTrue([referenceSet isEmpty]);
+ XCTAssertFalse([referenceSet containsKey:key1]);
+ XCTAssertFalse([referenceSet containsKey:key2]);
+ XCTAssertFalse([referenceSet containsKey:key3]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h
new file mode 100644
index 0000000..fa2a857
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTRemoteDocumentCache.h"
+
+#import <XCTest/XCTest.h>
+
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * These are tests for any implementation of the FSTRemoteDocumentCache protocol.
+ *
+ * To test a specific implementation of FSTRemoteDocumentCache:
+ *
+ * + Subclass FSTRemoteDocumentCacheTests
+ * + override -setUp, assigning to remoteDocumentCache and persistence
+ * + override -tearDown, cleaning up remoteDocumentCache and persistence
+ */
+@interface FSTRemoteDocumentCacheTests : XCTestCase
+@property(nonatomic, strong, nullable) id<FSTRemoteDocumentCache> remoteDocumentCache;
+@property(nonatomic, strong, nullable) id<FSTPersistence> persistence;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m
new file mode 100644
index 0000000..a875934
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.m
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTRemoteDocumentCacheTests.h"
+
+#import "Core/FSTQuery.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTWriteGroup.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTDocumentSet.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const kDocPath = @"a/b";
+static NSString *const kLongDocPath = @"a/b/c/d/e/f";
+static const int kVersion = 42;
+
+@implementation FSTRemoteDocumentCacheTests {
+ NSDictionary<NSString *, id> *_kDocData;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ // essentially a constant, but can't be a compile-time one.
+ _kDocData = @{ @"a" : @1, @"b" : @2 };
+}
+
+- (void)testReadDocumentNotInCache {
+ if (!self.remoteDocumentCache) return;
+
+ XCTAssertNil([self readEntryAtPath:kDocPath]);
+}
+
+// Helper for next two tests.
+- (void)setAndReadADocumentAtPath:(NSString *)path {
+ FSTDocument *written = [self setTestDocumentAtPath:path];
+ FSTMaybeDocument *read = [self readEntryAtPath:path];
+ XCTAssertEqualObjects(read, written);
+}
+
+- (void)testSetAndReadADocument {
+ if (!self.remoteDocumentCache) return;
+
+ [self setAndReadADocumentAtPath:kDocPath];
+}
+
+- (void)testSetAndReadADocumentAtDeepPath {
+ if (!self.remoteDocumentCache) return;
+
+ [self setAndReadADocumentAtPath:kLongDocPath];
+}
+
+- (void)testSetAndReadDeletedDocument {
+ if (!self.remoteDocumentCache) return;
+
+ FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc(kDocPath, kVersion);
+ [self addEntry:deletedDoc];
+
+ XCTAssertEqualObjects([self readEntryAtPath:kDocPath], deletedDoc);
+}
+
+- (void)testSetDocumentToNewValue {
+ if (!self.remoteDocumentCache) return;
+
+ [self setTestDocumentAtPath:kDocPath];
+ FSTDocument *newDoc = FSTTestDoc(kDocPath, kVersion, @{ @"data" : @2 }, NO);
+ [self addEntry:newDoc];
+ XCTAssertEqualObjects([self readEntryAtPath:kDocPath], newDoc);
+}
+
+- (void)testRemoveDocument {
+ if (!self.remoteDocumentCache) return;
+
+ [self setTestDocumentAtPath:kDocPath];
+ [self removeEntryAtPath:kDocPath];
+
+ XCTAssertNil([self readEntryAtPath:kDocPath]);
+}
+
+- (void)testRemoveNonExistentDocument {
+ if (!self.remoteDocumentCache) return;
+
+ // no-op, but make sure it doesn't throw.
+ XCTAssertNoThrow([self removeEntryAtPath:kDocPath]);
+}
+
+// TODO(mikelehen): Write more elaborate tests once we have more elaborate implementations.
+- (void)testDocumentsMatchingQuery {
+ if (!self.remoteDocumentCache) return;
+
+ [self setTestDocumentAtPath:@"a/1"];
+ [self setTestDocumentAtPath:@"b/1"];
+ [self setTestDocumentAtPath:@"b/2"];
+ [self setTestDocumentAtPath:@"c/1"];
+
+ FSTQuery *query = [FSTQuery queryWithPath:FSTTestPath(@"b")];
+ FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query];
+ NSArray *expected =
+ @[ FSTTestDoc(@"b/1", kVersion, _kDocData, NO), FSTTestDoc(@"b/2", kVersion, _kDocData, NO) ];
+ for (FSTDocument *doc in expected) {
+ XCTAssertEqualObjects([results objectForKey:doc.key], doc);
+ }
+
+ // TODO(mikelehen): Perhaps guard against extra documents in the result set once our
+ // implementations are smarter.
+}
+
+#pragma mark - Helpers
+
+- (FSTDocument *)setTestDocumentAtPath:(NSString *)path {
+ FSTDocument *doc = FSTTestDoc(path, kVersion, _kDocData, NO);
+ [self addEntry:doc];
+ return doc;
+}
+
+- (void)addEntry:(FSTMaybeDocument *)maybeDoc {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"addEntry"];
+ [self.remoteDocumentCache addEntry:maybeDoc group:group];
+ [self.persistence commitGroup:group];
+}
+
+- (FSTMaybeDocument *_Nullable)readEntryAtPath:(NSString *)path {
+ return [self.remoteDocumentCache entryForKey:FSTTestDocKey(path)];
+}
+
+- (void)removeEntryAtPath:(NSString *)path {
+ FSTWriteGroup *group = [self.persistence startGroupWithAction:@"removeEntryAtPath"];
+ [self.remoteDocumentCache removeEntryForKey:FSTTestDocKey(path) group:group];
+ [self.persistence commitGroup:group];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m
new file mode 100644
index 0000000..ebf7713
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentChangeBufferTests.m
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTRemoteDocumentChangeBuffer.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTRemoteDocumentCache.h"
+#import "Model/FSTDocument.h"
+
+#import "FSTHelpers.h"
+#import "FSTPersistenceTestHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTRemoteDocumentChangeBufferTests : XCTestCase
+@end
+
+@implementation FSTRemoteDocumentChangeBufferTests {
+ FSTLevelDB *_db;
+ id<FSTRemoteDocumentCache> _remoteDocumentCache;
+ FSTRemoteDocumentChangeBuffer *_remoteDocumentBuffer;
+
+ FSTMaybeDocument *_kInitialADoc;
+ FSTMaybeDocument *_kInitialBDoc;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ _db = [FSTPersistenceTestHelpers levelDBPersistence];
+ _remoteDocumentCache = [_db remoteDocumentCache];
+
+ // Add a couple initial items to the cache.
+ FSTWriteGroup *group = [_db startGroupWithAction:@"Add initial docs."];
+ _kInitialADoc = FSTTestDoc(@"coll/a", 42, @{@"test" : @"data"}, NO);
+ [_remoteDocumentCache addEntry:_kInitialADoc group:group];
+
+ _kInitialBDoc =
+ [FSTDeletedDocument documentWithKey:FSTTestDocKey(@"coll/b") version:FSTTestVersion(314)];
+ [_remoteDocumentCache addEntry:_kInitialBDoc group:group];
+ [_db commitGroup:group];
+
+ _remoteDocumentBuffer =
+ [FSTRemoteDocumentChangeBuffer changeBufferWithCache:_remoteDocumentCache];
+}
+
+- (void)tearDown {
+ _remoteDocumentBuffer = nil;
+ _remoteDocumentCache = nil;
+ _db = nil;
+
+ [super tearDown];
+}
+
+- (void)testReadUnchangedEntry {
+ XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")],
+ _kInitialADoc);
+}
+
+- (void)testAddEntryAndReadItBack {
+ FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO);
+ [_remoteDocumentBuffer addEntry:newADoc];
+ XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc);
+
+ // B should still be unchanged.
+ XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/b")],
+ _kInitialBDoc);
+}
+
+- (void)testApplyChanges {
+ FSTMaybeDocument *newADoc = FSTTestDoc(@"coll/a", 43, @{@"new" : @"data"}, NO);
+ [_remoteDocumentBuffer addEntry:newADoc];
+ XCTAssertEqualObjects([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")], newADoc);
+
+ // Reading directly against the cache should still yield the old result.
+ XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], _kInitialADoc);
+
+ FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"];
+ [_remoteDocumentBuffer applyToWriteGroup:group];
+ [_db commitGroup:group];
+
+ // Reading against the cache should now yield the new result.
+ XCTAssertEqualObjects([_remoteDocumentCache entryForKey:FSTTestDocKey(@"coll/a")], newADoc);
+}
+
+- (void)testMethodsThrowAfterApply {
+ FSTWriteGroup *group = [_db startGroupWithAction:@"Apply changes"];
+ [_remoteDocumentBuffer applyToWriteGroup:group];
+ [_db commitGroup:group];
+
+ XCTAssertThrows([_remoteDocumentBuffer entryForKey:FSTTestDocKey(@"coll/a")]);
+ XCTAssertThrows([_remoteDocumentBuffer addEntry:_kInitialADoc]);
+ XCTAssertThrows([_remoteDocumentBuffer applyToWriteGroup:group]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm b/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm
new file mode 100644
index 0000000..1cd2feb
--- /dev/null
+++ b/Firestore/Example/Tests/Local/FSTWriteGroupTests.mm
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Local/FSTWriteGroup.h"
+
+#import <XCTest/XCTest.h>
+#include <leveldb/db.h>
+
+#import "Protos/objc/firestore/local/Mutation.pbobjc.h"
+#import "Local/FSTLevelDB.h"
+#import "Local/FSTLevelDBKey.h"
+
+#import "FSTPersistenceTestHelpers.h"
+
+using leveldb::ReadOptions;
+using leveldb::Status;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTWriteGroupTests : XCTestCase
+@end
+
+@implementation FSTWriteGroupTests {
+ FSTLevelDB *_db;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ _db = [FSTPersistenceTestHelpers levelDBPersistence];
+}
+
+- (void)tearDown {
+ _db = nil;
+
+ [super tearDown];
+}
+
+- (void)testCommit {
+ std::string key = [FSTLevelDBMutationKey keyWithUserID:"user1" batchID:42];
+ FSTPBWriteBatch *message = [FSTPBWriteBatch message];
+ message.batchId = 42;
+
+ // This is a test that shows that committing an empty group does not fail. There are no side
+ // effects to verify though.
+ FSTWriteGroup *group = [_db startGroupWithAction:@"Empty commit"];
+ XCTAssertNoThrow([_db commitGroup:group]);
+
+ group = [_db startGroupWithAction:@"Put"];
+ [group setMessage:message forKey:key];
+
+ std::string value;
+ Status status = _db.ptr->Get(ReadOptions(), key, &value);
+ XCTAssertTrue(status.IsNotFound());
+
+ [_db commitGroup:group];
+ status = _db.ptr->Get(ReadOptions(), key, &value);
+ XCTAssertTrue(status.ok());
+
+ group = [_db startGroupWithAction:@"Delete"];
+ [group removeMessageForKey:key];
+ status = _db.ptr->Get(ReadOptions(), key, &value);
+ XCTAssertTrue(status.ok());
+
+ [_db commitGroup:group];
+ status = _db.ptr->Get(ReadOptions(), key, &value);
+ XCTAssertTrue(status.IsNotFound());
+}
+
+- (void)testDescription {
+ std::string key = [FSTLevelDBMutationKey keyWithUserID:"user1" batchID:42];
+ FSTPBWriteBatch *message = [FSTPBWriteBatch message];
+ message.batchId = 42;
+
+ FSTWriteGroup *group = [FSTWriteGroup groupWithAction:@"Action"];
+ XCTAssertEqualObjects([group description], @"<FSTWriteGroup for Action: 0 changes (0 bytes):>");
+
+ [group setMessage:message forKey:key];
+ XCTAssertEqualObjects([group description],
+ @"<FSTWriteGroup for Action: 1 changes (2 bytes):\n"
+ " - Put [mutation: userID=user1 batchID=42] (2 bytes)>");
+
+ [group removeMessageForKey:key];
+ XCTAssertEqualObjects([group description],
+ @"<FSTWriteGroup for Action: 2 changes (2 bytes):\n"
+ " - Put [mutation: userID=user1 batchID=42] (2 bytes)\n"
+ " - Delete [mutation: userID=user1 batchID=42]>");
+}
+
+- (void)testCommittingWrongGroupThrows {
+ // If you don't create the group through persistence, it should throw.
+ FSTWriteGroup *group = [FSTWriteGroup groupWithAction:@"group"];
+ XCTAssertThrows([_db commitGroup:group]);
+}
+
+- (void)testCommittingTwiceThrows {
+ FSTWriteGroup *group = [_db startGroupWithAction:@"group"];
+ [_db commitGroup:group];
+ XCTAssertThrows([_db commitGroup:group]);
+}
+
+- (void)testNestingGroupsThrows {
+ [_db startGroupWithAction:@"group1"];
+ XCTAssertThrows([_db startGroupWithAction:@"group2"]);
+}
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m
new file mode 100644
index 0000000..9e7299f
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTDatabaseIDTests.m
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Model/FSTDatabaseID.h"
+
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDatabaseIDTests : XCTestCase
+@end
+
+@implementation FSTDatabaseIDTests
+
+- (void)testConstructor {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ XCTAssertEqualObjects(databaseID.projectID, @"p");
+ XCTAssertEqualObjects(databaseID.databaseID, @"d");
+ XCTAssertFalse([databaseID isDefaultDatabase]);
+}
+
+- (void)testDefaultDatabase {
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:@"p" database:kDefaultDatabaseID];
+ XCTAssertEqualObjects(databaseID.projectID, @"p");
+ XCTAssertEqualObjects(databaseID.databaseID, @"(default)");
+ XCTAssertTrue([databaseID isDefaultDatabase]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m
new file mode 100644
index 0000000..ba68009
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTDocumentKeyTests.m
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Model/FSTDocumentKey.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTPath.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDocumentKeyTests : XCTestCase
+@end
+
+@implementation FSTDocumentKeyTests
+
+- (void)testConstructor {
+ FSTResourcePath *path =
+ [FSTResourcePath pathWithSegments:@[ @"rooms", @"firestore", @"messages", @"1" ]];
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:path];
+ XCTAssertEqual(path, key.path);
+}
+
+- (void)testComparison {
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithSegments:@[ @"a", @"b", @"c", @"d" ]];
+ FSTDocumentKey *key3 = [FSTDocumentKey keyWithSegments:@[ @"x", @"y", @"z", @"w" ]];
+ XCTAssertTrue([key1 isEqualToKey:key2]);
+ XCTAssertFalse([key1 isEqualToKey:key3]);
+
+ FSTDocumentKey *empty = [FSTDocumentKey keyWithSegments:@[]];
+ FSTDocumentKey *a = [FSTDocumentKey keyWithSegments:@[ @"a", @"a" ]];
+ FSTDocumentKey *b = [FSTDocumentKey keyWithSegments:@[ @"b", @"b" ]];
+ FSTDocumentKey *ab = [FSTDocumentKey keyWithSegments:@[ @"a", @"a", @"b", @"b" ]];
+
+ XCTAssertEqual(NSOrderedAscending, [empty compare:a]);
+ XCTAssertEqual(NSOrderedAscending, [a compare:b]);
+ XCTAssertEqual(NSOrderedAscending, [a compare:ab]);
+
+ XCTAssertEqual(NSOrderedDescending, [a compare:empty]);
+ XCTAssertEqual(NSOrderedDescending, [b compare:a]);
+ XCTAssertEqual(NSOrderedDescending, [ab compare:a]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTDocumentSetTests.m b/Firestore/Example/Tests/Model/FSTDocumentSetTests.m
new file mode 100644
index 0000000..e473088
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTDocumentSetTests.m
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Model/FSTDocumentSet.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTDocument.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDocumentSetTests : XCTestCase
+@end
+
+@implementation FSTDocumentSetTests {
+ NSComparator _comp;
+ FSTDocument *_doc1;
+ FSTDocument *_doc2;
+ FSTDocument *_doc3;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ _comp = FSTTestDocComparator(@"sort");
+ _doc1 = FSTTestDoc(@"docs/1", 0, @{ @"sort" : @2 }, NO);
+ _doc2 = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @3 }, NO);
+ _doc3 = FSTTestDoc(@"docs/3", 0, @{ @"sort" : @1 }, NO);
+}
+
+- (void)testCount {
+ XCTAssertEqual([FSTTestDocSet(_comp, @[]) count], 0);
+ XCTAssertEqual([FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]) count], 3);
+}
+
+- (void)testHasKey {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]);
+
+ XCTAssertTrue([set containsKey:_doc1.key]);
+ XCTAssertTrue([set containsKey:_doc2.key]);
+ XCTAssertFalse([set containsKey:_doc3.key]);
+}
+
+- (void)testDocumentForKey {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2 ]);
+
+ XCTAssertEqualObjects([set documentForKey:_doc1.key], _doc1);
+ XCTAssertEqualObjects([set documentForKey:_doc2.key], _doc2);
+ XCTAssertNil([set documentForKey:_doc3.key]);
+}
+
+- (void)testFirstAndLastDocument {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[]);
+ XCTAssertNil([set firstDocument]);
+ XCTAssertNil([set lastDocument]);
+
+ set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+ XCTAssertEqualObjects([set firstDocument], _doc3);
+ XCTAssertEqualObjects([set lastDocument], _doc2);
+}
+
+- (void)testKeepsDocumentsInTheRightOrder {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+ XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ]));
+}
+
+- (void)testPredecessorDocumentForKey {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+
+ XCTAssertNil([set predecessorDocumentForKey:_doc3.key]);
+ XCTAssertEqualObjects([set predecessorDocumentForKey:_doc1.key], _doc3);
+ XCTAssertEqualObjects([set predecessorDocumentForKey:_doc2.key], _doc1);
+}
+
+- (void)testDeletes {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+
+ FSTDocumentSet *setWithoutDoc1 = [set documentSetByRemovingKey:_doc1.key];
+ XCTAssertEqualObjects([[setWithoutDoc1 documentEnumerator] allObjects], (@[ _doc3, _doc2 ]));
+ XCTAssertEqual([setWithoutDoc1 count], 2);
+
+ // Original remains unchanged
+ XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, _doc2 ]));
+
+ FSTDocumentSet *setWithoutDoc3 = [setWithoutDoc1 documentSetByRemovingKey:_doc3.key];
+ XCTAssertEqualObjects([[setWithoutDoc3 documentEnumerator] allObjects], (@[ _doc2 ]));
+ XCTAssertEqual([setWithoutDoc3 count], 1);
+}
+
+- (void)testUpdates {
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+
+ FSTDocument *doc2Prime = FSTTestDoc(@"docs/2", 0, @{ @"sort" : @9 }, NO);
+
+ set = [set documentSetByAddingDocument:doc2Prime];
+ XCTAssertEqual([set count], 3);
+ XCTAssertEqualObjects([set documentForKey:doc2Prime.key], doc2Prime);
+ XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc3, _doc1, doc2Prime ]));
+}
+
+- (void)testAddsDocsWithEqualComparisonValues {
+ FSTDocument *doc4 = FSTTestDoc(@"docs/4", 0, @{ @"sort" : @2 }, NO);
+
+ FSTDocumentSet *set = FSTTestDocSet(_comp, @[ _doc1, doc4 ]);
+ XCTAssertEqualObjects([[set documentEnumerator] allObjects], (@[ _doc1, doc4 ]));
+}
+
+- (void)testIsEqual {
+ FSTDocumentSet *set1 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]);
+ FSTDocumentSet *set2 = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2, _doc3 ]);
+ XCTAssertEqualObjects(set1, set1);
+ XCTAssertEqualObjects(set1, set2);
+ XCTAssertNotEqualObjects(set1, nil);
+
+ FSTDocumentSet *sortedSet1 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+ FSTDocumentSet *sortedSet2 = FSTTestDocSet(_comp, @[ _doc1, _doc2, _doc3 ]);
+ XCTAssertEqualObjects(sortedSet1, sortedSet1);
+ XCTAssertEqualObjects(sortedSet1, sortedSet2);
+ XCTAssertNotEqualObjects(sortedSet1, nil);
+
+ FSTDocumentSet *shortSet = FSTTestDocSet(FSTDocumentComparatorByKey, @[ _doc1, _doc2 ]);
+ XCTAssertNotEqualObjects(set1, shortSet);
+ XCTAssertNotEqualObjects(set1, sortedSet1);
+}
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTDocumentTests.m b/Firestore/Example/Tests/Model/FSTDocumentTests.m
new file mode 100644
index 0000000..9ceb0cd
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTDocumentTests.m
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Model/FSTDocument.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTSnapshotVersion.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTDocumentTests : XCTestCase
+@end
+
+@implementation FSTDocumentTests
+
+- (void)testConstructor {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"messages/first"];
+ FSTSnapshotVersion *version = FSTTestVersion(1);
+ FSTObjectValue *data = FSTTestObjectValue(@{ @"a" : @1 });
+ FSTDocument *doc =
+ [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO];
+
+ XCTAssertEqualObjects(doc.key, [FSTDocumentKey keyWithPathString:@"messages/first"]);
+ XCTAssertEqualObjects(doc.version, version);
+ XCTAssertEqualObjects(doc.data, data);
+ XCTAssertEqual(doc.hasLocalMutations, NO);
+}
+
+- (void)testExtractsFields {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"rooms/eros"];
+ FSTSnapshotVersion *version = FSTTestVersion(1);
+ FSTObjectValue *data = FSTTestObjectValue(@{
+ @"desc" : @"Discuss all the project related stuff",
+ @"owner" : @{@"name" : @"Jonny", @"title" : @"scallywag"}
+ });
+ FSTDocument *doc =
+ [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO];
+
+ XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"desc")],
+ [FSTStringValue stringValue:@"Discuss all the project related stuff"]);
+ XCTAssertEqualObjects([doc fieldForPath:FSTTestFieldPath(@"owner.title")],
+ [FSTStringValue stringValue:@"scallywag"]);
+}
+
+- (void)testIsEqual {
+ FSTDocumentKey *key1 = [FSTDocumentKey keyWithPathString:@"messages/first"];
+ FSTDocumentKey *key2 = [FSTDocumentKey keyWithPathString:@"messages/second"];
+ FSTObjectValue *data1 = FSTTestObjectValue(@{ @"a" : @1 });
+ FSTObjectValue *data2 = FSTTestObjectValue(@{ @"b" : @1 });
+ FSTSnapshotVersion *version1 = FSTTestVersion(1);
+
+ FSTDocument *doc1 =
+ [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO];
+ FSTDocument *doc2 =
+ [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:NO];
+
+ XCTAssertEqualObjects(doc1, doc2);
+ XCTAssertEqualObjects(
+ doc1, [FSTDocument documentWithData:FSTTestObjectValue(
+ @{ @"a" : @1 })
+ key:[FSTDocumentKey keyWithPathString:@"messages/first"]
+ version:version1
+ hasLocalMutations:NO]);
+
+ FSTSnapshotVersion *version2 = FSTTestVersion(2);
+ XCTAssertNotEqualObjects(
+ doc1, [FSTDocument documentWithData:data2 key:key1 version:version1 hasLocalMutations:NO]);
+ XCTAssertNotEqualObjects(
+ doc1, [FSTDocument documentWithData:data1 key:key2 version:version1 hasLocalMutations:NO]);
+ XCTAssertNotEqualObjects(
+ doc1, [FSTDocument documentWithData:data1 key:key1 version:version2 hasLocalMutations:NO]);
+ XCTAssertNotEqualObjects(
+ doc1, [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES]);
+
+ XCTAssertEqualObjects(
+ [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:YES],
+ [FSTDocument documentWithData:data1 key:key1 version:version1 hasLocalMutations:5]);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Model/FSTFieldValueTests.m b/Firestore/Example/Tests/Model/FSTFieldValueTests.m
new file mode 100644
index 0000000..a357e60
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTFieldValueTests.m
@@ -0,0 +1,576 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Model/FSTFieldValue.h"
+
+#import <XCTest/XCTest.h>
+
+#import "API/FIRFirestore+Internal.h"
+#import "API/FSTUserDataConverter.h"
+#import "Core/FSTTimestamp.h"
+#import "Firestore/FIRGeoPoint.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+/** Helper to wrap the values in a set of equality groups using FSTTestFieldValue(). */
+NSArray *FSTWrapGroups(NSArray *groups) {
+ NSMutableArray *wrapped = [NSMutableArray array];
+ for (NSArray<id> *group in groups) {
+ NSMutableArray *wrappedGroup = [NSMutableArray array];
+ for (id value in group) {
+ FSTFieldValue *wrappedValue;
+ // Server Timestamp values can't be parsed directly, so we have a couple predefined sentinel
+ // strings that can be used instead.
+ if ([value isEqual:@"server-timestamp-1"]) {
+ wrappedValue = [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 5, 20, 10, 20, 0)];
+ } else if ([value isEqual:@"server-timestamp-2"]) {
+ wrappedValue = [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:FSTTestTimestamp(2016, 10, 21, 15, 32, 0)];
+ } else if ([value isKindOfClass:[FSTDocumentKeyReference class]]) {
+ // We directly convert these here so that the databaseIDs can be different.
+ FSTDocumentKeyReference *reference = (FSTDocumentKeyReference *)value;
+ wrappedValue =
+ [FSTReferenceValue referenceValue:reference.key databaseID:reference.databaseID];
+ } else {
+ wrappedValue = FSTTestFieldValue(value);
+ }
+ [wrappedGroup addObject:wrappedValue];
+ }
+ [wrapped addObject:wrappedGroup];
+ }
+ return wrapped;
+}
+
+@interface FSTFieldValueTests : XCTestCase
+@end
+
+@implementation FSTFieldValueTests {
+ NSDate *date1;
+ NSDate *date2;
+}
+
+- (void)setUp {
+ [super setUp];
+ // Create a couple date objects for use in tests.
+ date1 = FSTTestDate(2016, 5, 20, 10, 20, 0);
+ date2 = FSTTestDate(2016, 10, 21, 15, 32, 0);
+}
+
+- (void)testWrapIntegers {
+ NSArray *values = @[
+ @(INT_MIN), @(-1), @0, @1, @2, @(UCHAR_MAX), @(INT_MAX), // Standard integers
+ @(LONG_MIN), @(LONG_MAX), @(LLONG_MIN), @(LLONG_MAX) // Larger values
+ ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTIntegerValue class]);
+ XCTAssertEqualObjects([wrapped value], @([value longLongValue]));
+ }
+}
+
+- (void)testWrapsDoubles {
+ // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal
+ // number: <https://en.wikipedia.org/wiki/Denormal_number>.
+ NSArray *values = @[
+ @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * -1.0), @(-1.1), @(-0x1.0p-1074), @(-0.0), @(0.0),
+ @(0x1.0p-1074), @(DBL_MIN), @(1.1), @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY)
+ ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTDoubleValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+}
+
+- (void)testWrapsNilAndNSNull {
+ FSTNullValue *nullValue = [FSTNullValue nullValue];
+ XCTAssertEqual(FSTTestFieldValue(nil), nullValue);
+ XCTAssertEqual(FSTTestFieldValue([NSNull null]), nullValue);
+ XCTAssertEqual([nullValue value], [NSNull null]);
+}
+
+- (void)testWrapsBooleans {
+ NSArray *values = @[ @YES, @NO, [NSNumber numberWithChar:1], [NSNumber numberWithChar:0] ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTBooleanValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+
+ // Unsigned chars could conceivably be handled consistently with signed chars but on arm64 these
+ // end up being stored as signed shorts.
+ FSTFieldValue *wrapped = FSTTestFieldValue([NSNumber numberWithUnsignedChar:1]);
+ XCTAssertEqualObjects(wrapped, [FSTIntegerValue integerValue:1]);
+}
+
+union DoubleBits {
+ double d;
+ uint64_t bits;
+};
+
+- (void)testNormalizesNaNs {
+ // NOTE: With v1beta1 query semantics, it's no longer as important that our NaN representation
+ // matches the backend, since all NaNs are defined to sort as equal, but we preserve the
+ // normalization and this test regardless for now.
+
+ // We use a canonical NaN bit pattern that's common for both Java and Objective-C. Specifically:
+ // - sign: 0
+ // - exponent: 11 bits, all 1
+ // - significand: 52 bits, MSB=1, rest=0
+ //
+ // This matches the Firestore backend which uses Java's Double.doubleToLongBits which is defined
+ // to normalize all NaNs to this value.
+ union DoubleBits canonical = {.bits = 0x7ff8000000000000ULL};
+
+ // IEEE 754 specifies that NaN isn't equal to itself.
+ XCTAssertTrue(isnan(canonical.d));
+ XCTAssertEqual(canonical.bits, canonical.bits);
+ XCTAssertNotEqual(canonical.d, canonical.d);
+
+ // All permutations of the 51 other non-MSB significand bits are also NaNs.
+ union DoubleBits alternate = {.bits = 0x7fff000000000000ULL};
+ XCTAssertTrue(isnan(alternate.d));
+ XCTAssertNotEqual(alternate.bits, canonical.bits);
+ XCTAssertNotEqual(alternate.d, canonical.d);
+
+ // Even though at the C-level assignment preserves non-canonical NaNs, NSNumber normalizes all
+ // NaNs to single shared instance, kCFNumberNaN. That NaN has no public definition for its value
+ // but it happens to match what we need.
+ union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:alternate.d] doubleValue]};
+ XCTAssertEqual(normalized.bits, canonical.bits);
+
+ // Ensure we get the same normalization behavior (currently implemented explicitly by checking
+ // for isnan() and then explicitly assigning NAN).
+ union DoubleBits result;
+ result.d = [[FSTDoubleValue doubleValue:canonical.d] internalValue];
+ XCTAssertEqual(result.bits, canonical.bits);
+
+ result.d = [[FSTDoubleValue doubleValue:alternate.d] internalValue];
+ XCTAssertEqual(result.bits, canonical.bits);
+
+ // A NaN that's canonical except it has the sign bit set (would be negative if signs mattered)
+ union DoubleBits negative = {.bits = 0xfff8000000000000ULL};
+ result.d = [[FSTDoubleValue doubleValue:negative.d] internalValue];
+ XCTAssertTrue(isnan(negative.d));
+ XCTAssertEqual(result.bits, canonical.bits);
+
+ // A signaling NaN with significand where MSB is 0, and some non-MSB bit is one.
+ union DoubleBits signaling = {.bits = 0xfff4000000000000ULL};
+ XCTAssertTrue(isnan(signaling.d));
+ result.d = [[FSTDoubleValue doubleValue:signaling.d] internalValue];
+ XCTAssertEqual(result.bits, canonical.bits);
+}
+
+- (void)testZeros {
+ // Floating point numbers have an explicit sign bit so it's possible to end up with negative
+ // zero as a distinct value from positive zero.
+ union DoubleBits zero = {.d = 0.0};
+ union DoubleBits negativeZero = {.d = -0.0};
+
+ // IEEE 754 requires these two zeros to compare equal.
+ XCTAssertNotEqual(zero.bits, negativeZero.bits);
+ XCTAssertEqual(zero.d, negativeZero.d);
+
+ // NSNumber preserves the negative zero value but compares equal according to IEEE 754.
+ union DoubleBits normalized = {.d = [[NSNumber numberWithDouble:negativeZero.d] doubleValue]};
+ XCTAssertEqual(normalized.bits, negativeZero.bits);
+ XCTAssertEqualObjects([NSNumber numberWithDouble:0.0], [NSNumber numberWithDouble:-0.0]);
+
+ // FSTDoubleValue preserves positive/negative zero
+ union DoubleBits result;
+ result.d = [[[FSTDoubleValue doubleValue:zero.d] value] doubleValue];
+ XCTAssertEqual(result.bits, zero.bits);
+ result.d = [[[FSTDoubleValue doubleValue:negativeZero.d] value] doubleValue];
+ XCTAssertEqual(result.bits, negativeZero.bits);
+
+ // ... but compares positive/negative zero as unequal, compatibly with Firestore.
+ XCTAssertNotEqualObjects([FSTDoubleValue doubleValue:0.0], [FSTDoubleValue doubleValue:-0.0]);
+}
+
+- (void)testWrapStrings {
+ NSArray *values = @[ @"", @"abc" ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTStringValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+}
+
+- (void)testWrapDates {
+ NSArray *values = @[ FSTTestDate(1900, 12, 1, 1, 20, 30), FSTTestDate(2017, 4, 24, 13, 20, 30) ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTTimestampValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+
+ XCTAssertEqualObjects(((FSTTimestampValue *)wrapped).internalValue,
+ [FSTTimestamp timestampWithDate:value]);
+ }
+}
+
+- (void)testWrapGeoPoints {
+ NSArray *values = @[ FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-20, 100) ];
+
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTGeoPointValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+}
+
+- (void)testWrapBlobs {
+ NSArray *values = @[ FSTTestData(1, 2, 3), FSTTestData(1, 2) ];
+ for (id value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTBlobValue class]);
+ XCTAssertEqualObjects([wrapped value], value);
+ }
+}
+
+- (void)testWrapResourceNames {
+ NSArray *values = @[
+ FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar"),
+ FSTTestRef(@"project", kDefaultDatabaseID, @"foo/baz")
+ ];
+ for (FSTDocumentKeyReference *value in values) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(value);
+ XCTAssertEqualObjects([wrapped class], [FSTReferenceValue class]);
+ XCTAssertEqualObjects([wrapped value], value.key);
+ XCTAssertEqualObjects(((FSTDatabaseID *)wrapped).databaseID, value.databaseID);
+ }
+}
+
+- (void)testWrapsEmptyObjects {
+ XCTAssertEqualObjects(FSTTestFieldValue(@{}), [FSTObjectValue objectValue]);
+}
+
+- (void)testWrapsSimpleObjects {
+ FSTObjectValue *actual = FSTTestObjectValue(
+ @{ @"a" : @"foo",
+ @"b" : @(1L),
+ @"c" : @YES,
+ @"d" : [NSNull null] });
+ FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{
+ @"a" : [FSTStringValue stringValue:@"foo"],
+ @"b" : [FSTIntegerValue integerValue:1LL],
+ @"c" : [FSTBooleanValue trueValue],
+ @"d" : [FSTNullValue nullValue]
+ }];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testWrapsNestedObjects {
+ FSTObjectValue *actual = FSTTestObjectValue(@{ @"a" : @{@"b" : @{@"c" : @"foo"}, @"d" : @YES} });
+ FSTObjectValue *expected = [[FSTObjectValue alloc] initWithDictionary:@{
+ @"a" : [[FSTObjectValue alloc] initWithDictionary:@{
+ @"b" :
+ [[FSTObjectValue alloc] initWithDictionary:@{@"c" : [FSTStringValue stringValue:@"foo"]}],
+ @"d" : [FSTBooleanValue booleanValue:YES]
+ }]
+ }];
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testExtractsFields {
+ FSTObjectValue *obj = FSTTestObjectValue(@{ @"foo" : @{@"a" : @YES, @"b" : @"string"} });
+ FSTAssertIsKindOfClass(obj, FSTObjectValue);
+
+ FSTAssertIsKindOfClass([obj valueForPath:FSTTestFieldPath(@"foo")], FSTObjectValue);
+ XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.a")], [FSTBooleanValue trueValue]);
+ XCTAssertEqualObjects([obj valueForPath:FSTTestFieldPath(@"foo.b")],
+ [FSTStringValue stringValue:@"string"]);
+
+ XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"foo.a.b")]);
+ XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar")]);
+ XCTAssertNil([obj valueForPath:FSTTestFieldPath(@"bar.a")]);
+}
+
+- (void)testOverwritesExistingFields {
+ FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"});
+ FSTObjectValue *mod =
+ [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")];
+
+ // Should return a new object, leaving the old one unmodified.
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"}));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"}));
+}
+
+- (void)testAddsNewFields {
+ FSTObjectValue *empty = [FSTObjectValue objectValue];
+ FSTObjectValue *mod =
+ [empty objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a")];
+ XCTAssertNotEqual(empty, mod);
+ XCTAssertEqualObjects(empty, FSTTestFieldValue(@{}));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{@"a" : @"mod"}));
+
+ FSTObjectValue *old = mod;
+ mod = [old objectBySettingValue:FSTTestFieldValue(@1) forPath:FSTTestFieldPath(@"b")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"mod"}));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @"mod", @"b" : @1 }));
+}
+
+- (void)testImplicitlyCreatesObjects {
+ FSTObjectValue *old = FSTTestObjectValue(@{@"a" : @"old"});
+ FSTObjectValue *mod =
+ [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"b.c.d")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{@"a" : @"old"}));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(
+ @{ @"a" : @"old",
+ @"b" : @{@"c" : @{@"d" : @"mod"}} }));
+}
+
+- (void)testCanOverwritePrimitivesWithObjects {
+ FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} });
+ FSTObjectValue *mod =
+ [old objectBySettingValue:FSTTestFieldValue(@{@"b" : @"mod"}) forPath:FSTTestFieldPath(@"a")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"mod"} }));
+}
+
+- (void)testAddsToNestedObjects {
+ FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @"old"} });
+ FSTObjectValue *mod =
+ [old objectBySettingValue:FSTTestFieldValue(@"mod") forPath:FSTTestFieldPath(@"a.c")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old"} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @"old", @"c" : @"mod"} }));
+}
+
+- (void)testDeletesKeys {
+ FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @1, @"b" : @2 });
+ FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @1, @"b" : @2 }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 }));
+
+ FSTObjectValue *empty = [mod objectByDeletingPath:FSTTestFieldPath(@"b")];
+ XCTAssertNotEqual(mod, empty);
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"b" : @2 }));
+ XCTAssertEqualObjects(empty, FSTTestFieldValue(@{}));
+}
+
+- (void)testDeletesHandleMissingKeys {
+ FSTObjectValue *old = FSTTestObjectValue(@{ @"a" : @{@"b" : @1, @"c" : @2} });
+ FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"b")];
+ XCTAssertEqualObjects(old, mod);
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }));
+
+ mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.d")];
+ XCTAssertEqualObjects(old, mod);
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }));
+
+ mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.b.c")];
+ XCTAssertEqualObjects(old, mod);
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @2} }));
+}
+
+- (void)testDeletesNestedKeys {
+ FSTObjectValue *old = FSTTestObjectValue(
+ @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} });
+ FSTObjectValue *mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c.d")];
+ XCTAssertNotEqual(old, mod);
+ XCTAssertEqualObjects(old, FSTTestFieldValue(
+ @{ @"a" : @{@"b" : @1, @"c" : @{@"d" : @2, @"e" : @3}} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} }));
+
+ old = mod;
+ mod = [old objectByDeletingPath:FSTTestFieldPath(@"a.c")];
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1, @"c" : @{@"e" : @3}} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} }));
+
+ old = mod;
+ mod = [old objectByDeletingPath:FSTTestFieldPath(@"a")];
+ XCTAssertEqualObjects(old, FSTTestFieldValue(@{ @"a" : @{@"b" : @1} }));
+ XCTAssertEqualObjects(mod, FSTTestFieldValue(@{}));
+}
+
+- (void)testArrays {
+ FSTArrayValue *expected = [[FSTArrayValue alloc]
+ initWithValueNoCopy:@[ [FSTStringValue stringValue:@"value"], [FSTBooleanValue trueValue] ]];
+
+ FSTArrayValue *actual = (FSTArrayValue *)FSTTestFieldValue(@[ @"value", @YES ]);
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testValueEquality {
+ NSArray *groups = @[
+ @[ FSTTestFieldValue(@YES), [FSTBooleanValue booleanValue:YES] ],
+ @[ FSTTestFieldValue(@NO), [FSTBooleanValue booleanValue:NO] ],
+ @[ FSTTestFieldValue([NSNull null]), [FSTNullValue nullValue] ],
+ @[ FSTTestFieldValue(@(0.0 / 0.0)), FSTTestFieldValue(@(NAN)), [FSTDoubleValue nanValue] ],
+ // -0.0 and 0.0 compare: the same (but are not isEqual:)
+ @[ FSTTestFieldValue(@(-0.0)) ], @[ FSTTestFieldValue(@0.0) ],
+ @[ FSTTestFieldValue(@1), FSTTestFieldValue(@1LL), [FSTIntegerValue integerValue:1LL] ],
+ // double and unit64_t values can compare: the same (but won't be isEqual:)
+ @[ FSTTestFieldValue(@1.0), [FSTDoubleValue doubleValue:1.0] ],
+ @[ FSTTestFieldValue(@1.1), [FSTDoubleValue doubleValue:1.1] ],
+ @[
+ FSTTestFieldValue(FSTTestData(0, 1, 2, -1)), [FSTBlobValue blobValue:FSTTestData(0, 1, 2, -1)]
+ ],
+ @[ FSTTestFieldValue(FSTTestData(0, 1, -1)) ],
+ @[ FSTTestFieldValue(@"string"), [FSTStringValue stringValue:@"string"] ],
+ @[ FSTTestFieldValue(@"strin") ],
+ @[ FSTTestFieldValue(@"e\u0301b") ], // latin small letter e + combining acute accent
+ @[ FSTTestFieldValue(@"\u00e9a") ], // latin small letter e with acute accent
+ @[
+ FSTTestFieldValue(date1),
+ [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:date1]]
+ ],
+ @[ FSTTestFieldValue(date2) ],
+ @[
+ // NOTE: ServerTimestampValues can't be parsed via FSTTestFieldValue().
+ [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]],
+ [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date1]]
+ ],
+ @[ [FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:[FSTTimestamp timestampWithDate:date2]] ],
+ @[
+ FSTTestFieldValue(FSTTestGeoPoint(0, 1)),
+ [FSTGeoPointValue geoPointValue:FSTTestGeoPoint(0, 1)]
+ ],
+ @[ FSTTestFieldValue(FSTTestGeoPoint(1, 0)) ],
+ @[
+ [FSTReferenceValue referenceValue:FSTTestDocKey(@"coll/doc1")
+ databaseID:[FSTDatabaseID databaseIDWithProject:@"project"
+ database:kDefaultDatabaseID]],
+ FSTTestFieldValue(FSTTestRef(@"project", kDefaultDatabaseID, @"coll/doc1"))
+ ],
+ @[ FSTTestRef(@"project", @"(default)", @"coll/doc2") ],
+ @[ FSTTestFieldValue(@[ @"foo", @"bar" ]), FSTTestFieldValue(@[ @"foo", @"bar" ]) ],
+ @[ FSTTestFieldValue(@[ @"foo", @"bar", @"baz" ]) ], @[ FSTTestFieldValue(@[ @"foo" ]) ],
+ @[
+ FSTTestFieldValue(
+ @{ @"bar" : @1,
+ @"foo" : @2 }),
+ FSTTestFieldValue(
+ @{ @"foo" : @2,
+ @"bar" : @1 })
+ ],
+ @[ FSTTestFieldValue(
+ @{ @"bar" : @2,
+ @"foo" : @1 }) ],
+ @[ FSTTestFieldValue(
+ @{ @"bar" : @1,
+ @"foo" : @1 }) ],
+ @[ FSTTestFieldValue(
+ @{ @"foo" : @1 }) ]
+ ];
+
+ FSTAssertEqualityGroups(groups);
+}
+
+- (void)testValueOrdering {
+ NSArray *groups = @[
+ // null first
+ @[ [NSNull null] ],
+
+ // booleans
+ @[ @NO ], @[ @YES ],
+
+ // numbers
+ @[ @(0.0 / 0.0) ], @[ @(-INFINITY) ], @[ @(-DBL_MAX) ], @[ @(LLONG_MIN) ], @[ @(-1.1) ],
+ @[ @(-1.0), @(-1LL) ], // longs and doubles compare the same
+ @[ @(-DBL_MIN) ],
+ @[ @(-0x1.0p-1074) ], // negative smallest subnormal
+ @[ @(-0.0), @(0.0), @(0LL) ], // zeros all compare the same
+ @[ @(0x1.0p-1074) ], // positive smallest subnormal
+ @[ @(DBL_MIN) ], @[ @1.0, @1LL ], // longs and doubles compare the same
+ @[ @1.1 ], @[ @(LLONG_MAX) ], @[ @(DBL_MAX) ], @[ @(INFINITY) ],
+
+ // timestamps
+ @[ date1 ], @[ date2 ],
+
+ // server timestamps come after all concrete timestamps.
+ // NOTE: server timestamps can't be parsed directly, so we have special sentinel strings (see
+ // FSTWrapGroups()).
+ @[ @"server-timestamp-1" ], @[ @"server-timestamp-2" ],
+
+ // strings
+ @[ @"" ], @[ @"\000\ud7ff\ue000\uffff" ], @[ @"(╯°□°)╯︵ ┻━┻" ], @[ @"a" ], @[ @"abc def" ],
+ @[ @"e\u0301b" ], // latin small letter e + combining acute accent + latin small letter b
+ @[ @"æ" ],
+ @[ @"\u00e9a" ], // latin small letter e with acute accent + latin small letter a
+
+ // blobs
+ @[ FSTTestData(-1) ], @[ FSTTestData(0, -1) ], @[ FSTTestData(0, 1, 2, 3, 4, -1) ],
+ @[ FSTTestData(0, 1, 2, 4, 3, -1) ], @[ FSTTestData(255, -1) ],
+
+ // resource names
+ @[ FSTTestRef(@"p1", @"d1", @"c1/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c1/doc2") ],
+ @[ FSTTestRef(@"p1", @"d1", @"c10/doc1") ], @[ FSTTestRef(@"p1", @"d1", @"c2/doc1") ],
+ @[ FSTTestRef(@"p1", @"d2", @"c1/doc1") ], @[ FSTTestRef(@"p2", @"d1", @"c1/doc1") ],
+
+ // Geo points
+ @[ FSTTestGeoPoint(-90, -180) ], @[ FSTTestGeoPoint(-90, 0) ], @[ FSTTestGeoPoint(-90, 180) ],
+ @[ FSTTestGeoPoint(0, -180) ], @[ FSTTestGeoPoint(0, 0) ], @[ FSTTestGeoPoint(0, 180) ],
+ @[ FSTTestGeoPoint(1, -180) ], @[ FSTTestGeoPoint(1, 0) ], @[ FSTTestGeoPoint(1, 180) ],
+ @[ FSTTestGeoPoint(90, -180) ], @[ FSTTestGeoPoint(90, 0) ], @[ FSTTestGeoPoint(90, 180) ],
+
+ // Arrays
+ @[ @[] ], @[ @[ @"bar" ] ], @[ @[ @"foo" ] ], @[ @[ @"foo", @1 ] ], @[ @[ @"foo", @2 ] ],
+ @[ @[ @"foo", @"0" ] ],
+
+ // Objects
+ @[
+ @{ @"bar" : @0 }
+ ],
+ @[
+ @{ @"bar" : @0,
+ @"foo" : @1 }
+ ],
+ @[
+ @{ @"foo" : @1 }
+ ],
+ @[
+ @{ @"foo" : @2 }
+ ],
+ @[ @{@"foo" : @"0"} ]
+ ];
+
+ NSArray *wrapped = FSTWrapGroups(groups);
+ FSTAssertComparisons(wrapped);
+}
+
+- (void)testValue {
+ NSDate *date = [NSDate date];
+ id input = @{ @"array" : @[ @1, date ], @"obj" : @{@"date" : date, @"string" : @"hi"} };
+ FSTObjectValue *value = FSTTestObjectValue(input);
+ id output = [value value];
+ {
+ XCTAssertTrue([output[@"array"][1] isKindOfClass:[NSDate class]]);
+ NSDate *actual = output[@"array"][1];
+ XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970,
+ 0.000000001);
+ }
+ {
+ XCTAssertTrue([output[@"obj"][@"date"] isKindOfClass:[NSDate class]]);
+ NSDate *actual = output[@"obj"][@"date"];
+ XCTAssertEqualWithAccuracy(date.timeIntervalSince1970, actual.timeIntervalSince1970,
+ 0.000000001);
+ }
+}
+
+@end
diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.m b/Firestore/Example/Tests/Model/FSTMutationTests.m
new file mode 100644
index 0000000..5ad9f94
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTMutationTests.m
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Model/FSTMutation.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTTimestamp.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTPath.h"
+
+#import "FSTHelpers.h"
+
+@interface FSTMutationTests : XCTestCase
+@end
+
+@implementation FSTMutationTests {
+ FSTTimestamp *_timestamp;
+}
+
+- (void)setUp {
+ _timestamp = [FSTTimestamp timestamp];
+}
+
+- (void)testAppliesSetsToDocuments {
+ NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"bar" : @"bar-value"});
+ FSTMaybeDocument *setDoc = [set applyTo:baseDoc localWriteTime:_timestamp];
+
+ NSDictionary *expectedData = @{@"bar" : @"bar-value"};
+ XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES));
+}
+
+- (void)testAppliesPatchesToDocuments {
+ NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" };
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *patch =
+ FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil);
+ FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp];
+
+ NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" };
+ XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES));
+}
+
+- (void)testDeletesValuesFromTheFieldMask {
+ NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value", @"baz" : @"baz-value"} };
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:@"collection/key"];
+ FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:@[ FSTTestFieldPath(@"foo.bar") ]];
+ FSTMutation *patch = [[FSTPatchMutation alloc] initWithKey:key
+ fieldMask:mask
+ value:[FSTObjectValue objectValue]
+ precondition:[FSTPrecondition none]];
+ FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp];
+
+ NSDictionary *expectedData = @{ @"foo" : @{@"baz" : @"baz-value"} };
+ XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES));
+}
+
+- (void)testPatchesPrimitiveValue {
+ NSDictionary *docData = @{@"foo" : @"foo-value", @"baz" : @"baz-value"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *patch =
+ FSTTestPatchMutation(@"collection/key", @{@"foo.bar" : @"new-bar-value"}, nil);
+ FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp];
+
+ NSDictionary *expectedData = @{ @"foo" : @{@"bar" : @"new-bar-value"}, @"baz" : @"baz-value" };
+ XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, YES));
+}
+
+- (void)testPatchingDeletedDocumentsDoesNothing {
+ FSTMaybeDocument *baseDoc = FSTTestDeletedDoc(@"collection/key", 0);
+ FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"bar"}, nil);
+ FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc localWriteTime:_timestamp];
+ XCTAssertEqualObjects(patchedDoc, baseDoc);
+}
+
+- (void)testAppliesLocalTransformsToDocuments {
+ NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" };
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]);
+ FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc localWriteTime:_timestamp];
+
+ // Server timestamps aren't parsed, so we manually insert it.
+ FSTObjectValue *expectedData = FSTTestObjectValue(
+ @{ @"foo" : @{@"bar" : @"<server-timestamp>"},
+ @"baz" : @"baz-value" });
+ expectedData =
+ [expectedData objectBySettingValue:[FSTServerTimestampValue
+ serverTimestampValueWithLocalWriteTime:_timestamp]
+ forPath:FSTTestFieldPath(@"foo.bar")];
+
+ FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData
+ key:FSTTestDocKey(@"collection/key")
+ version:FSTTestVersion(0)
+ hasLocalMutations:YES];
+
+ XCTAssertEqualObjects(transformedDoc, expectedDoc);
+}
+
+- (void)testAppliesServerAckedTransformsToDocuments {
+ NSDictionary *docData = @{ @"foo" : @{@"bar" : @"bar-value"}, @"baz" : @"baz-value" };
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *transform = FSTTestTransformMutation(@"collection/key", @[ @"foo.bar" ]);
+
+ FSTMutationResult *mutationResult = [[FSTMutationResult alloc]
+ initWithVersion:FSTTestVersion(1)
+ transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]];
+
+ FSTMaybeDocument *transformedDoc =
+ [transform applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult];
+
+ NSDictionary *expectedData =
+ @{ @"foo" : @{@"bar" : _timestamp.approximateDateValue},
+ @"baz" : @"baz-value" };
+ XCTAssertEqualObjects(transformedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO));
+}
+
+- (void)testDeleteDeletes {
+ NSDictionary *docData = @{@"foo" : @"bar"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *mutation = FSTTestDeleteMutation(@"collection/key");
+ FSTMaybeDocument *result = [mutation applyTo:baseDoc localWriteTime:_timestamp];
+ XCTAssertEqualObjects(result, FSTTestDeletedDoc(@"collection/key", 0));
+}
+
+- (void)testSetWithMutationResult {
+ NSDictionary *docData = @{@"foo" : @"bar"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"});
+ FSTMutationResult *mutationResult =
+ [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil];
+ FSTMaybeDocument *setDoc =
+ [set applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult];
+
+ NSDictionary *expectedData = @{@"foo" : @"new-bar"};
+ XCTAssertEqualObjects(setDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO));
+}
+
+- (void)testPatchWithMutationResult {
+ NSDictionary *docData = @{@"foo" : @"bar"};
+ FSTDocument *baseDoc = FSTTestDoc(@"collection/key", 0, docData, NO);
+
+ FSTMutation *patch = FSTTestPatchMutation(@"collection/key", @{@"foo" : @"new-bar"}, nil);
+ FSTMutationResult *mutationResult =
+ [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil];
+ FSTMaybeDocument *patchedDoc =
+ [patch applyTo:baseDoc localWriteTime:_timestamp mutationResult:mutationResult];
+
+ NSDictionary *expectedData = @{@"foo" : @"new-bar"};
+ XCTAssertEqualObjects(patchedDoc, FSTTestDoc(@"collection/key", 0, expectedData, NO));
+}
+
+#define ASSERT_VERSION_TRANSITION(mutation, base, expected) \
+ do { \
+ FSTMutationResult *mutationResult = \
+ [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(0) transformResults:nil]; \
+ FSTMaybeDocument *actual = \
+ [mutation applyTo:base localWriteTime:_timestamp mutationResult:mutationResult]; \
+ XCTAssertEqualObjects(actual, expected); \
+ } while (0);
+
+/**
+ * Tests the transition table documented in FSTMutation.h.
+ */
+- (void)testTransitions {
+ FSTDocument *docV0 = FSTTestDoc(@"collection/key", 0, @{}, NO);
+ FSTDeletedDocument *deletedV0 = FSTTestDeletedDoc(@"collection/key", 0);
+
+ FSTDocument *docV3 = FSTTestDoc(@"collection/key", 3, @{}, NO);
+ FSTDeletedDocument *deletedV3 = FSTTestDeletedDoc(@"collection/key", 3);
+
+ FSTMutation *setMutation = FSTTestSetMutation(@"collection/key", @{});
+ FSTMutation *patchMutation = FSTTestPatchMutation(@"collection/key", @{}, nil);
+ FSTMutation *deleteMutation = FSTTestDeleteMutation(@"collection/key");
+
+ ASSERT_VERSION_TRANSITION(setMutation, docV3, docV3);
+ ASSERT_VERSION_TRANSITION(setMutation, deletedV3, docV0);
+ ASSERT_VERSION_TRANSITION(setMutation, nil, docV0);
+
+ ASSERT_VERSION_TRANSITION(patchMutation, docV3, docV3);
+ ASSERT_VERSION_TRANSITION(patchMutation, deletedV3, deletedV3);
+ ASSERT_VERSION_TRANSITION(patchMutation, nil, nil);
+
+ ASSERT_VERSION_TRANSITION(deleteMutation, docV3, deletedV0);
+ ASSERT_VERSION_TRANSITION(deleteMutation, deletedV3, deletedV0);
+ ASSERT_VERSION_TRANSITION(deleteMutation, nil, deletedV0);
+}
+
+#undef ASSERT_TRANSITION
+
+@end
diff --git a/Firestore/Example/Tests/Model/FSTPathTests.m b/Firestore/Example/Tests/Model/FSTPathTests.m
new file mode 100644
index 0000000..a5d3d80
--- /dev/null
+++ b/Firestore/Example/Tests/Model/FSTPathTests.m
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTHelpers.h"
+#import "Model/FSTPath.h"
+
+#import <XCTest/XCTest.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTFieldPathTests : XCTestCase
+@end
+
+@implementation FSTFieldPathTests
+
+- (void)testConstructor {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ XCTAssertEqual(3, path.length);
+}
+
+- (void)testIndexing {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ XCTAssertEqualObjects(@"rooms", path.firstSegment);
+ XCTAssertEqualObjects(@"rooms", [path segmentAtIndex:0]);
+ XCTAssertEqualObjects(@"rooms", path[0]);
+
+ XCTAssertEqualObjects(@"Eros", [path segmentAtIndex:1]);
+ XCTAssertEqualObjects(@"Eros", path[1]);
+
+ XCTAssertEqualObjects(@"messages", [path segmentAtIndex:2]);
+ XCTAssertEqualObjects(@"messages", path[2]);
+ XCTAssertEqualObjects(@"messages", path.lastSegment);
+}
+
+- (void)testPathByRemovingFirstSegment {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"Eros", @"messages" ]];
+ FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"messages" ]];
+ FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]];
+
+ XCTAssertEqualObjects(second, path.pathByRemovingFirstSegment);
+ XCTAssertEqualObjects(third, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment);
+ XCTAssertEqualObjects(
+ empty, path.pathByRemovingFirstSegment.pathByRemovingFirstSegment.pathByRemovingFirstSegment);
+ // unmodified original
+ XCTAssertEqualObjects(same, path);
+}
+
+- (void)testPathByRemovingLastSegment {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ FSTFieldPath *same = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros", @"messages" ]];
+ FSTFieldPath *second = [FSTFieldPath pathWithSegments:@[ @"rooms", @"Eros" ]];
+ FSTFieldPath *third = [FSTFieldPath pathWithSegments:@[ @"rooms" ]];
+ FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]];
+
+ XCTAssertEqualObjects(second, path.pathByRemovingLastSegment);
+ XCTAssertEqualObjects(third, path.pathByRemovingLastSegment.pathByRemovingLastSegment);
+ XCTAssertEqualObjects(
+ empty, path.pathByRemovingLastSegment.pathByRemovingLastSegment.pathByRemovingLastSegment);
+ // unmodified original
+ XCTAssertEqualObjects(same, path);
+}
+
+- (void)testPathByAppendingSegment {
+ FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ @"rooms" ]];
+ FSTFieldPath *rooms = [FSTFieldPath pathWithSegments:@[ @"rooms" ]];
+ FSTFieldPath *roomsEros = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros" ]];
+ FSTFieldPath *roomsEros1 = [FSTFieldPath pathWithSegments:@[ @"rooms", @"eros", @"1" ]];
+
+ XCTAssertEqualObjects(roomsEros, [path pathByAppendingSegment:@"eros"]);
+ XCTAssertEqualObjects(roomsEros1,
+ [[path pathByAppendingSegment:@"eros"] pathByAppendingSegment:@"1"]);
+ // unmodified original
+ XCTAssertEqualObjects(rooms, path);
+
+ FSTFieldPath *sub = [FSTTestFieldPath(@"rooms.eros.1") pathByRemovingFirstSegment];
+ FSTFieldPath *appended = [sub pathByAppendingSegment:@"2"];
+ XCTAssertEqualObjects(appended, FSTTestFieldPath(@"eros.1.2"));
+}
+
+- (void)testPathComparison {
+ FSTFieldPath *path1 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]];
+ FSTFieldPath *path2 = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]];
+ FSTFieldPath *path3 = [FSTFieldPath pathWithSegments:@[ @"x", @"y", @"z" ]];
+ XCTAssertTrue([path1 isEqual:path2]);
+ XCTAssertFalse([path1 isEqual:path3]);
+
+ FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]];
+ FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]];
+ FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]];
+ FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]];
+
+ XCTAssertEqual(NSOrderedAscending, [empty compare:a]);
+ XCTAssertEqual(NSOrderedAscending, [a compare:b]);
+ XCTAssertEqual(NSOrderedAscending, [a compare:ab]);
+
+ XCTAssertEqual(NSOrderedDescending, [a compare:empty]);
+ XCTAssertEqual(NSOrderedDescending, [b compare:a]);
+ XCTAssertEqual(NSOrderedDescending, [ab compare:a]);
+}
+
+- (void)testIsPrefixOfPath {
+ FSTFieldPath *empty = [FSTFieldPath pathWithSegments:@[]];
+ FSTFieldPath *a = [FSTFieldPath pathWithSegments:@[ @"a" ]];
+ FSTFieldPath *ab = [FSTFieldPath pathWithSegments:@[ @"a", @"b" ]];
+ FSTFieldPath *abc = [FSTFieldPath pathWithSegments:@[ @"a", @"b", @"c" ]];
+ FSTFieldPath *b = [FSTFieldPath pathWithSegments:@[ @"b" ]];
+ FSTFieldPath *ba = [FSTFieldPath pathWithSegments:@[ @"b", @"a" ]];
+
+ XCTAssertTrue([empty isPrefixOfPath:a]);
+ XCTAssertTrue([empty isPrefixOfPath:ab]);
+ XCTAssertTrue([empty isPrefixOfPath:abc]);
+ XCTAssertTrue([empty isPrefixOfPath:empty]);
+ XCTAssertTrue([empty isPrefixOfPath:b]);
+ XCTAssertTrue([empty isPrefixOfPath:ba]);
+
+ XCTAssertTrue([a isPrefixOfPath:a]);
+ XCTAssertTrue([a isPrefixOfPath:ab]);
+ XCTAssertTrue([a isPrefixOfPath:abc]);
+ XCTAssertFalse([a isPrefixOfPath:empty]);
+ XCTAssertFalse([a isPrefixOfPath:b]);
+ XCTAssertFalse([a isPrefixOfPath:ba]);
+
+ XCTAssertFalse([ab isPrefixOfPath:a]);
+ XCTAssertTrue([ab isPrefixOfPath:ab]);
+ XCTAssertTrue([ab isPrefixOfPath:abc]);
+ XCTAssertFalse([ab isPrefixOfPath:empty]);
+ XCTAssertFalse([ab isPrefixOfPath:b]);
+ XCTAssertFalse([ab isPrefixOfPath:ba]);
+
+ XCTAssertFalse([abc isPrefixOfPath:a]);
+ XCTAssertFalse([abc isPrefixOfPath:ab]);
+ XCTAssertTrue([abc isPrefixOfPath:abc]);
+ XCTAssertFalse([abc isPrefixOfPath:empty]);
+ XCTAssertFalse([abc isPrefixOfPath:b]);
+ XCTAssertFalse([abc isPrefixOfPath:ba]);
+}
+
+- (void)testInvalidPaths {
+ XCTAssertThrows(FSTTestFieldPath(@""));
+ XCTAssertThrows(FSTTestFieldPath(@"."));
+ XCTAssertThrows(FSTTestFieldPath(@".foo"));
+ XCTAssertThrows(FSTTestFieldPath(@"foo."));
+ XCTAssertThrows(FSTTestFieldPath(@"foo..bar"));
+}
+
+#define ASSERT_ROUND_TRIP(str, segments) \
+ do { \
+ FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:str]; \
+ XCTAssertEqual([path length], segments); \
+ NSString *canonical = [path canonicalString]; \
+ XCTAssertEqualObjects(canonical, str); \
+ } while (0);
+
+- (void)testCanonicalString {
+ ASSERT_ROUND_TRIP(@"foo", 1);
+ ASSERT_ROUND_TRIP(@"foo.bar", 2);
+ ASSERT_ROUND_TRIP(@"foo.bar.baz", 3);
+ ASSERT_ROUND_TRIP(@"`.foo\\\\`", 1);
+ ASSERT_ROUND_TRIP(@"`.foo\\\\`.`.foo`", 2);
+ ASSERT_ROUND_TRIP(@"foo.`\\``.bar", 3);
+}
+
+#undef ASSERT_ROUND_TRIP
+
+- (void)testCanonicalStringOfSubstring {
+ FSTFieldPath *path = [FSTFieldPath pathWithServerFormat:@"foo.bar.baz"];
+ XCTAssertEqualObjects([path canonicalString], @"foo.bar.baz");
+
+ FSTFieldPath *pathTail = [path pathByRemovingFirstSegment];
+ XCTAssertEqualObjects([pathTail canonicalString], @"bar.baz");
+
+ FSTFieldPath *pathHead = [path pathByRemovingLastSegment];
+ XCTAssertEqualObjects([pathHead canonicalString], @"foo.bar");
+
+ XCTAssertEqualObjects([[pathTail pathByRemovingLastSegment] canonicalString], @"bar");
+ XCTAssertEqualObjects([[pathHead pathByRemovingFirstSegment] canonicalString], @"bar");
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTDatastoreTests.m b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m
new file mode 100644
index 0000000..511de72
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTDatastoreTests.m
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Firestore/FIRFirestoreErrors.h"
+#import "Remote/FSTDatastore.h"
+
+#import <GRPCClient/GRPCCall.h>
+#import <XCTest/XCTest.h>
+
+@interface FSTDatastoreTests : XCTestCase
+@end
+
+@implementation FSTDatastoreTests
+
+- (void)testIsPermanentWriteError {
+ // From GRPCCall -cancel
+ NSError *error = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeCancelled
+ userInfo:@{NSLocalizedDescriptionKey : @"Canceled by app"}];
+ XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
+
+ // From GRPCCall -startNextRead
+ error =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeResourceExhausted
+ userInfo:@{
+ NSLocalizedDescriptionKey :
+ @"Client does not have enough memory to hold the server response."
+ }];
+ XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
+
+ // From GRPCCall -startWithWriteable
+ error = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeUnavailable
+ userInfo:@{NSLocalizedDescriptionKey : @"Connectivity lost."}];
+ XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
+
+ // User info doesn't matter:
+ error = [NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodeUnavailable
+ userInfo:nil];
+ XCTAssertFalse([FSTDatastore isPermanentWriteError:error]);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m
new file mode 100644
index 0000000..a172af7
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.m
@@ -0,0 +1,556 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Remote/FSTRemoteEvent.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Remote/FSTExistenceFilter.h"
+#import "Remote/FSTWatchChange.h"
+
+#import "FSTHelpers.h"
+#import "FSTWatchChange+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTRemoteEventTests : XCTestCase
+@end
+
+@implementation FSTRemoteEventTests {
+ NSData *_resumeToken1;
+ NSMutableDictionary<NSNumber *, NSNumber *> *_noPendingResponses;
+}
+
+- (void)setUp {
+ _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding];
+ _noPendingResponses = [NSMutableDictionary dictionary];
+}
+
+- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray<NSNumber *> *)targets
+ outstanding:
+ (NSDictionary<NSNumber *, NSNumber *> *)outstanding
+ changes:(NSArray<FSTWatchChange *> *)watchChanges {
+ NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
+ FSTQueryData *dummyQueryData = [FSTQueryData alloc];
+ for (NSNumber *targetID in targets) {
+ listens[targetID] = dummyQueryData;
+ }
+ FSTWatchChangeAggregator *aggregator =
+ [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(3)
+ listenTargets:listens
+ pendingTargetResponses:outstanding];
+ [aggregator addWatchChanges:watchChanges];
+ return aggregator;
+}
+
+- (void)testWillAccumulateDocumentAddedAndRemovedEvents {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ]
+ removedTargetIDs:@[ @4, @5, @6 ]
+ documentKey:doc1.key
+ document:doc1];
+
+ 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];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 2);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+
+ XCTAssertEqual(event.targetChanges.count, 6);
+
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+
+ FSTUpdateMapping *mapping2 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]];
+ XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2);
+
+ FSTUpdateMapping *mapping3 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3);
+
+ FSTUpdateMapping *mapping4 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]];
+ XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4);
+
+ FSTUpdateMapping *mapping5 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]];
+ XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5);
+
+ FSTUpdateMapping *mapping6 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]];
+ XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6);
+}
+
+- (void)testWillIgnoreEventsForPendingTargets {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+
+ FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc2.key
+ document:doc2];
+
+ // We're waiting for the unwatch and watch ack
+ NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2 };
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ]
+ outstanding:pendingResponses
+ changes:@[ change1, change2, change3, change4 ]];
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(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.count, 1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+
+ XCTAssertEqual(event.targetChanges.count, 1);
+}
+
+- (void)testWillIgnoreEventsForRemovedTargets {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+
+ FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ // We're waiting for the unwatch ack
+ NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @1 };
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ // doc1 is ignored because it was part of an inactive target
+ XCTAssertEqual(event.documentUpdates.count, 0);
+
+ // Target 1 is ignored because it was removed
+ XCTAssertEqual(event.targetChanges.count, 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);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+ // Reset stream, ignoring doc1
+ FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ // Add doc2, doc3
+ FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc2.key
+ document:doc2];
+ FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc3.key
+ document:doc3];
+
+ // Remove doc2 again, should not show up in reset mapping
+ FSTWatchChange *change5 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1 ]
+ documentKey:doc2.key
+ document:doc2];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2, change3, change4, change5 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 3);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+ XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3);
+
+ XCTAssertEqual(event.targetChanges.count, 1);
+
+ // Only doc3 is part of the new mapping
+ FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]];
+
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping);
+}
+
+- (void)testWillHandleSingleReset {
+ // Reset target
+ FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @1 ]
+ cause:nil];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 0);
+
+ XCTAssertEqual(event.targetChanges.count, 1);
+
+ // Reset mapping is empty
+ FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping);
+}
+
+- (void)testWillHandleTargetAddAndRemovalInSameBatch {
+ FSTDocument *doc1a = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc1b = FSTTestDoc(@"docs/1", 1, @{ @"value" : @2 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[ @2 ]
+ documentKey:doc1a.key
+ document:doc1a];
+
+ 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];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 1);
+ XCTAssertEqualObjects(event.documentUpdates[doc1b.key], doc1b);
+
+ XCTAssertEqual(event.targetChanges.count, 2);
+
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+
+ FSTUpdateMapping *mapping2 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2);
+}
+
+- (void)testTargetCurrentChangeWillMarkTheTargetCurrent {
+ FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 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);
+}
+
+- (void)testTargetAddedChangeWillResetPreviousState {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, 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];
+ FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[ @3 ]
+ documentKey:doc2.key
+ document:doc2];
+
+ NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2, @2 : @1 };
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1, @3 ]
+ outstanding:pendingResponses
+ changes:@[ change1, change2, change3, change4, change5, change6 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 2);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[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);
+
+ // 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
+ 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);
+}
+
+- (void)testNoChangeWillStillMarkTheAffectedTargets {
+ FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 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);
+}
+
+- (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];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1, @2 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2, change3 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 0);
+ XCTAssertEqual(event.targetChanges.count, 0);
+ XCTAssertEqual(aggregator.existenceFilters.count, 2);
+ XCTAssertEqual(aggregator.existenceFilters[@1], filter1);
+ XCTAssertEqual(aggregator.existenceFilters[@2], filter2);
+}
+
+- (void)testExistenceFilterMismatchResetsTarget {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+
+ FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc2.key
+ document:doc2];
+
+ FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:@[ @1 ]
+ resumeToken:_resumeToken1];
+
+ FSTWatchChangeAggregator *aggregator =
+ [self aggregatorWithTargets:@[ @1 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2, change3 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 2);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+
+ XCTAssertEqual(event.targetChanges.count, 1);
+
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+ XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
+
+ [event handleExistenceFilterMismatchForTargetID:@1];
+
+ // Mapping is reset
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]);
+ // Reset the resume snapshot
+ XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(0));
+ // Target needs to be set to not current
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent);
+ XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0);
+}
+
+- (void)testDocumentUpdate {
+ FSTDocument *doc1 = FSTTestDoc(@"docs/1", 1, @{ @"value" : @1 }, NO);
+ FSTDeletedDocument *deletedDoc1 =
+ [FSTDeletedDocument documentWithKey:doc1.key version:FSTTestVersion(3)];
+ FSTDocument *doc2 = FSTTestDoc(@"docs/2", 2, @{ @"value" : @2 }, NO);
+ FSTDocument *doc3 = FSTTestDoc(@"docs/3", 3, @{ @"value" : @3 }, NO);
+
+ FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc1.key
+ document:doc1];
+
+ FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ]
+ removedTargetIDs:@[]
+ documentKey:doc2.key
+ document:doc2];
+
+ FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ]
+ outstanding:_noPendingResponses
+ changes:@[ change1, change2 ]];
+
+ FSTRemoteEvent *event = [aggregator remoteEvent];
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 2);
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], doc1);
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+
+ // Update doc1
+ [event addDocumentUpdate:deletedDoc1];
+ [event addDocumentUpdate:doc3];
+
+ XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.documentUpdates.count, 3);
+ // doc1 is replaced
+ XCTAssertEqualObjects(event.documentUpdates[doc1.key], deletedDoc1);
+ // doc2 is untouched
+ XCTAssertEqualObjects(event.documentUpdates[doc2.key], doc2);
+ // doc3 is new
+ XCTAssertEqualObjects(event.documentUpdates[doc3.key], doc3);
+
+ // Target is unchanged
+ XCTAssertEqual(event.targetChanges.count, 1);
+
+ FSTUpdateMapping *mapping1 =
+ [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]];
+ XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1);
+}
+
+- (void)testResumeTokensHandledPerTarget {
+ 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);
+ XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1);
+
+ XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1);
+ XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2);
+}
+
+- (void)testLastResumeTokenWins {
+ NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding];
+ NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding];
+
+ 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);
+ XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent);
+ XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2);
+
+ XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1);
+ XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3));
+ XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone);
+ XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m
new file mode 100644
index 0000000..c4cf9df
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.m
@@ -0,0 +1,794 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Remote/FSTSerializerBeta.h"
+
+#import <GRPCClient/GRPCCall.h>
+#import <XCTest/XCTest.h>
+
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTTimestamp.h"
+#import "Firestore/FIRFieldPath.h"
+#import "Firestore/FIRFirestoreErrors.h"
+#import "Firestore/FIRGeoPoint.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTMutationBatch.h"
+#import "Model/FSTPath.h"
+#import "Protos/objc/firestore/local/MaybeDocument.pbobjc.h"
+#import "Protos/objc/firestore/local/Mutation.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Common.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Document.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Firestore.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Query.pbobjc.h"
+#import "Protos/objc/google/firestore/v1beta1/Write.pbobjc.h"
+#import "Protos/objc/google/rpc/Status.pbobjc.h"
+#import "Protos/objc/google/type/Latlng.pbobjc.h"
+#import "Remote/FSTWatchChange.h"
+
+#import "FSTHelpers.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTSerializerBeta (Test)
+- (GCFSValue *)encodedNull;
+- (GCFSValue *)encodedBool:(BOOL)value;
+- (GCFSValue *)encodedDouble:(double)value;
+- (GCFSValue *)encodedInteger:(int64_t)value;
+- (GCFSValue *)encodedString:(NSString *)value;
+- (GCFSValue *)encodedDate:(NSDate *)value;
+
+- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask;
+- (NSMutableArray<GCFSDocumentTransform_FieldTransform *> *)encodedFieldTransforms:
+ (NSArray<FSTFieldTransform *> *)fieldTransforms;
+
+- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter;
+@end
+
+@interface GCFSStructuredQuery_Order (Test)
++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending;
+@end
+
+@implementation GCFSStructuredQuery_Order (Test)
+
++ (instancetype)messageWithProperty:(NSString *)property ascending:(BOOL)ascending {
+ GCFSStructuredQuery_Order *order = [GCFSStructuredQuery_Order message];
+ order.field.fieldPath = property;
+ order.direction = ascending ? GCFSStructuredQuery_Direction_Ascending
+ : GCFSStructuredQuery_Direction_Descending;
+ return order;
+}
+@end
+
+@interface FSTSerializerBetaTests : XCTestCase
+@property(nonatomic, strong) FSTSerializerBeta *serializer;
+@end
+
+@implementation FSTSerializerBetaTests
+
+- (void)setUp {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"p" database:@"d"];
+ self.serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseID];
+}
+
+- (void)testEncodesNull {
+ FSTFieldValue *model = [FSTNullValue nullValue];
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.nullValue = GPBNullValue_NullValue;
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_NullValue];
+}
+
+- (void)testEncodesBool {
+ NSArray<NSNumber *> *examples = @[ @YES, @NO ];
+ for (NSNumber *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.booleanValue = [example boolValue];
+
+ [self assertRoundTripForModel:model
+ proto:proto
+ type:GCFSValue_ValueType_OneOfCase_BooleanValue];
+ }
+}
+
+- (void)testEncodesIntegers {
+ NSArray<NSNumber *> *examples = @[ @(LLONG_MIN), @(-100), @(-1), @0, @1, @100, @(LLONG_MAX) ];
+ for (NSNumber *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.integerValue = [example longLongValue];
+
+ [self assertRoundTripForModel:model
+ proto:proto
+ type:GCFSValue_ValueType_OneOfCase_IntegerValue];
+ }
+}
+
+- (void)testEncodesDoubles {
+ NSArray<NSNumber *> *examples = @[
+ // normal negative numbers.
+ @(-INFINITY), @(-DBL_MAX), @(LLONG_MIN * 1.0 - 1.0), @(-2.0), @(-1.1), @(-1.0), @(-DBL_MIN),
+
+ // negative smallest subnormal, zeroes, positive smallest subnormal
+ @(-0x1.0p-1074), @(-0.0), @(0.0), @(0x1.0p-1074),
+
+ // and the rest
+ @(DBL_MIN), @0.1, @1.1, @(LLONG_MAX * 1.0), @(DBL_MAX), @(INFINITY),
+
+ // NaN.
+ @(0.0 / 0.0)
+ ];
+ for (NSNumber *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.doubleValue = [example doubleValue];
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_DoubleValue];
+ }
+}
+
+- (void)testEncodesStrings {
+ NSArray<NSString *> *examples = @[
+ @"",
+ @"a",
+ @"abc def",
+ @"æ",
+ @"\0\ud7ff\ue000\uffff",
+ @"(╯°□°)╯︵ ┻━┻",
+ ];
+ for (NSString *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.stringValue = example;
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_StringValue];
+ }
+}
+
+- (void)testEncodesDates {
+ NSDateComponents *dateWithNanos = FSTTestDateComponents(2016, 1, 2, 10, 20, 50);
+ dateWithNanos.nanosecond = 500000000;
+
+ NSArray<NSDate *> *examples = @[
+ [[NSCalendar currentCalendar] dateFromComponents:dateWithNanos],
+ FSTTestDate(2016, 6, 17, 10, 50, 15)
+ ];
+
+ GCFSValue *timestamp1 = [GCFSValue message];
+ timestamp1.timestampValue.seconds = 1451730050;
+ timestamp1.timestampValue.nanos = 500000000;
+
+ GCFSValue *timestamp2 = [GCFSValue message];
+ timestamp2.timestampValue.seconds = 1466160615;
+ timestamp2.timestampValue.nanos = 0;
+ NSArray<GCFSValue *> *expectedTimestamps = @[ timestamp1, timestamp2 ];
+
+ for (NSUInteger i = 0; i < [examples count]; i++) {
+ [self assertRoundTripForModel:FSTTestFieldValue(examples[i])
+ proto:expectedTimestamps[i]
+ type:GCFSValue_ValueType_OneOfCase_TimestampValue];
+ }
+}
+
+- (void)testEncodesGeoPoints {
+ NSArray<FIRGeoPoint *> *examples =
+ @[ FSTTestGeoPoint(0, 0), FSTTestGeoPoint(1.24, 4.56), FSTTestGeoPoint(-90, 180) ];
+ for (FIRGeoPoint *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.geoPointValue = [GTPLatLng message];
+ proto.geoPointValue.latitude = example.latitude;
+ proto.geoPointValue.longitude = example.longitude;
+
+ [self assertRoundTripForModel:model
+ proto:proto
+ type:GCFSValue_ValueType_OneOfCase_GeoPointValue];
+ }
+}
+
+- (void)testEncodesBlobs {
+ NSArray<NSData *> *examples = @[
+ FSTTestData(-1),
+ FSTTestData(0, -1),
+ FSTTestData(0, 1, 2, -1),
+ FSTTestData(255, -1),
+ FSTTestData(0, 1, 255, -1),
+ ];
+ for (NSData *example in examples) {
+ FSTFieldValue *model = FSTTestFieldValue(example);
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.bytesValue = example;
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_BytesValue];
+ }
+}
+
+- (void)testEncodesResourceNames {
+ FSTDocumentKeyReference *reference = FSTTestRef(@"project", kDefaultDatabaseID, @"foo/bar");
+ GCFSValue *proto = [GCFSValue message];
+ proto.referenceValue = @"projects/project/databases/(default)/documents/foo/bar";
+
+ [self assertRoundTripForModel:FSTTestFieldValue(reference)
+ proto:proto
+ type:GCFSValue_ValueType_OneOfCase_ReferenceValue];
+}
+
+- (void)testEncodesArrays {
+ FSTFieldValue *model = FSTTestFieldValue(@[ @YES, @"foo" ]);
+
+ GCFSValue *proto = [GCFSValue message];
+ [proto.arrayValue.valuesArray addObjectsFromArray:@[
+ [self.serializer encodedBool:YES], [self.serializer encodedString:@"foo"]
+ ]];
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_ArrayValue];
+}
+
+- (void)testEncodesEmptyMap {
+ FSTFieldValue *model = [FSTObjectValue objectValue];
+
+ GCFSValue *proto = [GCFSValue message];
+ proto.mapValue = [GCFSMapValue message];
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue];
+}
+
+- (void)testEncodesNestedObjects {
+ FSTFieldValue *model = FSTTestFieldValue(@{
+ @"b" : @YES,
+ @"d" : @(DBL_MAX),
+ @"i" : @1,
+ @"n" : [NSNull null],
+ @"s" : @"foo",
+ @"a" : @[ @2, @"bar", @{@"b" : @NO} ],
+ @"o" : @{
+ @"d" : @100,
+ @"nested" : @{@"e" : @(LLONG_MIN)},
+ },
+ });
+
+ GCFSValue *innerObject = [GCFSValue message];
+ innerObject.mapValue.fields[@"b"] = [self.serializer encodedBool:NO];
+
+ GCFSValue *middleArray = [GCFSValue message];
+ [middleArray.arrayValue.valuesArray addObjectsFromArray:@[
+ [self.serializer encodedInteger:2], [self.serializer encodedString:@"bar"], innerObject
+ ]];
+
+ innerObject = [GCFSValue message];
+ innerObject.mapValue.fields[@"e"] = [self.serializer encodedInteger:LLONG_MIN];
+
+ GCFSValue *middleObject = [GCFSValue message];
+ [middleObject.mapValue.fields addEntriesFromDictionary:@{
+ @"d" : [self.serializer encodedInteger:100],
+ @"nested" : innerObject
+ }];
+
+ GCFSValue *proto = [GCFSValue message];
+ [proto.mapValue.fields addEntriesFromDictionary:@{
+ @"b" : [self.serializer encodedBool:YES],
+ @"d" : [self.serializer encodedDouble:DBL_MAX],
+ @"i" : [self.serializer encodedInteger:1],
+ @"n" : [self.serializer encodedNull],
+ @"s" : [self.serializer encodedString:@"foo"],
+ @"a" : middleArray,
+ @"o" : middleObject
+ }];
+
+ [self assertRoundTripForModel:model proto:proto type:GCFSValue_ValueType_OneOfCase_MapValue];
+}
+
+- (void)assertRoundTripForModel:(FSTFieldValue *)model
+ proto:(GCFSValue *)value
+ type:(GCFSValue_ValueType_OneOfCase)type {
+ GCFSValue *actualProto = [self.serializer encodedFieldValue:model];
+ XCTAssertEqual(actualProto.valueTypeOneOfCase, type);
+ XCTAssertEqualObjects(actualProto, value);
+
+ FSTFieldValue *actualModel = [self.serializer decodedFieldValue:value];
+ XCTAssertEqualObjects(actualModel, model);
+}
+
+- (void)testEncodesSetMutation {
+ FSTSetMutation *mutation = FSTTestSetMutation(@"docs/1", @{ @"a" : @"b", @"num" : @1 });
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key];
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)testEncodesPatchMutation {
+ FSTPatchMutation *mutation =
+ FSTTestPatchMutation(@"docs/1",
+ @{ @"a" : @"b",
+ @"num" : @1,
+ @"some.de\\\\ep.th\\ing'" : @2 },
+ nil);
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key];
+ proto.updateMask = [self.serializer encodedFieldMask:mutation.fieldMask];
+ proto.currentDocument.exists = YES;
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)testEncodesDeleteMutation {
+ FSTDeleteMutation *mutation = FSTTestDeleteMutation(@"docs/1");
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.delete_p = @"projects/p/databases/d/documents/docs/1";
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)testEncodesTransformMutation {
+ FSTTransformMutation *mutation = FSTTestTransformMutation(@"docs/1", @[ @"a", @"bar.baz" ]);
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.transform = [GCFSDocumentTransform message];
+ proto.transform.document = [self.serializer encodedDocumentKey:mutation.key];
+ proto.transform.fieldTransformsArray =
+ [self.serializer encodedFieldTransforms:mutation.fieldTransforms];
+ proto.currentDocument.exists = YES;
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)testEncodesSetMutationWithPrecondition {
+ FSTSetMutation *mutation = [[FSTSetMutation alloc]
+ initWithKey:FSTTestDocKey(@"foo/bar")
+ value:FSTTestObjectValue(
+ @{ @"a" : @"b",
+ @"num" : @1 })
+ precondition:[FSTPrecondition preconditionWithUpdateTime:FSTTestVersion(4)]];
+ GCFSWrite *proto = [GCFSWrite message];
+ proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key];
+ proto.currentDocument.updateTime =
+ [self.serializer encodedTimestamp:[[FSTTimestamp alloc] initWithSeconds:0 nanos:4000]];
+
+ [self assertRoundTripForMutation:mutation proto:proto];
+}
+
+- (void)assertRoundTripForMutation:(FSTMutation *)mutation proto:(GCFSWrite *)proto {
+ GCFSWrite *actualProto = [self.serializer encodedMutation:mutation];
+ XCTAssertEqualObjects(actualProto, proto);
+
+ FSTMutation *actualMutation = [self.serializer decodedMutation:proto];
+ XCTAssertEqualObjects(actualMutation, mutation);
+}
+
+- (void)testRoundTripSpecialFieldNames {
+ FSTMutation *set = FSTTestSetMutation(@"collection/key", @{
+ @"field" : [NSString stringWithFormat:@"field %d", 1],
+ @"field.dot" : @2,
+ @"field\\slash" : @3
+ });
+ GCFSWrite *encoded = [self.serializer encodedMutation:set];
+ FSTMutation *decoded = [self.serializer decodedMutation:encoded];
+ XCTAssertEqualObjects(set, decoded);
+}
+
+- (void)testEncodesListenRequestLabels {
+ FSTQuery *query = FSTTestQuery(@"collection/key");
+ FSTQueryData *queryData =
+ [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeListen];
+
+ NSDictionary<NSString *, NSString *> *result =
+ [self.serializer encodedListenRequestLabelsForQueryData:queryData];
+ XCTAssertNil(result);
+
+ queryData =
+ [[FSTQueryData alloc] initWithQuery:query targetID:2 purpose:FSTQueryPurposeLimboResolution];
+ result = [self.serializer encodedListenRequestLabelsForQueryData:queryData];
+ XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"});
+
+ queryData = [[FSTQueryData alloc] initWithQuery:query
+ targetID:2
+ purpose:FSTQueryPurposeExistenceFilterMismatch];
+ result = [self.serializer encodedListenRequestLabelsForQueryData:queryData];
+ XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"});
+}
+
+- (void)testEncodesRelationFilter {
+ FSTRelationFilter *input = FSTTestFilter(@"item.part.top", @"==", @"food");
+ GCFSStructuredQuery_Filter *actual = [self.serializer encodedRelationFilter:input];
+
+ GCFSStructuredQuery_Filter *expected = [GCFSStructuredQuery_Filter message];
+ GCFSStructuredQuery_FieldFilter *prop = expected.fieldFilter;
+ prop.field.fieldPath = @"item.part.top";
+ prop.op = GCFSStructuredQuery_FieldFilter_Operator_Equal;
+ prop.value.stringValue = @"food";
+ XCTAssertEqualObjects(actual, expected);
+}
+
+#pragma mark - encodedQuery
+
+- (void)testEncodesFirstLevelKeyQueries {
+ FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs/1")];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ [expected.documents.documentsArray addObject:@"projects/p/databases/d/documents/docs/1"];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesFirstLevelAncestorQueries {
+ FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"messages")];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"messages";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesNestedAncestorQueries {
+ FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"attachments";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesSingleFiltersAtFirstLevelCollections {
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")]
+ queryByAddingFilter:FSTTestFilter(@"prop", @"<", @(42))];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+
+ GCFSStructuredQuery_FieldFilter *filter = expected.query.structuredQuery.where.fieldFilter;
+ filter.field.fieldPath = @"prop";
+ filter.op = GCFSStructuredQuery_FieldFilter_Operator_LessThan;
+ filter.value.integerValue = 42;
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesMultipleFiltersOnDeeperCollections {
+ FSTQuery *q = [[[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")]
+ queryByAddingFilter:FSTTestFilter(@"prop", @">=", @(42))]
+ queryByAddingFilter:FSTTestFilter(@"author", @"==", @"dimond")];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"attachments";
+ [expected.query.structuredQuery.fromArray addObject:from];
+
+ GCFSStructuredQuery_Filter *filter1 = [GCFSStructuredQuery_Filter message];
+ GCFSStructuredQuery_FieldFilter *field1 = filter1.fieldFilter;
+ field1.field.fieldPath = @"prop";
+ field1.op = GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual;
+ field1.value.integerValue = 42;
+
+ GCFSStructuredQuery_Filter *filter2 = [GCFSStructuredQuery_Filter message];
+ GCFSStructuredQuery_FieldFilter *field2 = filter2.fieldFilter;
+ field2.field.fieldPath = @"author";
+ field2.op = GCFSStructuredQuery_FieldFilter_Operator_Equal;
+ field2.value.stringValue = @"dimond";
+
+ GCFSStructuredQuery_CompositeFilter *composite =
+ expected.query.structuredQuery.where.compositeFilter;
+ composite.op = GCFSStructuredQuery_CompositeFilter_Operator_And;
+ [composite.filtersArray addObject:filter1];
+ [composite.filtersArray addObject:filter2];
+
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesNullFilter {
+ [self unaryFilterTestWithValue:[NSNull null]
+ expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNull];
+}
+
+- (void)testEncodesNanFilter {
+ [self unaryFilterTestWithValue:@(NAN)
+ expectedUnaryOperator:GCFSStructuredQuery_UnaryFilter_Operator_IsNan];
+}
+
+- (void)unaryFilterTestWithValue:(id)value
+ expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator)
+ operator{
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")]
+ queryByAddingFilter:FSTTestFilter(@"prop", @"==", value)];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+
+ GCFSStructuredQuery_UnaryFilter *filter = expected.query.structuredQuery.where.unaryFilter;
+ filter.field.fieldPath = @"prop";
+ filter.op = operator;
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesSortOrders {
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")]
+ queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop")
+ ascending:YES]];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:YES]];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesSortOrdersDescending {
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"rooms/1/messages/10/attachments")]
+ queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(@"prop")
+ ascending:NO]];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"attachments";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:@"prop" ascending:NO]];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:NO]];
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesLimits {
+ FSTQuery *q = [[FSTQuery queryWithPath:FSTTestPath(@"docs")] queryBySettingLimit:26];
+ FSTQueryData *model = [self queryDataForQuery:q];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.query.structuredQuery.limit.value = 26;
+ expected.targetId = 1;
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (void)testEncodesResumeTokens {
+ FSTQuery *q = [FSTQuery queryWithPath:FSTTestPath(@"docs")];
+ FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:q
+ targetID:1
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:[FSTSnapshotVersion noVersion]
+ resumeToken:FSTTestData(1, 2, 3, -1)];
+
+ GCFSTarget *expected = [GCFSTarget message];
+ expected.query.parent = @"projects/p/databases/d";
+ GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message];
+ from.collectionId = @"docs";
+ [expected.query.structuredQuery.fromArray addObject:from];
+ [expected.query.structuredQuery.orderByArray
+ addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]];
+ expected.targetId = 1;
+ expected.resumeToken = FSTTestData(1, 2, 3, -1);
+
+ [self assertRoundTripForQueryData:model proto:expected];
+}
+
+- (FSTQueryData *)queryDataForQuery:(FSTQuery *)query {
+ return [[FSTQueryData alloc] initWithQuery:query
+ targetID:1
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:[FSTSnapshotVersion noVersion]
+ resumeToken:[NSData data]];
+}
+
+- (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget *)proto {
+ // Verify that the encoded FSTQueryData matches the target.
+ GCFSTarget *actualProto = [self.serializer encodedTarget:queryData];
+ XCTAssertEqualObjects(actualProto, proto);
+
+ // We don't have deserialization logic for full targets since they're not used for RPC
+ // interaction, but the query deserialization only *is* used for the local store.
+ FSTQuery *actualModel;
+ if (proto.targetTypeOneOfCase == GCFSTarget_TargetType_OneOfCase_Query) {
+ actualModel = [self.serializer decodedQueryFromQueryTarget:proto.query];
+ } else {
+ actualModel = [self.serializer decodedQueryFromDocumentsTarget:proto.documents];
+ }
+ XCTAssertEqualObjects(actualModel, queryData.query);
+}
+
+- (void)testConvertsTargetChangeWithAdded {
+ FSTWatchChange *expected =
+ [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateAdded
+ targetIDs:@[ @1, @4 ]
+ resumeToken:[NSData data]
+ cause:nil];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Add;
+ [listenResponse.targetChange.targetIdsArray addValue:1];
+ [listenResponse.targetChange.targetIdsArray addValue:4];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsTargetChangeWithRemoved {
+ FSTWatchChange *expected = [[FSTWatchTargetChange alloc]
+ initWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:@[ @1, @4 ]
+ resumeToken:FSTTestData(0, 1, 2, -1)
+ cause:[NSError errorWithDomain:FIRFirestoreErrorDomain
+ code:FIRFirestoreErrorCodePermissionDenied
+ userInfo:@{
+ NSLocalizedDescriptionKey : @"Error message",
+ }]];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Remove;
+ listenResponse.targetChange.cause.code = FIRFirestoreErrorCodePermissionDenied;
+ listenResponse.targetChange.cause.message = @"Error message";
+ listenResponse.targetChange.resumeToken = FSTTestData(0, 1, 2, -1);
+ [listenResponse.targetChange.targetIdsArray addValue:1];
+ [listenResponse.targetChange.targetIdsArray addValue:4];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsTargetChangeWithNoChange {
+ FSTWatchChange *expected =
+ [[FSTWatchTargetChange alloc] initWithState:FSTWatchTargetChangeStateNoChange
+ targetIDs:@[ @1, @4 ]
+ resumeToken:[NSData data]
+ cause:nil];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_NoChange;
+ [listenResponse.targetChange.targetIdsArray addValue:1];
+ [listenResponse.targetChange.targetIdsArray addValue:4];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsDocumentChangeWithTargetIds {
+ FSTWatchChange *expected = [[FSTDocumentWatchChange alloc]
+ initWithUpdatedTargetIDs:@[ @1, @2 ]
+ removedTargetIDs:@[]
+ documentKey:FSTTestDocKey(@"coll/1")
+ document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1";
+ listenResponse.documentChange.document.updateTime.nanos = 5000;
+ GCFSValue *fooValue = [GCFSValue message];
+ fooValue.stringValue = @"bar";
+ [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"];
+ [listenResponse.documentChange.targetIdsArray addValue:1];
+ [listenResponse.documentChange.targetIdsArray addValue:2];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsDocumentChangeWithRemovedTargetIds {
+ FSTWatchChange *expected = [[FSTDocumentWatchChange alloc]
+ initWithUpdatedTargetIDs:@[ @2 ]
+ removedTargetIDs:@[ @1 ]
+ documentKey:FSTTestDocKey(@"coll/1")
+ document:FSTTestDoc(@"coll/1", 5, @{@"foo" : @"bar"}, NO)];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.documentChange.document.name = @"projects/p/databases/d/documents/coll/1";
+ listenResponse.documentChange.document.updateTime.nanos = 5000;
+ GCFSValue *fooValue = [GCFSValue message];
+ fooValue.stringValue = @"bar";
+ [listenResponse.documentChange.document.fields setObject:fooValue forKey:@"foo"];
+ [listenResponse.documentChange.removedTargetIdsArray addValue:1];
+ [listenResponse.documentChange.targetIdsArray addValue:2];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsDocumentChangeWithDeletions {
+ FSTWatchChange *expected =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1, @2 ]
+ documentKey:FSTTestDocKey(@"coll/1")
+ document:FSTTestDeletedDoc(@"coll/1", 5)];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.documentDelete.document = @"projects/p/databases/d/documents/coll/1";
+ listenResponse.documentDelete.readTime.nanos = 5000;
+ [listenResponse.documentDelete.removedTargetIdsArray addValue:1];
+ [listenResponse.documentDelete.removedTargetIdsArray addValue:2];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+- (void)testConvertsDocumentChangeWithRemoves {
+ FSTWatchChange *expected =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:@[ @1, @2 ]
+ documentKey:FSTTestDocKey(@"coll/1")
+ document:nil];
+ GCFSListenResponse *listenResponse = [GCFSListenResponse message];
+ listenResponse.documentRemove.document = @"projects/p/databases/d/documents/coll/1";
+ [listenResponse.documentRemove.removedTargetIdsArray addValue:1];
+ [listenResponse.documentRemove.removedTargetIdsArray addValue:2];
+ FSTWatchChange *actual = [self.serializer decodedWatchChange:listenResponse];
+
+ XCTAssertEqualObjects(actual, expected);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTStreamTests.m b/Firestore/Example/Tests/Remote/FSTStreamTests.m
new file mode 100644
index 0000000..f27b200
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTStreamTests.m
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Remote/FSTDatastore.h"
+
+#import <OCMock/OCMock.h>
+#import <XCTest/XCTest.h>
+
+#import "Auth/FSTEmptyCredentialsProvider.h"
+#import "Core/FSTDatabaseInfo.h"
+#import "Model/FSTDatabaseID.h"
+#import "Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h"
+#import "Util/FSTDispatchQueue.h"
+
+/** Expose otherwise private methods for testing. */
+@interface FSTStream (Testing)
+
+- (void)writesFinishedWithError:(NSError *_Nullable)error;
+
+@end
+
+@interface FSTStreamTests : XCTestCase
+@end
+
+@implementation FSTStreamTests {
+ dispatch_queue_t _testQueue;
+ FSTDatabaseInfo *_databaseInfo;
+ FSTDispatchQueue *_workerDispatchQueue;
+ id<FSTCredentialsProvider> _credentials;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID];
+ _databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"test"
+ host:@"test-host"
+ sslEnabled:NO];
+
+ _testQueue = dispatch_queue_create("com.firebase.testing", DISPATCH_QUEUE_SERIAL);
+ _workerDispatchQueue = [FSTDispatchQueue queueWith:_testQueue];
+ _credentials = [[FSTEmptyCredentialsProvider alloc] init];
+}
+
+- (void)tearDown {
+ [super tearDown];
+}
+
+- (void)testWatchStreamStop {
+ id delegate = OCMStrictProtocolMock(@protocol(FSTWatchStreamDelegate));
+
+ FSTWatchStream *stream =
+ OCMPartialMock([[FSTWatchStream alloc] initWithDatabase:_databaseInfo
+ workerDispatchQueue:_workerDispatchQueue
+ credentials:_credentials
+ responseMessageClass:[GCFSWriteResponse class]
+ delegate:delegate]);
+ OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil);
+
+ // Start the stream up but that's not really the interesting bit. This is complicated by the fact
+ // that startup involves redispatching after credentials are returned.
+ dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0);
+ OCMStub([delegate watchStreamDidOpen]).andDo(^(NSInvocation *invocation) {
+ dispatch_semaphore_signal(openCompleted);
+ });
+ dispatch_async(_testQueue, ^{
+ [stream start];
+ });
+ dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER);
+ OCMVerifyAll(delegate);
+
+ // Stop must not call watchStreamDidClose because the full implementation of the delegate could
+ // attempt to restart the stream in the event it had pending watches.
+ dispatch_sync(_testQueue, ^{
+ [stream stop];
+ });
+ OCMVerifyAll(delegate);
+
+ // Simulate a final callback from GRPC
+ [stream writesFinishedWithError:nil];
+ // Drain queue
+ dispatch_sync(_testQueue, ^{
+ });
+ OCMVerifyAll(delegate);
+}
+
+- (void)testWriteStreamStop {
+ id delegate = OCMStrictProtocolMock(@protocol(FSTWriteStreamDelegate));
+
+ FSTWriteStream *stream =
+ OCMPartialMock([[FSTWriteStream alloc] initWithDatabase:_databaseInfo
+ workerDispatchQueue:_workerDispatchQueue
+ credentials:_credentials
+ responseMessageClass:[GCFSWriteResponse class]
+ delegate:delegate]);
+ OCMStub([stream createRPCWithRequestsWriter:[OCMArg any]]).andReturn(nil);
+
+ // Start the stream up but that's not really the interesting bit.
+ dispatch_semaphore_t openCompleted = dispatch_semaphore_create(0);
+ OCMStub([delegate writeStreamDidOpen]).andDo(^(NSInvocation *invocation) {
+ dispatch_semaphore_signal(openCompleted);
+ });
+ dispatch_async(_testQueue, ^{
+ [stream start];
+ });
+ dispatch_semaphore_wait(openCompleted, DISPATCH_TIME_FOREVER);
+ OCMVerifyAll(delegate);
+
+ // Stop must not call writeStreamDidClose because the full implementation of this delegate could
+ // attempt to restart the stream in the event it had pending writes.
+ dispatch_sync(_testQueue, ^{
+ [stream stop];
+ });
+ OCMVerifyAll(delegate);
+
+ // Simulate a final callback from GRPC
+ [stream writesFinishedWithError:nil];
+ // Drain queue
+ dispatch_sync(_testQueue, ^{
+ });
+ OCMVerifyAll(delegate);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h
new file mode 100644
index 0000000..f94fe05
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "Core/FSTTypes.h"
+#import "Remote/FSTWatchChange.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** FSTWatchTargetChange is a change to a watch target. */
+@interface FSTWatchTargetChange (Testing)
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs;
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ cause:(nullable NSError *)cause;
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ resumeToken:(nullable NSData *)resumeToken;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m
new file mode 100644
index 0000000..cb5e479
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTWatchChange+Testing.m
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTWatchChange+Testing.h"
+
+#import "Model/FSTDocument.h"
+#import "Remote/FSTWatchChange.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTWatchTargetChange (Testing)
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs {
+ return [[FSTWatchTargetChange alloc] initWithState:state
+ targetIDs:targetIDs
+ resumeToken:[NSData data]
+ cause:nil];
+}
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ cause:(nullable NSError *)cause {
+ return [[FSTWatchTargetChange alloc] initWithState:state
+ targetIDs:targetIDs
+ resumeToken:[NSData data]
+ cause:cause];
+}
+
++ (instancetype)changeWithState:(FSTWatchTargetChangeState)state
+ targetIDs:(NSArray<NSNumber *> *)targetIDs
+ resumeToken:(nullable NSData *)resumeToken {
+ return [[FSTWatchTargetChange alloc] initWithState:state
+ targetIDs:targetIDs
+ resumeToken:resumeToken
+ cause:nil];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m
new file mode 100644
index 0000000..ccbd644
--- /dev/null
+++ b/Firestore/Example/Tests/Remote/FSTWatchChangeTests.m
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Remote/FSTWatchChange.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Model/FSTDocument.h"
+#import "Remote/FSTExistenceFilter.h"
+
+#import "FSTHelpers.h"
+#import "FSTWatchChange+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTWatchChangeTests : XCTestCase
+@end
+
+@implementation FSTWatchChangeTests
+
+- (void)testDocumentChange {
+ FSTMaybeDocument *doc = FSTTestDoc(@"a/b", 1, @{}, NO);
+ FSTDocumentWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ]
+ removedTargetIDs:@[ @4, @5 ]
+ documentKey:doc.key
+ document:doc];
+ XCTAssertEqual(change.updatedTargetIDs.count, 3);
+ XCTAssertEqual(change.removedTargetIDs.count, 2);
+ // Testing object identity here is fine.
+ XCTAssertEqual(change.document, doc);
+}
+
+- (void)testExistenceFilterChange {
+ FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:7];
+ FSTExistenceFilterWatchChange *change =
+ [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:5];
+ XCTAssertEqual(change.filter.count, 7);
+ XCTAssertEqual(change.targetID, 5);
+}
+
+- (void)testWatchTargetChange {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:@[ @1, @2 ]
+ cause:nil];
+ XCTAssertEqual(change.state, FSTWatchTargetChangeStateReset);
+ XCTAssertEqual(change.targetIDs.count, 2);
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m
new file mode 100644
index 0000000..88b3f12
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.m
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTSpecTests.h"
+
+#import "Local/FSTLevelDB.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTSyncEngineTestDriver.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An implementation of FSTSpecTests that uses the LevelDB implementation of local storage.
+ *
+ * See the FSTSpecTests class comments for more information about how this works.
+ */
+@interface FSTLevelDBSpecTests : FSTSpecTests
+@end
+
+@implementation FSTLevelDBSpecTests
+
+/** Overrides -[FSTSpecTests persistence] */
+- (id<FSTPersistence>)persistence {
+ return [FSTPersistenceTestHelpers levelDBPersistence];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m
new file mode 100644
index 0000000..9cf1f39
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.m
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTSpecTests.h"
+
+#import "Local/FSTMemoryPersistence.h"
+
+#import "FSTPersistenceTestHelpers.h"
+#import "FSTSyncEngineTestDriver.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * An implementation of FSTSpecTests that uses the memory-only implementation of local storage.
+ *
+ * @see the FSTSpecTests class comments for more information about how this works.
+ */
+@interface FSTMemorySpecTests : FSTSpecTests
+@end
+
+@implementation FSTMemorySpecTests
+
+/** Overrides -[FSTSpecTests persistence] */
+- (id<FSTPersistence>)persistence {
+ return [FSTPersistenceTestHelpers memoryPersistence];
+}
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h
new file mode 100644
index 0000000..4ff1220
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "Remote/FSTDatastore.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTMockDatastore : FSTDatastore
+
++ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue;
+
+#pragma mark - Watch Stream manipulation.
+
+/** Injects an Added WatchChange containing the given targetIDs. */
+- (void)writeWatchTargetAddedWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs;
+
+/** Injects an Added WatchChange that marks the given targetIDs current. */
+- (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ resumeToken:(NSData *)resumeToken;
+
+/** Injects a WatchChange as though it had come from the backend. */
+- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap;
+
+/** Injects a stream failure as though it had come from the backend. */
+- (void)failWatchStreamWithError:(NSError *)error;
+
+/** Returns the set of active targets on the watch stream. */
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)activeTargets;
+
+/** Helper method to expose watch stream state to verify in tests. */
+- (BOOL)isWatchStreamOpen;
+
+#pragma mark - Write Stream manipulation.
+
+/**
+ * Returns the next write that was "sent to the backend", failing if there are no queued sent
+ */
+- (NSArray<FSTMutation *> *)nextSentWrite;
+
+/** Returns the number of writes that have been sent to the backend but not waited on yet. */
+- (int)writesSent;
+
+/** Injects a write ack as though it had come from the backend in response to a write. */
+- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)results;
+
+/** Injects a stream failure as though it had come from the backend. */
+- (void)failWriteWithError:(NSError *_Nullable)error;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m
new file mode 100644
index 0000000..1a1f659
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.m
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTMockDatastore.h"
+
+#import "Auth/FSTEmptyCredentialsProvider.h"
+#import "Core/FSTDatabaseInfo.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTMutation.h"
+#import "Util/FSTAssert.h"
+#import "Util/FSTLogger.h"
+
+#import "FSTWatchChange+Testing.h"
+
+@class GRPCProtoCall;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTMockWatchStream
+
+@interface FSTMockWatchStream : FSTWatchStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ delegate:(id<FSTWatchStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWatchStreamDelegate>)delegate NS_UNAVAILABLE;
+
+@property(nonatomic, assign) BOOL open;
+
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *activeTargets;
+
+@end
+
+@implementation FSTMockWatchStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ delegate:(id<FSTWatchStreamDelegate>)delegate {
+ self = [super initWithDatabase:database
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials
+ responseMessageClass:[FSTWatchChange class]
+ delegate:delegate];
+ if (self) {
+ FSTAssert(database, @"Database must not be nil");
+ _activeTargets = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+#pragma mark - Overridden FSTWatchStream methods.
+
+- (void)start {
+ FSTAssert(!self.open, @"Trying to start already started watch stream");
+ self.open = YES;
+ [self handleStreamOpen];
+}
+
+- (BOOL)isOpen {
+ return self.open;
+}
+
+- (BOOL)isStarted {
+ return self.open;
+}
+
+- (void)handleStreamOpen {
+ [self.delegate watchStreamDidOpen];
+}
+
+- (void)watchQuery:(FSTQueryData *)query {
+ FSTLog(@"watchQuery: %d: %@", query.targetID, query.query);
+ // Snapshot version is ignored on the wire
+ FSTQueryData *sentQueryData =
+ [query queryDataByReplacingSnapshotVersion:[FSTSnapshotVersion noVersion]
+ resumeToken:query.resumeToken];
+ self.activeTargets[@(query.targetID)] = sentQueryData;
+}
+
+- (void)unwatchTargetID:(FSTTargetID)targetID {
+ FSTLog(@"unwatchTargetID: %d", targetID);
+ [self.activeTargets removeObjectForKey:@(targetID)];
+}
+
+- (void)failStreamWithError:(NSError *)error {
+ self.open = NO;
+ [self.delegate watchStreamDidClose:error];
+}
+
+#pragma mark - Helper methods.
+
+- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap {
+ if ([change isKindOfClass:[FSTWatchTargetChange class]]) {
+ FSTWatchTargetChange *targetChange = (FSTWatchTargetChange *)change;
+ if (targetChange.cause) {
+ for (NSNumber *targetID in targetChange.targetIDs) {
+ if (!self.activeTargets[targetID]) {
+ // Technically removing an unknown target is valid (e.g. it could race with a
+ // server-side removal), but we want to pay extra careful attention in tests
+ // that we only remove targets we listened too.
+ FSTFail(@"Removing a non-active target");
+ }
+ [self.activeTargets removeObjectForKey:targetID];
+ }
+ }
+ }
+ [self.delegate watchStreamDidChange:change snapshotVersion:snap];
+}
+
+@end
+
+#pragma mark - FSTMockWriteStream
+
+@interface FSTMockWriteStream : FSTWriteStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ delegate:(id<FSTWriteStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ responseMessageClass:(Class)responseMessageClass
+ delegate:(id<FSTWatchStreamDelegate>)delegate NS_UNAVAILABLE;
+
+@property(nonatomic, assign) BOOL open;
+@property(nonatomic, strong, readonly) NSMutableArray<NSArray<FSTMutation *> *> *sentMutations;
+
+@end
+
+@implementation FSTMockWriteStream
+
+- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database
+ workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
+ credentials:(id<FSTCredentialsProvider>)credentials
+ delegate:(id<FSTWriteStreamDelegate>)delegate {
+ self = [super initWithDatabase:database
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials
+ responseMessageClass:[FSTMutationResult class]
+ delegate:delegate];
+ if (self) {
+ _sentMutations = [NSMutableArray array];
+ }
+ return self;
+}
+
+#pragma mark - Overridden FSTWriteStream methods.
+
+- (void)start {
+ FSTAssert(!self.open, @"Trying to start already started write stream");
+ self.open = YES;
+ [self.sentMutations removeAllObjects];
+ [self handleStreamOpen];
+}
+
+- (BOOL)isOpen {
+ return self.open;
+}
+
+- (BOOL)isStarted {
+ return self.open;
+}
+
+- (void)writeHandshake {
+ self.handshakeComplete = YES;
+ [self.delegate writeStreamDidCompleteHandshake];
+}
+
+- (void)writeMutations:(NSArray<FSTMutation *> *)mutations {
+ [self.sentMutations addObject:mutations];
+}
+
+- (void)handleStreamOpen {
+ [self.delegate writeStreamDidOpen];
+}
+
+#pragma mark - Helper methods.
+
+/** Injects a write ack as though it had come from the backend in response to a write. */
+- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)results {
+ [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results];
+}
+
+/** Injects a failed write response as though it had come from the backend. */
+- (void)failStreamWithError:(NSError *)error {
+ self.open = NO;
+ [self.delegate writeStreamDidClose:error];
+}
+
+/**
+ * Returns the next write that was "sent to the backend", failing if there are no queued sent
+ */
+- (NSArray<FSTMutation *> *)nextSentWrite {
+ FSTAssert(self.sentMutations.count > 0,
+ @"Writes need to happen before you can call nextSentWrite.");
+ NSArray<FSTMutation *> *result = [self.sentMutations objectAtIndex:0];
+ [self.sentMutations removeObjectAtIndex:0];
+ return result;
+}
+
+/**
+ * Returns the number of mutations that have been sent to the backend but not retrieved via
+ * nextSentWrite yet.
+ */
+- (int)sentMutationsCount {
+ return (int)self.sentMutations.count;
+}
+
+@end
+
+#pragma mark - FSTMockDatastore
+
+@interface FSTMockDatastore ()
+@property(nonatomic, strong, nullable) FSTMockWatchStream *watchStream;
+@property(nonatomic, strong, nullable) FSTMockWriteStream *writeStream;
+
+/** Properties implemented in FSTDatastore that are nonpublic. */
+@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue;
+@property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentials;
+
+@end
+
+@implementation FSTMockDatastore
+
++ (instancetype)mockDatastoreWithWorkerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:@"project" database:@"database"];
+ FSTDatabaseInfo *databaseInfo = [FSTDatabaseInfo databaseInfoWithDatabaseID:databaseID
+ persistenceKey:@"persistence"
+ host:@"host"
+ sslEnabled:NO];
+
+ FSTEmptyCredentialsProvider *credentials = [[FSTEmptyCredentialsProvider alloc] init];
+
+ return [[FSTMockDatastore alloc] initWithDatabaseInfo:databaseInfo
+ workerDispatchQueue:workerDispatchQueue
+ credentials:credentials];
+}
+
+#pragma mark - Overridden FSTDatastore methods.
+
+- (FSTWatchStream *)createWatchStreamWithDelegate:(id<FSTWatchStreamDelegate>)delegate {
+ FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil");
+ self.watchStream = [[FSTMockWatchStream alloc] initWithDatabase:self.databaseInfo
+ workerDispatchQueue:self.workerDispatchQueue
+ credentials:self.credentials
+ delegate:delegate];
+ return self.watchStream;
+}
+
+- (FSTWriteStream *)createWriteStreamWithDelegate:(id<FSTWriteStreamDelegate>)delegate {
+ FSTAssert(self.databaseInfo, @"DatabaseInfo must not be nil");
+ self.writeStream = [[FSTMockWriteStream alloc] initWithDatabase:self.databaseInfo
+ workerDispatchQueue:self.workerDispatchQueue
+ credentials:self.credentials
+ delegate:delegate];
+ return self.writeStream;
+}
+
+- (void)authorizeAndStartRPC:(GRPCProtoCall *)rpc completion:(FSTVoidErrorBlock)completion {
+ FSTFail(@"FSTMockDatastore shouldn't be starting any RPCs.");
+}
+
+#pragma mark - Method exposed for tests to call.
+
+- (NSArray<FSTMutation *> *)nextSentWrite {
+ return [self.writeStream nextSentWrite];
+}
+
+- (int)writesSent {
+ return [self.writeStream sentMutationsCount];
+}
+
+- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)results {
+ [self.writeStream ackWriteWithVersion:commitVersion mutationResults:results];
+}
+
+- (void)failWriteWithError:(NSError *_Nullable)error {
+ [self.writeStream failStreamWithError:error];
+}
+
+- (void)writeWatchTargetAddedWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded
+ targetIDs:targetIDs
+ cause:nil];
+ [self writeWatchChange:change snapshotVersion:[FSTSnapshotVersion noVersion]];
+}
+
+- (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ resumeToken:(NSData *)resumeToken {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:targetIDs
+ resumeToken:resumeToken];
+ [self writeWatchChange:change snapshotVersion:snapshotVersion];
+}
+
+- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap {
+ [self.watchStream writeWatchChange:change snapshotVersion:snap];
+}
+
+- (void)failWatchStreamWithError:(NSError *)error {
+ [self.watchStream failStreamWithError:error];
+}
+
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)activeTargets {
+ return [self.watchStream.activeTargets copy];
+}
+
+- (BOOL)isWatchStreamOpen {
+ return self.watchStream.isOpen;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.h b/Firestore/Example/Tests/SpecTests/FSTSpecTests.h
new file mode 100644
index 0000000..3a3dbb2
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.h
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * FSTSpecTests run a set of portable event specifications from JSON spec files against a
+ * special isolated version of the Firestore client that allows precise control over when events
+ * are delivered. This allows us to test client behavior in a very reliable, deterministic way,
+ * including edge cases that would be difficult to reliably reproduce in a full integration test.
+ *
+ * Both events from user code (adding/removing listens, performing mutations) and events from the
+ * Datastore are simulated, while installing as much of the system in between as possible.
+ *
+ * FSTSpecTests is an abstract base class that must be subclassed to test against a specific local
+ * store implementation. To create a new variant of FSTSpecTests:
+ *
+ * + Subclass FSTSpecTests
+ * + override -persistence to create and return an appropriate id<FSTPersistence> implementation.
+ */
+@interface FSTSpecTests : XCTestCase
+
+/** Creates and returns an appropriate id<FSTPersistence> implementation. */
+- (id<FSTPersistence>)persistence;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.m b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m
new file mode 100644
index 0000000..f681347
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.m
@@ -0,0 +1,642 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTSpecTests.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+#import "Auth/FSTUser.h"
+#import "Core/FSTEventManager.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTViewSnapshot.h"
+#import "Firestore/FIRFirestoreErrors.h"
+#import "Local/FSTEagerGarbageCollector.h"
+#import "Local/FSTNoOpGarbageCollector.h"
+#import "Local/FSTPersistence.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTExistenceFilter.h"
+#import "Remote/FSTWatchChange.h"
+#import "Util/FSTAssert.h"
+#import "Util/FSTClasses.h"
+#import "Util/FSTLogger.h"
+
+#import "FSTHelpers.h"
+#import "FSTSyncEngineTestDriver.h"
+#import "FSTWatchChange+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Disables all other tests; useful for debugging. Multiple tests can have this tag and they'll all
+// be run (but all others won't).
+static NSString *const kExclusiveTag = @"exclusive";
+
+// A tag for tests that should be excluded from execution (on iOS), useful to allow the platforms
+// to temporarily diverge.
+static NSString *const kNoIOSTag = @"no-ios";
+
+@interface FSTSpecTests ()
+@property(nonatomic, strong) FSTSyncEngineTestDriver *driver;
+
+// Some config info for the currently running spec; used when restarting the driver (for doRestart).
+@property(nonatomic, assign) BOOL GCEnabled;
+@property(nonatomic, strong) id<FSTPersistence> driverPersistence;
+@end
+
+@implementation FSTSpecTests
+
+- (id<FSTPersistence>)persistence {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)setUpForSpecWithConfig:(NSDictionary *)config {
+ // Store persistence / GCEnabled so we can re-use it in doRestart.
+ self.driverPersistence = [self persistence];
+ NSNumber *GCEnabled = config[@"useGarbageCollection"];
+ self.GCEnabled = [GCEnabled boolValue];
+ self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence
+ garbageCollector:self.garbageCollector];
+ [self.driver start];
+}
+
+- (void)tearDownForSpec {
+ [self.driver shutdown];
+ [self.driverPersistence shutdown];
+}
+
+/**
+ * Creates the appropriate garbage collector for the test configuration: an eager collector if
+ * GC is enabled or a no-op collector otherwise.
+ */
+- (id<FSTGarbageCollector>)garbageCollector {
+ return self.GCEnabled ? [[FSTEagerGarbageCollector alloc] init]
+ : [[FSTNoOpGarbageCollector alloc] init];
+}
+
+/**
+ * Xcode will run tests from any class that extends XCTestCase, but this doesn't work for
+ * FSTSpecTests since it is incomplete without the implementations supplied by its subclasses.
+ */
+- (BOOL)isTestBaseClass {
+ return [self class] == [FSTSpecTests class];
+}
+
+#pragma mark - Methods for constructing objects from specs.
+
+- (nullable FSTQuery *)parseQuery:(id)querySpec {
+ if ([querySpec isKindOfClass:[NSString class]]) {
+ return [FSTQuery queryWithPath:[FSTResourcePath pathWithString:querySpec]];
+ } else if ([querySpec isKindOfClass:[NSDictionary class]]) {
+ NSDictionary *queryDict = (NSDictionary *)querySpec;
+ NSString *path = queryDict[@"path"];
+ __block FSTQuery *query = [FSTQuery queryWithPath:[FSTResourcePath pathWithString:path]];
+ if (queryDict[@"limit"]) {
+ NSNumber *limit = queryDict[@"limit"];
+ query = [query queryBySettingLimit:limit.integerValue];
+ }
+ if (queryDict[@"filters"]) {
+ NSArray *filters = queryDict[@"filters"];
+ [filters enumerateObjectsUsingBlock:^(NSArray *_Nonnull filter, NSUInteger idx,
+ BOOL *_Nonnull stop) {
+ query = [query queryByAddingFilter:FSTTestFilter(filter[0], filter[1], filter[2])];
+ }];
+ }
+ if (queryDict[@"orderBys"]) {
+ NSArray *orderBys = queryDict[@"orderBys"];
+ [orderBys enumerateObjectsUsingBlock:^(NSArray *_Nonnull orderBy, NSUInteger idx,
+ BOOL *_Nonnull stop) {
+ query = [query queryByAddingSortOrder:FSTTestOrderBy(orderBy[0], orderBy[1])];
+ }];
+ }
+ return query;
+ } else {
+ XCTFail(@"Invalid query: %@", querySpec);
+ return nil;
+ }
+}
+
+- (FSTSnapshotVersion *)parseVersion:(NSNumber *_Nullable)version {
+ return FSTTestVersion(version.longLongValue);
+}
+
+- (FSTDocumentViewChange *)parseChange:(NSArray *)change ofType:(FSTDocumentViewChangeType)type {
+ BOOL hasMutations = NO;
+ for (NSUInteger i = 3; i < change.count; ++i) {
+ if ([change[i] isEqual:@"local"]) {
+ hasMutations = YES;
+ }
+ }
+ NSNumber *version = change[1];
+ FSTDocument *doc = FSTTestDoc(change[0], version.longLongValue, change[2], hasMutations);
+ return [FSTDocumentViewChange changeWithDocument:doc type:type];
+}
+
+#pragma mark - Methods for doing the steps of the spec test.
+
+- (void)doListen:(NSArray *)listenSpec {
+ FSTQuery *query = [self parseQuery:listenSpec[1]];
+ FSTTargetID actualID = [self.driver addUserListenerWithQuery:query];
+
+ FSTTargetID expectedID = [listenSpec[0] intValue];
+ XCTAssertEqual(actualID, expectedID);
+}
+
+- (void)doUnlisten:(NSArray *)unlistenSpec {
+ FSTQuery *query = [self parseQuery:unlistenSpec[1]];
+ [self.driver removeUserListenerWithQuery:query];
+}
+
+- (void)doSet:(NSArray *)setSpec {
+ [self.driver writeUserMutation:FSTTestSetMutation(setSpec[0], setSpec[1])];
+}
+
+- (void)doPatch:(NSArray *)patchSpec {
+ [self.driver writeUserMutation:FSTTestPatchMutation(patchSpec[0], patchSpec[1], nil)];
+}
+
+- (void)doDelete:(NSString *)key {
+ [self.driver writeUserMutation:FSTTestDeleteMutation(key)];
+}
+
+- (void)doWatchAck:(NSArray<NSNumber *> *)ackedTargets snapshot:(NSNumber *)watchSnapshot {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded
+ targetIDs:ackedTargets
+ cause:nil];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+}
+
+- (void)doWatchCurrent:(NSArray<id> *)currentSpec snapshot:(NSNumber *)watchSnapshot {
+ NSArray<NSNumber *> *currentTargets = currentSpec[0];
+ NSData *resumeToken = [currentSpec[1] dataUsingEncoding:NSUTF8StringEncoding];
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent
+ targetIDs:currentTargets
+ resumeToken:resumeToken];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+}
+
+- (void)doWatchRemove:(NSDictionary *)watchRemoveSpec snapshot:(NSNumber *)watchSnapshot {
+ NSError *error = nil;
+ NSDictionary *cause = watchRemoveSpec[@"cause"];
+ if (cause) {
+ int code = ((NSNumber *)cause[@"code"]).intValue;
+ NSDictionary *userInfo = @{
+ NSLocalizedDescriptionKey : @"Error from watchRemove.",
+ };
+ error = [NSError errorWithDomain:FIRFirestoreErrorDomain code:code userInfo:userInfo];
+ }
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved
+ targetIDs:watchRemoveSpec[@"targetIds"]
+ cause:error];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+ // Unlike web, the FSTMockDatastore detects a watch removal with cause and will remove active
+ // targets
+}
+
+- (void)doWatchEntity:(NSDictionary *)watchEntity snapshot:(NSNumber *_Nullable)watchSnapshot {
+ if (watchEntity[@"docs"]) {
+ FSTAssert(!watchEntity[@"doc"], @"Exactly one of |doc| or |docs| needs to be set.");
+ int count = 0;
+ NSArray *docs = watchEntity[@"docs"];
+ for (NSDictionary *doc in docs) {
+ count++;
+ bool isLast = (count == docs.count);
+ NSMutableDictionary *watchSpec = [NSMutableDictionary dictionary];
+ watchSpec[@"doc"] = doc;
+ if (watchEntity[@"targets"]) {
+ watchSpec[@"targets"] = watchEntity[@"targets"];
+ }
+ if (watchEntity[@"removedTargets"]) {
+ watchSpec[@"removedTargets"] = watchEntity[@"removedTargets"];
+ }
+ NSNumber *_Nullable version = nil;
+ if (isLast) {
+ version = watchSnapshot;
+ }
+ [self doWatchEntity:watchSpec snapshot:version];
+ }
+ } else if (watchEntity[@"doc"]) {
+ NSArray *docSpec = watchEntity[@"doc"];
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:docSpec[0]];
+ FSTObjectValue *value = FSTTestObjectValue(docSpec[2]);
+ FSTSnapshotVersion *version = [self parseVersion:docSpec[1]];
+ FSTMaybeDocument *doc =
+ [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO];
+ FSTWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"]
+ removedTargetIDs:watchEntity[@"removedTargets"]
+ documentKey:doc.key
+ document:doc];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+ } else if (watchEntity[@"key"]) {
+ FSTDocumentKey *docKey = [FSTDocumentKey keyWithPathString:watchEntity[@"key"]];
+ FSTWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[]
+ removedTargetIDs:watchEntity[@"removedTargets"]
+ documentKey:docKey
+ document:nil];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+ } else {
+ FSTFail(@"Either key, doc or docs must be set.");
+ }
+}
+
+- (void)doWatchFilter:(NSArray *)watchFilter snapshot:(NSNumber *_Nullable)watchSnapshot {
+ NSArray<NSNumber *> *targets = watchFilter[0];
+ FSTAssert(targets.count == 1, @"ExistenceFilters currently support exactly one target only.");
+
+ int keyCount = watchFilter.count == 0 ? 0 : (int)watchFilter.count - 1;
+
+ // TODO(dimond): extend this with different existence filters over time.
+ FSTExistenceFilter *filter = [FSTExistenceFilter filterWithCount:keyCount];
+ FSTExistenceFilterWatchChange *change =
+ [FSTExistenceFilterWatchChange changeWithFilter:filter targetID:targets[0].intValue];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+}
+
+- (void)doWatchReset:(NSArray<NSNumber *> *)watchReset snapshot:(NSNumber *_Nullable)watchSnapshot {
+ FSTWatchTargetChange *change =
+ [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset
+ targetIDs:watchReset
+ cause:nil];
+ [self.driver receiveWatchChange:change snapshotVersion:[self parseVersion:watchSnapshot]];
+}
+
+- (void)doWatchStreamClose:(NSDictionary *)closeSpec {
+ NSDictionary *errorSpec = closeSpec[@"error"];
+ int code = ((NSNumber *)(errorSpec[@"code"])).intValue;
+ [self.driver receiveWatchStreamError:code userInfo:errorSpec];
+}
+
+- (void)doWriteAck:(NSDictionary *)spec {
+ FSTSnapshotVersion *version = [self parseVersion:spec[@"version"]];
+ NSNumber *expectUserCallback = spec[@"expectUserCallback"];
+
+ FSTMutationResult *mutationResult =
+ [[FSTMutationResult alloc] initWithVersion:version transformResults:nil];
+ FSTOutstandingWrite *write =
+ [self.driver receiveWriteAckWithVersion:version mutationResults:@[ mutationResult ]];
+
+ if (expectUserCallback.boolValue) {
+ FSTAssert(write.done, @"Write should be done");
+ FSTAssert(!write.error, @"Ack should not fail");
+ }
+}
+
+- (void)doFailWrite:(NSDictionary *)spec {
+ NSDictionary *errorSpec = spec[@"error"];
+ NSNumber *expectUserCallback = spec[@"expectUserCallback"];
+
+ int code = ((NSNumber *)(errorSpec[@"code"])).intValue;
+ FSTOutstandingWrite *write = [self.driver receiveWriteError:code userInfo:errorSpec];
+
+ if (expectUserCallback.boolValue) {
+ FSTAssert(write.done, @"Write should be done");
+ XCTAssertNotNil(write.error, @"Write should have failed");
+ XCTAssertEqualObjects(write.error.domain, FIRFirestoreErrorDomain);
+ XCTAssertEqual(write.error.code, code);
+ }
+}
+
+- (void)doChangeUser:(id)UID {
+ FSTUser *user = [UID isEqual:[NSNull null]] ? [FSTUser unauthenticatedUser]
+ : [[FSTUser alloc] initWithUID:UID];
+ [self.driver changeUser:user];
+}
+
+- (void)doRestart {
+ // Any outstanding user writes should be automatically re-sent, so we want to preserve them
+ // when re-creating the driver.
+ FSTOutstandingWriteQueues *outstandingWrites = self.driver.outstandingWrites;
+
+ [self.driver shutdown];
+
+ // NOTE: We intentionally don't shutdown / re-create driverPersistence, since we want to
+ // preserve the persisted state. This is a bit of a cheat since it means we're not exercising
+ // the initialization / start logic that would normally be hit, but simplifies the plumbing and
+ // allows us to run these tests against FSTMemoryPersistence as well (there would be no way to
+ // re-create FSTMemoryPersistence without losing all persisted state).
+
+ self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:self.driverPersistence
+ garbageCollector:self.garbageCollector
+ initialUser:self.driver.currentUser
+ outstandingWrites:outstandingWrites];
+ [self.driver start];
+}
+
+- (void)doStep:(NSDictionary *)step {
+ if (step[@"userListen"]) {
+ [self doListen:step[@"userListen"]];
+ } else if (step[@"userUnlisten"]) {
+ [self doUnlisten:step[@"userUnlisten"]];
+ } else if (step[@"userSet"]) {
+ [self doSet:step[@"userSet"]];
+ } else if (step[@"userPatch"]) {
+ [self doPatch:step[@"userPatch"]];
+ } else if (step[@"userDelete"]) {
+ [self doDelete:step[@"userDelete"]];
+ } else if (step[@"watchAck"]) {
+ [self doWatchAck:step[@"watchAck"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchCurrent"]) {
+ [self doWatchCurrent:step[@"watchCurrent"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchRemove"]) {
+ [self doWatchRemove:step[@"watchRemove"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchEntity"]) {
+ [self doWatchEntity:step[@"watchEntity"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchFilter"]) {
+ [self doWatchFilter:step[@"watchFilter"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchReset"]) {
+ [self doWatchReset:step[@"watchReset"] snapshot:step[@"watchSnapshot"]];
+ } else if (step[@"watchStreamClose"]) {
+ [self doWatchStreamClose:step[@"watchStreamClose"]];
+ } else if (step[@"watchProto"]) {
+ // watchProto isn't yet used, and it's unclear how to create arbitrary protos from JSON.
+ FSTFail(@"watchProto is not yet supported.");
+ } else if (step[@"writeAck"]) {
+ [self doWriteAck:step[@"writeAck"]];
+ } else if (step[@"failWrite"]) {
+ [self doFailWrite:step[@"failWrite"]];
+ } else if (step[@"changeUser"]) {
+ [self doChangeUser:step[@"changeUser"]];
+ } else if (step[@"restart"]) {
+ [self doRestart];
+ } else {
+ XCTFail(@"Unknown step: %@", step);
+ }
+}
+
+- (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected {
+ FSTQuery *expectedQuery = [self parseQuery:expected[@"query"]];
+ XCTAssertEqualObjects(actual.query, expectedQuery);
+ if ([expected[@"errorCode"] integerValue] != 0) {
+ XCTAssertNotNil(actual.error);
+ XCTAssertEqual(actual.error.code, [expected[@"errorCode"] integerValue]);
+ } else {
+ NSMutableArray *expectedChanges = [NSMutableArray array];
+ NSMutableArray *removed = expected[@"removed"];
+ for (NSArray *changeSpec in removed) {
+ [expectedChanges
+ addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeRemoved]];
+ }
+ NSMutableArray *added = expected[@"added"];
+ for (NSArray *changeSpec in added) {
+ [expectedChanges
+ addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeAdded]];
+ }
+ NSMutableArray *modified = expected[@"modified"];
+ for (NSArray *changeSpec in modified) {
+ [expectedChanges
+ addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeModified]];
+ }
+ NSMutableArray *metadata = expected[@"metadata"];
+ for (NSArray *changeSpec in metadata) {
+ [expectedChanges
+ addObject:[self parseChange:changeSpec ofType:FSTDocumentViewChangeTypeMetadata]];
+ }
+ XCTAssertEqualObjects(actual.viewSnapshot.documentChanges, expectedChanges);
+
+ BOOL expectedHasPendingWrites =
+ expected[@"hasPendingWrites"] ? [expected[@"hasPendingWrites"] boolValue] : NO;
+ BOOL expectedIsFromCache = expected[@"fromCache"] ? [expected[@"fromCache"] boolValue] : NO;
+ XCTAssertEqual(actual.viewSnapshot.hasPendingWrites, expectedHasPendingWrites,
+ @"hasPendingWrites");
+ XCTAssertEqual(actual.viewSnapshot.isFromCache, expectedIsFromCache, @"isFromCache");
+ }
+}
+
+- (void)validateStepExpectations:(NSMutableArray *_Nullable)stepExpectations {
+ NSArray<FSTQueryEvent *> *events = self.driver.capturedEventsSinceLastCall;
+
+ if (!stepExpectations) {
+ XCTAssertEqual(events.count, 0);
+ for (FSTQueryEvent *event in events) {
+ XCTFail(@"Unexpected event: %@", event);
+ }
+ return;
+ }
+
+ events =
+ [events sortedArrayUsingComparator:^NSComparisonResult(FSTQueryEvent *q1, FSTQueryEvent *q2) {
+ return [q1.query.canonicalID compare:q2.query.canonicalID];
+ }];
+
+ XCTAssertEqual(events.count, stepExpectations.count);
+ NSUInteger i = 0;
+ for (; i < stepExpectations.count && i < events.count; ++i) {
+ [self validateEvent:events[i] matches:stepExpectations[i]];
+ }
+ for (; i < stepExpectations.count; ++i) {
+ XCTFail(@"Missing event: %@", stepExpectations[i]);
+ }
+ for (; i < events.count; ++i) {
+ XCTFail(@"Unexpected event: %@", events[i]);
+ }
+}
+
+- (void)validateStateExpectations:(nullable NSDictionary *)expected {
+ if (expected) {
+ if (expected[@"numOutstandingWrites"]) {
+ XCTAssertEqual([self.driver sentWritesCount], [expected[@"numOutstandingWrites"] intValue]);
+ }
+ if (expected[@"limboDocs"]) {
+ NSMutableSet<FSTDocumentKey *> *expectedLimboDocuments = [NSMutableSet set];
+ NSArray *docNames = expected[@"limboDocs"];
+ for (NSString *name in docNames) {
+ [expectedLimboDocuments addObject:FSTTestDocKey(name)];
+ }
+ // Update the expected limbo documents
+ self.driver.expectedLimboDocuments = expectedLimboDocuments;
+ }
+ if (expected[@"activeTargets"]) {
+ NSMutableDictionary *expectedActiveTargets = [NSMutableDictionary dictionary];
+ [expected[@"activeTargets"] enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString,
+ NSDictionary *queryData,
+ BOOL *stop) {
+ FSTTargetID targetID = [targetIDString intValue];
+ FSTQuery *query = [self parseQuery:queryData[@"query"]];
+ NSData *resumeToken = [queryData[@"resumeToken"] dataUsingEncoding:NSUTF8StringEncoding];
+ // TODO(mcg): populate the purpose of the target once it's possible to encode that in the
+ // spec tests. For now, hard-code that it's a listen despite the fact that it's not always
+ // the right value.
+ expectedActiveTargets[@(targetID)] =
+ [[FSTQueryData alloc] initWithQuery:query
+ targetID:targetID
+ purpose:FSTQueryPurposeListen
+ snapshotVersion:[FSTSnapshotVersion noVersion]
+ resumeToken:resumeToken];
+ }];
+ self.driver.expectedActiveTargets = expectedActiveTargets;
+ }
+ }
+
+ // Always validate that the expected limbo docs match the actual limbo docs.
+ [self validateLimboDocuments];
+ // Always validate that the expected active targets match the actual active targets.
+ [self validateActiveTargets];
+}
+
+- (void)validateLimboDocuments {
+ // Make a copy so it can modified while checking against the expected limbo docs.
+ NSMutableDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *actualLimboDocs =
+ [NSMutableDictionary dictionaryWithDictionary:self.driver.currentLimboDocuments];
+
+ // Validate that each limbo doc has an expected active target
+ [actualLimboDocs enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key,
+ FSTBoxedTargetID *targetID, BOOL *stop) {
+ XCTAssertNotNil(self.driver.expectedActiveTargets[targetID],
+ @"Found limbo doc without an expected active target");
+ }];
+
+ for (FSTDocumentKey *expectedLimboDoc in self.driver.expectedLimboDocuments) {
+ XCTAssertNotNil(actualLimboDocs[expectedLimboDoc],
+ @"Expected doc to be in limbo, but was not: %@", expectedLimboDoc);
+ [actualLimboDocs removeObjectForKey:expectedLimboDoc];
+ }
+ XCTAssertTrue(actualLimboDocs.count == 0, "Unexpected docs in limbo: %@", actualLimboDocs);
+}
+
+- (void)validateActiveTargets {
+ // Create a copy so we can modify it in tests
+ NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *actualTargets =
+ [NSMutableDictionary dictionaryWithDictionary:self.driver.activeTargets];
+
+ [self.driver.expectedActiveTargets enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID,
+ FSTQueryData *queryData,
+ BOOL *stop) {
+ XCTAssertNotNil(actualTargets[targetID], @"Expected active target not found: %@", queryData);
+
+ // TODO(mcg): validate the purpose of the target once it's possible to encode that in the
+ // spec tests. For now, only validate properties that can be validated.
+ // XCTAssertEqualObjects(actualTargets[targetID], queryData);
+
+ FSTQueryData *actual = actualTargets[targetID];
+ XCTAssertEqualObjects(actual.query, queryData.query);
+ XCTAssertEqual(actual.targetID, queryData.targetID);
+ XCTAssertEqualObjects(actual.snapshotVersion, queryData.snapshotVersion);
+ XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken);
+
+ [actualTargets removeObjectForKey:targetID];
+ }];
+ XCTAssertTrue(actualTargets.count == 0, "Unexpected active targets: %@", actualTargets);
+}
+
+- (void)runSpecTestSteps:(NSArray *)steps config:(NSDictionary *)config {
+ @try {
+ [self setUpForSpecWithConfig:config];
+ for (NSDictionary *step in steps) {
+ FSTLog(@"Doing step %@", step);
+ [self doStep:step];
+ [self validateStepExpectations:step[@"expect"]];
+ [self validateStateExpectations:step[@"stateExpect"]];
+ }
+ [self.driver validateUsage];
+ } @finally {
+ // Ensure that the driver is torn down even if the test is failing due to a thrown exception so
+ // that any resources held by the driver are released. This is important when the driver is
+ // backed by LevelDB because LevelDB locks its database. If -tearDownForSpec were not called
+ // after an exception then subsequent attempts to open the LevelDB will fail, making it harder
+ // to zero in on the spec tests as a culprit.
+ [self tearDownForSpec];
+ }
+}
+
+#pragma mark - The actual test methods.
+
+- (void)testSpecTests {
+ if ([self isTestBaseClass]) return;
+
+ // Enumerate the .json files containing the spec tests.
+ NSMutableArray<NSString *> *specFiles = [NSMutableArray array];
+ NSMutableArray<NSDictionary *> *parsedSpecs = [NSMutableArray array];
+ NSBundle *bundle = [NSBundle bundleForClass:[self class]];
+ NSFileManager *fs = [NSFileManager defaultManager];
+ BOOL exclusiveMode = NO;
+ for (NSString *file in [fs enumeratorAtPath:[bundle bundlePath]]) {
+ if (![@"json" isEqual:[file pathExtension]]) {
+ continue;
+ }
+
+ // Read and parse the JSON from the file.
+ NSString *fileName = [file stringByDeletingPathExtension];
+ NSString *path = [bundle pathForResource:fileName ofType:@"json"];
+ NSData *json = [NSData dataWithContentsOfFile:path];
+ XCTAssertNotNil(json);
+ NSError *error = nil;
+ id _Nullable parsed = [NSJSONSerialization JSONObjectWithData:json options:0 error:&error];
+ XCTAssertNil(error, @"%@", error);
+ XCTAssertTrue([parsed isKindOfClass:[NSDictionary class]]);
+ NSDictionary *testDict = (NSDictionary *)parsed;
+
+ exclusiveMode = exclusiveMode || [self anyTestsAreMarkedExclusive:testDict];
+ [specFiles addObject:fileName];
+ [parsedSpecs addObject:testDict];
+ }
+
+ // Now iterate over them and run them.
+ __block bool ranAtLeastOneTest = NO;
+ for (NSUInteger i = 0; i < specFiles.count; i++) {
+ NSLog(@"Spec test file: %@", specFiles[i]);
+ // Iterate over the tests in the file and run them.
+ [parsedSpecs[i] enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]);
+ NSDictionary *testDescription = (NSDictionary *)obj;
+ NSString *describeName = testDescription[@"describeName"];
+ NSString *itName = testDescription[@"itName"];
+ NSString *name = [NSString stringWithFormat:@"%@ %@", describeName, itName];
+ NSDictionary *config = testDescription[@"config"];
+ NSArray *steps = testDescription[@"steps"];
+ NSArray<NSString *> *tags = testDescription[@"tags"];
+
+ BOOL runTest = !exclusiveMode || [tags indexOfObject:kExclusiveTag] != NSNotFound;
+ if ([tags indexOfObject:kNoIOSTag] != NSNotFound) {
+ runTest = NO;
+ }
+ if (runTest) {
+ NSLog(@" Spec test: %@", name);
+ [self runSpecTestSteps:steps config:config];
+ ranAtLeastOneTest = YES;
+ } else {
+ NSLog(@" [SKIPPED] Spec test: %@", name);
+ }
+ }];
+ }
+ XCTAssertTrue(ranAtLeastOneTest);
+}
+
+- (BOOL)anyTestsAreMarkedExclusive:(NSDictionary *)tests {
+ __block BOOL found = NO;
+ [tests enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+ XCTAssertTrue([obj isKindOfClass:[NSDictionary class]]);
+ NSDictionary *testDescription = (NSDictionary *)obj;
+ NSArray<NSString *> *tags = testDescription[@"tags"];
+ if ([tags indexOfObject:kExclusiveTag] != NSNotFound) {
+ found = YES;
+ *stop = YES;
+ }
+ }];
+ return found;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h
new file mode 100644
index 0000000..0643d76
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h
@@ -0,0 +1,248 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "Core/FSTTypes.h"
+
+@class FSTDocumentKey;
+@class FSTMutation;
+@class FSTMutationResult;
+@class FSTQuery;
+@class FSTQueryData;
+@class FSTSnapshotVersion;
+@class FSTUser;
+@class FSTViewSnapshot;
+@class FSTWatchChange;
+@protocol FSTGarbageCollector;
+@protocol FSTPersistence;
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+ * Interface used for object that contain exactly one of either a view snapshot or an error for the
+ * given query.
+ */
+@interface FSTQueryEvent : NSObject
+@property(nonatomic, strong) FSTQuery *query;
+@property(nonatomic, strong, nullable) FSTViewSnapshot *viewSnapshot;
+@property(nonatomic, strong, nullable) NSError *error;
+@end
+
+/** Holds an outstanding write and its result. */
+@interface FSTOutstandingWrite : NSObject
+/** The write that is outstanding. */
+@property(nonatomic, strong, readwrite) FSTMutation *write;
+/** Whether this write is done (regardless of whether it was successful or not). */
+@property(nonatomic, assign, readwrite) BOOL done;
+/** The error - if any - of this write. */
+@property(nonatomic, strong, nullable, readwrite) NSError *error;
+@end
+
+/** Mapping of user => array of FSTMutations for that user. */
+typedef NSDictionary<FSTUser *, NSArray<FSTOutstandingWrite *> *> FSTOutstandingWriteQueues;
+
+/**
+ * A test driver for FSTSyncEngine that allows simulated event delivery and capture. As much as
+ * possible, all sources of nondeterminism are removed so that test execution is consistent and
+ * reliable.
+ *
+ * FSTSyncEngineTestDriver:
+ *
+ * + constructs an FSTSyncEngine using a mocked FSTDatastore for the backend;
+ * + allows the caller to trigger events (user API calls and incoming FSTDatastore messages);
+ * + performs sequencing validation internally (e.g. that when a user mutation is initiated, the
+ * FSTSyncEngine correctly sends it to the remote store); and
+ * + exposes the set of FSTQueryEvents generated for the caller to verify.
+ *
+ * Events come in three major flavors:
+ *
+ * + user events: simulate user API calls
+ * + watch events: simulate RPC interactions with the Watch backend
+ * + write events: simulate RPC interactions with the Streaming Write backend
+ *
+ * Each method on the driver injects a different event into the system.
+ */
+@interface FSTSyncEngineTestDriver : NSObject
+
+/**
+ * Initializes the underlying FSTSyncEngine with the given local persistence implementation and
+ * garbage collection policy.
+ */
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector;
+
+/**
+ * Initializes the underlying FSTSyncEngine with the given local persistence implementation and
+ * a set of existing outstandingWrites (useful when your FSTPersistence object has
+ * persisted mutation queues).
+ */
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector
+ initialUser:(FSTUser *)initialUser
+ outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites
+ NS_DESIGNATED_INITIALIZER;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/** Starts the FSTSyncEngine and its underlying components. */
+- (void)start;
+
+/** Validates that the API has been used correctly after a test is complete. */
+- (void)validateUsage;
+
+/** Shuts the FSTSyncEngine down. */
+- (void)shutdown;
+
+/**
+ * Adds a listener to the FSTSyncEngine as if the user had initiated a new listen for the given
+ * query.
+ *
+ * Resulting events are captured and made available via the capturedEventsSinceLastCall method.
+ *
+ * @param query A valid query to execute against the backend.
+ * @return The target ID assigned by the system to track the query.
+ */
+- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query;
+
+/**
+ * Removes a listener from the FSTSyncEngine as if the user had removed a listener corresponding
+ * to the given query.
+ *
+ * Resulting events are captured and made available via the capturedEventsSinceLastCall method.
+ *
+ * @param query An identical query corresponding to one passed to -addUserListenerWithQuery.
+ */
+- (void)removeUserListenerWithQuery:(FSTQuery *)query;
+
+/**
+ * Delivers a WatchChange RPC to the FSTSyncEngine as if it were received from the backend watch
+ * service, either in response to addUserListener: or removeUserListener calls or because the
+ * simulated backend has new data.
+ *
+ * Resulting events are captured and made available via the capturedEventsSinceLastCall method.
+ *
+ * @param change Any type of watch change
+ * @param snapshot A snapshot version to attach, if applicable. This should be sent when
+ * simulating the server having sent a complete snapshot.
+ */
+- (void)receiveWatchChange:(FSTWatchChange *)change
+ snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot;
+
+/**
+ * Delivers a watch stream error as if the Streaming Watch backend has generated some kind of error.
+ *
+ * @param errorCode A FIRFirestoreErrorCode value, from FIRFirestoreErrors.h
+ * @param userInfo Any additional details that the server might have sent along with the error.
+ * For the moment this is effectively unused, but is logged.
+ */
+- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary<NSString *, id> *)userInfo;
+
+/**
+ * Performs a mutation against the FSTSyncEngine as if the user had written the mutation through
+ * the API.
+ *
+ * Also retains the mutation so that the driver can validate that the sync engine sent the mutation
+ * to the remote store before receiveWatchChange:snapshotVersion: and receiveWriteError:userInfo:
+ * events are processed.
+ *
+ * @param mutation Any type of valid mutation.
+ */
+- (void)writeUserMutation:(FSTMutation *)mutation;
+
+/**
+ * Delivers a write error as if the Streaming Write backend has generated some kind of error.
+ *
+ * For the moment write errors are usually must be in response to a mutation that has been written
+ * with writeUserMutation:. Spontaneously errors due to idle timeout, server restart, or credential
+ * expiration aren't yet supported.
+ *
+ * @param errorCode A FIRFirestoreErrorCode value, from FIRFirestoreErrors.h
+ * @param userInfo Any additional details that the server might have sent along with the error.
+ * For the moment this is effectively unused, but is logged.
+ */
+- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode
+ userInfo:(NSDictionary<NSString *, id> *)userInfo;
+
+/**
+ * Delivers a write acknowledgement as if the Streaming Write backend has acknowledged a write with
+ * the snapshot version at which the write was committed.
+ *
+ * @param commitVersion The snapshot version at which the simulated server has committed
+ * the mutation. Snapshot versions must be monotonically increasing.
+ * @param mutationResults The mutation results for the write that is being acked.
+ */
+- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:(NSArray<FSTMutationResult *> *)mutationResults;
+
+/**
+ * A count of the mutations written to the write stream by the FSTSyncEngine, but not yet
+ * acknowledged via receiveWriteError: or receiveWriteAckWithVersion:mutationResults.
+ */
+@property(nonatomic, readonly) int sentWritesCount;
+
+/**
+ * Switches the FSTSyncEngine to a new user. The test driver tracks the outstanding mutations for
+ * each user, so future receiveWriteAck/Error operations will validate the write sent to the mock
+ * datastore matches the next outstanding write for that user.
+ */
+- (void)changeUser:(FSTUser *)user;
+
+/**
+ * Returns all query events generated by the FSTSyncEngine in response to the event injection
+ * methods called previously. The events are cleared after each invocation of this method.
+ */
+- (NSArray<FSTQueryEvent *> *)capturedEventsSinceLastCall;
+
+/**
+ * The writes that have been sent to the FSTSyncEngine via writeUserMutation: but not yet
+ * acknowledged by calling receiveWriteAck/Error:. They are tracked per-user.
+ *
+ * It is mostly an implementation detail used internally to validate that the writes sent to the
+ * mock backend by the FSTSyncEngine match the user mutations that initiated them.
+ *
+ * It is exposed specifically for use with the
+ * initWithPersistence:GCEnabled:outstandingWrites: initializer to test persistence
+ * scenarios where the FSTSyncEngine is restarted while the FSTPersistence implementation still has
+ * outstanding persisted mutations.
+ *
+ * Note: The size of the list for the current user will generally be the same as
+ * sentWritesCount, but not necessarily, since the FSTRemoteStore limits the number of
+ * outstanding writes to the backend at a given time.
+ */
+@property(nonatomic, strong, readonly) FSTOutstandingWriteQueues *outstandingWrites;
+
+/** The current user for the FSTSyncEngine; determines which mutation queue is active. */
+@property(nonatomic, strong, readonly) FSTUser *currentUser;
+
+/** The current set of documents in limbo. */
+@property(nonatomic, strong, readonly)
+ NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *currentLimboDocuments;
+
+/** The expected set of documents in limbo. */
+@property(nonatomic, strong, readwrite) NSSet<FSTDocumentKey *> *expectedLimboDocuments;
+
+/** The set of active targets as observed on the watch stream. */
+@property(nonatomic, strong, readonly)
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *activeTargets;
+
+/** The expected set of active targets, keyed by target ID. */
+@property(nonatomic, strong, readwrite)
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *expectedActiveTargets;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m
new file mode 100644
index 0000000..b4c0b02
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.m
@@ -0,0 +1,291 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTSyncEngineTestDriver.h"
+
+#import <GRPCClient/GRPCCall.h>
+
+#import "Auth/FSTUser.h"
+#import "Core/FSTEventManager.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTSyncEngine.h"
+#import "Firestore/FIRFirestoreErrors.h"
+#import "Local/FSTLocalStore.h"
+#import "Local/FSTPersistence.h"
+#import "Model/FSTMutation.h"
+#import "Remote/FSTDatastore.h"
+#import "Remote/FSTWatchChange.h"
+#import "Util/FSTAssert.h"
+#import "Util/FSTDispatchQueue.h"
+#import "Util/FSTLogger.h"
+
+#import "FSTMockDatastore.h"
+#import "FSTSyncEngine+Testing.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTQueryEvent
+
+- (NSString *)description {
+ // The Query is also included in the view, so we skip it.
+ return [NSString stringWithFormat:@"<FSTQueryEvent: viewSnapshot=%@, error=%@>",
+ self.viewSnapshot, self.error];
+}
+
+@end
+
+@implementation FSTOutstandingWrite
+@end
+
+@interface FSTSyncEngineTestDriver ()
+
+#pragma mark - Parts of the Firestore system that the spec tests need to control.
+
+@property(nonatomic, strong, readonly) FSTMockDatastore *datastore;
+@property(nonatomic, strong, readonly) FSTEventManager *eventManager;
+@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore;
+@property(nonatomic, strong, readonly) FSTLocalStore *localStore;
+@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine;
+
+#pragma mark - Data structures for holding events sent by the watch stream.
+
+/** A block for the FSTEventAggregator to use to report events to the test. */
+@property(nonatomic, strong, readonly) void (^eventHandler)(FSTQueryEvent *);
+/** The events received by our eventHandler and not yet retrieved via capturedEventsSinceLastCall */
+@property(nonatomic, strong, readonly) NSMutableArray<FSTQueryEvent *> *events;
+/** A dictionary for tracking the listens on queries. */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTQuery *, FSTQueryListener *> *queryListeners;
+
+#pragma mark - Other data structures.
+@property(nonatomic, strong, readwrite) FSTUser *currentUser;
+
+@end
+
+@implementation FSTSyncEngineTestDriver {
+ // ivar is declared as mutable.
+ NSMutableDictionary<FSTUser *, NSMutableArray<FSTOutstandingWrite *> *> *_outstandingWrites;
+}
+
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector {
+ return [self initWithPersistence:persistence
+ garbageCollector:garbageCollector
+ initialUser:[FSTUser unauthenticatedUser]
+ outstandingWrites:@{}];
+}
+
+- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
+ garbageCollector:(id<FSTGarbageCollector>)garbageCollector
+ initialUser:(FSTUser *)initialUser
+ outstandingWrites:(FSTOutstandingWriteQueues *)outstandingWrites {
+ if (self = [super init]) {
+ // Create mutable copy of outstandingWrites.
+ _outstandingWrites = [NSMutableDictionary dictionary];
+ [outstandingWrites enumerateKeysAndObjectsUsingBlock:^(
+ FSTUser *user, NSArray<FSTOutstandingWrite *> *writes, BOOL *stop) {
+ _outstandingWrites[user] = [writes mutableCopy];
+ }];
+
+ _events = [NSMutableArray array];
+
+ // Set up the sync engine and various stores.
+ dispatch_queue_t mainQueue = dispatch_get_main_queue();
+ FSTDispatchQueue *dispatchQueue = [FSTDispatchQueue queueWith:mainQueue];
+ _localStore = [[FSTLocalStore alloc] initWithPersistence:persistence
+ garbageCollector:garbageCollector
+ initialUser:initialUser];
+ _datastore = [FSTMockDatastore mockDatastoreWithWorkerDispatchQueue:dispatchQueue];
+
+ _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:_datastore];
+
+ _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore
+ remoteStore:_remoteStore
+ initialUser:initialUser];
+ _remoteStore.syncEngine = _syncEngine;
+ _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine];
+
+ _remoteStore.onlineStateDelegate = _eventManager;
+
+ // Set up internal event tracking for the spec tests.
+ NSMutableArray<FSTQueryEvent *> *events = [NSMutableArray array];
+ _eventHandler = ^(FSTQueryEvent *e) {
+ [events addObject:e];
+ };
+ _events = events;
+
+ _queryListeners = [NSMutableDictionary dictionary];
+
+ _expectedLimboDocuments = [NSSet set];
+
+ _expectedActiveTargets = [NSDictionary dictionary];
+
+ _currentUser = initialUser;
+ }
+ return self;
+}
+
+- (void)start {
+ [self.localStore start];
+ [self.remoteStore start];
+}
+
+- (void)validateUsage {
+ // We could relax this if we found a reason to.
+ FSTAssert(self.events.count == 0,
+ @"You must clear all pending events by calling"
+ " capturedEventsSinceLastCall before calling shutdown.");
+}
+
+- (void)shutdown {
+ [self.remoteStore shutdown];
+ [self.localStore shutdown];
+}
+
+- (void)validateNextWriteSent:(FSTMutation *)expectedWrite {
+ NSArray<FSTMutation *> *request = [self.datastore nextSentWrite];
+ // Make sure the write went through the pipe like we expected it to.
+ FSTAssert(request.count == 1, @"Only single mutation requests are supported at the moment");
+ FSTMutation *actualWrite = request[0];
+ FSTAssert([actualWrite isEqual:expectedWrite],
+ @"Mock datastore received write %@ but first outstanding mutation was %@", actualWrite,
+ expectedWrite);
+ FSTLog(@"A write was sent: %@", actualWrite);
+}
+
+- (int)sentWritesCount {
+ return [self.datastore writesSent];
+}
+
+- (void)changeUser:(FSTUser *)user {
+ self.currentUser = user;
+ [self.syncEngine userDidChange:user];
+}
+
+- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion
+ mutationResults:
+ (NSArray<FSTMutationResult *> *)mutationResults {
+ FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject;
+ [[self currentOutstandingWrites] removeObjectAtIndex:0];
+ [self validateNextWriteSent:write.write];
+
+ [self.datastore ackWriteWithVersion:commitVersion mutationResults:mutationResults];
+
+ return write;
+}
+
+- (FSTOutstandingWrite *)receiveWriteError:(int)errorCode
+ userInfo:(NSDictionary<NSString *, id> *)userInfo {
+ NSError *error =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo];
+
+ FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject;
+ [self validateNextWriteSent:write.write];
+
+ // If this is a permanent error, the mutation is not expected to be sent again so we remove it
+ // from currentOutstandingWrites.
+ if ([FSTDatastore isPermanentWriteError:error]) {
+ [[self currentOutstandingWrites] removeObjectAtIndex:0];
+ }
+
+ FSTLog(@"Failing a write.");
+ [self.datastore failWriteWithError:error];
+
+ return write;
+}
+
+- (NSArray<FSTQueryEvent *> *)capturedEventsSinceLastCall {
+ NSArray<FSTQueryEvent *> *result = [self.events copy];
+ [self.events removeAllObjects];
+ return result;
+}
+
+- (FSTTargetID)addUserListenerWithQuery:(FSTQuery *)query {
+ // TODO(dimond): Allow customizing listen options in spec tests
+ // TODO(dimond): Change spec tests to verify isFromCache on snapshots
+ FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES
+ includeDocumentMetadataChanges:YES
+ waitForSyncWhenOnline:NO];
+ FSTQueryListener *listener = [[FSTQueryListener alloc]
+ initWithQuery:query
+ options:options
+ viewSnapshotHandler:^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) {
+ FSTQueryEvent *event = [[FSTQueryEvent alloc] init];
+ event.query = query;
+ event.viewSnapshot = snapshot;
+ event.error = error;
+ [self.events addObject:event];
+ }];
+ self.queryListeners[query] = listener;
+ return [self.eventManager addListener:listener];
+}
+
+- (void)removeUserListenerWithQuery:(FSTQuery *)query {
+ FSTQueryListener *listener = self.queryListeners[query];
+ [self.queryListeners removeObjectForKey:query];
+ [self.eventManager removeListener:listener];
+}
+
+- (void)writeUserMutation:(FSTMutation *)mutation {
+ FSTOutstandingWrite *write = [[FSTOutstandingWrite alloc] init];
+ write.write = mutation;
+ [[self currentOutstandingWrites] addObject:write];
+ FSTLog(@"sending a user write.");
+ [self.syncEngine writeMutations:@[ mutation ]
+ completion:^(NSError *_Nullable error) {
+ FSTLog(@"A callback was called with error: %@", error);
+ write.done = YES;
+ write.error = error;
+ }];
+}
+
+- (void)receiveWatchChange:(FSTWatchChange *)change
+ snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot {
+ [self.datastore writeWatchChange:change snapshotVersion:snapshot];
+}
+
+- (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary<NSString *, id> *)userInfo {
+ NSError *error =
+ [NSError errorWithDomain:FIRFirestoreErrorDomain code:errorCode userInfo:userInfo];
+
+ [self.datastore failWatchStreamWithError:error];
+ // Unlike web, stream should re-open synchronously
+ FSTAssert(self.datastore.isWatchStreamOpen, @"Watch stream is open");
+}
+
+- (NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments {
+ return [self.syncEngine currentLimboDocuments];
+}
+
+- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)activeTargets {
+ return [[self.datastore activeTargets] copy];
+}
+
+#pragma mark - Helper Methods
+
+- (NSMutableArray<FSTOutstandingWrite *> *)currentOutstandingWrites {
+ NSMutableArray<FSTOutstandingWrite *> *writes = _outstandingWrites[self.currentUser];
+ if (!writes) {
+ writes = [NSMutableArray array];
+ _outstandingWrites[self.currentUser] = writes;
+ }
+ return writes;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/SpecTests/json/README.md b/Firestore/Example/Tests/SpecTests/json/README.md
new file mode 100644
index 0000000..bcc9b38
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/README.md
@@ -0,0 +1,3 @@
+These json files are generated from the web test sources.
+
+TODO(mikelehen): Re-add instructions for generating these.
diff --git a/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json
new file mode 100644
index 0000000..ef41afe
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json
@@ -0,0 +1,147 @@
+{
+ "Events are raised after watch ack": {
+ "describeName": "Collections:",
+ "itName": "Events are raised after watch ack",
+ "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/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Events are raised for local sets before watch ack": {
+ "describeName": "Collections:",
+ "itName": "Events are raised for local sets before watch ack",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
new file mode 100644
index 0000000..ab42241
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json
@@ -0,0 +1,738 @@
+{
+ "Existence filter mismatch triggers re-run of query": {
+ "describeName": "Existence Filters:",
+ "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
+ },
+ "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
+ ],
+ "existence-filter-resume-token"
+ ],
+ "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
+ }
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "existence-filter-resume-token"
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "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 limbo resolution is denied": {
+ "describeName": "Existence Filters:",
+ "itName": "Existence filter limbo resolution is denied",
+ "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": ""
+ }
+ }
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 1
+ ],
+ "cause": {
+ "code": 7
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ },
+ "limboDocs": []
+ },
+ "watchSnapshot": 3000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/2",
+ 1000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
new file mode 100644
index 0000000..ee2d883
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json
@@ -0,0 +1,1150 @@
+{
+ "Limbo documents are deleted without an existence filter": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents are deleted without an existence filter",
+ "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,
+ {
+ "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
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-2"
+ ],
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Limbo documents are deleted with an existence filter": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents are deleted with an existence filter",
+ "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,
+ {
+ "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
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchFilter": [
+ [
+ 1
+ ]
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Limbo documents are resolved with updates": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents are resolved with updates",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "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": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 1
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Limbo documents are resolved with updates in different snapshot than \"current\"": {
+ "describeName": "Limbo Documents:",
+ "itName": "Limbo documents are resolved with updates in different snapshot than \"current\"",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "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": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 1,
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "a"
+ ]
+ ],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "key",
+ "==",
+ "b"
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-1003"
+ ],
+ "watchSnapshot": 1003
+ }
+ ]
+ },
+ "Document remove message will cause docs to go in limbo": {
+ "describeName": "Limbo Documents:",
+ "itName": "Document remove message will cause docs to go in limbo",
+ "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,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "key": "collection/b",
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1003,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/b"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/b",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-1004"
+ ],
+ "watchSnapshot": 1004,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
new file mode 100644
index 0000000..5a02463
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json
@@ -0,0 +1,1626 @@
+{
+ "Documents in limit are replaced by remote event": {
+ "describeName": "Limits:",
+ "itName": "Documents in limit are replaced by remote event",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1002,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Deleted Document in limbo in full limit query": {
+ "describeName": "Limits:",
+ "itName": "Deleted Document in limbo in full limit query",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1002"
+ ],
+ "watchSnapshot": 1002,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ],
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Documents in limit can handle removed messages": {
+ "describeName": "Limits:",
+ "itName": "Documents in limit can handle removed messages",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "key": "collection/c",
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Documents in limit are can handle removed messages for only one of many query": {
+ "describeName": "Limits:",
+ "itName": "Documents in limit are can handle removed messages for only one of many query",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2,
+ 4
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "key": "collection/c",
+ "removedTargets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1002,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/c",
+ 1001,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 3,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 1002,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Multiple docs in limbo in full limit query": {
+ "describeName": "Limits:",
+ "itName": "Multiple docs in limbo in full limit query",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ],
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ],
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ],
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ],
+ [
+ "collection/e",
+ 1004,
+ {
+ "key": "e"
+ }
+ ],
+ [
+ "collection/f",
+ 1005,
+ {
+ "key": "f"
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-1005"
+ ],
+ "watchSnapshot": 1005,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ],
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ],
+ [
+ "collection/e",
+ 1004,
+ {
+ "key": "e"
+ }
+ ],
+ [
+ "collection/f",
+ 1005,
+ {
+ "key": "f"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchReset": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/e",
+ 1004,
+ {
+ "key": "e"
+ }
+ ],
+ [
+ "collection/f",
+ 1005,
+ {
+ "key": "f"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/a",
+ "collection/b"
+ ],
+ "activeTargets": {
+ "1": {
+ "query": {
+ "path": "collection/a",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "3": {
+ "query": {
+ "path": "collection/b",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 1
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 1
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/b",
+ "collection/c"
+ ],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "3": {
+ "query": {
+ "path": "collection/b",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "5": {
+ "query": {
+ "path": "collection/c",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 1
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 3
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 3
+ ],
+ "resume-token-2001"
+ ],
+ "watchSnapshot": 2001,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/c",
+ "collection/d"
+ ],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "5": {
+ "query": {
+ "path": "collection/c",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "7": {
+ "query": {
+ "path": "collection/d",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 3
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 5
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 5
+ ],
+ "resume-token-2002"
+ ],
+ "watchSnapshot": 2002,
+ "stateExpect": {
+ "limboDocs": [
+ "collection/d"
+ ],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "7": {
+ "query": {
+ "path": "collection/d",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/e",
+ 1004,
+ {
+ "key": "e"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/c",
+ 1002,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 5
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 7
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 7
+ ],
+ "resume-token-2003"
+ ],
+ "watchSnapshot": 2003,
+ "stateExpect": {
+ "limboDocs": [],
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ },
+ {
+ "query": {
+ "path": "collection",
+ "limit": 2,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/f",
+ 1005,
+ {
+ "key": "f"
+ }
+ ]
+ ],
+ "removed": [
+ [
+ "collection/d",
+ 1003,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 7
+ ]
+ }
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
new file mode 100644
index 0000000..35704f2
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json
@@ -0,0 +1,1524 @@
+{
+ "Contents of query are cleared when listen is removed.": {
+ "describeName": "Listens:",
+ "itName": "Contents of query are cleared when listen is removed.",
+ "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,
+ {
+ "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": {}
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ }
+ ]
+ },
+ "Contents of query update when new data is received.": {
+ "describeName": "Listens:",
+ "itName": "Contents of query update when new data is received.",
+ "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,
+ {
+ "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
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Ensure correct query results with latency-compensated deletes": {
+ "describeName": "Listens:",
+ "itName": "Ensure correct query results with latency-compensated deletes",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userDelete": "collection/b"
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "a": true
+ }
+ ],
+ [
+ "collection/b",
+ 1000,
+ {
+ "b": true
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "a": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "limit": 10,
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ },
+ "4": {
+ "query": {
+ "path": "collection",
+ "limit": 10,
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "limit": 10,
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "a": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Will process removals without waiting for a consistent snapshot": {
+ "describeName": "Listens:",
+ "itName": "Will process removals without waiting for a consistent snapshot",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ],
+ "cause": {
+ "code": 8
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {}
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 8,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Will gracefully process failed targets": {
+ "describeName": "Listens:",
+ "itName": "Will gracefully process failed targets",
+ "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": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection1/a",
+ 1000,
+ {
+ "a": true
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection2/a",
+ 1001,
+ {
+ "b": true
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ],
+ "cause": {
+ "code": 8
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection1",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 8,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection2",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection2/a",
+ 1001,
+ {
+ "b": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Will gracefully handle watch stream reverting snapshots": {
+ "describeName": "Listens:",
+ "itName": "Will gracefully handle watch stream reverting snapshots",
+ "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,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "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",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Will gracefully handle watch stream reverting snapshots (with restart)": {
+ "describeName": "Listens:",
+ "itName": "Will gracefully handle watch stream reverting snapshots (with restart)",
+ "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,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": []
+ }
+ },
+ {
+ "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",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Individual documents cannot revert": {
+ "describeName": "Listens:",
+ "itName": "Individual documents cannot revert",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000",
+ "visible": true
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000",
+ "visible": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": "v1000",
+ "visible": true
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 3000,
+ {
+ "v": "v3000",
+ "visible": false
+ }
+ ]
+ ],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-4000"
+ ],
+ "watchSnapshot": 4000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 3000,
+ {
+ "v": "v3000",
+ "visible": false
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 4
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-1000"
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": "v2000",
+ "visible": false
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-5000"
+ ],
+ "watchSnapshot": 5000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [
+ [
+ "visible",
+ "==",
+ true
+ ]
+ ],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-4000"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 3000,
+ {
+ "v": "v3000",
+ "visible": false
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 4
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [],
+ "targets": [
+ 4
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 4
+ ],
+ "resume-token-6000"
+ ],
+ "watchSnapshot": 6000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json
new file mode 100644
index 0000000..e58bae1
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json
@@ -0,0 +1,151 @@
+{
+ "Empty queries are resolved if client goes offline": {
+ "describeName": "Offline:",
+ "itName": "Empty queries are resolved if client goes offline",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ }
+ ]
+ },
+ "A successful message delays offline status": {
+ "describeName": "Offline:",
+ "itName": "A successful message delays offline status",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ }
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json
new file mode 100644
index 0000000..1009206
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json
@@ -0,0 +1,155 @@
+{
+ "orderBy applies filtering based on local state": {
+ "describeName": "OrderBy:",
+ "itName": "orderBy applies filtering based on local state",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "key": "a",
+ "sort": 1
+ }
+ ]
+ },
+ {
+ "userPatch": [
+ "collection/b",
+ {
+ "sort": 2
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/c",
+ {
+ "key": "b"
+ }
+ ]
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": [
+ [
+ "sort",
+ "asc"
+ ]
+ ]
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": [
+ [
+ "sort",
+ "asc"
+ ]
+ ]
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": [
+ [
+ "sort",
+ "asc"
+ ]
+ ]
+ },
+ "added": [
+ [
+ "collection/a",
+ 0,
+ {
+ "key": "a",
+ "sort": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-2000"
+ ],
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": [
+ [
+ "sort",
+ "asc"
+ ]
+ ]
+ },
+ "added": [
+ [
+ "collection/b",
+ 1001,
+ {
+ "key": "b",
+ "sort": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json
new file mode 100644
index 0000000..158e337
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json
@@ -0,0 +1,858 @@
+{
+ "Local mutations are persisted and re-sent": {
+ "describeName": "Persistence:",
+ "itName": "Local mutations are persisted and re-sent",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "collection/key1",
+ {
+ "foo": "bar"
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key2",
+ {
+ "baz": "quu"
+ }
+ ]
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": [],
+ "numOutstandingWrites": 2
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1,
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 2,
+ "expectUserCallback": false
+ },
+ "stateExpect": {
+ "numOutstandingWrites": 0
+ }
+ }
+ ]
+ },
+ "Persisted local mutations are visible to listeners": {
+ "describeName": "Persistence:",
+ "itName": "Persisted local mutations are visible to listeners",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "collection/key1",
+ {
+ "foo": "bar"
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key2",
+ {
+ "baz": "quu"
+ }
+ ]
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": []
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key1",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ],
+ [
+ "collection/key2",
+ 0,
+ {
+ "baz": "quu"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ }
+ ]
+ },
+ "Remote documents are persisted": {
+ "describeName": "Persistence:",
+ "itName": "Remote documents are persisted",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": []
+ }
+ },
+ {
+ "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/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Remote documents from watch are not GC'd": {
+ "describeName": "Persistence:",
+ "itName": "Remote documents from watch are not GC'd",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "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/key",
+ 1000,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Remote documents from user sets are not GC'd": {
+ "describeName": "Persistence:",
+ "itName": "Remote documents from user sets are not GC'd",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Mutation Queue is persisted across uid switches": {
+ "describeName": "Persistence:",
+ "itName": "Mutation Queue is persisted across uid switches",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "users/anon",
+ {
+ "uid": "anon"
+ }
+ ]
+ },
+ {
+ "changeUser": "user1",
+ "stateExpect": {
+ "numOutstandingWrites": 0
+ }
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1"
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1",
+ "extra": true
+ }
+ ]
+ },
+ {
+ "changeUser": null,
+ "stateExpect": {
+ "numOutstandingWrites": 1
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "changeUser": "user1",
+ "stateExpect": {
+ "numOutstandingWrites": 2
+ }
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": true
+ }
+ }
+ ]
+ },
+ "Mutation Queue is persisted across uid switches (with restarts)": {
+ "describeName": "Persistence:",
+ "itName": "Mutation Queue is persisted across uid switches (with restarts)",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userSet": [
+ "users/anon",
+ {
+ "uid": "anon"
+ }
+ ]
+ },
+ {
+ "changeUser": "user1",
+ "stateExpect": {
+ "numOutstandingWrites": 0
+ }
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1"
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1",
+ "extra": true
+ }
+ ]
+ },
+ {
+ "changeUser": null
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": [],
+ "numOutstandingWrites": 1
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": false
+ }
+ },
+ {
+ "changeUser": "user1"
+ },
+ {
+ "restart": true,
+ "stateExpect": {
+ "activeTargets": {},
+ "limboDocs": [],
+ "numOutstandingWrites": 2
+ }
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": false
+ }
+ }
+ ]
+ },
+ "Visible mutations reflect uid switches": {
+ "describeName": "Persistence:",
+ "itName": "Visible mutations reflect uid switches",
+ "tags": [
+ "persistence"
+ ],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "users/existing",
+ 0,
+ {
+ "uid": "existing"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-500"
+ ],
+ "watchSnapshot": 500,
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "users/existing",
+ 0,
+ {
+ "uid": "existing"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "users/anon",
+ {
+ "uid": "anon"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "users/anon",
+ 0,
+ {
+ "uid": "anon"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "changeUser": "user1",
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "resume-token-500"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "users/anon",
+ 0,
+ {
+ "uid": "anon"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "users/user1",
+ {
+ "uid": "user1"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "users/user1",
+ 0,
+ {
+ "uid": "user1"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "changeUser": null,
+ "expect": [
+ {
+ "query": {
+ "path": "users",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "users/anon",
+ 0,
+ {
+ "uid": "anon"
+ },
+ "local"
+ ]
+ ],
+ "removed": [
+ [
+ "users/user1",
+ 0,
+ {
+ "uid": "user1"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json
new file mode 100644
index 0000000..edb0751
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json
@@ -0,0 +1,559 @@
+{
+ "Waits for watch to remove targets": {
+ "describeName": "Remote store:",
+ "itName": "Waits for watch to remove targets",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token"
+ ],
+ "watchSnapshot": 1000
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Waits for watch to ack last target add": {
+ "describeName": "Remote store:",
+ "itName": "Waits for watch to ack last target add",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token"
+ ],
+ "watchSnapshot": 1000
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 1000,
+ {
+ "key": "b"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/c",
+ 1000,
+ {
+ "key": "c"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001
+ },
+ {
+ "watchRemove": {
+ "targetIds": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/d",
+ 1000,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/d",
+ 1000,
+ {
+ "key": "d"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Cleans up watch state correctly": {
+ "describeName": "Remote store:",
+ "itName": "Cleans up watch state correctly",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": false
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1001"
+ ],
+ "watchSnapshot": 1001,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json
new file mode 100644
index 0000000..25ea84a
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json
@@ -0,0 +1,250 @@
+{
+ "Resume tokens are sent after watch stream restarts": {
+ "describeName": "Resume tokens:",
+ "itName": "Resume tokens are sent after watch stream restarts",
+ "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,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "custom-query-resume-token"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchStreamClose": {
+ "error": {
+ "code": 14,
+ "message": "Simulated Backend Error"
+ }
+ },
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "custom-query-resume-token"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "Resume tokens are used across new listens": {
+ "describeName": "Resume tokens:",
+ "itName": "Resume tokens are used across new listens",
+ "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
+ ],
+ "custom-query-resume-token"
+ ],
+ "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": {}
+ }
+ },
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": "custom-query-resume-token"
+ }
+ }
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "key": "a"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "watchAck": [
+ 2
+ ],
+ "watchSnapshot": 1001
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/SpecTests/json/write_spec_test.json b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json
new file mode 100644
index 0000000..60ed107
--- /dev/null
+++ b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json
@@ -0,0 +1,5437 @@
+{
+ "Two sequential writes to different documents smoke test.": {
+ "describeName": "Writes:",
+ "itName": "Two sequential writes to different documents smoke test.",
+ "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,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/b",
+ 500,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/b",
+ 500,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/b",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/b",
+ 500,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 2500,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 3000
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/b",
+ 2500,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Event is raised for a local set before and after the write ack": {
+ "describeName": "Writes:",
+ "itName": "Event is raised for a local set before and after the write ack",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Cache will not keep data for an outdated write ack": {
+ "describeName": "Writes:",
+ "itName": "Cache will not keep data for an outdated write ack",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 10000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 10000
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 10000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Cache raises correct event if write is acked before watch delivers it": {
+ "describeName": "Writes:",
+ "itName": "Cache raises correct event if write is acked before watch delivers it",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Cache will hold local write until watch catches up": {
+ "describeName": "Writes:",
+ "itName": "Cache will hold local write until watch catches up",
+ "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/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "v": 3
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/key",
+ 1000,
+ {
+ "v": 3
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 2000,
+ {
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 3000,
+ {
+ "doc": "b"
+ }
+ ],
+ [
+ "collection/key",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 3000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 3000,
+ {
+ "doc": "b"
+ }
+ ]
+ ],
+ "metadata": [
+ [
+ "collection/key",
+ 3000,
+ {
+ "v": 3
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes are pipelined": {
+ "describeName": "Writes:",
+ "itName": "Writes are pipelined",
+ "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"
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a0",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a0",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a1",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a1",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a2",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a2",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a3",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a3",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a4",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a4",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a5",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a5",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a6",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a6",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a7",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a7",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a8",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a8",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a9",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a9",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a10",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a10",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a11",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a11",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a12",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a12",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a13",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a13",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a14",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a14",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ],
+ "stateExpect": {
+ "numOutstandingWrites": 10
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a0",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a0",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a1",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a1",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 3000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a2",
+ 3000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 3000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a2",
+ 3000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 4000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a3",
+ 4000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 4000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a3",
+ 4000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 5000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a4",
+ 5000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 5000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a4",
+ 5000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 6000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a5",
+ 6000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 6000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a5",
+ 6000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 7000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a6",
+ 7000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 7000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a6",
+ 7000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 8000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a7",
+ 8000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 8000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a7",
+ 8000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 9000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a8",
+ 9000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 9000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a8",
+ 9000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 10000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a9",
+ 10000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 10000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a9",
+ 10000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 11000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a10",
+ 11000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 11000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a10",
+ 11000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 12000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a11",
+ 12000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 12000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a11",
+ 12000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 13000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a12",
+ 13000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 13000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a12",
+ 13000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 14000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a13",
+ 14000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 14000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a13",
+ 14000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 15000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a14",
+ 15000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 15000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a14",
+ 15000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Pipelined writes can fail": {
+ "describeName": "Writes:",
+ "itName": "Pipelined writes can fail",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/a0",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a0",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a1",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a1",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a2",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a2",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a3",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a3",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a4",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a4",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a5",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a5",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a6",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a6",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a7",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a7",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a8",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a8",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a9",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a9",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a10",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a10",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a11",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a11",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a12",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a12",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a13",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a13",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a14",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a14",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ],
+ "stateExpect": {
+ "numOutstandingWrites": 10
+ }
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a0",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a1",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a2",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a3",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a4",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a5",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a6",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a7",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a8",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a9",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a10",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a11",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a12",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a13",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/a14",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ],
+ "stateExpect": {
+ "numOutstandingWrites": 0
+ }
+ }
+ ]
+ },
+ "Failed writes are released immediately.": {
+ "describeName": "Writes:",
+ "itName": "Failed writes are released immediately.",
+ "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,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/b",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "v": 2
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/b",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Held writes are not re-sent.": {
+ "describeName": "Writes:",
+ "itName": "Held writes are not re-sent.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-500"
+ ],
+ "watchSnapshot": 500,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "userSet": [
+ "collection/b",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/b",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/b",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/a",
+ 1000,
+ {
+ "v": 1
+ }
+ ],
+ [
+ "collection/b",
+ 2000,
+ {
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Held writes are released when there are no queries left.": {
+ "describeName": "Writes:",
+ "itName": "Held writes are released when there are no queries left.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-500"
+ ],
+ "watchSnapshot": 500,
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userSet": [
+ "collection/a",
+ {
+ "v": 1
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/a",
+ 0,
+ {
+ "v": 1
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "userUnlisten": [
+ 2,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {}
+ }
+ },
+ {
+ "userListen": [
+ 4,
+ {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "4": {
+ "query": {
+ "path": "collection",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ }
+ ]
+ },
+ "Writes that fail with code invalid-argument are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code invalid-argument are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 3
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code not-found are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code not-found are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 5
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code already-exists are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code already-exists are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 6
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code permission-denied are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code permission-denied are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 7
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code failed-precondition are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code failed-precondition are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 9
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code aborted are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code aborted are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 10
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code out-of-range are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code out-of-range are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 11
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code unimplemented are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code unimplemented are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 12
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code data-loss are rejected": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code data-loss are rejected",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 15
+ },
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "removed": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code cancelled are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code cancelled are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 1
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code unknown are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code unknown are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 2
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code deadline-exceeded are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code deadline-exceeded are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 4
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code resource-exhausted are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code resource-exhausted are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 8
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code internal are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code internal are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 13
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code unavailable are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code unavailable are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 14
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Writes that fail with code unauthenticated are retried": {
+ "describeName": "Writes:",
+ "itName": "Writes that fail with code unauthenticated are retried",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "userSet": [
+ "collection/key",
+ {
+ "foo": "bar"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": true,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "failWrite": {
+ "error": {
+ "code": 16
+ },
+ "expectUserCallback": false
+ }
+ },
+ {
+ "writeAck": {
+ "version": 1000,
+ "expectUserCallback": true
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-1000"
+ ],
+ "watchSnapshot": 1000,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/key",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/key",
+ 0,
+ {
+ "foo": "bar"
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ },
+ "Ensure correct events after patching a doc (including a delete) and getting watcher events.": {
+ "describeName": "Writes:",
+ "itName": "Ensure correct events after patching a doc (including a delete) and getting watcher events.",
+ "tags": [],
+ "config": {
+ "useGarbageCollection": true
+ },
+ "steps": [
+ {
+ "userListen": [
+ 2,
+ {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ }
+ ],
+ "stateExpect": {
+ "activeTargets": {
+ "2": {
+ "query": {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ },
+ "resumeToken": ""
+ }
+ }
+ }
+ },
+ {
+ "watchAck": [
+ 2
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/doc",
+ 1000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 1
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ }
+ },
+ {
+ "watchCurrent": [
+ [
+ 2
+ ],
+ "resume-token-500"
+ ],
+ "watchSnapshot": 500,
+ "expect": [
+ {
+ "query": {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ },
+ "added": [
+ [
+ "collection/doc",
+ 1000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 1
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ },
+ {
+ "userPatch": [
+ "collection/doc",
+ {
+ "v": 2,
+ "a.c": "<DELETE>"
+ }
+ ],
+ "expect": [
+ {
+ "query": {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ },
+ "modified": [
+ [
+ "collection/doc",
+ 1000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 2
+ },
+ "local"
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": true
+ }
+ ]
+ },
+ {
+ "watchEntity": {
+ "docs": [
+ [
+ "collection/doc",
+ 2000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 2
+ }
+ ]
+ ],
+ "targets": [
+ 2
+ ]
+ },
+ "watchSnapshot": 2000
+ },
+ {
+ "writeAck": {
+ "version": 2000,
+ "expectUserCallback": true
+ },
+ "expect": [
+ {
+ "query": {
+ "path": "collection/doc",
+ "filters": [],
+ "orderBys": []
+ },
+ "metadata": [
+ [
+ "collection/doc",
+ 2000,
+ {
+ "a": {
+ "b": 2
+ },
+ "v": 2
+ }
+ ]
+ ],
+ "errorCode": 0,
+ "fromCache": false,
+ "hasPendingWrites": false
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/Firestore/Example/Tests/Tests-Info.plist b/Firestore/Example/Tests/Tests-Info.plist
new file mode 100644
index 0000000..169b6f7
--- /dev/null
+++ b/Firestore/Example/Tests/Tests-Info.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Firestore/Example/Tests/Util/FSTAssertTests.m b/Firestore/Example/Tests/Util/FSTAssertTests.m
new file mode 100644
index 0000000..f0734df
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTAssertTests.m
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Util/FSTAssert.h"
+
+#import <XCTest/XCTest.h>
+
+@interface FSTAssertTests : XCTestCase
+@end
+
+@implementation FSTAssertTests
+
+- (void)testFail {
+ @try {
+ [self failingMethod];
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar");
+ }
+}
+
+// A method guaranteed to fail. Note that the return type is intentionally something that is
+// not actually returned in the body to ensure that the function attribute declarations in the
+// macro properly mark this macro invocation as never returning.
+- (int)failingMethod {
+ FSTFail(@"%d:%s:%@", 0, "foo", @"bar");
+}
+
+- (void)testCFail {
+ @try {
+ failingFunction();
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: 0:foo:bar");
+ }
+}
+
+// A function guaranteed to fail. Note that the return type is intentionally something that is
+// not actually returned in the body to ensure that the function attribute declarations in the
+// macro properly mark this macro invocation as never returning.
+int failingFunction() {
+ FSTCFail(@"%d:%s:%@", 0, "foo", @"bar");
+}
+
+- (void)testAssertNonFailing {
+ @try {
+ FSTAssert(YES, @"shouldn't fail");
+ } @catch (NSException *ex) {
+ XCTFail("Should not have failed, but got %@", ex);
+ }
+}
+
+- (void)testAssertFailing {
+ @try {
+ FSTAssert(NO, @"should fail");
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail");
+ }
+}
+
+- (void)testCAssertNonFailing {
+ @try {
+ nonAssertingFunction();
+ } @catch (NSException *ex) {
+ XCTFail("Should not have failed, but got %@", ex);
+ }
+}
+
+int nonAssertingFunction() {
+ FSTCAssert(YES, @"shouldn't fail");
+ return 0;
+}
+
+- (void)testCAssertFailing {
+ @try {
+ assertingFunction();
+ XCTFail("Should not have succeeded");
+ } @catch (NSException *ex) {
+ XCTAssertEqualObjects(ex.name, NSInternalInconsistencyException);
+ XCTAssertEqualObjects(ex.reason, @"FIRESTORE INTERNAL ASSERTION FAILED: should fail");
+ }
+}
+
+int assertingFunction() {
+ FSTCAssert(NO, @"should fail");
+}
+
+@end
diff --git a/Firestore/Example/Tests/Util/FSTComparisonTests.m b/Firestore/Example/Tests/Util/FSTComparisonTests.m
new file mode 100644
index 0000000..9728f20
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTComparisonTests.m
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Util/FSTComparison.h"
+
+#import <XCTest/XCTest.h>
+
+union DoubleBits {
+ double d;
+ uint64_t bits;
+};
+
+#define ASSERT_BIT_EQUALS(expected, actual) \
+ do { \
+ union DoubleBits expectedBits = {.d = expected}; \
+ union DoubleBits actualBits = {.d = expected}; \
+ if (expectedBits.bits != actualBits.bits) { \
+ XCTFail(@"Expected <%f> to compare equal to <%f> with bits <%llX> equal to <%llX>", actual, \
+ expected, actualBits.bits, expectedBits.bits); \
+ } \
+ } while (0);
+
+#define ASSERT_ORDERED_SAME(doubleValue, longValue) \
+ do { \
+ NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \
+ if (result != NSOrderedSame) { \
+ XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \
+ } \
+ } while (0);
+
+#define ASSERT_ORDERED_DESCENDING(doubleValue, longValue) \
+ do { \
+ NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \
+ if (result != NSOrderedDescending) { \
+ XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \
+ } \
+ } while (0);
+
+#define ASSERT_ORDERED_ASCENDING(doubleValue, longValue) \
+ do { \
+ NSComparisonResult result = FSTCompareMixed(doubleValue, longValue); \
+ if (result != NSOrderedAscending) { \
+ XCTFail(@"Expected <%f> to compare equal to <%lld>", doubleValue, longValue); \
+ } \
+ } while (0);
+
+@interface FSTComparisonTests : XCTestCase
+@end
+
+@implementation FSTComparisonTests
+
+- (void)testMixedComparison {
+ // Infinities
+ ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MIN);
+ ASSERT_ORDERED_ASCENDING(-INFINITY, LLONG_MAX);
+ ASSERT_ORDERED_ASCENDING(-INFINITY, 0LL);
+
+ ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MIN);
+ ASSERT_ORDERED_DESCENDING(INFINITY, LLONG_MAX);
+ ASSERT_ORDERED_DESCENDING(INFINITY, 0LL);
+
+ // NaN
+ ASSERT_ORDERED_ASCENDING(NAN, LLONG_MIN);
+ ASSERT_ORDERED_ASCENDING(NAN, LLONG_MAX);
+ ASSERT_ORDERED_ASCENDING(NAN, 0LL);
+
+ // Large values (note DBL_MIN is positive and near zero).
+ ASSERT_ORDERED_ASCENDING(-DBL_MAX, LLONG_MIN);
+
+ // Tests around LLONG_MIN
+ ASSERT_BIT_EQUALS((double)LLONG_MIN, -0x1.0p63);
+ ASSERT_ORDERED_SAME(-0x1.0p63, LLONG_MIN);
+ ASSERT_ORDERED_ASCENDING(-0x1.0p63, LLONG_MIN + 1);
+
+ XCTAssertLessThan(-0x1.0000000000001p63, -0x1.0p63);
+ ASSERT_ORDERED_ASCENDING(-0x1.0000000000001p63, LLONG_MIN);
+ ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp62, LLONG_MIN);
+
+ // Tests around LLONG_MAX
+ // Note LLONG_MAX cannot be exactly represented by a double, so the system rounds it to the
+ // nearest double, which is 2^63. This number, in turn is larger than the maximum representable
+ // as a long.
+ ASSERT_BIT_EQUALS(0x1.0p63, (double)LLONG_MAX);
+ ASSERT_ORDERED_DESCENDING(0x1.0p63, LLONG_MAX);
+
+ // The largest value with an exactly long representation
+ XCTAssertEqual((long)0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL);
+ ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC00LL);
+
+ ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFB00LL);
+ ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFBFFLL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFC01LL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp62, 0x7FFFFFFFFFFFFD00LL);
+
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp62, 0x7FFFFFFFFFFFFC00LL);
+
+ // Tests around MAX_SAFE_INTEGER
+ ASSERT_ORDERED_SAME(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_DESCENDING(0x1.FFFFFFFFFFFFFp52, 0x1FFFFFFFFFFFFELL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFEp52, 0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_ASCENDING(0x1.FFFFFFFFFFFFFp52, 0x20000000000000LL);
+
+ // Tests around MIN_SAFE_INTEGER
+ ASSERT_ORDERED_SAME(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_ASCENDING(-0x1.FFFFFFFFFFFFFp52, -0x1FFFFFFFFFFFFELL);
+ ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFEp52, -0x1FFFFFFFFFFFFFLL);
+ ASSERT_ORDERED_DESCENDING(-0x1.FFFFFFFFFFFFFp52, -0x20000000000000LL);
+
+ // Tests around zero.
+ ASSERT_ORDERED_SAME(-0.0, 0LL);
+ ASSERT_ORDERED_SAME(0.0, 0LL);
+
+ // The smallest representable positive value should be greater than zero
+ ASSERT_ORDERED_DESCENDING(DBL_MIN, 0LL);
+ ASSERT_ORDERED_ASCENDING(-DBL_MIN, 0LL);
+
+ // Note that 0x1.0p-1074 is a hex floating point literal representing the minimum subnormal
+ // number: <https://en.wikipedia.org/wiki/Denormal_number>.
+ double minSubNormal = 0x1.0p-1074;
+ ASSERT_ORDERED_DESCENDING(minSubNormal, 0LL);
+ ASSERT_ORDERED_ASCENDING(-minSubNormal, 0LL);
+
+ // Other sanity checks
+ ASSERT_ORDERED_ASCENDING(0.5, 1LL);
+ ASSERT_ORDERED_DESCENDING(0.5, 0LL);
+ ASSERT_ORDERED_ASCENDING(1.5, 2LL);
+ ASSERT_ORDERED_DESCENDING(1.5, 1LL);
+}
+
+@end
diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.h b/Firestore/Example/Tests/Util/FSTEventAccumulator.h
new file mode 100644
index 0000000..ae5392c
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRDocumentSnapshot;
+@class FIRQuerySnapshot;
+@class XCTestCase;
+@class XCTestExpectation;
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef void (^FSTGenericEventHandler)(id _Nullable, NSError *error);
+
+@interface FSTEventAccumulator : NSObject
+
++ (instancetype)accumulatorForTest:(XCTestCase *)testCase;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+- (id)awaitEventWithName:(NSString *)name;
+
+- (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name;
+
+@property(nonatomic, strong, readonly) FSTGenericEventHandler handler;
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.m b/Firestore/Example/Tests/Util/FSTEventAccumulator.m
new file mode 100644
index 0000000..c7e5b41
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.m
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTEventAccumulator.h"
+
+#import <XCTest/XCTest.h>
+
+#import "Util/FSTAssert.h"
+
+#import "XCTestCase+Await.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTEventAccumulator ()
+- (instancetype)initForTest:(XCTestCase *)testCase NS_DESIGNATED_INITIALIZER;
+
+@property(nonatomic, weak, readonly) XCTestCase *testCase;
+
+@property(nonatomic, assign) NSUInteger maxEvents;
+@property(nonatomic, strong, nullable) XCTestExpectation *expectation;
+@end
+
+@implementation FSTEventAccumulator {
+ NSMutableArray<id> *_events;
+}
+
++ (instancetype)accumulatorForTest:(XCTestCase *)testCase {
+ return [[FSTEventAccumulator alloc] initForTest:testCase];
+}
+
+- (instancetype)initForTest:(XCTestCase *)testCase {
+ if (self = [super init]) {
+ _testCase = testCase;
+ _events = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name {
+ @synchronized(self) {
+ FSTAssert(!self.expectation, @"Existing expectation still pending?");
+ self.expectation = [self.testCase expectationWithDescription:name];
+ self.maxEvents = self.maxEvents + events;
+ [self checkFulfilled];
+ }
+
+ // Don't await within @synchronized block to avoid deadlocking.
+ [self.testCase awaitExpectations];
+
+ return [_events subarrayWithRange:NSMakeRange(self.maxEvents - events, events)];
+}
+
+- (id)awaitEventWithName:(NSString *)name {
+ NSArray<id> *events = [self awaitEvents:1 name:name];
+ return events[0];
+}
+
+// Overrides the handler property
+- (void (^)(id _Nullable, NSError *))handler {
+ return ^void(id _Nullable value, NSError *error) {
+ // We can't store nil in the _events array, but these are still interesting to tests so store
+ // NSNull instead.
+ id event = value ? value : [NSNull null];
+
+ @synchronized(self) {
+ [_events addObject:event];
+ [self checkFulfilled];
+ }
+ };
+}
+
+- (void)checkFulfilled {
+ if (_events.count >= self.maxEvents) {
+ [self.expectation fulfill];
+ self.expectation = nil;
+ }
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h
new file mode 100644
index 0000000..3facc9c
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTHelpers.h
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "API/FIRDocumentReference+Internal.h"
+#import "Core/FSTTypes.h"
+#import "Model/FSTDocumentDictionary.h"
+#import "Model/FSTDocumentKeySet.h"
+
+@class FIRGeoPoint;
+@class FSTDeleteMutation;
+@class FSTDeletedDocument;
+@class FSTDocument;
+@class FSTDocumentKeyReference;
+@class FSTDocumentSet;
+@class FSTFieldPath;
+@class FSTFieldValue;
+@class FSTLocalViewChanges;
+@class FSTPatchMutation;
+@class FSTQuery;
+@class FSTRemoteEvent;
+@class FSTResourceName;
+@class FSTResourcePath;
+@class FSTSetMutation;
+@class FSTSnapshotVersion;
+@class FSTSortOrder;
+@class FSTTargetChange;
+@class FSTTimestamp;
+@class FSTTransformMutation;
+@class FSTView;
+@class FSTViewSnapshot;
+@class FSTObjectValue;
+@protocol FSTFilter;
+
+NS_ASSUME_NONNULL_BEGIN
+
+#if __cplusplus
+extern "C" {
+#endif
+
+#define FSTAssertIsKindOfClass(value, classType) \
+ do { \
+ XCTAssertEqualObjects([value class], [classType class]); \
+ } while (0);
+
+/**
+ * Asserts that the given NSSet of FSTDocumentKeys contains exactly the given expected keys.
+ * This is a macro instead of a method so that the failure shows up on the right line.
+ *
+ * @param actualSet An NSSet of FSTDocumentKeys.
+ * @param expectedArray A sorted array of keys that actualSet must be equal to (after converting
+ * to an array and sorting).
+ */
+#define FSTAssertEqualSets(actualSet, expectedArray) \
+ do { \
+ NSArray<FSTDocumentKey *> *actual = [(actualSet)allObjects]; \
+ actual = [actual sortedArrayUsingSelector:@selector(compare:)]; \
+ XCTAssertEqualObjects(actual, (expectedArray)); \
+ } while (0)
+
+/**
+ * Takes an array of "equality group" arrays and asserts that the compare: selector returns the
+ * same as compare: on the indexes of the "equality groups" (NSOrderedSame for items in the same
+ * group).
+ */
+#define FSTAssertComparisons(values) \
+ do { \
+ for (NSUInteger i = 0; i < [values count]; i++) { \
+ for (id left in values[i]) { \
+ for (NSUInteger j = 0; j < [values count]; j++) { \
+ for (id right in values[j]) { \
+ NSComparisonResult expected = [@(i) compare:@(j)]; \
+ NSComparisonResult result = [left compare:right]; \
+ NSComparisonResult inverseResult = [right compare:left]; \
+ XCTAssertEqual(result, expected, @"comparing %@ with %@ at (%lu, %lu)", left, right, \
+ i, j); \
+ XCTAssertEqual(inverseResult, -expected, @"comparing %@ with %@ at (%lu, %lu)", right, \
+ left, j, i); \
+ } \
+ } \
+ } \
+ } \
+ } while (0)
+
+/**
+ * Takes an array of "equality group" arrays and asserts that the isEqual: selector returns TRUE
+ * if-and-only-if items are in the same group.
+ *
+ * Additionally checks that the hash: selector returns the same value for items in the same group.
+ */
+#define FSTAssertEqualityGroups(values) \
+ do { \
+ for (NSUInteger i = 0; i < [values count]; i++) { \
+ for (id left in values[i]) { \
+ for (NSUInteger j = 0; j < [values count]; j++) { \
+ for (id right in values[j]) { \
+ if (i == j) { \
+ XCTAssertEqualObjects(left, right); \
+ XCTAssertEqual([left hash], [right hash], @"comparing hash of %@ with hash of %@", \
+ left, right); \
+ } else { \
+ XCTAssertNotEqualObjects(left, right); \
+ } \
+ } \
+ } \
+ } \
+ } \
+ } while (0)
+
+// Helper for validating API exceptions.
+#define FSTAssertThrows(expression, exceptionReason, ...) \
+ ({ \
+ BOOL __didThrow = NO; \
+ @try { \
+ (void)(expression); \
+ } @catch (NSException * exception) { \
+ __didThrow = YES; \
+ XCTAssertEqualObjects(exception.reason, exceptionReason); \
+ } \
+ XCTAssertTrue(__didThrow, ##__VA_ARGS__); \
+ })
+
+/** Creates a new FSTTimestamp from components. Note that year, month, and day are all one-based. */
+FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second);
+
+/** Creates a new NSDate from components. Note that year, month, and day are all one-based. */
+NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second);
+
+/**
+ * Creates a new NSData from the var args of bytes, must be terminated with a negative byte
+ */
+NSData *FSTTestData(int bytes, ...);
+
+/** Creates a new GeoPoint from the latitude and longitude values */
+FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude);
+
+/**
+ * Creates a new NSDateComponents from components. Note that year, month, and day are all
+ * one-based.
+ */
+NSDateComponents *FSTTestDateComponents(
+ int year, int month, int day, int hour, int minute, int second);
+
+FSTFieldPath *FSTTestFieldPath(NSString *field);
+
+/** Wraps a plain value into an FSTFieldValue instance. */
+FSTFieldValue *FSTTestFieldValue(id _Nullable value);
+
+/** Wraps a NSDictionary value into an FSTObjectValue instance. */
+FSTObjectValue *FSTTestObjectValue(NSDictionary<NSString *, id> *data);
+
+/** A convenience method for creating document keys for tests. */
+FSTDocumentKey *FSTTestDocKey(NSString *path);
+
+/** A convenience method for creating a document key set for tests. */
+FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys);
+
+/** Allow tests to just use an int literal for versions. */
+typedef int64_t FSTTestSnapshotVersion;
+
+/** A convenience method for creating snapshot versions for tests. */
+FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion version);
+
+/** A convenience method for creating docs for tests. */
+FSTDocument *FSTTestDoc(NSString *path,
+ FSTTestSnapshotVersion version,
+ NSDictionary<NSString *, id> *data,
+ BOOL hasMutations);
+
+/** A convenience method for creating deleted docs for tests. */
+FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version);
+
+/** A convenience method for creating resource paths from a path string. */
+FSTResourcePath *FSTTestPath(NSString *path);
+
+/**
+ * A convenience method for creating a document reference from a path string.
+ */
+FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *databaseID, NSString *path);
+
+/** A convenience method for creating a query for the given path (without any other filters). */
+FSTQuery *FSTTestQuery(NSString *path);
+
+/**
+ * A convenience method to create a FSTFilter using a string representation for both field
+ * and operator (<, <=, ==, >=, >).
+ */
+id<FSTFilter> FSTTestFilter(NSString *field, NSString *op, id value);
+
+/** A convenience method for creating sort orders. */
+FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction);
+
+/**
+ * Creates an NSComparator that will compare FSTDocuments by the given fieldPath string then by
+ * key.
+ */
+NSComparator FSTTestDocComparator(NSString *fieldPath);
+
+/**
+ * Creates a FSTDocumentSet based on the given comparator, initially containing the given
+ * documents.
+ */
+FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray<FSTDocument *> *docs);
+
+/** Computes changes to the view with the docs and then applies them and returns the snapshot. */
+FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view,
+ NSArray<FSTMaybeDocument *> *docs,
+ FSTTargetChange *_Nullable targetChange);
+
+/** Creates a set mutation for the document key at the given path. */
+FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary<NSString *, id> *values);
+
+/** Creates a patch mutation for the document key at the given path. */
+FSTPatchMutation *FSTTestPatchMutation(NSString *path,
+ NSDictionary<NSString *, id> *values,
+ NSArray<FSTFieldPath *> *_Nullable updateMask);
+
+FSTTransformMutation *FSTTestTransformMutation(NSString *path,
+ NSArray<NSString *> *serverTimestampFields);
+
+/** Creates a delete mutation for the document key at the given path. */
+FSTDeleteMutation *FSTTestDeleteMutation(NSString *path);
+
+/** Converts a list of documents to a sorted map. */
+FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs);
+
+/** Creates a remote event with changes to a document. */
+FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
+ NSArray<NSNumber *> *updatedInTargets,
+ NSArray<NSNumber *> *removedFromTargets);
+
+/** Creates a test view changes. */
+FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query,
+ NSArray<NSString *> *addedKeys,
+ NSArray<NSString *> *removedKeys);
+
+/** Creates a resume token to match the given snapshot version. */
+NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot);
+
+#if __cplusplus
+} // extern "C"
+#endif
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTHelpers.m b/Firestore/Example/Tests/Util/FSTHelpers.m
new file mode 100644
index 0000000..3b7f47f
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTHelpers.m
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FSTHelpers.h"
+
+#import "API/FIRFieldPath+Internal.h"
+#import "API/FSTUserDataConverter.h"
+#import "Core/FSTQuery.h"
+#import "Core/FSTSnapshotVersion.h"
+#import "Core/FSTTimestamp.h"
+#import "Core/FSTView.h"
+#import "Firestore/FIRFieldPath.h"
+#import "Firestore/FIRGeoPoint.h"
+#import "Local/FSTLocalViewChanges.h"
+#import "Local/FSTQueryData.h"
+#import "Model/FSTDatabaseID.h"
+#import "Model/FSTDocument.h"
+#import "Model/FSTDocumentKey.h"
+#import "Model/FSTDocumentSet.h"
+#import "Model/FSTFieldValue.h"
+#import "Model/FSTMutation.h"
+#import "Model/FSTPath.h"
+#import "Remote/FSTRemoteEvent.h"
+#import "Remote/FSTWatchChange.h"
+#import "Util/FSTAssert.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** A string sentinel that can be used with FSTTestPatchMutation() to mark a field for deletion. */
+static NSString *const kDeleteSentinel = @"<DELETE>";
+
+static const int kMicrosPerSec = 1000000;
+static const int kMillisPerSec = 1000;
+
+FSTTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second) {
+ NSDate *date = FSTTestDate(year, month, day, hour, minute, second);
+ return [FSTTimestamp timestampWithDate:date];
+}
+
+NSDate *FSTTestDate(int year, int month, int day, int hour, int minute, int second) {
+ NSDateComponents *comps = FSTTestDateComponents(year, month, day, hour, minute, second);
+ return [[NSCalendar currentCalendar] dateFromComponents:comps];
+}
+
+NSData *FSTTestData(int bytes, ...) {
+ va_list args;
+ va_start(args, bytes); /* Initialize the argument list. */
+
+ NSMutableData *data = [NSMutableData data];
+
+ int next = bytes;
+ while (next >= 0) {
+ uint8_t byte = (uint8_t)next;
+ [data appendBytes:&byte length:1];
+ next = va_arg(args, int);
+ }
+
+ va_end(args);
+ return [data copy];
+}
+
+FIRGeoPoint *FSTTestGeoPoint(double latitude, double longitude) {
+ return [[FIRGeoPoint alloc] initWithLatitude:latitude longitude:longitude];
+}
+
+NSDateComponents *FSTTestDateComponents(
+ int year, int month, int day, int hour, int minute, int second) {
+ NSDateComponents *comps = [[NSDateComponents alloc] init];
+ comps.year = year;
+ comps.month = month;
+ comps.day = day;
+ comps.hour = hour;
+ comps.minute = minute;
+ comps.second = second;
+
+ // Force time zone to UTC to avoid these values changing due to daylight saving.
+ comps.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
+ return comps;
+}
+
+FSTFieldPath *FSTTestFieldPath(NSString *field) {
+ return [FIRFieldPath pathWithDotSeparatedString:field].internalValue;
+}
+
+FSTFieldValue *FSTTestFieldValue(id _Nullable value) {
+ FSTDatabaseID *databaseID =
+ [FSTDatabaseID databaseIDWithProject:@"project" database:kDefaultDatabaseID];
+ FSTUserDataConverter *converter =
+ [[FSTUserDataConverter alloc] initWithDatabaseID:databaseID
+ preConverter:^id _Nullable(id _Nullable input) {
+ return input;
+ }];
+ // HACK: We use parsedQueryValue: since it accepts scalars as well as arrays / objects, and
+ // our tests currently use FSTTestFieldValue() pretty generically so we don't know the intent.
+ return [converter parsedQueryValue:value];
+}
+
+FSTObjectValue *FSTTestObjectValue(NSDictionary<NSString *, id> *data) {
+ FSTFieldValue *wrapped = FSTTestFieldValue(data);
+ FSTCAssert([wrapped isKindOfClass:[FSTObjectValue class]], @"Unsupported value: %@", data);
+ return (FSTObjectValue *)wrapped;
+}
+
+FSTDocumentKey *FSTTestDocKey(NSString *path) {
+ return [FSTDocumentKey keyWithPathString:path];
+}
+
+FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys) {
+ FSTDocumentKeySet *result = [FSTDocumentKeySet keySet];
+ for (FSTDocumentKey *key in keys) {
+ result = [result setByAddingObject:key];
+ }
+ return result;
+}
+
+FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion versionMicroseconds) {
+ int64_t seconds = versionMicroseconds / kMicrosPerSec;
+ int32_t nanos = (int32_t)(versionMicroseconds % kMicrosPerSec) * kMillisPerSec;
+
+ FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos];
+ return [FSTSnapshotVersion versionWithTimestamp:timestamp];
+}
+
+FSTDocument *FSTTestDoc(NSString *path,
+ FSTTestSnapshotVersion version,
+ NSDictionary<NSString *, id> *data,
+ BOOL hasMutations) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path];
+ return [FSTDocument documentWithData:FSTTestObjectValue(data)
+ key:key
+ version:FSTTestVersion(version)
+ hasLocalMutations:hasMutations];
+}
+
+FSTDeletedDocument *FSTTestDeletedDoc(NSString *path, FSTTestSnapshotVersion version) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:path];
+ return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)];
+}
+
+static NSArray<NSString *> *FSTTestSplitPath(NSString *path) {
+ if ([path isEqualToString:@""]) {
+ return @[];
+ } else {
+ return [path componentsSeparatedByString:@"/"];
+ }
+}
+
+FSTResourcePath *FSTTestPath(NSString *path) {
+ return [FSTResourcePath pathWithSegments:FSTTestSplitPath(path)];
+}
+
+FSTDocumentKeyReference *FSTTestRef(NSString *projectID, NSString *database, NSString *path) {
+ FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database];
+ return [[FSTDocumentKeyReference alloc] initWithKey:FSTTestDocKey(path) databaseID:databaseID];
+}
+
+FSTQuery *FSTTestQuery(NSString *path) {
+ return [FSTQuery queryWithPath:FSTTestPath(path)];
+}
+
+id<FSTFilter> FSTTestFilter(NSString *field, NSString *opString, id value) {
+ FSTFieldPath *path = FSTTestFieldPath(field);
+ FSTRelationFilterOperator op;
+ if ([opString isEqualToString:@"<"]) {
+ op = FSTRelationFilterOperatorLessThan;
+ } else if ([opString isEqualToString:@"<="]) {
+ op = FSTRelationFilterOperatorLessThanOrEqual;
+ } else if ([opString isEqualToString:@"=="]) {
+ op = FSTRelationFilterOperatorEqual;
+ } else if ([opString isEqualToString:@">="]) {
+ op = FSTRelationFilterOperatorGreaterThanOrEqual;
+ } else if ([opString isEqualToString:@">"]) {
+ op = FSTRelationFilterOperatorGreaterThan;
+ } else {
+ FSTCFail(@"Unsupported operator type: %@", opString);
+ }
+
+ FSTFieldValue *data = FSTTestFieldValue(value);
+ if ([data isEqual:[FSTDoubleValue nanValue]]) {
+ FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with NAN.");
+ return [[FSTNanFilter alloc] initWithField:path];
+ } else if ([data isEqual:[FSTNullValue nullValue]]) {
+ FSTCAssert(op == FSTRelationFilterOperatorEqual, @"Must use == with Null.");
+ return [[FSTNullFilter alloc] initWithField:path];
+ } else {
+ return [FSTRelationFilter filterWithField:path filterOperator:op value:data];
+ }
+}
+
+FSTSortOrder *FSTTestOrderBy(NSString *field, NSString *direction) {
+ FSTFieldPath *path = FSTTestFieldPath(field);
+ BOOL ascending;
+ if ([direction isEqualToString:@"asc"]) {
+ ascending = YES;
+ } else if ([direction isEqualToString:@"desc"]) {
+ ascending = NO;
+ } else {
+ FSTCFail(@"Unsupported direction: %@", direction);
+ }
+ return [FSTSortOrder sortOrderWithFieldPath:path ascending:ascending];
+}
+
+NSComparator FSTTestDocComparator(NSString *fieldPath) {
+ FSTQuery *query = [[FSTQuery queryWithPath:[FSTResourcePath pathWithSegments:@[ @"docs" ]]]
+ queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:FSTTestFieldPath(fieldPath)
+ ascending:YES]];
+ return [query comparator];
+}
+
+FSTDocumentSet *FSTTestDocSet(NSComparator comp, NSArray<FSTDocument *> *docs) {
+ FSTDocumentSet *docSet = [FSTDocumentSet documentSetWithComparator:comp];
+ for (FSTDocument *doc in docs) {
+ docSet = [docSet documentSetByAddingDocument:doc];
+ }
+ return docSet;
+}
+
+FSTSetMutation *FSTTestSetMutation(NSString *path, NSDictionary<NSString *, id> *values) {
+ return [[FSTSetMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path]
+ value:FSTTestObjectValue(values)
+ precondition:[FSTPrecondition none]];
+}
+
+FSTPatchMutation *FSTTestPatchMutation(NSString *path,
+ NSDictionary<NSString *, id> *values,
+ NSArray<FSTFieldPath *> *_Nullable updateMask) {
+ BOOL merge = updateMask != nil;
+
+ __block FSTObjectValue *objectValue = [FSTObjectValue objectValue];
+ NSMutableArray<FSTFieldPath *> *fieldMaskPaths = [NSMutableArray array];
+ [values enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+ FSTFieldPath *path = FSTTestFieldPath(key);
+ [fieldMaskPaths addObject:path];
+ if (![value isEqual:kDeleteSentinel]) {
+ FSTFieldValue *parsedValue = FSTTestFieldValue(value);
+ objectValue = [objectValue objectBySettingValue:parsedValue forPath:path];
+ }
+ }];
+
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)];
+ FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:merge ? updateMask : fieldMaskPaths];
+ return [[FSTPatchMutation alloc] initWithKey:key
+ fieldMask:mask
+ value:objectValue
+ precondition:[FSTPrecondition preconditionWithExists:YES]];
+}
+
+// For now this only creates TransformMutations with server timestamps.
+FSTTransformMutation *FSTTestTransformMutation(NSString *path,
+ NSArray<NSString *> *serverTimestampFields) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPath:FSTTestPath(path)];
+ NSMutableArray<FSTFieldTransform *> *fieldTransforms = [NSMutableArray array];
+ for (NSString *field in serverTimestampFields) {
+ FSTFieldPath *fieldPath = FSTTestFieldPath(field);
+ id<FSTTransformOperation> transformOp = [FSTServerTimestampTransform serverTimestampTransform];
+ FSTFieldTransform *transform =
+ [[FSTFieldTransform alloc] initWithPath:fieldPath transform:transformOp];
+ [fieldTransforms addObject:transform];
+ }
+ return [[FSTTransformMutation alloc] initWithKey:key fieldTransforms:fieldTransforms];
+}
+
+FSTDeleteMutation *FSTTestDeleteMutation(NSString *path) {
+ return [[FSTDeleteMutation alloc] initWithKey:[FSTDocumentKey keyWithPathString:path]
+ precondition:[FSTPrecondition none]];
+}
+
+FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs) {
+ FSTMaybeDocumentDictionary *updates = [FSTMaybeDocumentDictionary maybeDocumentDictionary];
+ for (FSTMaybeDocument *doc in docs) {
+ updates = [updates dictionaryBySettingObject:doc forKey:doc.key];
+ }
+ return updates;
+}
+
+FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view,
+ NSArray<FSTMaybeDocument *> *docs,
+ FSTTargetChange *_Nullable targetChange) {
+ return [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)]
+ targetChange:targetChange]
+ .snapshot;
+}
+
+FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc,
+ NSArray<NSNumber *> *updatedInTargets,
+ NSArray<NSNumber *> *removedFromTargets) {
+ FSTDocumentWatchChange *change =
+ [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets
+ removedTargetIDs:removedFromTargets
+ documentKey:doc.key
+ document:doc];
+ NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary];
+ FSTQueryData *dummyQueryData = [FSTQueryData alloc];
+ for (NSNumber *targetID in updatedInTargets) {
+ listens[targetID] = dummyQueryData;
+ }
+ for (NSNumber *targetID in removedFromTargets) {
+ listens[targetID] = dummyQueryData;
+ }
+ NSMutableDictionary<NSNumber *, NSNumber *> *pending = [NSMutableDictionary dictionary];
+ FSTWatchChangeAggregator *aggregator =
+ [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version
+ listenTargets:listens
+ pendingTargetResponses:pending];
+ [aggregator addWatchChange:change];
+ return [aggregator remoteEvent];
+}
+
+/** Creates a resume token to match the given snapshot version. */
+NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion snapshotVersion) {
+ if (snapshotVersion == 0) {
+ return nil;
+ }
+
+ NSString *snapshotString = [NSString stringWithFormat:@"snapshot-%" PRId64, snapshotVersion];
+ return [snapshotString dataUsingEncoding:NSUTF8StringEncoding];
+}
+
+FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query,
+ NSArray<NSString *> *addedKeys,
+ NSArray<NSString *> *removedKeys) {
+ FSTDocumentKeySet *added = [FSTDocumentKeySet keySet];
+ for (NSString *keyPath in addedKeys) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath];
+ added = [added setByAddingObject:key];
+ }
+ FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet];
+ for (NSString *keyPath in removedKeys) {
+ FSTDocumentKey *key = [FSTDocumentKey keyWithPathString:keyPath];
+ removed = [removed setByAddingObject:key];
+ }
+ return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed];
+}
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h
new file mode 100644
index 0000000..cefd669
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <XCTest/XCTest.h>
+
+#import "XCTestCase+Await.h"
+
+@class FIRCollectionReference;
+@class FIRDocumentSnapshot;
+@class FIRDocumentReference;
+@class FIRQuerySnapshot;
+@class FIRFirestore;
+@class FIRFirestoreSettings;
+@class FIRQuery;
+@class FSTEventAccumulator;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FSTIntegrationTestCase : XCTestCase
+
+/** Returns the default Firestore project ID for testing. */
++ (NSString *)projectID;
+
+/** Returns a FirestoreSettings configured to use either hexa or the emulator. */
++ (FIRFirestoreSettings *)settings;
+
+/** Returns a new Firestore connected to the "test-db" project. */
+- (FIRFirestore *)firestore;
+
+/** Returns a new Firestore connected to the project with the given projectID. */
+- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID;
+
+/** Synchronously shuts down the given firestore. */
+- (void)shutdownFirestore:(FIRFirestore *)firestore;
+
+- (NSString *)documentPath;
+
+- (FIRDocumentReference *)documentRef;
+
+- (FIRCollectionReference *)collectionRef;
+
+- (FIRCollectionReference *)collectionRefWithDocuments:
+ (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents;
+
+- (void)writeAllDocuments:(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents
+ toCollection:(FIRCollectionReference *)collection;
+
+- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path,
+ FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef))action;
+
+- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref;
+
+- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query;
+
+- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data;
+
+- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data;
+
+- (void)deleteDocumentRef:(FIRDocumentReference *)ref;
+
+/**
+ * "Blocks" the current thread/run loop until the block returns YES.
+ * Should only be called on the main thread.
+ * The block is invoked frequently and in a loop (every couple of millseconds) to ensure fast
+ * test progress and make sure actions to be run on main thread are not blocked by this method.
+ */
+- (void)waitUntil:(BOOL (^)())predicate;
+
+@property(nonatomic, strong) FIRFirestore *db;
+@property(nonatomic, strong) FSTEventAccumulator *eventAccumulator;
+@end
+
+/** Converts the FIRQuerySnapshot to an NSArray containing the data of the documents in order. */
+NSArray<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs);
+
+/** Converts the FIRQuerySnapshot to an NSArray containing the document IDs in order. */
+NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs);
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m
new file mode 100644
index 0000000..87a78c3
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.m
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@import Firestore;
+
+#import "FSTIntegrationTestCase.h"
+
+#import <FirebaseCommunity/FIRLogger.h>
+#import <GRPCClient/GRPCCall+ChannelArg.h>
+#import <GRPCClient/GRPCCall+Tests.h>
+
+#import "API/FIRFirestore+Internal.h"
+#import "Auth/FSTEmptyCredentialsProvider.h"
+#import "Local/FSTLevelDB.h"
+#import "Model/FSTDatabaseID.h"
+#import "Util/FSTDispatchQueue.h"
+#import "Util/FSTUtil.h"
+
+#import "FSTEventAccumulator.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FSTIntegrationTestCase {
+ NSMutableArray<FIRFirestore *> *_firestores;
+}
+
+- (void)setUp {
+ [super setUp];
+
+ [self clearPersistence];
+
+ _firestores = [NSMutableArray array];
+ self.db = [self firestore];
+ self.eventAccumulator = [FSTEventAccumulator accumulatorForTest:self];
+}
+
+- (void)tearDown {
+ @try {
+ for (FIRFirestore *firestore in _firestores) {
+ [self shutdownFirestore:firestore];
+ }
+ } @finally {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ [GRPCCall closeOpenConnections];
+#pragma clang diagnostic pop
+ _firestores = nil;
+ [super tearDown];
+ }
+}
+
+- (void)clearPersistence {
+ NSString *levelDBDir = [FSTLevelDB documentsDirectory];
+ NSError *error;
+ if (![[NSFileManager defaultManager] removeItemAtPath:levelDBDir error:&error]) {
+ // file not found is okay.
+ XCTAssertTrue(
+ [error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError,
+ @"Failed to clear LevelDB Persistence: %@", error);
+ }
+}
+
+- (FIRFirestore *)firestore {
+ return [self firestoreWithProjectID:[FSTIntegrationTestCase projectID]];
+}
+
++ (NSString *)projectID {
+ NSString *project = [[NSProcessInfo processInfo] environment][@"PROJECT_ID"];
+ if (!project) {
+ project = @"test-db";
+ }
+ return project;
+}
+
++ (FIRFirestoreSettings *)settings {
+ FIRFirestoreSettings *settings = [[FIRFirestoreSettings alloc] init];
+ NSString *host = [[NSProcessInfo processInfo] environment][@"DATASTORE_HOST"];
+ settings.sslEnabled = YES;
+ if (!host) {
+ // If host is nil, there is no GoogleService-Info.plist. Check if a hexa integration test
+ // configuration is configured. The first bundle location is used by bazel builds. The
+ // second is used for github clones.
+ host = @"localhost:8081";
+ settings.sslEnabled = YES;
+ NSString *certsPath =
+ [[NSBundle mainBundle] pathForResource:@"PlugIns/IntegrationTests.xctest/CAcert"
+ ofType:@"pem"];
+ if (certsPath == nil) {
+ certsPath = [[NSBundle bundleForClass:[self class]] pathForResource:@"CAcert" ofType:@"pem"];
+ }
+ unsigned long long fileSize =
+ [[[NSFileManager defaultManager] attributesOfItemAtPath:certsPath error:nil] fileSize];
+
+ if (fileSize == 0) {
+ NSLog(
+ @"The cert is not properly configured. Make sure setup_integration_tests.py "
+ "has been run.");
+ }
+ [GRPCCall useTestCertsPath:certsPath testName:@"test_cert_2" forHost:host];
+ }
+ settings.host = host;
+ settings.persistenceEnabled = YES;
+ NSLog(@"Configured integration test for %@ with SSL: %@", settings.host,
+ settings.sslEnabled ? @"YES" : @"NO");
+ return settings;
+}
+
+- (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID {
+ NSString *persistenceKey = [NSString stringWithFormat:@"db%lu", (unsigned long)_firestores.count];
+
+ FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue
+ queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)];
+
+ FSTEmptyCredentialsProvider *credentialsProvider = [[FSTEmptyCredentialsProvider alloc] init];
+
+ FIRSetLoggerLevel(FIRLoggerLevelDebug);
+ // HACK: FIRFirestore expects a non-nil app, but for tests we cheat.
+ FIRApp *app = nil;
+ FIRFirestore *firestore = [[FIRFirestore alloc] initWithProjectID:projectID
+ database:kDefaultDatabaseID
+ persistenceKey:persistenceKey
+ credentialsProvider:credentialsProvider
+ workerDispatchQueue:workerDispatchQueue
+ firebaseApp:app];
+
+ firestore.settings = [FSTIntegrationTestCase settings];
+
+ [_firestores addObject:firestore];
+ return firestore;
+}
+
+- (void)shutdownFirestore:(FIRFirestore *)firestore {
+ XCTestExpectation *shutdownCompletion = [self expectationWithDescription:@"shutdown"];
+ [firestore shutdownWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [shutdownCompletion fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (NSString *)documentPath {
+ return [@"test-collection/" stringByAppendingString:[FSTUtil autoID]];
+}
+
+- (FIRDocumentReference *)documentRef {
+ return [self.db documentWithPath:[self documentPath]];
+}
+
+- (FIRCollectionReference *)collectionRef {
+ NSString *collectionName = [@"test-collection-" stringByAppendingString:[FSTUtil autoID]];
+ return [self.db collectionWithPath:collectionName];
+}
+
+- (FIRCollectionReference *)collectionRefWithDocuments:
+ (NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents {
+ FIRCollectionReference *collection = [self collectionRef];
+ // Use a different instance to write the documents
+ [self writeAllDocuments:documents
+ toCollection:[[self firestore] collectionWithPath:collection.path]];
+ return collection;
+}
+
+- (void)writeAllDocuments:(NSDictionary<NSString *, NSDictionary<NSString *, id> *> *)documents
+ toCollection:(FIRCollectionReference *)collection {
+ [documents enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSDictionary<NSString *, id> *value,
+ BOOL *stop) {
+ FIRDocumentReference *ref = [collection documentWithPath:key];
+ [self writeDocumentRef:ref data:value];
+ }];
+}
+
+- (void)readerAndWriterOnDocumentRef:(void (^)(NSString *path,
+ FIRDocumentReference *readerRef,
+ FIRDocumentReference *writerRef))action {
+ FIRFirestore *reader = self.db; // for clarity
+ FIRFirestore *writer = [self firestore];
+
+ NSString *path = [self documentPath];
+ FIRDocumentReference *readerRef = [reader documentWithPath:path];
+ FIRDocumentReference *writerRef = [writer documentWithPath:path];
+ action(path, readerRef, writerRef);
+}
+
+- (FIRDocumentSnapshot *)readDocumentForRef:(FIRDocumentReference *)ref {
+ __block FIRDocumentSnapshot *result;
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"getData"];
+ [ref getDocumentWithCompletion:^(FIRDocumentSnapshot *doc, NSError *_Nullable error) {
+ XCTAssertNil(error);
+ result = doc;
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ return result;
+}
+
+- (FIRQuerySnapshot *)readDocumentSetForRef:(FIRQuery *)query {
+ __block FIRQuerySnapshot *result;
+
+ XCTestExpectation *expectation = [self expectationWithDescription:@"getData"];
+ [query getDocumentsWithCompletion:^(FIRQuerySnapshot *documentSet, NSError *error) {
+ XCTAssertNil(error);
+ result = documentSet;
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+
+ return result;
+}
+
+- (void)writeDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<NSString *, id> *)data {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"setData"];
+ [ref setData:data
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)updateDocumentRef:(FIRDocumentReference *)ref data:(NSDictionary<id, id> *)data {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"updateData"];
+ [ref updateData:data
+ completion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)deleteDocumentRef:(FIRDocumentReference *)ref {
+ XCTestExpectation *expectation = [self expectationWithDescription:@"deleteDocument"];
+ [ref deleteDocumentWithCompletion:^(NSError *_Nullable error) {
+ XCTAssertNil(error);
+ [expectation fulfill];
+ }];
+ [self awaitExpectations];
+}
+
+- (void)waitUntil:(BOOL (^)())predicate {
+ NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
+ double waitSeconds = [self defaultExpectationWaitSeconds];
+ while (!predicate() && ([NSDate timeIntervalSinceReferenceDate] - start < waitSeconds)) {
+ // This waits for the next event or until the 100ms timeout is reached
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
+ beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
+ }
+ if (!predicate()) {
+ XCTFail(@"Timeout");
+ }
+}
+
+NSArray<NSDictionary<NSString *, id> *> *FIRQuerySnapshotGetData(FIRQuerySnapshot *docs) {
+ NSMutableArray<NSDictionary<NSString *, id> *> *result = [NSMutableArray array];
+ for (FIRDocumentSnapshot *doc in docs.documents) {
+ [result addObject:doc.data];
+ }
+ return result;
+}
+
+NSArray<NSString *> *FIRQuerySnapshotGetIDs(FIRQuerySnapshot *docs) {
+ NSMutableArray<NSString *> *result = [NSMutableArray array];
+ for (FIRDocumentSnapshot *doc in docs.documents) {
+ [result addObject:doc.documentID];
+ }
+ return result;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Firestore/Example/Tests/Util/FSTUtilTests.m b/Firestore/Example/Tests/Util/FSTUtilTests.m
new file mode 100644
index 0000000..998832d
--- /dev/null
+++ b/Firestore/Example/Tests/Util/FSTUtilTests.m
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "Util/FSTUtil.h"
+
+#import <XCTest/XCTest.h>
+
+@interface FSTUtilTests : XCTestCase
+@end
+
+@implementation FSTUtilTests
+
+- (void)testAutoID {
+ NSString *autoID = [FSTUtil autoID];
+ XCTAssertEqual([autoID length], 20);
+ for (NSUInteger i = 0; i < 20; i++) {
+ unichar c = [autoID characterAtIndex:i];
+ XCTAssert(c >= ' ' && c <= '~', @"Should be printable ascii characters.");
+ }
+}
+
+@end
diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.h b/Firestore/Example/Tests/Util/XCTestCase+Await.h
new file mode 100644
index 0000000..9d575f9
--- /dev/null
+++ b/Firestore/Example/Tests/Util/XCTestCase+Await.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <XCTest/XCTest.h>
+
+@interface XCTestCase (Await)
+
+/**
+ * Await all outstanding expectations with a reasonable timeout, and if any of them fail, XCTFail
+ * the test.
+ */
+- (void)awaitExpectations;
+
+/**
+ * Returns a reasonable timeout for testing against Firestore.
+ */
+- (double)defaultExpectationWaitSeconds;
+
+@end
diff --git a/Firestore/Example/Tests/Util/XCTestCase+Await.m b/Firestore/Example/Tests/Util/XCTestCase+Await.m
new file mode 100644
index 0000000..e200c8c
--- /dev/null
+++ b/Firestore/Example/Tests/Util/XCTestCase+Await.m
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "XCTestCase+Await.h"
+
+#import <Foundation/Foundation.h>
+
+static const double kExpectationWaitSeconds = 10.0;
+
+@implementation XCTestCase (Await)
+
+- (void)awaitExpectations {
+ [self waitForExpectationsWithTimeout:kExpectationWaitSeconds
+ handler:^(NSError *_Nullable expectationError) {
+ if (expectationError) {
+ XCTFail(@"Error waiting for timeout: %@", expectationError);
+ }
+ }];
+}
+
+- (double)defaultExpectationWaitSeconds {
+ return kExpectationWaitSeconds;
+}
+
+@end
diff --git a/Firestore/Example/Tests/en.lproj/InfoPlist.strings b/Firestore/Example/Tests/en.lproj/InfoPlist.strings
new file mode 100644
index 0000000..477b28f
--- /dev/null
+++ b/Firestore/Example/Tests/en.lproj/InfoPlist.strings
@@ -0,0 +1,2 @@
+/* Localized versions of Info.plist keys */
+