diff options
Diffstat (limited to 'Firestore/Example')
23 files changed, 3932 insertions, 443 deletions
diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index ca8b598..68b3a55 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -24,7 +24,9 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 0535C1B65DADAE1CE47FA3CA /* string_format_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */; }; 132E3E53179DE287D875F3F2 /* FSTLevelDBTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */; }; + 1CAA9012B25F975D445D5978 /* strerror_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 358C3B5FE573B1D60A4F7592 /* strerror_test.cc */; }; 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; 54131E9720ADE679001DF3FF /* string_format_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54131E9620ADE678001DF3FF /* string_format_test.cc */; }; 54511E8E209805F8005BD28F /* hashing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54511E8D209805F8005BD28F /* hashing_test.cc */; }; @@ -130,6 +132,7 @@ 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 */; }; 54EB764D202277B30088B8F3 /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; }; + 5A080105CCBFDB6BF3F3772D /* path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 403DBF6EFB541DFD01582AA3 /* path_test.cc */; }; 5D405BE298CE4692CB00790A /* Pods_Firestore_Tests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B50B3A0DF77100EEE887891 /* Pods_Firestore_Tests_iOS.framework */; }; 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58F195388D20070C39A /* CoreGraphics.framework */; }; @@ -144,6 +147,23 @@ 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; 6161B5032047140C00A99DBB /* FIRFirestoreSourceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */; }; + 618BBEA620B89AAC00B5BCE7 /* target.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7D20B89AAC00B5BCE7 /* target.pb.cc */; }; + 618BBEA720B89AAC00B5BCE7 /* maybe_document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7E20B89AAC00B5BCE7 /* maybe_document.pb.cc */; }; + 618BBEA820B89AAC00B5BCE7 /* mutation.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */; }; + 618BBEA920B89AAC00B5BCE7 /* common.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8820B89AAC00B5BCE7 /* common.pb.cc */; }; + 618BBEAA20B89AAC00B5BCE7 /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8A20B89AAC00B5BCE7 /* firestore.pb.cc */; }; + 618BBEAB20B89AAC00B5BCE7 /* query.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8C20B89AAC00B5BCE7 /* query.pb.cc */; }; + 618BBEAC20B89AAC00B5BCE7 /* document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8E20B89AAC00B5BCE7 /* document.pb.cc */; }; + 618BBEAD20B89AAC00B5BCE7 /* write.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8F20B89AAC00B5BCE7 /* write.pb.cc */; }; + 618BBEAE20B89AAC00B5BCE7 /* latlng.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */; }; + 618BBEAF20B89AAC00B5BCE7 /* annotations.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9520B89AAC00B5BCE7 /* annotations.pb.cc */; }; + 618BBEB020B89AAC00B5BCE7 /* http.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */; }; + 618BBEB120B89AAC00B5BCE7 /* status.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */; }; + 61F72C5620BC48FD001A68CB /* serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 61F72C5520BC48FD001A68CB /* serializer_test.cc */; }; + 6EDD3B4620BF247500C33877 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; + 6EDD3B4820BF247500C33877 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; + 6EDD3B4920BF247500C33877 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; + 6EDD3B6020BF25AE00C33877 /* FSTFuzzTestsPrincipal.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6EDD3B5E20BF24D000C33877 /* FSTFuzzTestsPrincipal.mm */; }; 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; 7346E61D20325C6900FD6CEF /* FSTDispatchQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7346E61C20325C6900FD6CEF /* FSTDispatchQueueTests.mm */; }; 73866AA12082B0A5009BB4FF /* FIRArrayTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */; }; @@ -180,6 +200,8 @@ B6FB4690208F9BB300554BA2 /* executor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4688208F9B9100554BA2 /* executor_test.cc */; }; BF219E98F1C5A1DAEB5EEC86 /* Pods_Firestore_Example_iOS_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 379B34A1536045869826D82A /* Pods_Firestore_Example_iOS_SwiftBuildTest.framework */; }; C1AA536F90A0A576CA2816EB /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB92EB03E3F92485023F64ED /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */; }; + C482E724F4B10968417C3F78 /* Pods_Firestore_FuzzTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B79CA87A1A01FC5329031C9B /* Pods_Firestore_FuzzTests_iOS.framework */; }; + C80B10E79CDD7EF7843C321E /* type_traits_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2A0CF41BA5AED6049B0BEB2C /* type_traits_apple_test.mm */; }; C8D3CE2343E53223E6487F2C /* Pods_Firestore_Example_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5918805E993304321A05E82B /* Pods_Firestore_Example_iOS.framework */; }; DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; @@ -215,6 +237,13 @@ remoteGlobalIDString = 6003F589195388D20070C39A; remoteInfo = Firestore; }; + 6EDD3AD320BF247500C33877 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6003F582195388D10070C39A /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6003F589195388D20070C39A; + remoteInfo = Firestore; + }; DE03B2961F2149D600A30B9C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6003F582195388D10070C39A /* Project object */; @@ -251,11 +280,14 @@ 1277F98C20D2DF0867496976 /* Pods-Firestore_IntegrationTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS.debug.xcconfig"; sourceTree = "<group>"; }; 12F4357299652983A615F886 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; }; 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBTransactionTests.mm; sourceTree = "<group>"; }; + 2A0CF41BA5AED6049B0BEB2C /* type_traits_apple_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = type_traits_apple_test.mm; sourceTree = "<group>"; }; 2B50B3A0DF77100EEE887891 /* Pods_Firestore_Tests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 358C3B5FE573B1D60A4F7592 /* strerror_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = strerror_test.cc; sourceTree = "<group>"; }; 379B34A1536045869826D82A /* Pods_Firestore_Example_iOS_SwiftBuildTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_iOS_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>"; }; 3C81DE3772628FE297055662 /* Pods-Firestore_Example_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS.debug.xcconfig"; sourceTree = "<group>"; }; 3F0992A4B83C60841C52E960 /* Pods-Firestore_Example_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS.release.xcconfig"; sourceTree = "<group>"; }; + 403DBF6EFB541DFD01582AA3 /* path_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = path_test.cc; sourceTree = "<group>"; }; 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = hard_assert_test.cc; sourceTree = "<group>"; }; 54131E9620ADE678001DF3FF /* string_format_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = string_format_test.cc; sourceTree = "<group>"; }; 54511E8D209805F8005BD28F /* hashing_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = hashing_test.cc; sourceTree = "<group>"; }; @@ -392,13 +424,44 @@ 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>"; }; 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFirestoreSourceTests.mm; sourceTree = "<group>"; }; + 618BBE7D20B89AAC00B5BCE7 /* target.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = target.pb.cc; sourceTree = "<group>"; }; + 618BBE7E20B89AAC00B5BCE7 /* maybe_document.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = maybe_document.pb.cc; sourceTree = "<group>"; }; + 618BBE7F20B89AAC00B5BCE7 /* target.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = target.pb.h; sourceTree = "<group>"; }; + 618BBE8020B89AAC00B5BCE7 /* maybe_document.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = maybe_document.pb.h; sourceTree = "<group>"; }; + 618BBE8120B89AAC00B5BCE7 /* mutation.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = mutation.pb.h; sourceTree = "<group>"; }; + 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = mutation.pb.cc; sourceTree = "<group>"; }; + 618BBE8620B89AAC00B5BCE7 /* query.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = query.pb.h; sourceTree = "<group>"; }; + 618BBE8720B89AAC00B5BCE7 /* common.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = common.pb.h; sourceTree = "<group>"; }; + 618BBE8820B89AAC00B5BCE7 /* common.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = common.pb.cc; sourceTree = "<group>"; }; + 618BBE8920B89AAC00B5BCE7 /* firestore.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = firestore.pb.h; sourceTree = "<group>"; }; + 618BBE8A20B89AAC00B5BCE7 /* firestore.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = firestore.pb.cc; sourceTree = "<group>"; }; + 618BBE8B20B89AAC00B5BCE7 /* write.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = write.pb.h; sourceTree = "<group>"; }; + 618BBE8C20B89AAC00B5BCE7 /* query.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = query.pb.cc; sourceTree = "<group>"; }; + 618BBE8D20B89AAC00B5BCE7 /* document.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = document.pb.h; sourceTree = "<group>"; }; + 618BBE8E20B89AAC00B5BCE7 /* document.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = document.pb.cc; sourceTree = "<group>"; }; + 618BBE8F20B89AAC00B5BCE7 /* write.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = write.pb.cc; sourceTree = "<group>"; }; + 618BBE9120B89AAC00B5BCE7 /* latlng.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = latlng.pb.h; sourceTree = "<group>"; }; + 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = latlng.pb.cc; sourceTree = "<group>"; }; + 618BBE9420B89AAC00B5BCE7 /* http.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = http.pb.h; sourceTree = "<group>"; }; + 618BBE9520B89AAC00B5BCE7 /* annotations.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = annotations.pb.cc; sourceTree = "<group>"; }; + 618BBE9620B89AAC00B5BCE7 /* annotations.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = annotations.pb.h; sourceTree = "<group>"; }; + 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = http.pb.cc; sourceTree = "<group>"; }; + 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = status.pb.cc; sourceTree = "<group>"; }; + 618BBE9A20B89AAC00B5BCE7 /* status.pb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = status.pb.h; sourceTree = "<group>"; }; + 61F72C5520BC48FD001A68CB /* serializer_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = serializer_test.cc; sourceTree = "<group>"; }; 69E6C311558EC77729A16CF1 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig"; sourceTree = "<group>"; }; + 6EDD3B5B20BF247500C33877 /* Firestore_FuzzTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_FuzzTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 6EDD3B5C20BF247500C33877 /* Firestore_FuzzTests_iOS-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Firestore_FuzzTests_iOS-Info.plist"; sourceTree = "<group>"; }; + 6EDD3B5E20BF24D000C33877 /* FSTFuzzTestsPrincipal.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTFuzzTestsPrincipal.mm; sourceTree = "<group>"; }; 71719F9E1E33DC2100824A3D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 7346E61C20325C6900FD6CEF /* FSTDispatchQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTDispatchQueueTests.mm; sourceTree = "<group>"; }; 73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRArrayTransformTests.mm; sourceTree = "<group>"; }; 74ACEC3603BE58B57A7E8D4C /* Pods-Firestore_Example_iOS-SwiftBuildTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-SwiftBuildTest.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-SwiftBuildTest/Pods-Firestore_Example_iOS-SwiftBuildTest.release.xcconfig"; sourceTree = "<group>"; }; + 84434E57CA72951015FC71BC /* Pods-Firestore_FuzzTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; sourceTree = "<group>"; }; 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>"; }; + 97C492D2524E92927C11F425 /* Pods-Firestore_FuzzTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_FuzzTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS.release.xcconfig"; sourceTree = "<group>"; }; + 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_format_apple_test.mm; sourceTree = "<group>"; }; AB356EF6200EA5EB0089B766 /* field_value_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = field_value_test.cc; sourceTree = "<group>"; }; AB380CF82019382300D97691 /* target_id_generator_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = target_id_generator_test.cc; sourceTree = "<group>"; }; AB380CFC201A2EE200D97691 /* string_util_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = string_util_test.cc; sourceTree = "<group>"; }; @@ -431,6 +494,7 @@ B6FB4688208F9B9100554BA2 /* executor_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = executor_test.cc; sourceTree = "<group>"; }; B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = executor_libdispatch_test.mm; sourceTree = "<group>"; }; B6FB468A208F9B9100554BA2 /* executor_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = executor_test.h; sourceTree = "<group>"; }; + B79CA87A1A01FC5329031C9B /* Pods_Firestore_FuzzTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_FuzzTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BB92EB03E3F92485023F64ED /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D3CC3DC5338DCAF43A211155 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; }; DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_IntegrationTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -485,6 +549,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6EDD3B4520BF247500C33877 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6EDD3B4620BF247500C33877 /* Foundation.framework in Frameworks */, + C482E724F4B10968417C3F78 /* Pods_Firestore_FuzzTests_iOS.framework in Frameworks */, + 6EDD3B4820BF247500C33877 /* UIKit.framework in Frameworks */, + 6EDD3B4920BF247500C33877 /* XCTest.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; DE03B2D31F2149D600A30B9C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -530,6 +605,7 @@ isa = PBXGroup; children = ( 546854A820A36867004BDBD5 /* datastore_test.cc */, + 61F72C5520BC48FD001A68CB /* serializer_test.cc */, ); path = remote; sourceTree = "<group>"; @@ -554,12 +630,16 @@ 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */, 54C2294E1FECABAE007D065B /* log_test.cc */, AB380D03201BC6E400D97691 /* ordered_code_test.cc */, + 403DBF6EFB541DFD01582AA3 /* path_test.cc */, 54740A531FC913E500713A1A /* secure_random_test.cc */, 54A0352C20A3B3D7003E0143 /* status_test.cc */, 54A0352B20A3B3D7003E0143 /* status_test_util.h */, 54A0352D20A3B3D7003E0143 /* statusor_test.cc */, + 358C3B5FE573B1D60A4F7592 /* strerror_test.cc */, + 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */, 54131E9620ADE678001DF3FF /* string_format_test.cc */, AB380CFC201A2EE200D97691 /* string_util_test.cc */, + 2A0CF41BA5AED6049B0BEB2C /* type_traits_apple_test.mm */, ); path = util; sourceTree = "<group>"; @@ -624,6 +704,8 @@ 6003F581195388D10070C39A = { isa = PBXGroup; children = ( + 618BBE7A20B89AAC00B5BCE7 /* CoreTestsProtos */, + 6EDD3B5D20BF24A700C33877 /* FuzzTests */, 543B4F0520A91E4B001F506D /* App */, 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */, 6003F5B5195388D20070C39A /* Tests */, @@ -640,6 +722,7 @@ isa = PBXGroup; children = ( 6003F58A195388D20070C39A /* Firestore_Example_iOS.app */, + 6EDD3B5B20BF247500C33877 /* Firestore_FuzzTests_iOS.xctest */, DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests_iOS.xctest */, 54C9EDF12040E16300A969CD /* Firestore_SwiftTests_iOS.xctest */, 6003F5AE195388D20070C39A /* Firestore_Tests_iOS.xctest */, @@ -656,6 +739,7 @@ 5918805E993304321A05E82B /* Pods_Firestore_Example_iOS.framework */, BB92EB03E3F92485023F64ED /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */, 379B34A1536045869826D82A /* Pods_Firestore_Example_iOS_SwiftBuildTest.framework */, + B79CA87A1A01FC5329031C9B /* Pods_Firestore_FuzzTests_iOS.framework */, ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */, 2B50B3A0DF77100EEE887891 /* Pods_Firestore_Tests_iOS.framework */, 6003F591195388D20070C39A /* UIKit.framework */, @@ -725,6 +809,111 @@ name = "Podspec Metadata"; sourceTree = "<group>"; }; + 618BBE7A20B89AAC00B5BCE7 /* CoreTestsProtos */ = { + isa = PBXGroup; + children = ( + 618BBE7B20B89AAC00B5BCE7 /* firestore */, + 618BBE8320B89AAC00B5BCE7 /* google */, + ); + name = CoreTestsProtos; + path = ../Protos/cpp; + sourceTree = "<group>"; + }; + 618BBE7B20B89AAC00B5BCE7 /* firestore */ = { + isa = PBXGroup; + children = ( + 618BBE7C20B89AAC00B5BCE7 /* local */, + ); + path = firestore; + sourceTree = "<group>"; + }; + 618BBE7C20B89AAC00B5BCE7 /* local */ = { + isa = PBXGroup; + children = ( + 618BBE7E20B89AAC00B5BCE7 /* maybe_document.pb.cc */, + 618BBE8020B89AAC00B5BCE7 /* maybe_document.pb.h */, + 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */, + 618BBE8120B89AAC00B5BCE7 /* mutation.pb.h */, + 618BBE7D20B89AAC00B5BCE7 /* target.pb.cc */, + 618BBE7F20B89AAC00B5BCE7 /* target.pb.h */, + ); + path = local; + sourceTree = "<group>"; + }; + 618BBE8320B89AAC00B5BCE7 /* google */ = { + isa = PBXGroup; + children = ( + 618BBE9320B89AAC00B5BCE7 /* api */, + 618BBE8420B89AAC00B5BCE7 /* firestore */, + 618BBE9820B89AAC00B5BCE7 /* rpc */, + 618BBE9020B89AAC00B5BCE7 /* type */, + ); + path = google; + sourceTree = "<group>"; + }; + 618BBE8420B89AAC00B5BCE7 /* firestore */ = { + isa = PBXGroup; + children = ( + 618BBE8520B89AAC00B5BCE7 /* v1beta1 */, + ); + path = firestore; + sourceTree = "<group>"; + }; + 618BBE8520B89AAC00B5BCE7 /* v1beta1 */ = { + isa = PBXGroup; + children = ( + 618BBE8820B89AAC00B5BCE7 /* common.pb.cc */, + 618BBE8720B89AAC00B5BCE7 /* common.pb.h */, + 618BBE8E20B89AAC00B5BCE7 /* document.pb.cc */, + 618BBE8D20B89AAC00B5BCE7 /* document.pb.h */, + 618BBE8A20B89AAC00B5BCE7 /* firestore.pb.cc */, + 618BBE8920B89AAC00B5BCE7 /* firestore.pb.h */, + 618BBE8C20B89AAC00B5BCE7 /* query.pb.cc */, + 618BBE8620B89AAC00B5BCE7 /* query.pb.h */, + 618BBE8F20B89AAC00B5BCE7 /* write.pb.cc */, + 618BBE8B20B89AAC00B5BCE7 /* write.pb.h */, + ); + path = v1beta1; + sourceTree = "<group>"; + }; + 618BBE9020B89AAC00B5BCE7 /* type */ = { + isa = PBXGroup; + children = ( + 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */, + 618BBE9120B89AAC00B5BCE7 /* latlng.pb.h */, + ); + path = type; + sourceTree = "<group>"; + }; + 618BBE9320B89AAC00B5BCE7 /* api */ = { + isa = PBXGroup; + children = ( + 618BBE9520B89AAC00B5BCE7 /* annotations.pb.cc */, + 618BBE9620B89AAC00B5BCE7 /* annotations.pb.h */, + 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */, + 618BBE9420B89AAC00B5BCE7 /* http.pb.h */, + ); + path = api; + sourceTree = "<group>"; + }; + 618BBE9820B89AAC00B5BCE7 /* rpc */ = { + isa = PBXGroup; + children = ( + 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */, + 618BBE9A20B89AAC00B5BCE7 /* status.pb.h */, + ); + path = rpc; + sourceTree = "<group>"; + }; + 6EDD3B5D20BF24A700C33877 /* FuzzTests */ = { + isa = PBXGroup; + children = ( + 6EDD3B5E20BF24D000C33877 /* FSTFuzzTestsPrincipal.mm */, + 6EDD3B5C20BF247500C33877 /* Firestore_FuzzTests_iOS-Info.plist */, + ); + path = FuzzTests; + sourceTree = "<group>"; + }; AAEA2A72CFD1FA5AD34462F7 /* Pods */ = { isa = PBXGroup; children = ( @@ -734,6 +923,8 @@ 74ACEC3603BE58B57A7E8D4C /* Pods-Firestore_Example_iOS-SwiftBuildTest.release.xcconfig */, 3C81DE3772628FE297055662 /* Pods-Firestore_Example_iOS.debug.xcconfig */, 3F0992A4B83C60841C52E960 /* Pods-Firestore_Example_iOS.release.xcconfig */, + 84434E57CA72951015FC71BC /* Pods-Firestore_FuzzTests_iOS.debug.xcconfig */, + 97C492D2524E92927C11F425 /* Pods-Firestore_FuzzTests_iOS.release.xcconfig */, 1277F98C20D2DF0867496976 /* Pods-Firestore_IntegrationTests_iOS.debug.xcconfig */, F354C0FE92645B56A6C6FD44 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */, E592181BFD7C53C305123739 /* Pods-Firestore_Tests_iOS.debug.xcconfig */, @@ -1039,6 +1230,26 @@ productReference = 6003F5AE195388D20070C39A /* Firestore_Tests_iOS.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 6EDD3AD120BF247500C33877 /* Firestore_FuzzTests_iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6EDD3B5820BF247500C33877 /* Build configuration list for PBXNativeTarget "Firestore_FuzzTests_iOS" */; + buildPhases = ( + 6EDD3AD420BF247500C33877 /* [CP] Check Pods Manifest.lock */, + 6EDD3AD520BF247500C33877 /* Sources */, + 6EDD3B4520BF247500C33877 /* Frameworks */, + 6EDD3B4A20BF247500C33877 /* Resources */, + 6EDD3B5720BF247500C33877 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 6EDD3AD220BF247500C33877 /* PBXTargetDependency */, + ); + name = Firestore_FuzzTests_iOS; + productName = FirestoreTests; + productReference = 6EDD3B5B20BF247500C33877 /* Firestore_FuzzTests_iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests_iOS */ = { isa = PBXNativeTarget; buildConfigurationList = DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests_iOS" */; @@ -1098,6 +1309,9 @@ DevelopmentTeam = EQHXZ8M8AV; TestTargetID = 6003F589195388D20070C39A; }; + 6EDD3AD120BF247500C33877 = { + DevelopmentTeam = EQHXZ8M8AV; + }; DE03B2941F2149D600A30B9C = { DevelopmentTeam = EQHXZ8M8AV; }; @@ -1131,6 +1345,7 @@ DE03B2941F2149D600A30B9C /* Firestore_IntegrationTests_iOS */, DE29E7F51F2174B000909613 /* AllTests_iOS */, DE0761E31F2FE611003233AF /* SwiftBuildTest */, + 6EDD3AD120BF247500C33877 /* Firestore_FuzzTests_iOS */, ); }; /* End PBXProject section */ @@ -1173,6 +1388,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6EDD3B4A20BF247500C33877 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; DE03B2D81F2149D600A30B9C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1274,12 +1496,14 @@ "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", "${BUILT_PRODUCTS_DIR}/GoogleTest/GoogleTest.framework", "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", + "${BUILT_PRODUCTS_DIR}/ProtobufCpp/ProtobufCpp.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleTest.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtobufCpp.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1304,6 +1528,42 @@ 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; }; + 6EDD3AD420BF247500C33877 /* [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_FuzzTests_iOS-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; + }; + 6EDD3B5720BF247500C33877 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/LibFuzzer/LibFuzzer.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LibFuzzer.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; 83F2AB95D08093BB076EE521 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -1517,6 +1777,7 @@ 5492E0CA2021557E00B64F25 /* FSTWatchChangeTests.mm in Sources */, 5492E0AB2021552D00B64F25 /* StringViewTests.mm in Sources */, 5492E03C2021401F00B64F25 /* XCTestCase+Await.mm in Sources */, + 618BBEAF20B89AAC00B5BCE7 /* annotations.pb.cc in Sources */, 5467FB08203E6A44009C9584 /* app_testing.mm in Sources */, 54EB764D202277B30088B8F3 /* array_sorted_map_test.cc in Sources */, B6FB4684208EA0EC00554BA2 /* async_queue_libdispatch_test.mm in Sources */, @@ -1524,11 +1785,13 @@ B6FB467D208E9D3C00554BA2 /* async_queue_test.cc in Sources */, 54740A581FC914F000713A1A /* autoid_test.cc in Sources */, AB380D02201BC69F00D97691 /* bits_test.cc in Sources */, + 618BBEA920B89AAC00B5BCE7 /* common.pb.cc in Sources */, 548DB929200D59F600E00ABC /* comparison_test.cc in Sources */, ABC1D7DC2023A04B00BA84F0 /* credentials_provider_test.cc in Sources */, ABE6637A201FA81900ED349A /* database_id_test.cc in Sources */, AB38D93020236E21000A432D /* database_info_test.cc in Sources */, 546854AA20A36867004BDBD5 /* datastore_test.cc in Sources */, + 618BBEAC20B89AAC00B5BCE7 /* document.pb.cc in Sources */, B6152AD7202A53CB000E5744 /* document_key_test.cc in Sources */, AB6B908420322E4D00CC290A /* document_test.cc in Sources */, ABC1D7DD2023A04F00BA84F0 /* empty_credentials_provider_test.cc in Sources */, @@ -1540,32 +1803,54 @@ 54A0352620A3AED0003E0143 /* field_transform_test.mm in Sources */, AB356EF7200EA5EB0089B766 /* field_value_test.cc in Sources */, ABC1D7E42024AFDE00BA84F0 /* firebase_credentials_provider_test.mm in Sources */, + 618BBEAA20B89AAC00B5BCE7 /* firestore.pb.cc in Sources */, AB7BAB342012B519001E0872 /* geo_point_test.cc in Sources */, 73FE5066020EF9B2892C86BF /* hard_assert_test.cc in Sources */, 54511E8E209805F8005BD28F /* hashing_test.cc in Sources */, + 618BBEB020B89AAC00B5BCE7 /* http.pb.cc in Sources */, 54A0353520A3D8CB003E0143 /* iterator_adaptors_test.cc in Sources */, + 618BBEAE20B89AAC00B5BCE7 /* latlng.pb.cc in Sources */, 54995F6F205B6E12004EFFA0 /* leveldb_key_test.cc in Sources */, 54C2294F1FECABAE007D065B /* log_test.cc in Sources */, + 618BBEA720B89AAC00B5BCE7 /* maybe_document.pb.cc in Sources */, AB6B908620322E6D00CC290A /* maybe_document_test.cc in Sources */, + 618BBEA820B89AAC00B5BCE7 /* mutation.pb.cc in Sources */, AB6B908820322E8800CC290A /* no_document_test.cc in Sources */, AB380D04201BC6E400D97691 /* ordered_code_test.cc in Sources */, + 5A080105CCBFDB6BF3F3772D /* path_test.cc in Sources */, 549CCA5920A36E1F00BCEB75 /* precondition_test.cc in Sources */, + 618BBEAB20B89AAC00B5BCE7 /* query.pb.cc in Sources */, B686F2B22025000D0028D6BE /* resource_path_test.cc in Sources */, 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */, + 61F72C5620BC48FD001A68CB /* serializer_test.cc in Sources */, ABA495BB202B7E80008A7851 /* snapshot_version_test.cc in Sources */, 549CCA5220A36DBC00BCEB75 /* sorted_map_test.cc in Sources */, 549CCA5020A36DBC00BCEB75 /* sorted_set_test.cc in Sources */, + 618BBEB120B89AAC00B5BCE7 /* status.pb.cc in Sources */, 54A0352F20A3B3D8003E0143 /* status_test.cc in Sources */, 54A0353020A3B3D8003E0143 /* statusor_test.cc in Sources */, + 1CAA9012B25F975D445D5978 /* strerror_test.cc in Sources */, + 0535C1B65DADAE1CE47FA3CA /* string_format_apple_test.mm in Sources */, 54131E9720ADE679001DF3FF /* string_format_test.cc in Sources */, AB380CFE201A2F4500D97691 /* string_util_test.cc in Sources */, + 618BBEA620B89AAC00B5BCE7 /* target.pb.cc in Sources */, AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */, 54A0352A20A3B3BD003E0143 /* testutil.cc in Sources */, ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */, ABC1D7E12023A40C00BA84F0 /* token_test.cc in Sources */, 54A0352720A3AED0003E0143 /* transform_operations_test.mm in Sources */, 549CCA5120A36DBC00BCEB75 /* tree_sorted_map_test.cc in Sources */, + C80B10E79CDD7EF7843C321E /* type_traits_apple_test.mm in Sources */, ABC1D7DE2023A05300BA84F0 /* user_test.cc in Sources */, + 618BBEAD20B89AAC00B5BCE7 /* write.pb.cc in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6EDD3AD520BF247500C33877 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6EDD3B6020BF25AE00C33877 /* FSTFuzzTestsPrincipal.mm in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1621,6 +1906,11 @@ target = 6003F589195388D20070C39A /* Firestore_Example_iOS */; targetProxy = 6003F5B3195388D20070C39A /* PBXContainerItemProxy */; }; + 6EDD3AD220BF247500C33877 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6003F589195388D20070C39A /* Firestore_Example_iOS */; + targetProxy = 6EDD3AD320BF247500C33877 /* PBXContainerItemProxy */; + }; DE03B2951F2149D600A30B9C /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6003F589195388D20070C39A /* Firestore_Example_iOS */; @@ -1785,6 +2075,7 @@ HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 8.0; ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1818,6 +2109,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 8.0; + OTHER_CFLAGS = ""; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -1897,10 +2189,61 @@ "\"${PODS_ROOT}/GoogleTest/googlemock/include\"", "\"${PODS_ROOT}/GoogleTest/googletest/include\"", "\"${PODS_ROOT}/leveldb-library/include\"", + "\"${PODS_ROOT}/../../../Firestore/Protos/cpp\"", + "\"${PODS_ROOT}/ProtobufCpp/src\"", ); INFOPLIST_FILE = "Tests/Tests-Info.plist"; + OTHER_CFLAGS = ( + "$(inherited)", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GoogleTest/GoogleTest.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/OCMock/OCMock.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ProtobufCpp/ProtobufCpp.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/leveldb-library/leveldb.framework/Headers\"", + "$(inherited)", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/BoringSSL/openssl.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseAuth/FirebaseAuth.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseFirestore/FirebaseFirestore.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Protobuf/Protobuf.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/gRPC-Core/grpc.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/gRPC-ProtoRPC/ProtoRPC.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/gRPC-RxLibrary/RxLibrary.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/gRPC/GRPCClient.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/leveldb-library/leveldb.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb.framework/Headers\"", + "-isystem", + "\"${PODS_ROOT}/Headers/Public\"", + "-isystem", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "-isystem", + "\"${PODS_ROOT}/Headers/Public/FirebaseAnalytics\"", + "-isystem", + "\"${PODS_ROOT}/Headers/Public/FirebaseInstanceID\"", + "-DPB_FIELD_32BIT", + "-DPB_NO_PACKED_STRUCTS=1", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; + SYSTEM_HEADER_SEARCH_PATHS = "\"${PODS_ROOT}/nanopb\""; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example_iOS.app/Firestore_Example_iOS"; WRAPPER_EXTENSION = xctest; }; @@ -1931,8 +2274,131 @@ "\"${PODS_ROOT}/GoogleTest/googlemock/include\"", "\"${PODS_ROOT}/GoogleTest/googletest/include\"", "\"${PODS_ROOT}/leveldb-library/include\"", + "\"${PODS_ROOT}/../../../Firestore/Protos/cpp\"", + "\"${PODS_ROOT}/ProtobufCpp/src\"", ); INFOPLIST_FILE = "Tests/Tests-Info.plist"; + OTHER_CFLAGS = ( + "$(inherited)", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GoogleTest/GoogleTest.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/OCMock/OCMock.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ProtobufCpp/ProtobufCpp.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/leveldb-library/leveldb.framework/Headers\"", + "$(inherited)", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/BoringSSL/openssl.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseAuth/FirebaseAuth.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseCore/FirebaseCore.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/FirebaseFirestore/FirebaseFirestore.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Protobuf/Protobuf.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/gRPC-Core/grpc.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/gRPC-ProtoRPC/ProtoRPC.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/gRPC-RxLibrary/RxLibrary.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/gRPC/GRPCClient.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/leveldb-library/leveldb.framework/Headers\"", + "-iquote", + "\"${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb.framework/Headers\"", + "-isystem", + "\"${PODS_ROOT}/Headers/Public\"", + "-isystem", + "\"${PODS_ROOT}/Headers/Public/Firebase\"", + "-isystem", + "\"${PODS_ROOT}/Headers/Public/FirebaseAnalytics\"", + "-isystem", + "\"${PODS_ROOT}/Headers/Public/FirebaseInstanceID\"", + "-DPB_FIELD_32BIT", + "-DPB_NO_PACKED_STRUCTS=1", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SYSTEM_HEADER_SEARCH_PATHS = "\"${PODS_ROOT}/nanopb\""; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example_iOS.app/Firestore_Example_iOS"; + WRAPPER_EXTENSION = xctest; + }; + name = Release; + }; + 6EDD3B5920BF247500C33877 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 84434E57CA72951015FC71BC /* Pods-Firestore_FuzzTests_iOS.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}/../../..\"", + "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", + "\"${PODS_ROOT}/nanopb\"", + ); + INFOPLIST_FILE = "FuzzTests/Firestore_FuzzTests_iOS-Info.plist"; + OTHER_CFLAGS = ( + "$(inherited)", + "-fsanitize-coverage=trace-pc-guard", + ); + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example_iOS.app/Firestore_Example_iOS"; + WRAPPER_EXTENSION = xctest; + }; + name = Debug; + }; + 6EDD3B5A20BF247500C33877 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 97C492D2524E92927C11F425 /* Pods-Firestore_FuzzTests_iOS.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}/../../..\"", + "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", + "\"${PODS_ROOT}/nanopb\"", + ); + INFOPLIST_FILE = "FuzzTests/Firestore_FuzzTests_iOS-Info.plist"; + OTHER_CFLAGS = ( + "$(inherited)", + "-fsanitize-coverage=trace-pc-guard", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example_iOS.app/Firestore_Example_iOS"; @@ -2123,6 +2589,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 6EDD3B5820BF247500C33877 /* Build configuration list for PBXNativeTarget "Firestore_FuzzTests_iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6EDD3B5920BF247500C33877 /* Debug */, + 6EDD3B5A20BF247500C33877 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests_iOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_FuzzTests_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_FuzzTests_iOS.xcscheme new file mode 100644 index 0000000..039273b --- /dev/null +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_FuzzTests_iOS.xcscheme @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "0930" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + enableAddressSanitizer = "YES" + shouldUseLaunchSchemeArgsEnv = "YES"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "6EDD3AD120BF247500C33877" + BuildableName = "Firestore_FuzzTests_iOS.xctest" + BlueprintName = "Firestore_FuzzTests_iOS" + ReferencedContainer = "container:Firestore.xcodeproj"> + </BuildableReference> + </TestableReference> + </Testables> + <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"> + <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/FuzzTests/FSTFuzzTestsPrincipal.mm b/Firestore/Example/FuzzTests/FSTFuzzTestsPrincipal.mm new file mode 100644 index 0000000..038e687 --- /dev/null +++ b/Firestore/Example/FuzzTests/FSTFuzzTestsPrincipal.mm @@ -0,0 +1,74 @@ +/* + * Copyright 2018 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/NSObject.h> + +#include "LibFuzzer/FuzzerDefs.h" + +#include "Firestore/core/src/firebase/firestore/remote/serializer.h" + +using firebase::firestore::remote::Serializer; + +namespace { + +// Fuzz-test the deserialization process in Firestore. The Serializer reads raw +// bytes and converts them to a model object. +void FuzzTestDeserialization(const uint8_t *data, size_t size) { + // TODO(minafarid): fuzz-test Serializer. +} + +// Contains the code to be fuzzed. Called by the fuzzing library with +// different argument values for `data` and `size`. +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + FuzzTestDeserialization(data, size); + return 0; +} + +// Simulates calling the main() function of libFuzzer (FuzzerMain.cpp). +int RunFuzzTestingMain() { + // Arguments to libFuzzer main() function should be added to this array, + // e.g., dictionaries, corpus, number of runs, jobs, etc. + char *program_args[] = { + const_cast<char *>("RunFuzzTestingMain") // First arg is program name. + }; + char **argv = program_args; + int argc = sizeof(program_args) / sizeof(program_args[0]); + + // Start fuzzing using libFuzzer's driver. + return fuzzer::FuzzerDriver(&argc, &argv, LLVMFuzzerTestOneInput); +} + +} // namespace + +/** + * This class is registered as the NSPrincipalClass in the + * Firestore_FuzzTests_iOS bundle's Info.plist. XCTest instantiates this class + * to perform one-time setup for the test bundle, as documented here: + * + * https://developer.apple.com/documentation/xctest/xctestobservationcenter + */ +@interface FSTFuzzTestsPrincipal : NSObject +@end + +@implementation FSTFuzzTestsPrincipal + +- (instancetype)init { + self = [super init]; + RunFuzzTestingMain(); + return self; +} + +@end diff --git a/Firestore/Example/FuzzTests/Firestore_FuzzTests_iOS-Info.plist b/Firestore/Example/FuzzTests/Firestore_FuzzTests_iOS-Info.plist new file mode 100644 index 0000000..0d53e5f --- /dev/null +++ b/Firestore/Example/FuzzTests/Firestore_FuzzTests_iOS-Info.plist @@ -0,0 +1,24 @@ +<?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>NSPrincipalClass</key> + <string>FSTFuzzTestsPrincipal</string> + <key>CFBundleVersion</key> + <string>1</string> +</dict> +</plist> diff --git a/Firestore/Example/Podfile b/Firestore/Example/Podfile index 88cf93a..110c676 100644 --- a/Firestore/Example/Podfile +++ b/Firestore/Example/Podfile @@ -10,7 +10,7 @@ target 'Firestore_Example_iOS' do # The next line is the forcing function for the Firebase pod. The Firebase # version's subspecs should depend on the component versions in their # corresponding podspec's. - pod 'Firebase/Core', '5.1.0' + pod 'Firebase/Core', '5.2.0' pod 'FirebaseAuth', :path => '../../' pod 'FirebaseCore', :path => '../../' @@ -22,6 +22,7 @@ target 'Firestore_Example_iOS' do pod 'leveldb-library' pod 'OCMock' pod 'GoogleTest', :podspec => 'GoogleTest.podspec' + pod 'ProtobufCpp', :podspec => 'ProtobufCpp.podspec' end target 'Firestore_IntegrationTests_iOS' do @@ -37,4 +38,10 @@ target 'Firestore_Example_iOS' do target 'SwiftBuildTest' do platform :ios, '8.0' end + + target 'Firestore_FuzzTests_iOS' do + inherit! :search_paths + + pod 'LibFuzzer', :podspec => 'LibFuzzer.podspec' + end end diff --git a/Firestore/Example/ProtobufCpp.podspec b/Firestore/Example/ProtobufCpp.podspec new file mode 100644 index 0000000..c809c06 --- /dev/null +++ b/Firestore/Example/ProtobufCpp.podspec @@ -0,0 +1,74 @@ +# Copyright 2018 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. + +# A Private podspec for Protobuf which exposes the C++ headers (rather than +# only the Obj-C headers). Suitable only for use inside this source tree. + +Pod::Spec.new do |s| + s.name = 'ProtobufCpp' + s.version = '3.5.2' + s.summary = 'Protocol Buffers v.3 runtime library for C++.' + s.homepage = 'https://github.com/google/protobuf' + s.license = '3-Clause BSD License' + s.authors = { 'The Protocol Buffers contributors' => 'protobuf@googlegroups.com' } + s.cocoapods_version = '>= 1.0' + + s.source = { + :git => 'https://github.com/google/protobuf.git', + :tag => "v#{s.version}" + } + + s.source_files = 'src/**/*.{h,cc}' + s.exclude_files = # skip test files. (Yes, the test files are intermixed with + # the source. No there doesn't seem to be a common/simple + # pattern we could use to exclude them; 'test' appears in + # various places throughout the file names and also in a + # non-test file. So, we'll exclude all files that either + # start with 'test' or include test and have a previous + # character that isn't "y" (so that bytestream isn't + # matched.)) + 'src/**/test*.*', + 'src/**/*[^y]test*.*', + 'src/**/testing/**', + 'src/**/mock*', + # skip the javascript handling code. + 'src/**/js/**', + # skip the protoc compiler + 'src/google/protobuf/compiler/**/*' + + s.header_mappings_dir = 'src/' + + # Set a CPP symbol so the code knows to use framework imports. + s.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => + '$(inherited) ' + + 'GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1 ' + + 'HAVE_PTHREAD=1', + 'HEADER_SEARCH_PATHS' => '"${PODS_ROOT}/ProtobufCpp/src"', + + # Cocoapods flattens header imports, leading to much anguish. The + # following two statements work around this. + # - https://github.com/CocoaPods/CocoaPods/issues/1437 + 'USE_HEADERMAP' => 'NO', + 'ALWAYS_SEARCH_USER_PATHS' => 'NO', + } + + # Disable warnings that upstream does not concern itself with + s.compiler_flags = '$(inherited) ' + + '-Wno-comma ' + + '-Wno-shorten-64-to-32' + + s.requires_arc = false + s.library = 'c++' +end diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm index 5629075..ddd831a 100644 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm @@ -118,11 +118,7 @@ NS_ASSUME_NONNULL_BEGIN FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); - - FSTTargetChange *ackTarget = - [FSTTargetChange changeWithDocuments:@[] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; - FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); + FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], FSTTestTargetChangeMarkCurrent()); [listener queryDidChangeViewSnapshot:snap1]; XCTAssertEqualObjects(accum, @[]); @@ -188,9 +184,7 @@ NS_ASSUME_NONNULL_BEGIN FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); - FSTTargetChange *ackTarget = - [FSTTargetChange changeWithDocuments:@[ doc1 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; + FSTTargetChange *ackTarget = FSTTestTargetChangeAckDocuments({doc1.key}); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[], ackTarget); FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc2 ], nil); @@ -342,9 +336,7 @@ NS_ASSUME_NONNULL_BEGIN FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); FSTViewSnapshot *snap3 = - FSTTestApplyChanges(view, @[], - [FSTTargetChange changeWithDocuments:@[ doc1, doc2 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + FSTTestApplyChanges(view, @[], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key})); [listener applyChangedOnlineState:FSTOnlineStateOnline]; // no event [listener queryDidChangeViewSnapshot:snap1]; diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm index ec62d82..8c8d8a0 100644 --- a/Firestore/Example/Tests/Core/FSTViewTests.mm +++ b/Firestore/Example/Tests/Core/FSTViewTests.mm @@ -56,10 +56,8 @@ NS_ASSUME_NONNULL_BEGIN FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); FSTDocument *doc3 = FSTTestDoc("rooms/other/messages/1", 0, @{@"text" : @"msg3"}, NO); - FSTViewSnapshot *_Nullable snapshot = - FSTTestApplyChanges(view, @[ doc1, doc2, doc3 ], - [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + FSTViewSnapshot *_Nullable snapshot = FSTTestApplyChanges( + view, @[ doc1, doc2, doc3 ], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key, doc3.key})); XCTAssertEqual(snapshot.query, query); @@ -90,8 +88,7 @@ NS_ASSUME_NONNULL_BEGIN // delete doc2, add doc3 FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[ FSTTestDeletedDoc("rooms/eros/messages/2", 0), doc3 ], - [FSTTargetChange changeWithDocuments:@[ doc1, doc3 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + FSTTestTargetChangeAckDocuments({doc1.key, doc3.key})); XCTAssertEqual(snapshot.query, query); @@ -216,10 +213,8 @@ NS_ASSUME_NONNULL_BEGIN FSTTestApplyChanges(view, @[ doc1, doc3 ], nil); // add doc2, which should push out doc3 - FSTViewSnapshot *snapshot = - FSTTestApplyChanges(view, @[ doc2 ], - [FSTTargetChange changeWithDocuments:@[ doc1, doc2, doc3 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]); + FSTViewSnapshot *snapshot = FSTTestApplyChanges( + view, @[ doc2 ], FSTTestTargetChangeAckDocuments({doc1.key, doc2.key, doc3.key})); XCTAssertEqual(snapshot.query, query); @@ -263,9 +258,8 @@ NS_ASSUME_NONNULL_BEGIN previousChanges:viewDocChanges]; FSTViewSnapshot *snapshot = [view applyChangesToDocuments:viewDocChanges - targetChange:[FSTTargetChange - changeWithDocuments:@[ doc1, doc2, doc3, doc4 ] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]] + targetChange:FSTTestTargetChangeAckDocuments( + {doc1.key, doc2.key, doc3.key, doc4.key})] .snapshot; XCTAssertEqual(snapshot.query, query); @@ -294,27 +288,21 @@ NS_ASSUME_NONNULL_BEGIN applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1 ])]]; XCTAssertEqualObjects(change.limboChanges, @[]); - change = - [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] - targetChange:[FSTTargetChange - changeWithDocuments:@[] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]]; + change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:FSTTestTargetChangeMarkCurrent()]; XCTAssertEqualObjects( change.limboChanges, @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:doc1.key] ]); - change = [view - applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] - targetChange:[FSTTargetChange changeWithDocuments:@[ doc1 ] - currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[])] + targetChange:FSTTestTargetChangeAckDocuments({doc1.key})]; XCTAssertEqualObjects( change.limboChanges, @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:doc1.key] ]); - change = [view - applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])] - targetChange:[FSTTargetChange changeWithDocuments:@[ doc2 ] - currentStatusUpdate:FSTCurrentStatusUpdateNone]]; + change = + [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2 ])] + targetChange:FSTTestTargetChangeAckDocuments({doc2.key})]; XCTAssertEqualObjects(change.limboChanges, @[]); change = [view @@ -343,11 +331,9 @@ NS_ASSUME_NONNULL_BEGIN FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{doc1.key, doc2.key}]; - FSTTargetChange *markCurrent = - [FSTTargetChange changeWithDocuments:@[] - currentStatusUpdate:FSTCurrentStatusUpdateMarkCurrent]; FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[])]; - FSTViewChange *change = [view applyChangesToDocuments:changes targetChange:markCurrent]; + FSTViewChange *change = + [view applyChangesToDocuments:changes targetChange:FSTTestTargetChangeMarkCurrent()]; XCTAssertEqualObjects(change.limboChanges, @[]); } diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm index f8091c0..e048a40 100644 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm @@ -162,8 +162,10 @@ NSDictionary<NSString *, id> *initialData = @{ @"updated" : @NO, }; - NSDictionary<NSString *, id> *mergeData = - @{@"time" : [FIRFieldValue fieldValueForServerTimestamp]}; + NSDictionary<NSString *, id> *mergeData = @{ + @"time" : [FIRFieldValue fieldValueForServerTimestamp], + @"nested" : @{@"time" : [FIRFieldValue fieldValueForServerTimestamp]} + }; [self writeDocumentRef:doc data:initialData]; @@ -182,6 +184,7 @@ FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; XCTAssertEqual(document[@"updated"], @NO); XCTAssertTrue([document[@"time"] isKindOfClass:[FIRTimestamp class]]); + XCTAssertTrue([document[@"nested.time"] isKindOfClass:[FIRTimestamp class]]); } - (void)testCanDeleteFieldUsingMerge { @@ -218,6 +221,81 @@ XCTAssertNil(document[@"nested.foo"]); } +- (void)testCanDeleteFieldUsingMergeFields { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ + @"untouched" : @YES, + @"foo" : @"bar", + @"inner" : @{@"removed" : @YES, @"foo" : @"bar"}, + @"nested" : @{@"untouched" : @YES, @"foo" : @"bar"} + }; + NSDictionary<NSString *, id> *mergeData = @{ + @"foo" : [FIRFieldValue fieldValueForDelete], + @"inner" : @{@"foo" : [FIRFieldValue fieldValueForDelete]}, + @"nested" : @{ + @"untouched" : [FIRFieldValue fieldValueForDelete], + @"foo" : [FIRFieldValue fieldValueForDelete] + } + }; + NSDictionary<NSString *, id> *finalData = + @{ @"untouched" : @YES, + @"inner" : @{}, + @"nested" : @{@"untouched" : @YES} }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:mergeData + mergeFields:@[ @"foo", @"inner", @"nested.foo" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects([document data], finalData); +} + +- (void)testCanSetServerTimestampsUsingMergeFields { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = @{ + @"untouched" : @YES, + @"foo" : @"bar", + @"nested" : @{@"untouched" : @YES, @"foo" : @"bar"} + }; + NSDictionary<NSString *, id> *mergeData = @{ + @"foo" : [FIRFieldValue fieldValueForServerTimestamp], + @"inner" : @{@"foo" : [FIRFieldValue fieldValueForServerTimestamp]}, + @"nested" : @{@"foo" : [FIRFieldValue fieldValueForServerTimestamp]} + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanMergeDataWithAnExistingDocumentUsingSet"]; + + [doc setData:mergeData + mergeFields:@[ @"foo", @"inner", @"nested.foo" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertTrue([document exists]); + XCTAssertTrue([document[@"foo"] isKindOfClass:[FIRTimestamp class]]); + XCTAssertTrue([document[@"inner.foo"] isKindOfClass:[FIRTimestamp class]]); + XCTAssertTrue([document[@"nested.foo"] isKindOfClass:[FIRTimestamp class]]); +} + - (void)testMergeReplacesArrays { FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index e6e1a19..cba9017 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -48,6 +48,7 @@ namespace util = firebase::firestore::util; using firebase::firestore::auth::EmptyCredentialsProvider; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::Precondition; using firebase::firestore::model::TargetId; @@ -121,6 +122,10 @@ NS_ASSUME_NONNULL_BEGIN HARD_FAIL("Not implemented"); } +- (DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetId { + return DocumentKeySet{}; +} + - (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { [self.listenEvents addObject:remoteEvent]; XCTestExpectation *expectation = [self.listenEventExpectations objectAtIndex:0]; diff --git a/Firestore/Example/Tests/Integration/FSTStreamTests.mm b/Firestore/Example/Tests/Integration/FSTStreamTests.mm index b944a93..3ad6868 100644 --- a/Firestore/Example/Tests/Integration/FSTStreamTests.mm +++ b/Firestore/Example/Tests/Integration/FSTStreamTests.mm @@ -18,8 +18,11 @@ #import <GRPCClient/GRPCCall.h> +#import <FirebaseFirestore/FIRFirestoreErrors.h> #import <FirebaseFirestore/FIRFirestoreSettings.h> +#include <utility> + #import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" #import "Firestore/Source/Remote/FSTDatastore.h" @@ -327,4 +330,74 @@ using firebase::firestore::model::SnapshotVersion; ]]; } +class MockCredentialsProvider : public firebase::firestore::auth::EmptyCredentialsProvider { + public: + MockCredentialsProvider() { + observed_states_ = [NSMutableArray new]; + } + + void GetToken(firebase::firestore::auth::TokenListener completion) override { + [observed_states_ addObject:@"GetToken"]; + EmptyCredentialsProvider::GetToken(std::move(completion)); + } + + void InvalidateToken() override { + [observed_states_ addObject:@"InvalidateToken"]; + EmptyCredentialsProvider::InvalidateToken(); + } + + NSMutableArray<NSString *> *observed_states() const { + return observed_states_; + } + + private: + NSMutableArray<NSString *> *observed_states_; +}; + +- (void)testStreamRefreshesTokenUponExpiration { + MockCredentialsProvider credentials; + FSTDatastore *datastore = [[FSTDatastore alloc] initWithDatabaseInfo:&_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:&credentials]; + FSTWatchStream *watchStream = [datastore createWatchStream]; + + [_delegate awaitNotificationFromBlock:^{ + [watchStream startWithDelegate:_delegate]; + }]; + + // Simulate callback from GRPC with an unauthenticated error -- this should invalidate the token. + NSError *unauthenticatedError = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnauthenticated + userInfo:nil]; + dispatch_async(_testQueue, ^{ + [watchStream.callbackFilter writesFinishedWithError:unauthenticatedError]; + }); + // Drain the queue. + dispatch_sync(_testQueue, ^{ + }); + + // Try reconnecting. + [_delegate awaitNotificationFromBlock:^{ + [watchStream startWithDelegate:_delegate]; + }]; + // Simulate a different error -- token should not be invalidated this time. + NSError *unavailableError = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:nil]; + dispatch_async(_testQueue, ^{ + [watchStream.callbackFilter writesFinishedWithError:unavailableError]; + }); + dispatch_sync(_testQueue, ^{ + }); + + [_delegate awaitNotificationFromBlock:^{ + [watchStream startWithDelegate:_delegate]; + }]; + dispatch_sync(_testQueue, ^{ + }); + + NSArray<NSString *> *expected = @[ @"GetToken", @"InvalidateToken", @"GetToken", @"GetToken" ]; + XCTAssertEqualObjects(credentials.observed_states(), expected); +} + @end diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm index 3da8083..e6ab720 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm @@ -42,7 +42,7 @@ using leveldb::Status; @end @implementation FSTLevelDBMigrationsTests { - std::shared_ptr<DB> _db; + std::unique_ptr<DB> _db; } - (void)setUp { @@ -62,12 +62,12 @@ using leveldb::Status; } - (void)testAddsTargetGlobal { - FSTPBTargetGlobal *metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db]; + FSTPBTargetGlobal *metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()]; XCTAssertNil(metadata, @"Not expecting metadata yet, we should have an empty db"); LevelDbTransaction transaction(_db.get(), "testAddsTargetGlobal"); [FSTLevelDBMigrations runMigrationsWithTransaction:&transaction]; transaction.Commit(); - metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db]; + metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()]; XCTAssertNotNil(metadata, @"Migrations should have added the metadata"); } @@ -107,7 +107,7 @@ using leveldb::Status; LevelDbTransaction transaction(_db.get(), "testCountsQueries"); [FSTLevelDBMigrations runMigrationsWithTransaction:&transaction]; transaction.Commit(); - FSTPBTargetGlobal *metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db]; + FSTPBTargetGlobal *metadata = [FSTLevelDBQueryCache readTargetMetadataFromDB:_db.get()]; XCTAssertEqual(expected, metadata.targetCount, @"Failed to count all of the targets we added"); } } diff --git a/Firestore/Example/Tests/Local/FSTLevelDBTransactionTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBTransactionTests.mm index 29f5d6c..5c2718b 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBTransactionTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBTransactionTests.mm @@ -46,7 +46,7 @@ using firebase::firestore::local::LevelDbTransaction; @end @implementation FSTLevelDBTransactionTests { - std::shared_ptr<DB> _db; + std::unique_ptr<DB> _db; } - (void)setUp { diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index e10fb12..8760571 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -609,8 +609,8 @@ NS_ASSUME_NONNULL_BEGIN FSTQuery *query = FSTTestQuery("foo"); FSTTargetID targetID = [self allocateQuery:query]; - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO), - @[ @(targetID) ], @[])]; + [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO), + @[ @(targetID) ])]; [self collectGarbage]; FSTAssertContains(FSTTestDoc("foo/bar", 2, @{@"foo" : @"bar"}, NO)); @@ -703,8 +703,8 @@ NS_ASSUME_NONNULL_BEGIN FSTQuery *query = FSTTestQuery("foo"); FSTTargetID targetID = [self allocateQuery:query]; - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO), - @[ @(targetID) ], @[])]; + [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO), + @[ @(targetID) ])]; [self writeMutation:FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"})]; [self collectGarbage]; FSTAssertContains(FSTTestDoc("foo/bar", 1, @{@"foo" : @"bar"}, NO)); @@ -805,21 +805,19 @@ NS_ASSUME_NONNULL_BEGIN FSTBoxedTargetID *targetID = @(queryData.targetID); NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); - FSTWatchChange *watchChange = + FSTWatchTargetChange *watchChange = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ targetID ] resumeToken:resumeToken]; NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *listens = [NSMutableDictionary dictionary]; listens[targetID] = queryData; - NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingResponses = - [NSMutableDictionary dictionary]; - FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(1000) - listenTargets:listens - pendingTargetResponses:pendingResponses]; - [aggregator addWatchChanges:@[ watchChange ]]; - FSTRemoteEvent *remoteEvent = [aggregator remoteEvent]; + FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc] + initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider + providerWithSingleResultForKey:testutil::Key("foo/bar") + targets:@[ targetID ]]]; + [aggregator handleTargetChange:watchChange]; + FSTRemoteEvent *remoteEvent = [aggregator remoteEventAtSnapshotVersion:testutil::Version(1000)]; [self applyRemoteEvent:remoteEvent]; // Stop listening so that the query should become inactive (but persistent) @@ -842,10 +840,10 @@ NS_ASSUME_NONNULL_BEGIN [self allocateQuery:query]; FSTAssertTargetID(2); - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/baz", 10, @{@"a" : @"b"}, NO), - @[ @2 ], @[])]; - [self applyRemoteEvent:FSTTestUpdateRemoteEvent(FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, NO), - @[ @2 ], @[])]; + [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/baz", 10, @{@"a" : @"b"}, NO), + @[ @2 ])]; + [self applyRemoteEvent:FSTTestAddedRemoteEvent(FSTTestDoc("foo/bar", 20, @{@"a" : @"b"}, NO), + @[ @2 ])]; [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; diff --git a/Firestore/Example/Tests/Remote/FSTDatastoreTests.mm b/Firestore/Example/Tests/Remote/FSTDatastoreTests.mm index 6d6e912..9783e37 100644 --- a/Firestore/Example/Tests/Remote/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Remote/FSTDatastoreTests.mm @@ -53,6 +53,12 @@ code:FIRFirestoreErrorCodeUnavailable userInfo:nil]; XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); + + // "unauthenticated" is considered a recoverable error due to expired token. + error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnauthenticated + userInfo:nil]; + XCTAssertFalse([FSTDatastore isPermanentWriteError:error]); } @end diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm index 84d0fa1..c6936f7 100644 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -34,6 +34,7 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -42,86 +43,204 @@ NS_ASSUME_NONNULL_BEGIN @implementation FSTRemoteEventTests { NSData *_resumeToken1; - NSMutableDictionary<NSNumber *, NSNumber *> *_noPendingResponses; + NSMutableDictionary<NSNumber *, NSNumber *> *_noOutstandingResponses; + FSTTestTargetMetadataProvider *_targetMetadataProvider; } - (void)setUp { _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; - _noPendingResponses = [NSMutableDictionary dictionary]; + _noOutstandingResponses = [NSMutableDictionary dictionary]; + _targetMetadataProvider = [FSTTestTargetMetadataProvider new]; } -- (FSTWatchChangeAggregator *)aggregatorWithTargets:(NSArray<NSNumber *> *)targets - outstanding: - (NSDictionary<NSNumber *, NSNumber *> *)outstanding - changes:(NSArray<FSTWatchChange *> *)watchChanges { - NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary]; - FSTQueryData *dummyQueryData = [FSTQueryData alloc]; - for (NSNumber *targetID in targets) { - listens[targetID] = dummyQueryData; +/** + * Creates a map with query data for the provided target IDs. All targets are considered active + * and query a collection named "coll". + */ +- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)queryDataForTargets: + (NSArray<FSTBoxedTargetID *> *)targetIDs { + NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *targets = + [NSMutableDictionary dictionary]; + for (FSTBoxedTargetID *targetID in targetIDs) { + FSTQuery *query = FSTTestQuery("coll"); + targets[targetID] = [[FSTQueryData alloc] initWithQuery:query + targetID:targetID.intValue + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen]; + } + return targets; +} + +/** + * Creates a map with query data for the provided target IDs. All targets are marked as limbo + * queries for the document at "coll/limbo". + */ +- (NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)queryDataForLimboTargets: + (NSArray<FSTBoxedTargetID *> *)targetIDs { + NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *targets = + [NSMutableDictionary dictionary]; + for (FSTBoxedTargetID *targetID in targetIDs) { + FSTQuery *query = FSTTestQuery("coll/limbo"); + targets[targetID] = [[FSTQueryData alloc] initWithQuery:query + targetID:targetID.intValue + listenSequenceNumber:0 + purpose:FSTQueryPurposeLimboResolution]; } + return targets; +} + +/** + * Creates an aggregator initialized with the set of provided FSTWatchChanges. Tests can add further + * changes via `handleDocumentChange`, `handleTargetChange` and `handleExistenceFilterChange`. + * + * @param snapshotVersion The version at which to create the remote event. This corresponds to the + * snapshot version provided by a NO_CHANGE event. + * @param targetMap A map of query data for all active targets. The map must include an entry for + * every target referenced by any of the watch changes. + * @param outstandingResponses The number of outstanding ACKs a target has to receive before it is + * considered active, or `_noOutstandingResponses` if all targets are already active. + * @param existingKeys The set of documents that are considered synced with the test targets as + * part of a previous listen. To modify this set during test execution, invoke + * `[_targetMetadataProvider setSyncedKeys:forQueryData:]`. + * @param watchChanges The watch changes to apply before returning the aggregator. Supported + * changes are FSTDocumentWatchChange and FSTWatchTargetChange. + */ +- (FSTWatchChangeAggregator *) +aggregatorWithTargetMap:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)targetMap + outstandingResponses: + (nullable NSDictionary<FSTBoxedTargetID *, NSNumber *> *)outstandingResponses + existingKeys:(DocumentKeySet)existingKeys + changes:(NSArray<FSTWatchChange *> *)watchChanges { FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3) - listenTargets:listens - pendingTargetResponses:outstanding]; - [aggregator addWatchChanges:watchChanges]; + [[FSTWatchChangeAggregator alloc] initWithTargetMetadataProvider:_targetMetadataProvider]; + + NSMutableArray<FSTBoxedTargetID *> *targetIDs = [NSMutableArray array]; + [targetMap enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID, + FSTQueryData *queryData, BOOL *stop) { + [targetIDs addObject:targetID]; + [_targetMetadataProvider setSyncedKeys:existingKeys forQueryData:queryData]; + }]; + + [outstandingResponses + enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *targetID, NSNumber *count, BOOL *stop) { + for (int i = 0; i < count.intValue; ++i) { + [aggregator recordTargetRequest:targetID]; + } + }]; + + for (FSTWatchChange *change in watchChanges) { + if ([change isKindOfClass:[FSTDocumentWatchChange class]]) { + [aggregator handleDocumentChange:(FSTDocumentWatchChange *)change]; + } else if ([change isKindOfClass:[FSTWatchTargetChange class]]) { + [aggregator handleTargetChange:(FSTWatchTargetChange *)change]; + } else { + HARD_ASSERT("Encountered unexpected type of FSTWatchChange"); + } + } + + [aggregator handleTargetChange:[[FSTWatchTargetChange alloc] + initWithState:FSTWatchTargetChangeStateNoChange + targetIDs:targetIDs + resumeToken:_resumeToken1 + cause:nil]]; + return aggregator; } +/** + * Creates a single remote event that includes target changes for all provided FSTWatchChanges. + * + * @param snapshotVersion The version at which to create the remote event. This corresponds to the + * snapshot version provided by the NO_CHANGE event. + * @param targetMap A map of query data for all active targets. The map must include an entry for + * every target referenced by any of the watch changes. + * @param outstandingResponses The number of outstanding ACKs a target has to receive before it is + * considered active, or `_noOutstandingResponses` if all targets are already active. + * @param existingKeys The set of documents that are considered synced with the test targets as + * part of a previous listen. + * @param watchChanges The watch changes to apply before creating the remote event. Supported + * changes are FSTDocumentWatchChange and FSTWatchTargetChange. + */ +- (FSTRemoteEvent *) +remoteEventAtSnapshotVersion:(FSTTestSnapshotVersion)snapshotVersion + targetMap:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)targetMap + outstandingResponses: + (nullable NSDictionary<FSTBoxedTargetID *, NSNumber *> *)outstandingResponses + existingKeys:(DocumentKeySet)existingKeys + changes:(NSArray<FSTWatchChange *> *)watchChanges { + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:outstandingResponses + existingKeys:existingKeys + changes:watchChanges]; + return [aggregator remoteEventAtSnapshotVersion:testutil::Version(snapshotVersion)]; +} + - (void)testWillAccumulateDocumentAddedAndRemovedEvents { - FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); + // The target map that contains an entry for every target in this test. If a target ID is omitted, + // the target is considered inactive and FSTTestTargetMetadataProvider will fail on access. + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForTargets:@[ @1, @2, @3, @4, @5, @6 ]]; + FSTDocument *existingDoc = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2, @3 ] removedTargetIDs:@[ @4, @5, @6 ] - documentKey:doc1.key - document:doc1]; + documentKey:existingDoc.key + document:existingDoc]; + FSTDocument *newDoc = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @4 ] removedTargetIDs:@[ @2, @6 ] - documentKey:doc2.key - document:doc2]; - - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2, @3, @4, @5, @6 ] - outstanding:_noPendingResponses - changes:@[ change1, change2 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; + documentKey:newDoc.key + document:newDoc]; + + // Create a remote event that includes both `change1` and `change2` as well as a NO_CHANGE event + // with the default resume token (`_resumeToken1`). + // As `existingDoc` is provided as an existing key, any updates to this document will be treated + // as modifications rather than adds. + FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet{existingDoc.key} + changes:@[ change1, change2 ]]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); - XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + XCTAssertEqualObjects(event.documentUpdates.at(existingDoc.key), existingDoc); + XCTAssertEqualObjects(event.documentUpdates.at(newDoc.key), newDoc); - XCTAssertEqual(event.targetChanges.count, 6); + // 'change1' and 'change2' affect six different targets + XCTAssertEqual(event.targetChanges.size(), 6); - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + FSTTargetChange *targetChange1 = + FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key}, + DocumentKeySet{}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); - FSTUpdateMapping *mapping2 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; - XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); + FSTTargetChange *targetChange2 = FSTTestTargetChange( + DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); - FSTUpdateMapping *mapping3 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); + FSTTargetChange *targetChange3 = FSTTestTargetChange( + DocumentKeySet{}, DocumentKeySet{existingDoc.key}, DocumentKeySet{}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3); - FSTUpdateMapping *mapping4 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[ doc1 ]]; - XCTAssertEqualObjects(event.targetChanges[@4].mapping, mapping4); + FSTTargetChange *targetChange4 = + FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{}, + DocumentKeySet{existingDoc.key}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(4), targetChange4); - FSTUpdateMapping *mapping5 = - [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1 ]]; - XCTAssertEqualObjects(event.targetChanges[@5].mapping, mapping5); + FSTTargetChange *targetChange5 = FSTTestTargetChange( + DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(5), targetChange5); - FSTUpdateMapping *mapping6 = - [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1, doc2 ]]; - XCTAssertEqualObjects(event.targetChanges[@6].mapping, mapping6); + FSTTargetChange *targetChange6 = FSTTestTargetChange( + DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{existingDoc.key}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(6), targetChange6); } - (void)testWillIgnoreEventsForPendingTargets { - FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]]; + FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc1.key @@ -135,31 +254,34 @@ NS_ASSUME_NONNULL_BEGIN targetIDs:@[ @1 ] cause:nil]; + FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc2.key document:doc2]; // We're waiting for the unwatch and watch ack - NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2 }; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] - outstanding:pendingResponses - changes:@[ change1, change2, change3, change4 ]]; - FSTRemoteEvent *event = [aggregator remoteEvent]; + NSDictionary<NSNumber *, NSNumber *> *outstandingResponses = @{ @1 : @2 }; + + FSTRemoteEvent *event = + [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:outstandingResponses + existingKeys:DocumentKeySet {} + changes:@[ change1, change2, change3, change4 ]]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); // doc1 is ignored because it was part of an inactive target, but doc2 is in the changes // because it become active. XCTAssertEqual(event.documentUpdates.size(), 1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); - XCTAssertEqual(event.targetChanges.count, 1); + XCTAssertEqual(event.targetChanges.size(), 1); } - (void)testWillIgnoreEventsForRemovedTargets { - FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[]]; + FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc1.key @@ -170,25 +292,25 @@ NS_ASSUME_NONNULL_BEGIN cause:nil]; // We're waiting for the unwatch ack - NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @1 }; + NSDictionary<NSNumber *, NSNumber *> *outstandingResponses = @{ @1 : @1 }; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; + FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:outstandingResponses + existingKeys:DocumentKeySet {} + changes:@[ change1, change2 ]]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); // doc1 is ignored because it was part of an inactive target XCTAssertEqual(event.documentUpdates.size(), 0); // Target 1 is ignored because it was removed - XCTAssertEqual(event.targetChanges.count, 0); + XCTAssertEqual(event.targetChanges.size(), 0); } - (void)testWillKeepResetMappingEvenWithUpdates { - FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO); + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]]; + FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc1.key @@ -199,10 +321,13 @@ NS_ASSUME_NONNULL_BEGIN cause:nil]; // Add doc2, doc3 + FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); FSTWatchChange *change3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc2.key document:doc2]; + + FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO); FSTWatchChange *change4 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc3.key @@ -213,203 +338,208 @@ NS_ASSUME_NONNULL_BEGIN removedTargetIDs:@[ @1 ] documentKey:doc2.key document:doc2]; - - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] - outstanding:_noPendingResponses - changes:@[ change1, change2, change3, change4, change5 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; + FSTRemoteEvent *event = + [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet{doc1.key} + changes:@[ change1, change2, change3, change4, change5 ]]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 3); XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3); - XCTAssertEqual(event.targetChanges.count, 1); + XCTAssertEqual(event.targetChanges.size(), 1); // Only doc3 is part of the new mapping - FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[ doc3 ]]; - - XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); + FSTTargetChange *expectedChange = FSTTestTargetChange( + DocumentKeySet{doc3.key}, DocumentKeySet{}, DocumentKeySet{doc1.key}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange); } - (void)testWillHandleSingleReset { + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]]; + // Reset target - FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @1 ] - cause:nil]; + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset + targetIDs:@[ @1 ] + cause:nil]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[]]; + [aggregator handleTargetChange:change]; + + FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; - FSTRemoteEvent *event = [aggregator remoteEvent]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); - - XCTAssertEqual(event.targetChanges.count, 1); + XCTAssertEqual(event.targetChanges.size(), 1); // Reset mapping is empty - FSTResetMapping *expectedMapping = [FSTResetMapping mappingWithDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, expectedMapping); + FSTTargetChange *expectedChange = + FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO); + XCTAssertEqualObjects(event.targetChanges.at(1), expectedChange); } - (void)testWillHandleTargetAddAndRemovalInSameBatch { - FSTDocument *doc1a = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc1b = FSTTestDoc("docs/1", 1, @{ @"value" : @2 }, NO); + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForTargets:@[ @1, @2 ]]; + FSTDocument *doc1a = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[ @2 ] documentKey:doc1a.key document:doc1a]; + FSTDocument *doc1b = FSTTestDoc("docs/1", 1, @{ @"value" : @2 }, NO); FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] removedTargetIDs:@[ @1 ] documentKey:doc1b.key document:doc1b]; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] - outstanding:_noPendingResponses - changes:@[ change1, change2 ]]; - FSTRemoteEvent *event = [aggregator remoteEvent]; + FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet{doc1a.key} + changes:@[ change1, change2 ]]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 1); XCTAssertEqualObjects(event.documentUpdates.at(doc1b.key), doc1b); - XCTAssertEqual(event.targetChanges.count, 2); + XCTAssertEqual(event.targetChanges.size(), 2); - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[ doc1b ]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + FSTTargetChange *targetChange1 = FSTTestTargetChange( + DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1b.key}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); - FSTUpdateMapping *mapping2 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1b ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping2); + FSTTargetChange *targetChange2 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc1b.key}, + DocumentKeySet{}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); } - (void)testTargetCurrentChangeWillMarkTheTargetCurrent { + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]]; + FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1 ] resumeToken:_resumeToken1]; + FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[ change ]]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.targetChanges.count, 1); - FSTTargetChange *targetChange = event.targetChanges[@1]; - XCTAssertEqualObjects(targetChange.mapping, [[FSTUpdateMapping alloc] init]); - XCTAssertEqual(targetChange.currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(targetChange.resumeToken, _resumeToken1); + XCTAssertEqual(event.targetChanges.size(), 1); + + FSTTargetChange *targetChange = + FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); } - (void)testTargetAddedChangeWillResetPreviousState { - FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForTargets:@[ @1, @3 ]]; + FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @3 ] removedTargetIDs:@[ @2 ] documentKey:doc1.key document:doc1]; + FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1, @2, @3 ] resumeToken:_resumeToken1]; + FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved targetIDs:@[ @1 ] cause:nil]; + FSTWatchChange *change4 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateRemoved targetIDs:@[ @2 ] cause:nil]; + FSTWatchChange *change5 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded targetIDs:@[ @1 ] cause:nil]; + + FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); FSTWatchChange *change6 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[ @3 ] documentKey:doc2.key document:doc2]; - NSDictionary<NSNumber *, NSNumber *> *pendingResponses = @{ @1 : @2, @2 : @1 }; + NSDictionary<NSNumber *, NSNumber *> *outstandingResponses = @{ @1 : @2, @2 : @1 }; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1, @3 ] - outstanding:pendingResponses - changes:@[ change1, change2, change3, change4, change5, change6 ]]; + FSTRemoteEvent *event = + [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:outstandingResponses + existingKeys:DocumentKeySet{doc2.key} + changes:@[ change1, change2, change3, change4, change5, change6 ]]; - FSTRemoteEvent *event = [aggregator remoteEvent]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); // target 1 and 3 are affected (1 because of re-add), target 2 is not because of remove - XCTAssertEqual(event.targetChanges.count, 2); + XCTAssertEqual(event.targetChanges.size(), 2); - // doc1 was before the remove, so it does not show up in the mapping - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc2 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - // Current was before the remove - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); + // doc1 was before the remove, so it does not show up in the mapping. + // Current was before the remove. + FSTTargetChange *targetChange1 = FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{doc2.key}, + DocumentKeySet{}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); // Doc1 was before the remove - FSTUpdateMapping *mapping3 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1 ] removedDocuments:@[ doc2 ]]; - XCTAssertEqualObjects(event.targetChanges[@3].mapping, mapping3); // Current was before the remove - XCTAssertEqual(event.targetChanges[@3].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@3].resumeToken, _resumeToken1); + FSTTargetChange *targetChange3 = FSTTestTargetChange( + DocumentKeySet{doc1.key}, DocumentKeySet{}, DocumentKeySet{doc2.key}, _resumeToken1, YES); + XCTAssertEqualObjects(event.targetChanges.at(3), targetChange3); } - (void)testNoChangeWillStillMarkTheAffectedTargets { - FSTWatchChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange - targetIDs:@[ @1 ] - resumeToken:_resumeToken1]; + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.targetChanges.count, 1); - XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]); - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateNone); - XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); -} + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[]]; -- (void)testExistenceFiltersWillReplacePreviousExistenceFilters { - FSTExistenceFilter *filter1 = [FSTExistenceFilter filterWithCount:1]; - FSTExistenceFilter *filter2 = [FSTExistenceFilter filterWithCount:2]; - FSTWatchChange *change1 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:1]; - FSTWatchChange *change2 = [FSTExistenceFilterWatchChange changeWithFilter:filter1 targetID:2]; - // replace filter1 for target 2 - FSTWatchChange *change3 = [FSTExistenceFilterWatchChange changeWithFilter:filter2 targetID:2]; + FSTWatchTargetChange *change = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + [aggregator handleTargetChange:change]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1, @2 ] - outstanding:_noPendingResponses - changes:@[ change1, change2, change3 ]]; + FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; - FSTRemoteEvent *event = [aggregator remoteEvent]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); - XCTAssertEqual(event.targetChanges.count, 0); - XCTAssertEqual(aggregator.existenceFilters.count, 2); - XCTAssertEqual(aggregator.existenceFilters[@1], filter1); - XCTAssertEqual(aggregator.existenceFilters[@2], filter2); + XCTAssertEqual(event.targetChanges.size(), 1); + + FSTTargetChange *targetChange = + FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); } -- (void)testExistenceFilterMismatchResetsTarget { - FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); - FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); +- (void)testExistenceFilterMismatchClearsTarget { + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForTargets:@[ @1, @2 ]]; + FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc1.key document:doc1]; + FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc2.key @@ -420,270 +550,353 @@ NS_ASSUME_NONNULL_BEGIN resumeToken:_resumeToken1]; FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] - outstanding:_noPendingResponses - changes:@[ change1, change2, change3 ]]; + [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet{doc1.key, doc2.key} + changes:@[ change1, change2, change3 ]]; + + FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; - FSTRemoteEvent *event = [aggregator remoteEvent]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); - XCTAssertEqual(event.targetChanges.count, 1); + XCTAssertEqual(event.targetChanges.size(), 2); + + FSTTargetChange *targetChange1 = FSTTestTargetChange( + DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, DocumentKeySet{}, _resumeToken1, YES); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + + FSTTargetChange *targetChange2 = + FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); + + // The existence filter mismatch will remove the document from target 1, + // but not synthesize a document delete. + FSTExistenceFilterWatchChange *change4 = + [FSTExistenceFilterWatchChange changeWithFilter:[FSTExistenceFilter filterWithCount:1] + targetID:1]; + [aggregator handleExistenceFilter:change4]; - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); + event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(4)]; - [event handleExistenceFilterMismatchForTargetID:@1]; + FSTTargetChange *targetChange3 = FSTTestTargetChange( + DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key, doc2.key}, [NSData data], NO); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange3); - // Mapping is reset - XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]); - // Reset the resume snapshot - XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(0)); - // Target needs to be set to not current - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkNotCurrent); - XCTAssertEqual(event.targetChanges[@1].resumeToken.length, 0); + XCTAssertEqual(event.targetChanges.size(), 1); + XCTAssertEqual(event.targetMismatches.size(), 1); + XCTAssertEqual(event.documentUpdates.size(), 0); } -- (void)testDocumentUpdate { +- (void)testExistenceFilterMismatchRemovesCurrentChanges { + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[]]; + + FSTWatchTargetChange *markCurrent = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + [aggregator handleTargetChange:markCurrent]; + FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); - FSTDeletedDocument *deletedDoc1 = - [FSTDeletedDocument documentWithKey:doc1.key version:testutil::Version(3)]; - FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); - FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO); + FSTDocumentWatchChange *addDoc = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc1.key + document:doc1]; + [aggregator handleDocumentChange:addDoc]; + + // The existence filter mismatch will remove the document from target 1, but not synthesize a + // document delete. + FSTExistenceFilterWatchChange *existenceFilter = + [FSTExistenceFilterWatchChange changeWithFilter:[FSTExistenceFilter filterWithCount:0] + targetID:1]; + [aggregator handleExistenceFilter:existenceFilter]; + + FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); + XCTAssertEqual(event.documentUpdates.size(), 1); + XCTAssertEqual(event.targetMismatches.size(), 1); + XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); + + XCTAssertEqual(event.targetChanges.size(), 1); + + FSTTargetChange *targetChange1 = + FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, [NSData data], NO); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); +} + +- (void)testDocumentUpdate { + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = [self queryDataForTargets:@[ @1 ]]; + + FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTWatchChange *change1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc1.key document:doc1]; + FSTDocument *doc2 = FSTTestDoc("docs/2", 2, @{ @"value" : @2 }, NO); FSTWatchChange *change2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:doc2.key document:doc2]; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ] - outstanding:_noPendingResponses - changes:@[ change1, change2 ]]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[ change1, change2 ]]; + + FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; - FSTRemoteEvent *event = [aggregator remoteEvent]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 2); XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), doc1); XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); - // Update doc1 - [event addDocumentUpdate:deletedDoc1]; - [event addDocumentUpdate:doc3]; + [_targetMetadataProvider setSyncedKeys:DocumentKeySet{doc1.key, doc2.key} + forQueryData:targetMap[@1]]; + + FSTDeletedDocument *deletedDoc1 = + [FSTDeletedDocument documentWithKey:doc1.key version:testutil::Version(3)]; + FSTDocumentWatchChange *change3 = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1 ] + documentKey:deletedDoc1.key + document:deletedDoc1]; + [aggregator handleDocumentChange:change3]; + + FSTDocument *updatedDoc2 = FSTTestDoc("docs/2", 3, @{ @"value" : @2 }, NO); + FSTDocumentWatchChange *change4 = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:updatedDoc2.key + document:updatedDoc2]; + [aggregator handleDocumentChange:change4]; + + FSTDocument *doc3 = FSTTestDoc("docs/3", 3, @{ @"value" : @3 }, NO); + FSTDocumentWatchChange *change5 = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc3.key + document:doc3]; + [aggregator handleDocumentChange:change5]; + + event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 3); // doc1 is replaced XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), deletedDoc1); - // doc2 is untouched - XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), doc2); + // doc2 is updated + XCTAssertEqualObjects(event.documentUpdates.at(doc2.key), updatedDoc2); // doc3 is new XCTAssertEqualObjects(event.documentUpdates.at(doc3.key), doc3); // Target is unchanged - XCTAssertEqual(event.targetChanges.count, 1); + XCTAssertEqual(event.targetChanges.size(), 1); - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); + FSTTargetChange *targetChange = + FSTTestTargetChange(DocumentKeySet{doc3.key}, DocumentKeySet{updatedDoc2.key}, + DocumentKeySet{deletedDoc1.key}, _resumeToken1, NO); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); } - (void)testResumeTokensHandledPerTarget { + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForTargets:@[ @1, @2 ]]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[]]; + + FSTWatchTargetChange *change1 = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + [aggregator handleTargetChange:change1]; + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; - FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ @1 ] - resumeToken:_resumeToken1]; - FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ @2 ] - resumeToken:resumeToken2]; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1, @2 ] - outstanding:_noPendingResponses - changes:@[ change1, change2 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqual(event.targetChanges.count, 2); - - FSTUpdateMapping *mapping1 = - [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); - - XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); - XCTAssertEqual(event.targetChanges[@2].snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2); + FSTWatchTargetChange *change2 = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @2 ] + resumeToken:resumeToken2]; + [aggregator handleTargetChange:change2]; + + FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + XCTAssertEqual(event.targetChanges.size(), 2); + + FSTTargetChange *targetChange1 = + FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, _resumeToken1, YES); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + + FSTTargetChange *targetChange2 = + FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES); + XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); } - (void)testLastResumeTokenWins { + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForTargets:@[ @1, @2 ]]; + + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[]]; + + FSTWatchTargetChange *change1 = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent + targetIDs:@[ @1 ] + resumeToken:_resumeToken1]; + [aggregator handleTargetChange:change1]; + NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchTargetChange *change2 = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @1 ] + resumeToken:resumeToken2]; + [aggregator handleTargetChange:change2]; + NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding]; + FSTWatchTargetChange *change3 = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange + targetIDs:@[ @2 ] + resumeToken:resumeToken3]; + [aggregator handleTargetChange:change3]; - FSTWatchChange *change1 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent - targetIDs:@[ @1 ] - resumeToken:_resumeToken1]; - FSTWatchChange *change2 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @1 ] - resumeToken:resumeToken2]; - FSTWatchChange *change3 = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @2 ] - resumeToken:resumeToken3]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1, @2 ] - outstanding:_noPendingResponses - changes:@[ change1, change2, change3 ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqual(event.targetChanges.count, 2); - - FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]]; - XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); - XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, resumeToken2); - - XCTAssertEqualObjects(event.targetChanges[@2].mapping, mapping1); - XCTAssertEqual(event.targetChanges[@2].snapshotVersion, testutil::Version(3)); - XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone); - XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3); + FSTRemoteEvent *event = [aggregator remoteEventAtSnapshotVersion:testutil::Version(3)]; + XCTAssertEqual(event.targetChanges.size(), 2); + + FSTTargetChange *targetChange1 = + FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken2, YES); + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange1); + + FSTTargetChange *targetChange2 = + FSTTestTargetChange(DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}, resumeToken3, NO); + XCTAssertEqualObjects(event.targetChanges.at(2), targetChange2); } - (void)testSynthesizeDeletes { - FSTWatchChange *shouldSynthesize = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1 ]]; + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForLimboTargets:@[ @1 ]]; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ] - outstanding:_noPendingResponses - changes:@[ shouldSynthesize ]]; + DocumentKey limboKey = testutil::Key("coll/limbo"); - FSTRemoteEvent *event = [aggregator remoteEvent]; - DocumentKey synthesized = DocumentKey::FromPathString("docs/2"); - XCTAssertEqual(event.documentUpdates.find(synthesized), event.documentUpdates.end()); + FSTWatchChange *resolveLimboTarget = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1 ]]; + + FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[ resolveLimboTarget ]]; - FSTTargetChange *limboTargetChange = event.targetChanges[@1]; - [event synthesizeDeleteForLimboTargetChange:limboTargetChange key:synthesized]; FSTDeletedDocument *expected = - [FSTDeletedDocument documentWithKey:synthesized version:event.snapshotVersion]; - XCTAssertEqualObjects(expected, event.documentUpdates.at(synthesized)); - XCTAssertTrue(event.limboDocumentChanges.contains(synthesized)); + [FSTDeletedDocument documentWithKey:limboKey version:event.snapshotVersion]; + XCTAssertEqualObjects(event.documentUpdates.at(limboKey), expected); + XCTAssertTrue(event.limboDocumentChanges.contains(limboKey)); } - (void)testDoesntSynthesizeDeletesForWrongState { - FSTWatchChange *wrongState = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @2 ]]; + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForLimboTargets:@[ @1 ]]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @2 ] outstanding:_noPendingResponses changes:@[ wrongState ]]; + FSTWatchChange *wrongState = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @1 ]]; - FSTRemoteEvent *event = [aggregator remoteEvent]; + FSTRemoteEvent *event = [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[ wrongState ]]; - DocumentKey notSynthesized = DocumentKey::FromPathString("docs/no1"); - [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@2] key:notSynthesized]; - XCTAssertEqual(event.documentUpdates.find(notSynthesized), event.documentUpdates.end()); - XCTAssertFalse(event.limboDocumentChanges.contains(notSynthesized)); + XCTAssertEqual(event.documentUpdates.size(), 0); + XCTAssertEqual(event.limboDocumentChanges.size(), 0); } - (void)testDoesntSynthesizeDeletesForExistingDoc { + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForLimboTargets:@[ @3 ]]; + FSTWatchChange *hasDocument = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @3 ]]; - FSTDocument *doc = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); - FSTWatchChange *docChange = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @3 ] - removedTargetIDs:@[] - documentKey:doc.key - document:doc]; - FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @3 ] - outstanding:_noPendingResponses - changes:@[ hasDocument, docChange ]]; - - FSTRemoteEvent *event = [aggregator remoteEvent]; - [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@3] key:doc.key]; - FSTMaybeDocument *docData = event.documentUpdates.at(doc.key); - XCTAssertFalse([docData isKindOfClass:[FSTDeletedDocument class]]); - XCTAssertFalse(event.limboDocumentChanges.contains(doc.key)); + + FSTRemoteEvent *event = + [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet{FSTTestDocKey(@"coll/limbo")} + changes:@[ hasDocument ]]; + + XCTAssertEqual(event.documentUpdates.size(), 0); + XCTAssertEqual(event.limboDocumentChanges.size(), 0); } -- (void)testFilterUpdates { +- (void)testSeparatesDocumentUpdates { + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [self queryDataForLimboTargets:@[ @1 ]]; + FSTDocument *newDoc = FSTTestDoc("docs/new", 1, @{@"key" : @"value"}, NO); - FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO); FSTWatchChange *newDocChange = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:newDoc.key document:newDoc]; + FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO); FSTWatchChange *existingDocChange = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:existingDoc.key document:existingDoc]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1 ] - outstanding:_noPendingResponses - changes:@[ newDocChange, existingDocChange ]]; - FSTRemoteEvent *event = [aggregator remoteEvent]; - DocumentKeySet existingKeys = DocumentKeySet{existingDoc.key}; - - FSTTargetChange *updateChange = event.targetChanges[@1]; - XCTAssertTrue([updateChange.mapping isKindOfClass:[FSTUpdateMapping class]]); - FSTUpdateMapping *update = (FSTUpdateMapping *)updateChange.mapping; - FSTDocumentKey *existingDocKey = existingDoc.key; - FSTDocumentKey *newDocKey = newDoc.key; - XCTAssertTrue(update.addedDocuments.contains(existingDocKey)); - - [update filterUpdatesUsingExistingKeys:existingKeys]; - // Now it's been filtered, since it already existed. - XCTAssertFalse(update.addedDocuments.contains(existingDocKey)); - XCTAssertTrue(update.addedDocuments.contains(newDocKey)); -} - -- (void)testDoesntFilterResets { - FSTDocument *existingDoc = FSTTestDoc("docs/existing", 1, @{@"some" : @"data"}, NO); - const DocumentKey &existingDocKey = existingDoc.key; - FSTWatchTargetChange *resetTargetChange = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @2 ] - resumeToken:_resumeToken1]; - FSTWatchChange *existingDocChange = - [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] - removedTargetIDs:@[] - documentKey:existingDocKey - document:existingDoc]; - FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @2 ] - outstanding:_noPendingResponses - changes:@[ resetTargetChange, existingDocChange ]]; - FSTRemoteEvent *event = [aggregator remoteEvent]; - DocumentKeySet existingKeys = DocumentKeySet{existingDocKey}; - - FSTTargetChange *resetChange = event.targetChanges[@2]; - XCTAssertTrue([resetChange.mapping isKindOfClass:[FSTResetMapping class]]); - FSTResetMapping *resetMapping = (FSTResetMapping *)resetChange.mapping; - XCTAssertTrue(resetMapping.documents.contains(existingDocKey)); - - [resetMapping filterUpdatesUsingExistingKeys:existingKeys]; - // Document is still there, even though it already exists. Reset mappings don't get filtered. - XCTAssertTrue(resetMapping.documents.contains(existingDocKey)); + FSTDeletedDocument *deletedDoc = FSTTestDeletedDoc("docs/deleted", 1); + FSTWatchChange *deletedDocChange = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1 ] + documentKey:deletedDoc.key + document:deletedDoc]; + + FSTDeletedDocument *missingDoc = FSTTestDeletedDoc("docs/missing", 1); + FSTWatchChange *missingDocChange = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:@[ @1 ] + documentKey:missingDoc.key + document:missingDoc]; + + FSTRemoteEvent *event = [self + remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet{existingDoc.key, deletedDoc.key} + changes:@[ + newDocChange, existingDocChange, deletedDocChange, missingDocChange + ]]; + + FSTTargetChange *targetChange = + FSTTestTargetChange(DocumentKeySet{newDoc.key}, DocumentKeySet{existingDoc.key}, + DocumentKeySet{deletedDoc.key}, _resumeToken1, NO); + + XCTAssertEqualObjects(event.targetChanges.at(1), targetChange); } - (void)testTracksLimboDocuments { + NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *targetMap = + [NSMutableDictionary dictionary]; + [targetMap addEntriesFromDictionary:[self queryDataForTargets:@[ @1 ]]]; + [targetMap addEntriesFromDictionary:[self queryDataForLimboTargets:@[ @2 ]]]; + // Add 3 docs: 1 is limbo and non-limbo, 2 is limbo-only, 3 is non-limbo FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{@"key" : @"value"}, NO); FSTDocument *doc2 = FSTTestDoc("docs/2", 1, @{@"key" : @"value"}, NO); FSTDocument *doc3 = FSTTestDoc("docs/3", 1, @{@"key" : @"value"}, NO); // Target 2 is a limbo target - FSTWatchChange *docChange1 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2 ] removedTargetIDs:@[] documentKey:doc1.key @@ -702,20 +915,13 @@ NS_ASSUME_NONNULL_BEGIN FSTWatchChange *targetsChange = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1, @2 ]]; - NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary]; - listens[@1] = [FSTQueryData alloc]; - listens[@2] = [[FSTQueryData alloc] initWithQuery:[FSTQuery alloc] - targetID:2 - listenSequenceNumber:1000 - purpose:FSTQueryPurposeLimboResolution]; - FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3) - listenTargets:listens - pendingTargetResponses:@{}]; - - [aggregator addWatchChanges:@[ docChange1, docChange2, docChange3, targetsChange ]]; + FSTRemoteEvent *event = + [self remoteEventAtSnapshotVersion:3 + targetMap:targetMap + outstandingResponses:_noOutstandingResponses + existingKeys:DocumentKeySet {} + changes:@[ docChange1, docChange2, docChange3, targetsChange ]]; - FSTRemoteEvent *event = [aggregator remoteEvent]; DocumentKeySet limboDocChanges = event.limboDocumentChanges; // Doc1 is in both limbo and non-limbo targets, therefore not tracked as limbo XCTAssertFalse(limboDocChanges.contains(doc1.key)); diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index 77010e5..c131f7e 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -256,12 +256,15 @@ static NSString *const kNoIOSTag = @"no-ios"; } else if (watchEntity[@"doc"]) { NSArray *docSpec = watchEntity[@"doc"]; FSTDocumentKey *key = FSTTestDocKey(docSpec[0]); - FSTObjectValue *value = FSTTestObjectValue(docSpec[2]); + FSTObjectValue *_Nullable value = + [docSpec[2] isKindOfClass:[NSNull class]] ? nil : FSTTestObjectValue(docSpec[2]); SnapshotVersion version = [self parseVersion:docSpec[1]]; - FSTMaybeDocument *doc = [FSTDocument documentWithData:value - key:key - version:std::move(version) - hasLocalMutations:NO]; + FSTMaybeDocument *doc = + value ? [FSTDocument documentWithData:value + key:key + version:std::move(version) + hasLocalMutations:NO] + : [FSTDeletedDocument documentWithKey:key version:std::move(version)]; FSTWatchChange *change = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"] removedTargetIDs:watchEntity[@"removedTargets"] diff --git a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json index abd2cf4..3e5d4fb 100644 --- a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json @@ -1,4 +1,458 @@ { + "Existence filter match": { + "describeName": "Existence Filters:", + "itName": "Existence filter match", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000 + } + ] + }, + "Existence filter match after pending update": { + "describeName": "Existence Filters:", + "itName": "Existence filter match after pending update", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Existence filter with empty target": { + "describeName": "Existence Filters:", + "itName": "Existence filter with empty target", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Existence filter ignored with pending target": { + "describeName": "Existence Filters:", + "itName": "Existence filter ignored with pending target", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 2000, + { + "v": 2 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ] + ] + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, "Existence filter mismatch triggers re-run of query": { "describeName": "Existence Filters:", "itName": "Existence filter mismatch triggers re-run of query", @@ -501,6 +955,332 @@ } ] }, + "Existence filter handled at global snapshot": { + "describeName": "Existence Filters:", + "itName": "Existence filter handled at global snapshot", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ], + "collection/1", + "collection/2" + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/3", + 3000, + { + "v": 3 + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/3", + 3000, + { + "v": 3 + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/1", + 1000, + { + "v": 1 + } + ], + [ + "collection/2", + 2000, + { + "v": 2 + } + ], + [ + "collection/3", + 3000, + { + "v": 3 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-3000" + ], + "watchSnapshot": 3000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/2", + 2000, + { + "v": 2 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Existence filter synthesizes deletes": { + "describeName": "Existence Filters:", + "itName": "Existence filter synthesizes deletes", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/a", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "watchFilter": [ + [ + 2 + ] + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "v": 1 + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, "Existence filter limbo resolution is denied": { "describeName": "Existence Filters:", "itName": "Existence filter limbo resolution is denied", diff --git a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json index ee2d883..a186496 100644 --- a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json @@ -1146,5 +1146,416 @@ ] } ] + }, + "Limbo documents handle receiving ack and then current": { + "describeName": "Limbo Documents:", + "itName": "Limbo documents handle receiving ack and then current", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ], + [ + "collection/b", + 1000, + { + "include": true, + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ], + [ + "collection/b", + 1000, + { + "include": true, + "key": "b" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 4, + { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userPatch": [ + "collection/a", + { + "include": false + } + ], + "expect": [ + { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1000, + { + "include": true, + "key": "b" + } + ] + ], + "removed": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ], + "stateExpect": { + "limboDocs": [ + "collection/b" + ], + "activeTargets": { + "1": { + "query": { + "path": "collection/b", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 1 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1000, + { + "include": true, + "key": "b" + } + ] + ], + "targets": [ + 1 + ] + } + }, + { + "watchCurrent": [ + [ + 1 + ], + "resume-token-3000" + ], + "watchSnapshot": 3000 + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "include": true, + "key": "a" + } + ] + ], + "removedTargets": [ + 4 + ] + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 1000, + { + "include": true, + "key": "b" + } + ] + ], + "targets": [ + 4 + ] + }, + "watchSnapshot": 4000, + "expect": [ + { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ], + "stateExpect": { + "limboDocs": [], + "activeTargets": { + "4": { + "query": { + "path": "collection", + "limit": 1, + "filters": [ + [ + "include", + "==", + true + ] + ], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + } + ] } } diff --git a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json index 5a02463..6aa1daa 100644 --- a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json @@ -287,7 +287,15 @@ "targets": [ 2 ] - }, + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], "watchSnapshot": 2000, "stateExpect": { "limboDocs": [ @@ -1144,7 +1152,15 @@ "targets": [ 2 ] - }, + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], "watchSnapshot": 2000, "stateExpect": { "limboDocs": [ diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json index 7bfe557..e838d2f 100644 --- a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json @@ -238,6 +238,177 @@ } ] }, + "Doesn't raise events for empty target": { + "describeName": "Listens:", + "itName": "Doesn't raise events for empty target", + "tags": [], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection1", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userListen": [ + 4, + { + "path": "collection2", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "userListen": [ + 6, + { + "path": "collection3", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection2", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "6": { + "query": { + "path": "collection3", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection2/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 4 + ] + } + }, + { + "watchAck": [ + 6 + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection1", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection2", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection2/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, "Ensure correct query results with latency-compensated deletes": { "describeName": "Listens:", "itName": "Ensure correct query results with latency-compensated deletes", @@ -385,6 +556,79 @@ } ] }, + "Does not raise event for initial document delete": { + "describeName": "Listens:", + "itName": "Does not raise event for initial document delete", + "tags": [""], + "config": { + "useGarbageCollection": true + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + null + ] + ], + "removedTargets": [ + 2 + ] + }, + "watchSnapshot": 1000 + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, "Will process removals without waiting for a consistent snapshot": { "describeName": "Listens:", "itName": "Will process removals without waiting for a consistent snapshot", @@ -1690,5 +1934,836 @@ ] } ] + }, + "Synthesizes deletes for missing document": { + "describeName": "Listens:", + "itName": "Synthesizes deletes for missing document", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ], + [ + "collection/b", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 4, + { + "path": "collection/a", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "4": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expect": [ + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 4, + { + "path": "collection/a", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 4 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/b", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Re-opens target without existence filter": { + "describeName": "Listens:", + "itName": "Re-opens target without existence filter", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 2000, + null + ] + ], + "removedTargets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "removed": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + } + ] + }, + "Ignores update from inactive target": { + "describeName": "Listens:", + "itName": "Ignores update from inactive target", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ], + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "watchEntity": { + "docs": [ + [ + "collection/b", + 2000, + { + "key": "b" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 2000 + }, + { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-1000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] + }, + "Does not synthesize deletes for previously acked documents": { + "describeName": "Listens:", + "itName": "Does not synthesize deletes for previously acked documents", + "tags": [], + "config": { + "useGarbageCollection": false + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection/a", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "targets": [ + 2 + ] + }, + "watchSnapshot": 1000, + "expect": [ + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-2000" + ], + "watchSnapshot": 2000, + "expect": [ + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userUnlisten": [ + 2, + { + "path": "collection/a", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": {} + } + }, + { + "userListen": [ + 2, + { + "path": "collection/a", + "filters": [], + "orderBys": [] + } + ], + "stateExpect": { + "activeTargets": { + "2": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "resume-token-2000" + } + } + }, + "expect": [ + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "added": [ + [ + "collection/a", + 1000, + { + "key": "a" + } + ] + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + } + ] } } diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index ccc01ca..7946c06 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -16,11 +16,11 @@ #import <Foundation/Foundation.h> -#include <map> #include <vector> #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" +#import "Firestore/Source/Remote/FSTRemoteEvent.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" @@ -148,6 +148,42 @@ inline NSString *FSTRemoveExceptionPrefix(NSString *exception) { XCTAssertTrue(didThrow, ##__VA_ARGS__); \ } while (0) +/** + * An implementation of FSTTargetMetadataProvider that provides controlled access to the + * `FSTTargetMetadataProvider` callbacks. Any target accessed via these callbacks must be + * registered beforehand via the factory methods or via `setSyncedKeys:forQueryData:`. + */ +@interface FSTTestTargetMetadataProvider : NSObject <FSTTargetMetadataProvider> + +/** + * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for + * each of the given targets, where each target has previously seen query results containing just + * the given documentKey. + * + * Internally this means that the `remoteKeysForTarget` callback for these targets will return just + * the documentKey and that the provided targets will be returned as active from the + * `queryDataForTarget` target. + */ ++ (instancetype)providerWithSingleResultForKey:(firebase::firestore::model::DocumentKey)documentKey + targets:(NSArray<FSTBoxedTargetID *> *)targets; + +/** + * Creates an FSTTestTargetMetadataProvider that behaves as if there's an established listen for + * each of the given targets, where each target has not seen any previous document. + * + * Internally this means that the `remoteKeysForTarget` callback for these targets will return an + * empty set of document keys and that the provided targets will be returned as active from the + * `queryDataForTarget` target. + */ ++ (instancetype)providerWithEmptyResultForKey:(firebase::firestore::model::DocumentKey)documentKey + targets:(NSArray<FSTBoxedTargetID *> *)targets; + +/** Sets or replaces the local state for the provided query data. */ +- (void)setSyncedKeys:(firebase::firestore::model::DocumentKeySet)keys + forQueryData:(FSTQueryData *)queryData; + +@end + /** Creates a new FIRTimestamp from components. Note that year, month, and day are all one-based. */ FIRTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second); @@ -250,6 +286,9 @@ FSTDeleteMutation *FSTTestDeleteMutation(NSString *path); /** Converts a list of documents to a sorted map. */ FSTMaybeDocumentDictionary *FSTTestDocUpdates(NSArray<FSTMaybeDocument *> *docs); +/** Creates a remote event that inserts a new document. */ +FSTRemoteEvent *FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, NSArray<NSNumber *> *addedToTargets); + /** Creates a remote event with changes to a document. */ FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, NSArray<NSNumber *> *updatedInTargets, @@ -260,6 +299,19 @@ FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, NSArray<NSString *> *addedKeys, NSArray<NSString *> *removedKeys); +/** Creates a test target change that acks all 'docs' and marks the target as CURRENT */ +FSTTargetChange *FSTTestTargetChangeAckDocuments(firebase::firestore::model::DocumentKeySet docs); + +/** Creates a test target change that marks the target as CURRENT */ +FSTTargetChange *FSTTestTargetChangeMarkCurrent(); + +/** Creates a test target change. */ +FSTTargetChange *FSTTestTargetChange(firebase::firestore::model::DocumentKeySet added, + firebase::firestore::model::DocumentKeySet modified, + firebase::firestore::model::DocumentKeySet removed, + NSData *resumeToken, + BOOL current); + /** Creates a resume token to match the given snapshot version. */ NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion watchSnapshot); diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index 5ed4fd5..8ece82f 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -22,7 +22,7 @@ #include <cinttypes> #include <list> -#include <map> +#include <unordered_map> #include <utility> #include <vector> @@ -58,6 +58,7 @@ namespace util = firebase::firestore::util; namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldPath; using firebase::firestore::model::FieldTransform; @@ -65,8 +66,8 @@ using firebase::firestore::model::FieldValue; using firebase::firestore::model::Precondition; using firebase::firestore::model::ResourcePath; using firebase::firestore::model::ServerTimestampTransform; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TransformOperation; -using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -298,29 +299,125 @@ FSTViewSnapshot *_Nullable FSTTestApplyChanges(FSTView *view, .snapshot; } +@implementation FSTTestTargetMetadataProvider { + std::unordered_map<FSTTargetID, DocumentKeySet> _syncedKeys; + std::unordered_map<FSTTargetID, FSTQueryData *> _queryData; +} + ++ (instancetype)providerWithSingleResultForKey:(DocumentKey)documentKey + targets:(NSArray<FSTBoxedTargetID *> *)targets { + FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new]; + FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()]; + + for (FSTBoxedTargetID *targetID in targets) { + FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:targetID.intValue + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen]; + [metadataProvider setSyncedKeys:DocumentKeySet{documentKey} forQueryData:queryData]; + } + + return metadataProvider; +} + ++ (instancetype)providerWithEmptyResultForKey:(DocumentKey)documentKey + targets:(NSArray<FSTBoxedTargetID *> *)targets { + FSTTestTargetMetadataProvider *metadataProvider = [FSTTestTargetMetadataProvider new]; + FSTQuery *query = [FSTQuery queryWithPath:documentKey.path()]; + + for (FSTBoxedTargetID *targetID in targets) { + FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:targetID.intValue + listenSequenceNumber:0 + purpose:FSTQueryPurposeListen]; + [metadataProvider setSyncedKeys:DocumentKeySet {} forQueryData:queryData]; + } + + return metadataProvider; +} + +- (void)setSyncedKeys:(DocumentKeySet)keys forQueryData:(FSTQueryData *)queryData { + _syncedKeys[queryData.targetID] = keys; + _queryData[queryData.targetID] = queryData; +} + +- (DocumentKeySet)remoteKeysForTarget:(FSTBoxedTargetID *)targetID { + auto it = _syncedKeys.find(targetID.intValue); + HARD_ASSERT(it != _syncedKeys.end(), "Cannot process unknown target %s", targetID.intValue); + return it->second; +} + +- (nullable FSTQueryData *)queryDataForTarget:(FSTBoxedTargetID *)targetID { + auto it = _queryData.find(targetID.intValue); + HARD_ASSERT(it != _queryData.end(), "Cannot process unknown target %s", targetID.intValue); + return it->second; +} + +@end + +FSTRemoteEvent *FSTTestAddedRemoteEvent(FSTMaybeDocument *doc, + NSArray<FSTBoxedTargetID *> *addedToTargets) { + HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations], + "Docs from remote updates shouldn't have local changes."); + FSTDocumentWatchChange *change = + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:addedToTargets + removedTargetIDs:{} + documentKey:doc.key + document:doc]; + FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc] + initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider + providerWithEmptyResultForKey:doc.key + targets:addedToTargets]]; + [aggregator handleDocumentChange:change]; + return [aggregator remoteEventAtSnapshotVersion:doc.version]; +} + FSTRemoteEvent *FSTTestUpdateRemoteEvent(FSTMaybeDocument *doc, - NSArray<NSNumber *> *updatedInTargets, - NSArray<NSNumber *> *removedFromTargets) { + NSArray<FSTBoxedTargetID *> *updatedInTargets, + NSArray<FSTBoxedTargetID *> *removedFromTargets) { + HARD_ASSERT(![doc isKindOfClass:[FSTDocument class]] || ![(FSTDocument *)doc hasLocalMutations], + "Docs from remote updates shouldn't have local changes."); FSTDocumentWatchChange *change = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedInTargets removedTargetIDs:removedFromTargets documentKey:doc.key document:doc]; - NSMutableDictionary<NSNumber *, FSTQueryData *> *listens = [NSMutableDictionary dictionary]; - FSTQueryData *dummyQueryData = [FSTQueryData alloc]; - for (NSNumber *targetID in updatedInTargets) { - listens[targetID] = dummyQueryData; - } - for (NSNumber *targetID in removedFromTargets) { - listens[targetID] = dummyQueryData; - } - NSMutableDictionary<NSNumber *, NSNumber *> *pending = [NSMutableDictionary dictionary]; - FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:doc.version - listenTargets:listens - pendingTargetResponses:pending]; - [aggregator addWatchChange:change]; - return [aggregator remoteEvent]; + NSArray<FSTBoxedTargetID *> *targets = + [updatedInTargets arrayByAddingObjectsFromArray:removedFromTargets]; + FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc] + initWithTargetMetadataProvider:[FSTTestTargetMetadataProvider + providerWithSingleResultForKey:doc.key + targets:targets]]; + [aggregator handleDocumentChange:change]; + return [aggregator remoteEventAtSnapshotVersion:doc.version]; +} + +FSTTargetChange *FSTTestTargetChangeMarkCurrent() { + return [[FSTTargetChange alloc] initWithResumeToken:[NSData data] + current:YES + addedDocuments:DocumentKeySet {} + modifiedDocuments:DocumentKeySet {} + removedDocuments:DocumentKeySet{}]; +} + +FSTTargetChange *FSTTestTargetChangeAckDocuments(DocumentKeySet docs) { + return [[FSTTargetChange alloc] initWithResumeToken:[NSData data] + current:YES + addedDocuments:docs + modifiedDocuments:DocumentKeySet {} + removedDocuments:DocumentKeySet{}]; +} + +FSTTargetChange *FSTTestTargetChange(DocumentKeySet added, + DocumentKeySet modified, + DocumentKeySet removed, + NSData *resumeToken, + BOOL current) { + return [[FSTTargetChange alloc] initWithResumeToken:resumeToken + current:current + addedDocuments:added + modifiedDocuments:modified + removedDocuments:removed]; } /** Creates a resume token to match the given snapshot version. */ |