diff options
184 files changed, 7490 insertions, 2701 deletions
diff --git a/.travis.yml b/.travis.yml index fff1ec4..7cec178 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ jobs: - ./scripts/check_whitespace.sh - ./scripts/check_copyright.sh - ./scripts/check_no_module_imports.sh + - ./scripts/check_test_inclusion.py - ./scripts/style.sh test-only $TRAVIS_COMMIT_RANGE # Google C++ style compliance - ./scripts/lint.sh $TRAVIS_COMMIT_RANGE @@ -27,37 +28,75 @@ jobs: # Primary platforms + # Run unit tests - stage: test env: - - PROJECT=Firebase PLATFORM=iOS + - PROJECT=Firebase PLATFORM=iOS METHOD=xcodebuild before_install: - # Add next line back with updated DeviceUDID for xcode9.1 if stability - # issues with simulator: - # - open -a "simulator" --args -CurrentDeviceUDID ABBD7191-486B-462F-80B4-AE08C5820DA1 + - npm install ios-sim -g + - ios-sim start --devicetypeid "com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus, 11.3" - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - - ./scripts/if_changed.sh ./scripts/pod_install.sh script: - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - # Disabled because of url validation failures on April 20, 2018 - #- ./scripts/if_changed.sh bundle exec pod lib lint FirebaseCore.podspec - - # TODO - Uncomment subsequent lines once FirebaseCore source repo is in public Specs repo - # - bundle exec pod lib lint FirebaseAuth.podspec - # - bundle exec pod lib lint FirebaseDatabase.podspec - # - bundle exec pod lib lint FirebaseMessaging.podspec - # - bundle exec pod lib lint FirebaseStorage.podspec - # - bundle exec pod lib lint FirebaseFirestore.podspec - - stage: test env: - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - - ./scripts/if_changed.sh ./scripts/pod_install.sh script: - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD + # TODO - add a script to make the lint checks more DRY. + # pod lib lint to check build and warnings for dynamic framework build (use_frameworks!) + - stage: test + env: + - PROJECT=Firebase PLATFORM=iOS METHOD=pod-lib-lint + before_install: + - ./scripts/if_changed.sh ./scripts/install_prereqs.sh + script: + - ./scripts/if_changed.sh bundle exec pod lib lint FirebaseCore.podspec + - ./scripts/if_changed.sh bundle exec pod lib lint FirebaseAuth.podspec + - ./scripts/if_changed.sh bundle exec pod lib lint FirebaseDatabase.podspec +# - ./scripts/if_changed.sh bundle exec pod lib lint FirebaseMessaging.podspec + - ./scripts/if_changed.sh bundle exec pod lib lint FirebaseStorage.podspec + - ./scripts/if_changed.sh bundle exec pod lib lint FirebaseFunctions.podspec + + - stage: test + env: + - PROJECT=Firestore PLATFORM=iOS METHOD=pod-lib-lint + before_install: + - ./scripts/if_changed.sh ./scripts/install_prereqs.sh + script: + # Eliminate the one warning from BoringSSL when CocoaPods 1.6.0 is available. + # The travis_wait is necessary because the command takes more than 10 minutes. + - travis_wait ./scripts/if_changed.sh bundle exec pod lib lint FirebaseFirestore.podspec --allow-warnings --no-subspecs + + # pod lib lint to check build and warnings for static library build - only on cron jobs + - stage: test + env: + - PROJECT=Firebase PLATFORM=iOS METHOD=pod-lib-lint + before_install: + - ./scripts/if_cron.sh ./scripts/install_prereqs.sh + script: + - ./scripts/if_cron.sh bundle exec pod lib lint FirebaseCore.podspec --use-libraries + - ./scripts/if_cron.sh bundle exec pod lib lint FirebaseAuth.podspec --use-libraries + - ./scripts/if_cron.sh bundle exec pod lib lint FirebaseDatabase.podspec --use-libraries + # The Protobuf dependency of FirebaseMessaging has warnings with --use-libraries +# - ./scripts/if_cron.sh bundle exec pod lib lint FirebaseMessaging.podspec --use-libraries --allow-warnings + - ./scripts/if_cron.sh bundle exec pod lib lint FirebaseStorage.podspec --use-libraries + - ./scripts/if_cron.sh bundle exec pod lib lint FirebaseFunctions.podspec --use-libraries + + - stage: test + env: + - PROJECT=Firestore PLATFORM=iOS METHOD=pod-lib-lint + before_install: + - ./scripts/if_cron.sh ./scripts/install_prereqs.sh + script: + # TBD - non-portable path warnings + # The travis_wait is necessary because the command takes more than 10 minutes. + - travis_wait ./scripts/if_cron.sh bundle exec pod lib lint FirebaseFirestore.podspec --use-libraries --allow-warnings --no-subspecs + # Alternative platforms - stage: test @@ -65,7 +104,6 @@ jobs: - PROJECT=Firestore PLATFORM=macOS METHOD=cmake before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - - ./scripts/if_changed.sh ./scripts/pod_install.sh script: - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD @@ -73,19 +111,17 @@ jobs: - stage: test env: - - PROJECT=Firebase PLATFORM=macOS + - PROJECT=Firebase PLATFORM=macOS METHOD=xcodebuild before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - - ./scripts/if_changed.sh ./scripts/pod_install.sh script: - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - stage: test env: - - PROJECT=Firebase PLATFORM=tvOS + - PROJECT=Firebase PLATFORM=tvOS METHOD=xcodebuild before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - - ./scripts/if_changed.sh ./scripts/pod_install.sh script: - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM @@ -96,7 +132,6 @@ jobs: - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild SANITIZERS=asan before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - - ./scripts/if_changed.sh ./scripts/pod_install.sh script: - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD @@ -105,7 +140,6 @@ jobs: - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild SANITIZERS=tsan before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - - ./scripts/if_changed.sh ./scripts/pod_install.sh script: - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD @@ -118,7 +152,6 @@ jobs: - PROJECT=Firestore PLATFORM=macOS METHOD=cmake SANITIZERS=asan before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - - ./scripts/if_changed.sh ./scripts/pod_install.sh script: - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD @@ -127,7 +160,6 @@ jobs: - PROJECT=Firestore PLATFORM=macOS METHOD=cmake SANITIZERS=tsan before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - - ./scripts/if_changed.sh ./scripts/pod_install.sh script: - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD diff --git a/Example/Auth/ApiTests/FirebaseAuthApiTests.m b/Example/Auth/ApiTests/FirebaseAuthApiTests.m index 741814c..d4f4da2 100644 --- a/Example/Auth/ApiTests/FirebaseAuthApiTests.m +++ b/Example/Auth/ApiTests/FirebaseAuthApiTests.m @@ -130,7 +130,7 @@ static NSTimeInterval const kExpectationsTimeout = 10; [self expectationWithDescription:@"Created account with email and password."]; [auth createUserWithEmail:kTestingEmailToCreateUser password:@"password" - completion:^(FIRUser *user, NSError *error) { + completion:^(FIRAuthDataResult *result, NSError *error) { if (error) { NSLog(@"createUserWithEmail has error: %@", error); } @@ -163,7 +163,7 @@ static NSTimeInterval const kExpectationsTimeout = 10; [self expectationWithDescription:@"Created account with email and password."]; [auth createUserWithEmail:kTestingEmailToCreateUser password:@"password" - completion:^(FIRUser *user, NSError *error) { + completion:^(FIRAuthDataResult *user, NSError *error) { apiError = error; [expectation fulfill]; }]; @@ -240,7 +240,7 @@ static NSTimeInterval const kExpectationsTimeout = 10; [self expectationWithDescription:@"Signed in existing account with email and password."]; [auth signInWithEmail:kExistingTestingEmailToSignIn password:@"password" - completion:^(FIRUser *user, NSError *error) { + completion:^(FIRAuthDataResult *user, NSError *error) { if (error) { NSLog(@"Signing in existing account has error: %@", error); } @@ -277,7 +277,7 @@ static NSTimeInterval const kExpectationsTimeout = 10; [self expectationWithDescription:@"CustomAuthToken sign-in finished."]; [auth signInWithCustomToken:customToken - completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { if (error) { NSLog(@"Valid token sign in error: %@", error); } @@ -314,7 +314,7 @@ static NSTimeInterval const kExpectationsTimeout = 10; __block NSError *apiError; [auth signInWithCustomToken:customToken - completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { if (error) { apiError = error; } @@ -349,7 +349,7 @@ static NSTimeInterval const kExpectationsTimeout = 10; [self expectationWithDescription:@"CustomAuthToken sign-in finished."]; __block NSError *rpcError; [auth signInWithCustomToken:customToken - completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { if (error) { rpcError = error; } @@ -390,7 +390,7 @@ static NSTimeInterval const kExpectationsTimeout = 10; [self expectationWithDescription:@"Invalid CustomAuthToken sign-in finished."]; [auth signInWithCustomToken:kInvalidCustomToken - completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { + completion:^(FIRAuthDataResult *_Nullable result, NSError *_Nullable error) { XCTAssertEqualObjects(error.localizedDescription, kInvalidTokenErrorMessage); [expectation fulfill]; @@ -583,7 +583,7 @@ static NSTimeInterval const kExpectationsTimeout = 10; XCTestExpectation *expectation = [self expectationWithDescription:@"Anonymousy sign-in finished."]; - [auth signInAnonymouslyWithCompletion:^(FIRUser *user, NSError *error) { + [auth signInAnonymouslyWithCompletion:^(FIRAuthDataResult *result, NSError *error) { if (error) { NSLog(@"Anonymousy sign in error: %@", error); } diff --git a/Example/Core/Tests/FIRAppTest.m b/Example/Core/Tests/FIRAppTest.m index 6825e6a..abf1d38 100644 --- a/Example/Core/Tests/FIRAppTest.m +++ b/Example/Core/Tests/FIRAppTest.m @@ -42,6 +42,9 @@ NSString *const kFIRTestAppName2 = @"test-app-name-2"; + (BOOL)validateAppIDFormat:(NSString *)appID withVersion:(NSString *)version; + (BOOL)validateAppIDFingerprint:(NSString *)appID withVersion:(NSString *)version; ++ (nullable NSNumber *)readDataCollectionSwitchFromPlist; ++ (nullable NSNumber *)readDataCollectionSwitchFromUserDefaultsForApp:(FIRApp *)app; + @end @interface FIRAppTest : FIRTestCase @@ -552,6 +555,133 @@ NSString *const kFIRTestAppName2 = @"test-app-name-2"; [FIRApp validateAppIDFingerprint:@"1:1337:ios:deadbeef:ab" withVersion:kGoodVersionV1]); } +#pragma mark - Automatic Data Collection Tests + +- (void)testGlobalDataCollectionNoFlags { + // Test: No flags set. + [FIRApp configure]; + OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(nil); + OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY]) + .andReturn(nil); + + XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled); +} + +- (void)testGlobalDataCollectionPlistSetEnabled { + // Test: Plist set to enabled, no override. + [FIRApp configure]; + OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(@YES); + OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY]) + .andReturn(nil); + + XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled); +} + +- (void)testGlobalDataCollectionPlistSetDisabled { + // Test: Plist set to disabled, no override. + [FIRApp configure]; + OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(@NO); + OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY]) + .andReturn(nil); + + XCTAssertFalse([FIRApp defaultApp].isAutomaticDataCollectionEnabled); +} + +- (void)testGlobalDataCollectionUserSpecifiedEnabled { + // Test: User specified as enabled, no plist value. + [FIRApp configure]; + OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(nil); + OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY]) + .andReturn(@YES); + + XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled); +} + +- (void)testGlobalDataCollectionUserSpecifiedDisabled { + // Test: User specified as disabled, no plist value. + [FIRApp configure]; + OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(nil); + OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY]) + .andReturn(@NO); + + XCTAssertFalse([FIRApp defaultApp].isAutomaticDataCollectionEnabled); +} + +- (void)testGlobalDataCollectionUserOverriddenEnabled { + // Test: User specified as enabled, with plist set as disabled. + [FIRApp configure]; + OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(@NO); + OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY]) + .andReturn(@YES); + + XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled); +} + +- (void)testGlobalDataCollectionUserOverriddenDisabled { + // Test: User specified as disabled, with plist set as enabled. + [FIRApp configure]; + OCMStub([self.appClassMock readDataCollectionSwitchFromPlist]).andReturn(@YES); + OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY]) + .andReturn(@NO); + + XCTAssertFalse([FIRApp defaultApp].isAutomaticDataCollectionEnabled); +} + +- (void)testGlobalDataCollectionWriteToDefaults { + id defaultsMock = OCMPartialMock([NSUserDefaults standardUserDefaults]); + [FIRApp configure]; + + FIRApp *app = [FIRApp defaultApp]; + app.automaticDataCollectionEnabled = YES; + NSString *key = + [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, app.name]; + OCMVerify([defaultsMock setObject:@YES forKey:key]); + + [FIRApp defaultApp].automaticDataCollectionEnabled = NO; + OCMVerify([defaultsMock setObject:@NO forKey:key]); + + [defaultsMock stopMocking]; +} + +- (void)testGlobalDataCollectionClearedAfterDelete { + // Configure and disable data collection for the default FIRApp. + [FIRApp configure]; + FIRApp *app = [FIRApp defaultApp]; + app.automaticDataCollectionEnabled = NO; + XCTAssertFalse(app.isAutomaticDataCollectionEnabled); + + // Delete the app, and verify that the switch was reset. + XCTestExpectation *deleteFinished = + [self expectationWithDescription:@"The app should successfully delete."]; + [app deleteApp:^(BOOL success) { + if (success) { + [deleteFinished fulfill]; + } + }]; + + // Wait for the delete to complete. + [self waitForExpectations:@[ deleteFinished ] timeout:1]; + + // Set up the default app again, and check the data collection flag. + [FIRApp configure]; + XCTAssertTrue([FIRApp defaultApp].isAutomaticDataCollectionEnabled); +} + +- (void)testGlobalDataCollectionNoDiagnosticsSent { + [FIRApp configure]; + + // Stub out reading from user defaults since stubbing out the BOOL has issues. If the data + // collection switch is disabled, the `sendLogs` call should return immediately and not fire a + // notification. + OCMStub([self.appClassMock readDataCollectionSwitchFromUserDefaultsForApp:OCMOCK_ANY]) + .andReturn(@NO); + OCMReject([self.notificationCenterMock postNotificationName:kFIRAppDiagnosticsNotification + object:OCMOCK_ANY + userInfo:OCMOCK_ANY]); + NSError *error = [NSError errorWithDomain:@"com.firebase" code:42 userInfo:nil]; + [[FIRApp defaultApp] sendLogsWithServiceName:@"Service" version:@"Version" error:error]; +} + #pragma mark - Internal Methods - (void)testAuthGetUID { diff --git a/Example/Core/Tests/FIROptionsTest.m b/Example/Core/Tests/FIROptionsTest.m index 20aec94..064745a 100644 --- a/Example/Core/Tests/FIROptionsTest.m +++ b/Example/Core/Tests/FIROptionsTest.m @@ -17,6 +17,7 @@ #import <FirebaseCore/FIRAppInternal.h> #import <FirebaseCore/FIRBundleUtil.h> #import <FirebaseCore/FIROptionsInternal.h> +#import <FirebaseCore/FIRVersion.h> extern NSString *const kFIRIsMeasurementEnabled; extern NSString *const kFIRIsAnalyticsCollectionEnabled; @@ -437,4 +438,13 @@ extern NSString *const kFIRLibraryVersionID; XCTAssertEqual(numberOfMatches, 1, @"Incorrect library version format."); } +- (void)testVersionConsistency { + const char *versionString = [kFIRLibraryVersionID UTF8String]; + int major = versionString[0] - '0'; + int minor = (versionString[1] - '0') * 10 + versionString[2] - '0'; + int patch = (versionString[3] - '0') * 10 + versionString[4] - '0'; + NSString *str = [NSString stringWithFormat:@"%d.%d.%d", major, minor, patch]; + XCTAssertEqualObjects(str, [NSString stringWithUTF8String:(const char *)FIRCoreVersionString]); +} + @end diff --git a/Example/Firebase.xcodeproj/project.pbxproj b/Example/Firebase.xcodeproj/project.pbxproj index 26e0c33..f924ed4 100644 --- a/Example/Firebase.xcodeproj/project.pbxproj +++ b/Example/Firebase.xcodeproj/project.pbxproj @@ -4707,6 +4707,10 @@ INFOPLIST_FILE = "Messaging/Sample/iOS/Messaging-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseMessagingSample.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4732,6 +4736,10 @@ INFOPLIST_FILE = "Messaging/Sample/iOS/Messaging-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = com.google.FirebaseMessagingSample.dev; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -5929,6 +5937,10 @@ INFOPLIST_FILE = "$(SRCROOT)/Database/App/iOS/Database-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Database-Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -5948,6 +5960,10 @@ INFOPLIST_FILE = "$(SRCROOT)/Database/App/iOS/Database-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Database-Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -6047,6 +6063,10 @@ INFOPLIST_FILE = "$(SRCROOT)/Auth/App/iOS/Auth-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -6069,6 +6089,10 @@ INFOPLIST_FILE = "$(SRCROOT)/Auth/App/iOS/Auth-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Auth-Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -6461,6 +6485,10 @@ ); INFOPLIST_FILE = "$(SRCROOT)/Storage/App/iOS/Storage-Info.plist"; MODULE_NAME = ExampleApp; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Storage-Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; @@ -6482,6 +6510,10 @@ ); INFOPLIST_FILE = "$(SRCROOT)/Storage/App/iOS/Storage-Info.plist"; MODULE_NAME = ExampleApp; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Storage-Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; @@ -6543,6 +6575,10 @@ INFOPLIST_FILE = "$(SRCROOT)/Core/App/iOS/Core-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Core-Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -6560,6 +6596,10 @@ INFOPLIST_FILE = "$(SRCROOT)/Core/App/iOS/Core-Info.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "com.google.Core-Example-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; }; diff --git a/Example/Messaging/Tests/FIRMessagingTest.m b/Example/Messaging/Tests/FIRMessagingTest.m index adc830d..61ff136 100644 --- a/Example/Messaging/Tests/FIRMessagingTest.m +++ b/Example/Messaging/Tests/FIRMessagingTest.m @@ -75,6 +75,46 @@ extern NSString *const kFIRMessagingFCMTokenFetchAPNSOption; XCTAssertTrue(_messaging.isAutoInitEnabled); } +- (void)testAutoInitEnableFlagOverrideGlobalTrue { + OCMStub([self.mockMessaging isGlobalAutomaticDataCollectionEnabled]).andReturn(YES); + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:kFIRMessagingPlistAutoInitEnabled]).andReturn(nil); + XCTAssertTrue(self.messaging.isAutoInitEnabled); + + self.messaging.autoInitEnabled = NO; + XCTAssertFalse(self.messaging.isAutoInitEnabled); + [bundleMock stopMocking]; +} + +- (void)testAutoInitEnableFlagOverrideGlobalFalse { + OCMStub([self.mockMessaging isGlobalAutomaticDataCollectionEnabled]).andReturn(YES); + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:kFIRMessagingPlistAutoInitEnabled]).andReturn(nil); + XCTAssertTrue(self.messaging.isAutoInitEnabled); + + self.messaging.autoInitEnabled = NO; + XCTAssertFalse(self.messaging.isAutoInitEnabled); + [bundleMock stopMocking]; +} + +- (void)testAutoInitEnableGlobalDefaultTrue { + OCMStub([self.mockMessaging isGlobalAutomaticDataCollectionEnabled]).andReturn(YES); + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:kFIRMessagingPlistAutoInitEnabled]).andReturn(nil); + + XCTAssertTrue(self.messaging.isAutoInitEnabled); + [bundleMock stopMocking]; +} + +- (void)testAutoInitEnableGlobalDefaultFalse { + OCMStub([self.mockMessaging isGlobalAutomaticDataCollectionEnabled]).andReturn(NO); + id bundleMock = OCMPartialMock([NSBundle mainBundle]); + OCMStub([bundleMock objectForInfoDictionaryKey:kFIRMessagingPlistAutoInitEnabled]).andReturn(nil); + + XCTAssertFalse(self.messaging.isAutoInitEnabled); + [bundleMock stopMocking]; +} + #pragma mark - Direct Channel Establishment Testing // Should connect with valid token and application in foreground diff --git a/Firebase/Auth/CHANGELOG.md b/Firebase/Auth/CHANGELOG.md index 858d2a7..5a3fcef 100644 --- a/Firebase/Auth/CHANGELOG.md +++ b/Firebase/Auth/CHANGELOG.md @@ -1,3 +1,10 @@ +# v5.0.0 +- Adds APIs for phone Auth testing to bypass the verification flow (#1192). +- Changes the callback block signature for sign in and create user methods + to provide an AuthDataResult that includes the user and user info (#1123, #1186). +- Removes GoogleToolboxForMac dependency (#1175). +- Removes misc. deprecated APIs (#1188, #1200) + # v4.6.1 - Fixes crash which occurred when certain Firebase IDTokens were being parsed (#1076). diff --git a/Firebase/Auth/Source/FIRAuthErrorUtils.m b/Firebase/Auth/Source/FIRAuthErrorUtils.m index f4dbb94..62c569c 100644 --- a/Firebase/Auth/Source/FIRAuthErrorUtils.m +++ b/Firebase/Auth/Source/FIRAuthErrorUtils.m @@ -357,7 +357,7 @@ static NSString *const kFIRAuthErrorMessageNotificationNotForwarded = @"If app d */ static NSString *const kFIRAuthErrorMessageAppNotVerified = @"Firebase could not retrieve the " "silent push notification and therefore could not verify your app. Ensure that you configured " - "your app correctly to recieve push notifications."; + "your app correctly to receive push notifications."; /** @var kFIRAuthErrorMessageCaptchaCheckFailed @brief Message for @c FIRAuthErrorCodeCaptchaCheckFailed error code. diff --git a/Firebase/Auth/Source/Public/FIRAuth.h b/Firebase/Auth/Source/Public/FIRAuth.h index a3ed960..cfc59b1 100644 --- a/Firebase/Auth/Source/Public/FIRAuth.h +++ b/Firebase/Auth/Source/Public/FIRAuth.h @@ -410,20 +410,6 @@ NS_SWIFT_NAME(Auth) @param password The user's password. @param completion Optionally; a block which is invoked when the sign in flow finishes, or is canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that email and password - accounts are not enabled. Enable them in the Auth section of the - Firebase console. - + `FIRAuthErrorCodeUserDisabled` - Indicates the user's account is disabled. - + `FIRAuthErrorCodeWrongPassword` - Indicates the user attempted - sign in with an incorrect password. - + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - - - - @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. */ - (void)signInAndRetrieveDataWithEmail:(NSString *)email password:(NSString *)password @@ -543,21 +529,6 @@ NS_SWIFT_NAME(Auth) `signInAnonymously(Completion:)` for Swift instead. @param completion Optionally; a block which is invoked when the sign in finishes, or is canceled. Invoked asynchronously on the main thread in the future. - - @remarks If there is already an anonymous user signed in, that user will be returned instead. - If there is any other existing user signed in, that user will be signed out. - - @remarks Possible error codes: - - + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that anonymous accounts are - not enabled. Enable them in the Auth section of the Firebase console. - - - @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. - - @remarks This method will only exist until the next major Firebase release following 4.x.x. - After the next major release the method `signInAnonymouslyWithCompletion` will support the - `FIRAuthDataResultCallback`. */ - (void)signInAnonymouslyAndRetrieveDataWithCompletion: (nullable FIRAuthDataResultCallback)completion @@ -592,22 +563,6 @@ NS_SWIFT_NAME(Auth) @param token A self-signed custom auth token. @param completion Optionally; a block which is invoked when the sign in finishes, or is canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `FIRAuthErrorCodeInvalidCustomToken` - Indicates a validation error with - the custom token. - - + `FIRAuthErrorCodeCustomTokenMismatch` - Indicates the service account and the API key - belong to different projects. - - - - @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. - - @remarks This method will only exist until the next major Firebase release following 4.x.x. - After the next major release the method `createUserWithEmail:password:completion:` will - support the `FIRAuthDataResultCallback`. */ - (void)signInAndRetrieveDataWithCustomToken:(NSString *)token completion:(nullable FIRAuthDataResultCallback)completion @@ -650,24 +605,6 @@ NS_SWIFT_NAME(Auth) @param password The user's desired password. @param completion Optionally; a block which is invoked when the sign up flow finishes, or is canceled. Invoked asynchronously on the main thread in the future. - - @remarks Possible error codes: - - + `FIRAuthErrorCodeInvalidEmail` - Indicates the email address is malformed. - + `FIRAuthErrorCodeEmailAlreadyInUse` - Indicates the email used to attempt sign up - already exists. Call fetchProvidersForEmail to check which sign-in mechanisms the user - used, and prompt the user to sign in with one of those. - + `FIRAuthErrorCodeOperationNotAllowed` - Indicates that email and password accounts - are not enabled. Enable them in the Auth section of the Firebase console. - + `FIRAuthErrorCodeWeakPassword` - Indicates an attempt to set a password that is - considered too weak. The NSLocalizedFailureReasonErrorKey field in the NSError.userInfo - dictionary object will contain more detailed explanation that can be shown to the user. - - @remarks See `FIRAuthErrors` for a list of error codes that are common to all API methods. - - @remarks This method will only exist until the next major Firebase release following 4.x.x. - After the next major release the method `createUserWithEmail:password:completion:` will - support the `FIRAuthDataResultCallback`. */ - (void)createUserAndRetrieveDataWithEmail:(NSString *)email password:(NSString *)password diff --git a/Firebase/Core/FIRApp.m b/Firebase/Core/FIRApp.m index 3d05f12..717da4e 100644 --- a/Firebase/Core/FIRApp.m +++ b/Firebase/Core/FIRApp.m @@ -46,6 +46,11 @@ NSString *const kFIRAppIsDefaultAppKey = @"FIRAppIsDefaultAppKey"; NSString *const kFIRAppNameKey = @"FIRAppNameKey"; NSString *const kFIRGoogleAppIDKey = @"FIRGoogleAppIDKey"; +NSString *const kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat = + @"/google/firebase/global_data_collection_enabled:%@"; +NSString *const kFIRGlobalAppDataCollectionEnabledPlistKey = + @"FirebaseAutomaticDataCollectionEnabled"; + NSString *const kFIRAppDiagnosticsNotification = @"FIRAppDiagnosticsNotification"; NSString *const kFIRAppDiagnosticsConfigurationTypeKey = @"ConfigType"; @@ -227,6 +232,7 @@ static NSMutableDictionary *sLibraryVersions; if (sAllApps && sAllApps[self.name]) { FIRLogDebug(kFIRLoggerCore, @"I-COR000006", @"Deleting app named %@", self.name); [sAllApps removeObjectForKey:self.name]; + [self clearDataCollectionSwitchFromUserDefaults]; if ([self.name isEqualToString:kFIRDefaultAppName]) { sDefaultApp = nil; } @@ -332,6 +338,30 @@ static NSMutableDictionary *sLibraryVersions; return [_options copy]; } +- (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled { + NSString *key = + [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, self.name]; + [[NSUserDefaults standardUserDefaults] setBool:automaticDataCollectionEnabled forKey:key]; +} + +- (BOOL)isAutomaticDataCollectionEnabled { + // Check if it's been manually set before in code, and use that as the higher priority value. + NSNumber *defaultsObject = [[self class] readDataCollectionSwitchFromUserDefaultsForApp:self]; + if (defaultsObject) { + return [defaultsObject boolValue]; + } + + // Read the Info.plist to see if the flag is set. If it's not set, it should default to `YES`. + // As per the implementation of `readDataCollectionSwitchFromPlist`, it's a cached value and has + // no performance impact calling multiple times. + NSNumber *collectionEnabledPlistValue = [[self class] readDataCollectionSwitchFromPlist]; + if (collectionEnabledPlistValue) { + return [collectionEnabledPlistValue boolValue]; + } + + return YES; +} + #pragma mark - private + (void)sendNotificationsToSDKs:(FIRApp *)app { @@ -613,11 +643,64 @@ static NSMutableDictionary *sLibraryVersions; } // end App ID validation -#pragma mark + +#pragma mark - Reading From Plist & User Defaults + +/** + * Clears the data collection switch from the standard NSUserDefaults for easier testing and + * readability. + */ +- (void)clearDataCollectionSwitchFromUserDefaults { + NSString *key = + [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, self.name]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; +} + +/** + * Reads the data collection switch from the standard NSUserDefaults for easier testing and + * readability. + */ ++ (nullable NSNumber *)readDataCollectionSwitchFromUserDefaultsForApp:(FIRApp *)app { + // Read the object in user defaults, and only return if it's an NSNumber. + NSString *key = + [NSString stringWithFormat:kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat, app.name]; + id collectionEnabledDefaultsObject = [[NSUserDefaults standardUserDefaults] objectForKey:key]; + if ([collectionEnabledDefaultsObject isKindOfClass:[NSNumber class]]) { + return collectionEnabledDefaultsObject; + } + + return nil; +} + +/** + * Reads the data collection switch from the Info.plist for easier testing and readability. Will + * only read once from the plist and return the cached value. + */ ++ (nullable NSNumber *)readDataCollectionSwitchFromPlist { + static NSNumber *collectionEnabledPlistObject; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Read the data from the `Info.plist`, only assign it if it's there and an NSNumber. + id plistValue = [[NSBundle mainBundle] + objectForInfoDictionaryKey:kFIRGlobalAppDataCollectionEnabledPlistKey]; + if (plistValue && [plistValue isKindOfClass:[NSNumber class]]) { + collectionEnabledPlistObject = (NSNumber *)plistValue; + } + }); + + return collectionEnabledPlistObject; +} + +#pragma mark - Sending Logs - (void)sendLogsWithServiceName:(NSString *)serviceName version:(NSString *)version error:(NSError *)error { + // If the user has manually turned off data collection, return and don't send logs. + if (![self isAutomaticDataCollectionEnabled]) { + return; + } + NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithDictionary:@{ kFIRAppDiagnosticsConfigurationTypeKey : @(FIRConfigTypeSDK), kFIRAppDiagnosticsSDKNameKey : serviceName, diff --git a/Firebase/Core/Private/FIRAppInternal.h b/Firebase/Core/Private/FIRAppInternal.h index b7cf5e8..66979eb 100644 --- a/Firebase/Core/Private/FIRAppInternal.h +++ b/Firebase/Core/Private/FIRAppInternal.h @@ -60,6 +60,22 @@ extern NSString *const kFIRAppIsDefaultAppKey; extern NSString *const kFIRAppNameKey; extern NSString *const kFIRGoogleAppIDKey; +/** + * The format string for the User Defaults key used for storing the data collection enabled flag. + * This includes formatting to append the Firebase App's name. + */ +extern NSString *const kFIRGlobalAppDataCollectionEnabledDefaultsKeyFormat; + +/** + * The plist key used for storing the data collection enabled flag. + */ +extern NSString *const kFIRGlobalAppDataCollectionEnabledPlistKey; + +/** + * A notification fired containing diagnostic information when SDK errors occur. + */ +extern NSString *const kFIRAppDiagnosticsNotification; + /** @var FIRAuthStateDidChangeInternalNotification @brief The name of the @c NSNotificationCenter notification which is posted when the auth state changes (e.g. a new token has been produced, a user logs in or out). The object parameter of @@ -181,6 +197,18 @@ typedef NSString *_Nullable (^FIRAppGetUIDImplementation)(void); */ - (nullable NSString *)getUID; +/** + * WARNING: THIS SETTING DOES NOT WORK YET. IT WILL BE MOVED TO THE PUBLIC HEADER ONCE ALL SDKS + * CONFORM TO THIS PREFERENCE. DO NOT RELY ON IT. + * + * Gets or sets whether automatic data collection is enabled for all products. Defaults to `YES` + * unless `FirebaseAutomaticDataCollectionEnabled` is set to `NO` in your app's Info.plist. This + * value is persisted across runs of the app so that it can be set once when users have consented to + * collection. + */ +@property(nonatomic, readwrite, getter=isAutomaticDataCollectionEnabled) + BOOL automaticDataCollectionEnabled; + @end NS_ASSUME_NONNULL_END diff --git a/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h b/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h index 4ff285b..c2d3871 100644 --- a/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h +++ b/Firebase/Database/Api/Private/FIRDataSnapshot_Private.h @@ -14,6 +14,7 @@ * limitations under the License. */ +#import "FIRDataSnapshot.h" #import "FIndexedNode.h" #import "FTypedefs_Private.h" diff --git a/Firebase/Messaging/FIRMessaging+FIRApp.m b/Firebase/Messaging/FIRMessaging+FIRApp.m index 58ae3af..d48a3b4 100644 --- a/Firebase/Messaging/FIRMessaging+FIRApp.m +++ b/Firebase/Messaging/FIRMessaging+FIRApp.m @@ -72,6 +72,7 @@ } self.fcmSenderID = [options.GCMSenderID copy]; + self.globalAutomaticDataCollectionEnabled = [app isAutomaticDataCollectionEnabled]; // Swizzle remote-notification-related methods (app delegate and UNUserNotificationCenter) if ([FIRMessagingRemoteNotificationsProxy canSwizzleMethods]) { diff --git a/Firebase/Messaging/FIRMessaging.m b/Firebase/Messaging/FIRMessaging.m index 339bd7d..6222420 100644 --- a/Firebase/Messaging/FIRMessaging.m +++ b/Firebase/Messaging/FIRMessaging.m @@ -75,7 +75,7 @@ NSString *const kFIRMessagingUserDefaultsKeyAutoInitEnabled = NSString *const kFIRMessagingAPNSTokenType = @"APNSTokenType"; // APNS Token type key stored in user info. -static NSString *const kFIRMessagingPlistAutoInitEnabled = +NSString *const kFIRMessagingPlistAutoInitEnabled = @"FirebaseMessagingAutoInitEnabled"; // Auto Init Enabled key stored in Info.plist @interface FIRMessagingMessageInfo () @@ -471,8 +471,9 @@ static NSString *const kFIRMessagingPlistAutoInitEnabled = if (isAutoInitEnabledObject) { return [isAutoInitEnabledObject boolValue]; } - // If none of above exists, we default assume FCM auto init is enabled. - return YES; + + // If none of above exists, we default to the global switch that comes from FIRApp. + return self.isGlobalAutomaticDataCollectionEnabled; } - (void)setAutoInitEnabled:(BOOL)autoInitEnabled { diff --git a/Firebase/Messaging/FIRMessaging_Private.h b/Firebase/Messaging/FIRMessaging_Private.h index 46daee0..6bac99d 100644 --- a/Firebase/Messaging/FIRMessaging_Private.h +++ b/Firebase/Messaging/FIRMessaging_Private.h @@ -25,6 +25,7 @@ typedef NS_ENUM(int8_t, FIRMessagingNetworkStatus) { kFIRMessagingReachabilityReachableViaWWAN, }; +FOUNDATION_EXPORT NSString *const kFIRMessagingPlistAutoInitEnabled; FOUNDATION_EXPORT NSString *const kFIRMessagingUserDefaultsKeyAutoInitEnabled; @interface FIRMessagingRemoteMessage () @@ -37,6 +38,9 @@ FOUNDATION_EXPORT NSString *const kFIRMessagingUserDefaultsKeyAutoInitEnabled; #pragma mark - Private API +// The data collection flag from Core. +@property(nonatomic, readwrite, getter=isGlobalAutomaticDataCollectionEnabled) BOOL globalAutomaticDataCollectionEnabled; + - (NSString *)defaultFcmToken; - (FIRMessagingClient *)client; - (FIRMessagingPubSub *)pubsub; diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index ad10553..8e6d7de 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -35,7 +35,6 @@ device, and it is completely free. 'GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1 ' + 'FIRMessaging_LIB_VERSION=' + String(s.version) } - s.framework = 'AddressBook' s.framework = 'SystemConfiguration' s.dependency 'FirebaseCore', '~> 5.0' s.dependency 'FirebaseInstanceID', '~> 3.0' diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 5543325..70bab4e 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,4 +1,9 @@ # Unreleased +- [fixed] Fixed an issue where `FirestoreSettings` would accept a concurrent + dispatch queue, but this configuration would trigger an assertion failure. + Passing a concurrent dispatch queue should now work correctly (#988). + +# v0.12.0 - [changed] Replaced the `DocumentListenOptions` object with a simple boolean. Instead of calling `addSnapshotListener(options: DocumentListenOptions.includeMetadataChanges(true))` @@ -24,6 +29,8 @@ Query.getDocuments() should fetch from server only, cache only, or attempt server and fall back to the cache (which was the only option previously, and is now the default.) +- [feature] Added new `mergeFields:(NSArray<id>*)` override for `set()` + which allows merging of a reduced subset of fields. # v0.11.0 - [fixed] Fixed a regression in the Firebase iOS SDK release 4.11.0 that could diff --git a/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard b/Firestore/Example/App/iOS/Base.lproj/LaunchScreen.storyboard index 66a7681..66a7681 100644 --- a/Firestore/Example/Firestore/Base.lproj/LaunchScreen.storyboard +++ b/Firestore/Example/App/iOS/Base.lproj/LaunchScreen.storyboard diff --git a/Firestore/Example/Firestore/Base.lproj/Main.storyboard b/Firestore/Example/App/iOS/Base.lproj/Main.storyboard index d164a23..d164a23 100644 --- a/Firestore/Example/Firestore/Base.lproj/Main.storyboard +++ b/Firestore/Example/App/iOS/Base.lproj/Main.storyboard diff --git a/Firestore/Example/Firestore/FIRAppDelegate.h b/Firestore/Example/App/iOS/FIRAppDelegate.h index 1eb5040..1eb5040 100644 --- a/Firestore/Example/Firestore/FIRAppDelegate.h +++ b/Firestore/Example/App/iOS/FIRAppDelegate.h diff --git a/Firestore/Example/Firestore/FIRAppDelegate.m b/Firestore/Example/App/iOS/FIRAppDelegate.m index 12ca249..12ca249 100644 --- a/Firestore/Example/Firestore/FIRAppDelegate.m +++ b/Firestore/Example/App/iOS/FIRAppDelegate.m diff --git a/Firestore/Example/Firestore/FIRViewController.h b/Firestore/Example/App/iOS/FIRViewController.h index 64b4b74..64b4b74 100644 --- a/Firestore/Example/Firestore/FIRViewController.h +++ b/Firestore/Example/App/iOS/FIRViewController.h diff --git a/Firestore/Example/Firestore/FIRViewController.m b/Firestore/Example/App/iOS/FIRViewController.m index cdad545..cdad545 100644 --- a/Firestore/Example/Firestore/FIRViewController.m +++ b/Firestore/Example/App/iOS/FIRViewController.m diff --git a/Firestore/Example/Firestore/Firestore-Info.plist b/Firestore/Example/App/iOS/Firestore-Info.plist index 7576a0d..7576a0d 100644 --- a/Firestore/Example/Firestore/Firestore-Info.plist +++ b/Firestore/Example/App/iOS/Firestore-Info.plist diff --git a/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json b/Firestore/Example/App/iOS/Images.xcassets/AppIcon.appiconset/Contents.json index d7070bc..d7070bc 100644 --- a/Firestore/Example/Firestore/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/Firestore/Example/App/iOS/Images.xcassets/AppIcon.appiconset/Contents.json diff --git a/Firestore/Example/Firestore/en.lproj/InfoPlist.strings b/Firestore/Example/App/iOS/en.lproj/InfoPlist.strings index 477b28f..477b28f 100644 --- a/Firestore/Example/Firestore/en.lproj/InfoPlist.strings +++ b/Firestore/Example/App/iOS/en.lproj/InfoPlist.strings diff --git a/Firestore/Example/Firestore/main.m b/Firestore/Example/App/iOS/main.m index 724fccf..724fccf 100644 --- a/Firestore/Example/Firestore/main.m +++ b/Firestore/Example/App/iOS/main.m diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 8aecc9f..ed75d8c 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -25,11 +25,12 @@ /* Begin PBXBuildFile section */ 132E3E53179DE287D875F3F2 /* FSTLevelDBTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */; }; - 347FDC6AA737A754541F7C8A /* Pods_Firestore_Tests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8525646842C83F703237BAA4 /* Pods_Firestore_Tests_iOS.framework */; }; 3B843E4C1F3A182900548890 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; 5436F32420008FAD006E51E3 /* string_printf_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5436F32320008FAD006E51E3 /* string_printf_test.cc */; }; + 54511E8E209805F8005BD28F /* hashing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54511E8D209805F8005BD28F /* hashing_test.cc */; }; 5467FB01203E5717009C9584 /* FIRFirestoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FAFF203E56F8009C9584 /* FIRFirestoreTests.mm */; }; 5467FB08203E6A44009C9584 /* app_testing.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FB07203E6A44009C9584 /* app_testing.mm */; }; + 546854AA20A36867004BDBD5 /* datastore_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 546854A820A36867004BDBD5 /* datastore_test.cc */; }; 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A531FC913E500713A1A /* secure_random_test.cc */; }; 54740A581FC914F000713A1A /* autoid_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A521FC913E500713A1A /* autoid_test.cc */; }; 54764FAF1FAA21B90085E60A /* FSTGoogleTestTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */; }; @@ -108,6 +109,17 @@ 5492E0CA2021557E00B64F25 /* FSTWatchChangeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0C52021557E00B64F25 /* FSTWatchChangeTests.mm */; }; 5495EB032040E90200EBA509 /* CodableGeoPointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5495EB022040E90200EBA509 /* CodableGeoPointTests.swift */; }; 54995F6F205B6E12004EFFA0 /* leveldb_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */; }; + 549CCA5020A36DBC00BCEB75 /* sorted_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4C20A36DBB00BCEB75 /* sorted_set_test.cc */; }; + 549CCA5120A36DBC00BCEB75 /* tree_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4D20A36DBB00BCEB75 /* tree_sorted_map_test.cc */; }; + 549CCA5220A36DBC00BCEB75 /* sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4E20A36DBB00BCEB75 /* sorted_map_test.cc */; }; + 549CCA5720A36E1F00BCEB75 /* field_mask_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5320A36E1F00BCEB75 /* field_mask_test.cc */; }; + 549CCA5920A36E1F00BCEB75 /* precondition_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5520A36E1F00BCEB75 /* precondition_test.cc */; }; + 54A0352620A3AED0003E0143 /* field_transform_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352320A3AEC3003E0143 /* field_transform_test.mm */; }; + 54A0352720A3AED0003E0143 /* transform_operations_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352220A3AEC3003E0143 /* transform_operations_test.mm */; }; + 54A0352A20A3B3BD003E0143 /* testutil.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352820A3B3BD003E0143 /* testutil.cc */; }; + 54A0352F20A3B3D8003E0143 /* status_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352C20A3B3D7003E0143 /* status_test.cc */; }; + 54A0353020A3B3D8003E0143 /* statusor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352D20A3B3D7003E0143 /* statusor_test.cc */; }; + 54A0353520A3D8CB003E0143 /* iterator_adaptors_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */; }; 54C2294F1FECABAE007D065B /* log_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54C2294E1FECABAE007D065B /* log_test.cc */; }; 54DA12A61F315EE100DD57A1 /* collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */; }; 54DA12A71F315EE100DD57A1 /* existence_filter_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129D1F315EE100DD57A1 /* existence_filter_spec_test.json */; }; @@ -120,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 */; }; + 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 */; }; 6003F592195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; @@ -137,6 +150,7 @@ 7346E61D20325C6900FD6CEF /* FSTDispatchQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7346E61C20325C6900FD6CEF /* FSTDispatchQueueTests.mm */; }; 73866AA12082B0A5009BB4FF /* FIRArrayTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */; }; 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; }; + 8C82D4D3F9AB63E79CC52DC8 /* Pods_Firestore_IntegrationTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */; }; AB356EF7200EA5EB0089B766 /* field_value_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB356EF6200EA5EB0089B766 /* field_value_test.cc */; }; AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CF82019382300D97691 /* target_id_generator_test.cc */; }; AB380CFE201A2F4500D97691 /* string_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CFC201A2EE200D97691 /* string_util_test.cc */; }; @@ -155,18 +169,22 @@ ABC1D7E42024AFDE00BA84F0 /* firebase_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = ABC1D7E22023CDC500BA84F0 /* firebase_credentials_provider_test.mm */; }; ABE6637A201FA81900ED349A /* database_id_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB71064B201FA60300344F18 /* database_id_test.cc */; }; ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; }; - AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */; }; B6152AD7202A53CB000E5744 /* document_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6152AD5202A5385000E5744 /* document_key_test.cc */; }; B65D34A9203C995B0076A5E1 /* FIRTimestampTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */; }; B686F2AF2023DDEE0028D6BE /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; }; B686F2B22025000D0028D6BE /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; }; - C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */; }; - CF08376B68945A0BB332D0C8 /* Pods_Firestore_IntegrationTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4EEE10E8E59CC91309335CA /* Pods_Firestore_IntegrationTests_iOS.framework */; }; - D9AF2279747DE7213156646C /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E5F50EF80014608B1868944 /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */; }; + B6FB467D208E9D3C00554BA2 /* async_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB467B208E9A8200554BA2 /* async_queue_test.cc */; }; + B6FB4684208EA0EC00554BA2 /* async_queue_libdispatch_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4680208EA0BE00554BA2 /* async_queue_libdispatch_test.mm */; }; + B6FB4685208EA0F000554BA2 /* async_queue_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4681208EA0BE00554BA2 /* async_queue_std_test.cc */; }; + B6FB468E208F9BAB00554BA2 /* executor_libdispatch_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.mm */; }; + B6FB468F208F9BAE00554BA2 /* executor_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4687208F9B9100554BA2 /* executor_std_test.cc */; }; + 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 */; }; + 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 */; }; DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; - DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */; }; DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */ = {isa = PBXBuildFile; fileRef = DE03B3621F215E1600A30B9C /* CAcert.pem */; }; DE0761F81F2FE68D003233AF /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0761F61F2FE68D003233AF /* main.swift */; }; @@ -174,7 +192,6 @@ DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */; }; DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */; }; DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */; }; - F06EE8EB234BBE8B7898D5EE /* Pods_Firestore_Example_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 24A6BEC38BF31BC4BF0E9DA7 /* Pods_Firestore_Example_iOS.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -230,27 +247,27 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.release.xcconfig"; sourceTree = "<group>"; }; - 0E5F50EF80014608B1868944 /* 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; }; + 05C3D82261C3BE976889FF09 /* Pods-Firestore_Example_iOS-SwiftBuildTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-SwiftBuildTest.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-SwiftBuildTest/Pods-Firestore_Example_iOS-SwiftBuildTest.debug.xcconfig"; sourceTree = "<group>"; }; + 11984BA0A99D7A7ABA5B0D90 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; sourceTree = "<group>"; }; + 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>"; }; - 245812330F6A31632BB4B623 /* Pods_Firestore_Example_Firestore_SwiftTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_Firestore_SwiftTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 24A6BEC38BF31BC4BF0E9DA7 /* Pods_Firestore_Example_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SwiftBuildTest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 2B50B3A0DF77100EEE887891 /* Pods_Firestore_Tests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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>"; }; - 3C7CE22C50805C4A854C73A1 /* 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>"; }; - 3F422FFBDA6E79396E2FB594 /* Pods-Firestore_Example-Firestore_SwiftTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example-Firestore_SwiftTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example-Firestore_SwiftTests_iOS/Pods-Firestore_Example-Firestore_SwiftTests_iOS.debug.xcconfig"; sourceTree = "<group>"; }; - 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.debug.xcconfig"; sourceTree = "<group>"; }; - 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.release.xcconfig"; sourceTree = "<group>"; }; - 5436F32320008FAD006E51E3 /* string_printf_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = string_printf_test.cc; path = ../../core/test/firebase/firestore/util/string_printf_test.cc; 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>"; }; + 5436F32320008FAD006E51E3 /* string_printf_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = string_printf_test.cc; sourceTree = "<group>"; }; + 54511E8D209805F8005BD28F /* hashing_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = hashing_test.cc; sourceTree = "<group>"; }; 5467FAFF203E56F8009C9584 /* FIRFirestoreTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFirestoreTests.mm; sourceTree = "<group>"; }; - 5467FB06203E6A44009C9584 /* app_testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = app_testing.h; path = ../../core/test/firebase/firestore/testutil/app_testing.h; sourceTree = "<group>"; }; - 5467FB07203E6A44009C9584 /* app_testing.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = app_testing.mm; path = ../../core/test/firebase/firestore/testutil/app_testing.mm; sourceTree = "<group>"; }; - 54740A521FC913E500713A1A /* autoid_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = autoid_test.cc; path = ../../core/test/firebase/firestore/util/autoid_test.cc; sourceTree = "<group>"; }; - 54740A531FC913E500713A1A /* secure_random_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = secure_random_test.cc; path = ../../core/test/firebase/firestore/util/secure_random_test.cc; sourceTree = "<group>"; }; - 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FSTGoogleTestTests.mm; path = GoogleTest/FSTGoogleTestTests.mm; sourceTree = "<group>"; }; - 548DB926200D590300E00ABC /* assert_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = assert_test.cc; path = ../../core/test/firebase/firestore/util/assert_test.cc; sourceTree = "<group>"; }; - 548DB928200D59F600E00ABC /* comparison_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = comparison_test.cc; path = ../../core/test/firebase/firestore/util/comparison_test.cc; sourceTree = "<group>"; }; + 5467FB06203E6A44009C9584 /* app_testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = app_testing.h; sourceTree = "<group>"; }; + 5467FB07203E6A44009C9584 /* app_testing.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = app_testing.mm; sourceTree = "<group>"; }; + 546854A820A36867004BDBD5 /* datastore_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = datastore_test.cc; sourceTree = "<group>"; }; + 54740A521FC913E500713A1A /* autoid_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = autoid_test.cc; sourceTree = "<group>"; }; + 54740A531FC913E500713A1A /* secure_random_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = secure_random_test.cc; sourceTree = "<group>"; }; + 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = FSTGoogleTestTests.mm; path = ../../../../Example/Tests/GoogleTest/FSTGoogleTestTests.mm; sourceTree = "<group>"; }; + 548DB926200D590300E00ABC /* assert_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = assert_test.cc; sourceTree = "<group>"; }; + 548DB928200D59F600E00ABC /* comparison_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = comparison_test.cc; sourceTree = "<group>"; }; 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTIntegrationTestCase.mm; sourceTree = "<group>"; }; 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTLevelDBSpecTests.mm; sourceTree = "<group>"; }; 5492E02D20213FFC00B64F25 /* FSTMockDatastore.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTMockDatastore.mm; sourceTree = "<group>"; }; @@ -327,8 +344,22 @@ 5492E0C42021557E00B64F25 /* FSTWatchChange+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FSTWatchChange+Testing.h"; sourceTree = "<group>"; }; 5492E0C52021557E00B64F25 /* FSTWatchChangeTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTWatchChangeTests.mm; sourceTree = "<group>"; }; 5495EB022040E90200EBA509 /* CodableGeoPointTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableGeoPointTests.swift; sourceTree = "<group>"; }; - 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = leveldb_key_test.cc; path = ../../core/test/firebase/firestore/local/leveldb_key_test.cc; sourceTree = "<group>"; }; - 54C2294E1FECABAE007D065B /* log_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = log_test.cc; path = ../../core/test/firebase/firestore/util/log_test.cc; sourceTree = "<group>"; }; + 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = leveldb_key_test.cc; sourceTree = "<group>"; }; + 549CCA4C20A36DBB00BCEB75 /* sorted_set_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = sorted_set_test.cc; sourceTree = "<group>"; }; + 549CCA4D20A36DBB00BCEB75 /* tree_sorted_map_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = tree_sorted_map_test.cc; sourceTree = "<group>"; }; + 549CCA4E20A36DBB00BCEB75 /* sorted_map_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = sorted_map_test.cc; sourceTree = "<group>"; }; + 549CCA4F20A36DBC00BCEB75 /* testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = testing.h; sourceTree = "<group>"; }; + 549CCA5320A36E1F00BCEB75 /* field_mask_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = field_mask_test.cc; sourceTree = "<group>"; }; + 549CCA5520A36E1F00BCEB75 /* precondition_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = precondition_test.cc; sourceTree = "<group>"; }; + 54A0352220A3AEC3003E0143 /* transform_operations_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = transform_operations_test.mm; sourceTree = "<group>"; }; + 54A0352320A3AEC3003E0143 /* field_transform_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = field_transform_test.mm; sourceTree = "<group>"; }; + 54A0352820A3B3BD003E0143 /* testutil.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = testutil.cc; sourceTree = "<group>"; }; + 54A0352920A3B3BD003E0143 /* testutil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = testutil.h; sourceTree = "<group>"; }; + 54A0352B20A3B3D7003E0143 /* status_test_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = status_test_util.h; sourceTree = "<group>"; }; + 54A0352C20A3B3D7003E0143 /* status_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = status_test.cc; sourceTree = "<group>"; }; + 54A0352D20A3B3D7003E0143 /* statusor_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = statusor_test.cc; sourceTree = "<group>"; }; + 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = iterator_adaptors_test.cc; sourceTree = "<group>"; }; + 54C2294E1FECABAE007D065B /* log_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = log_test.cc; sourceTree = "<group>"; }; 54C9EDF12040E16300A969CD /* Firestore_SwiftTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_SwiftTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54C9EDF52040E16300A969CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = collection_spec_test.json; sourceTree = "<group>"; }; @@ -344,9 +375,8 @@ 54E9281C1F33950B00C1953E /* FSTEventAccumulator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTEventAccumulator.h; sourceTree = "<group>"; }; 54E9281E1F33950B00C1953E /* FSTIntegrationTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FSTIntegrationTestCase.h; sourceTree = "<group>"; }; 54E9282A1F339CAD00C1953E /* XCTestCase+Await.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCTestCase+Await.h"; sourceTree = "<group>"; }; - 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = array_sorted_map_test.cc; path = ../../immutable/array_sorted_map_test.cc; sourceTree = "<group>"; }; - 555ACD6970390DC825A8F141 /* 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>"; }; - 5A3E3BE5F322D66EE3D6CB65 /* Pods-Firestore_Tests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.release.xcconfig"; sourceTree = "<group>"; }; + 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = array_sorted_map_test.cc; sourceTree = "<group>"; }; + 5918805E993304321A05E82B /* Pods_Firestore_Example_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6003F58A195388D20070C39A /* Firestore_Example_iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Firestore_Example_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -364,24 +394,18 @@ 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>"; }; - 618AC3C38A174084B9420162 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; sourceTree = "<group>"; }; - 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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>"; }; 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>"; }; - 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 80025E2E892B94823962D11D /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig"; sourceTree = "<group>"; }; - 8525646842C83F703237BAA4 /* Pods_Firestore_Tests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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>"; }; 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>"; }; - 9837221D251B8D40E7D7B454 /* 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>"; }; - 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.debug.xcconfig"; sourceTree = "<group>"; }; - 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example/Pods-Firestore_Example.debug.xcconfig"; sourceTree = "<group>"; }; 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; name = string_util_test.cc; path = ../../core/test/firebase/firestore/util/string_util_test.cc; sourceTree = "<group>"; }; - AB380D01201BC69F00D97691 /* bits_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = bits_test.cc; path = ../../core/test/firebase/firestore/util/bits_test.cc; sourceTree = "<group>"; }; - AB380D03201BC6E400D97691 /* ordered_code_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = ordered_code_test.cc; path = ../../core/test/firebase/firestore/util/ordered_code_test.cc; sourceTree = "<group>"; }; + AB380CFC201A2EE200D97691 /* string_util_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = string_util_test.cc; sourceTree = "<group>"; }; + AB380D01201BC69F00D97691 /* bits_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = bits_test.cc; sourceTree = "<group>"; }; + AB380D03201BC6E400D97691 /* ordered_code_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ordered_code_test.cc; sourceTree = "<group>"; }; AB38D92E20235D22000A432D /* database_info_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = database_info_test.cc; sourceTree = "<group>"; }; AB38D93220239654000A432D /* user_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = user_test.cc; sourceTree = "<group>"; }; AB38D9342023966E000A432D /* credentials_provider_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = credentials_provider_test.cc; sourceTree = "<group>"; }; @@ -390,23 +414,27 @@ AB6B908520322E6D00CC290A /* maybe_document_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = maybe_document_test.cc; sourceTree = "<group>"; }; AB6B908720322E8800CC290A /* no_document_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = no_document_test.cc; sourceTree = "<group>"; }; AB71064B201FA60300344F18 /* database_id_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = database_id_test.cc; sourceTree = "<group>"; }; - AB7BAB332012B519001E0872 /* geo_point_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = geo_point_test.cc; path = ../../core/test/firebase/firestore/geo_point_test.cc; sourceTree = "<group>"; }; + AB7BAB332012B519001E0872 /* geo_point_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = geo_point_test.cc; sourceTree = "<group>"; }; ABA495B9202B7E79008A7851 /* snapshot_version_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = snapshot_version_test.cc; sourceTree = "<group>"; }; ABC1D7DF2023A3EF00BA84F0 /* token_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = token_test.cc; sourceTree = "<group>"; }; ABC1D7E22023CDC500BA84F0 /* firebase_credentials_provider_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = firebase_credentials_provider_test.mm; sourceTree = "<group>"; }; - ABF6506B201131F8005F2C74 /* timestamp_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = timestamp_test.cc; path = ../../core/test/firebase/firestore/timestamp_test.cc; sourceTree = "<group>"; }; - B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B4EEE10E8E59CC91309335CA /* Pods_Firestore_IntegrationTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + ABF6506B201131F8005F2C74 /* timestamp_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = timestamp_test.cc; sourceTree = "<group>"; }; + B3F5B3AAE791A5911B9EAA82 /* Pods-Firestore_Tests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.release.xcconfig"; sourceTree = "<group>"; }; B6152AD5202A5385000E5744 /* document_key_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = document_key_test.cc; sourceTree = "<group>"; }; B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FIRTimestampTest.m; sourceTree = "<group>"; }; B686F2AD2023DDB20028D6BE /* field_path_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = field_path_test.cc; sourceTree = "<group>"; }; B686F2B02024FFD70028D6BE /* resource_path_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = resource_path_test.cc; sourceTree = "<group>"; }; - BE88081EE627C46349C918EF /* Pods-Firestore_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.debug.xcconfig"; sourceTree = "<group>"; }; - C1D89E5405935366C88CC3E5 /* Pods-Firestore_Example-Firestore_SwiftTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example-Firestore_SwiftTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example-Firestore_SwiftTests_iOS/Pods-Firestore_Example-Firestore_SwiftTests_iOS.release.xcconfig"; sourceTree = "<group>"; }; - CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests/Pods-Firestore_Tests.debug.xcconfig"; sourceTree = "<group>"; }; + B6FB467A208E9A8200554BA2 /* async_queue_test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = async_queue_test.h; sourceTree = "<group>"; }; + B6FB467B208E9A8200554BA2 /* async_queue_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = async_queue_test.cc; sourceTree = "<group>"; }; + B6FB4680208EA0BE00554BA2 /* async_queue_libdispatch_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = async_queue_libdispatch_test.mm; sourceTree = "<group>"; }; + B6FB4681208EA0BE00554BA2 /* async_queue_std_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = async_queue_std_test.cc; sourceTree = "<group>"; }; + B6FB4686208F9B9100554BA2 /* async_tests_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = async_tests_util.h; sourceTree = "<group>"; }; + B6FB4687208F9B9100554BA2 /* executor_std_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = executor_std_test.cc; sourceTree = "<group>"; }; + 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>"; }; + 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>"; }; - D8CA124A5F9A704EA0E12785 /* 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>"; }; - DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests/Pods-Firestore_IntegrationTests.release.xcconfig"; sourceTree = "<group>"; }; DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_IntegrationTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DE03B3621F215E1600A30B9C /* CAcert.pem */ = {isa = PBXFileReference; lastKnownFileType = text; path = CAcert.pem; sourceTree = "<group>"; }; DE0761E41F2FE611003233AF /* SwiftBuildTest.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftBuildTest.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -423,7 +451,9 @@ DE51B1981F0D48AC0013853F /* FSTSpecTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSpecTests.h; sourceTree = "<group>"; }; DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSyncEngineTestDriver.h; sourceTree = "<group>"; }; DE51B1A71F0D48AC0013853F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; }; - F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SwiftBuildTest.release.xcconfig"; path = "Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest.release.xcconfig"; sourceTree = "<group>"; }; + E592181BFD7C53C305123739 /* Pods-Firestore_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.debug.xcconfig"; sourceTree = "<group>"; }; + ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F354C0FE92645B56A6C6FD44 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS.release.xcconfig"; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -431,7 +461,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D9AF2279747DE7213156646C /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework in Frameworks */, + C1AA536F90A0A576CA2816EB /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -442,7 +472,7 @@ 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */, 6003F592195388D20070C39A /* UIKit.framework in Frameworks */, 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */, - F06EE8EB234BBE8B7898D5EE /* Pods_Firestore_Example_iOS.framework in Frameworks */, + C8D3CE2343E53223E6487F2C /* Pods_Firestore_Example_iOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -453,7 +483,7 @@ 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */, 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */, 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */, - 347FDC6AA737A754541F7C8A /* Pods_Firestore_Tests_iOS.framework in Frameworks */, + 5D405BE298CE4692CB00790A /* Pods_Firestore_Tests_iOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -464,9 +494,7 @@ DE03B2D41F2149D600A30B9C /* XCTest.framework in Frameworks */, DE03B2D51F2149D600A30B9C /* UIKit.framework in Frameworks */, DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */, - DE03B2D71F2149D600A30B9C /* Pods_Firestore_Tests.framework in Frameworks */, - AFE6114F0D4DAECBA7B7C089 /* Pods_Firestore_IntegrationTests.framework in Frameworks */, - CF08376B68945A0BB332D0C8 /* Pods_Firestore_IntegrationTests_iOS.framework in Frameworks */, + 8C82D4D3F9AB63E79CC52DC8 /* Pods_Firestore_IntegrationTests_iOS.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -474,36 +502,68 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C4E749275AD0FBDF9F4716A8 /* Pods_SwiftBuildTest.framework in Frameworks */, + BF219E98F1C5A1DAEB5EEC86 /* Pods_Firestore_Example_iOS_SwiftBuildTest.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 543B4F0520A91E4B001F506D /* App */ = { + isa = PBXGroup; + children = ( + 6003F593195388D20070C39A /* iOS */, + ); + path = App; + sourceTree = "<group>"; + }; 5467FB05203E652F009C9584 /* testutil */ = { isa = PBXGroup; children = ( 5467FB06203E6A44009C9584 /* app_testing.h */, 5467FB07203E6A44009C9584 /* app_testing.mm */, + 54A0352820A3B3BD003E0143 /* testutil.cc */, + 54A0352920A3B3BD003E0143 /* testutil.h */, ); - name = testutil; + path = testutil; + sourceTree = "<group>"; + }; + 546854A720A3681B004BDBD5 /* remote */ = { + isa = PBXGroup; + children = ( + 546854A820A36867004BDBD5 /* datastore_test.cc */, + ); + path = remote; sourceTree = "<group>"; }; 54740A561FC913EB00713A1A /* util */ = { isa = PBXGroup; children = ( 548DB926200D590300E00ABC /* assert_test.cc */, + B6FB4680208EA0BE00554BA2 /* async_queue_libdispatch_test.mm */, + B6FB4681208EA0BE00554BA2 /* async_queue_std_test.cc */, + B6FB467B208E9A8200554BA2 /* async_queue_test.cc */, + B6FB467A208E9A8200554BA2 /* async_queue_test.h */, + B6FB4686208F9B9100554BA2 /* async_tests_util.h */, 54740A521FC913E500713A1A /* autoid_test.cc */, AB380D01201BC69F00D97691 /* bits_test.cc */, 548DB928200D59F600E00ABC /* comparison_test.cc */, + B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.mm */, + B6FB4687208F9B9100554BA2 /* executor_std_test.cc */, + B6FB4688208F9B9100554BA2 /* executor_test.cc */, + B6FB468A208F9B9100554BA2 /* executor_test.h */, + 54511E8D209805F8005BD28F /* hashing_test.cc */, + 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */, 54C2294E1FECABAE007D065B /* log_test.cc */, AB380D03201BC6E400D97691 /* ordered_code_test.cc */, 54740A531FC913E500713A1A /* secure_random_test.cc */, + 54A0352B20A3B3D7003E0143 /* status_test_util.h */, + 54A0352C20A3B3D7003E0143 /* status_test.cc */, + 54A0352D20A3B3D7003E0143 /* statusor_test.cc */, 5436F32320008FAD006E51E3 /* string_printf_test.cc */, AB380CFC201A2EE200D97691 /* string_util_test.cc */, ); - name = util; + path = util; sourceTree = "<group>"; }; 54764FAC1FAA0C390085E60A /* GoogleTests */ = { @@ -514,6 +574,7 @@ 54EB764B202277970088B8F3 /* immutable */, 54995F70205B6E1A004EFFA0 /* local */, AB356EF5200E9D1A0089B766 /* model */, + 546854A720A3681B004BDBD5 /* remote */, 5467FB05203E652F009C9584 /* testutil */, 54740A561FC913EB00713A1A /* util */, 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */, @@ -521,6 +582,7 @@ ABF6506B201131F8005F2C74 /* timestamp_test.cc */, ); name = GoogleTests; + path = ../../core/test/firebase/firestore; sourceTree = "<group>"; }; 5495EB012040E90200EBA509 /* Codable */ = { @@ -536,7 +598,7 @@ children = ( 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */, ); - name = local; + path = local; sourceTree = "<group>"; }; 54C9EDF22040E16300A969CD /* SwiftTests */ = { @@ -553,22 +615,25 @@ isa = PBXGroup; children = ( 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */, + 549CCA4E20A36DBB00BCEB75 /* sorted_map_test.cc */, + 549CCA4C20A36DBB00BCEB75 /* sorted_set_test.cc */, + 549CCA4F20A36DBC00BCEB75 /* testing.h */, + 549CCA4D20A36DBB00BCEB75 /* tree_sorted_map_test.cc */, ); - name = immutable; - path = ../../core/test/firebase/firestore/core/immutable; + path = immutable; sourceTree = "<group>"; }; 6003F581195388D10070C39A = { isa = PBXGroup; children = ( + 543B4F0520A91E4B001F506D /* App */, 60FF7A9C1954A5C5007DD14C /* Podspec Metadata */, - 6003F593195388D20070C39A /* Example for Firestore */, 6003F5B5195388D20070C39A /* Tests */, 54C9EDF22040E16300A969CD /* SwiftTests */, DE0761E51F2FE611003233AF /* SwiftBuildTest */, 6003F58C195388D20070C39A /* Frameworks */, 6003F58B195388D20070C39A /* Products */, - A47A1BF74A48BCAEAFBCBF1E /* Pods */, + AAEA2A72CFD1FA5AD34462F7 /* Pods */, ); sourceTree = "<group>"; }; @@ -591,20 +656,16 @@ 6003F58F195388D20070C39A /* CoreGraphics.framework */, 6003F591195388D20070C39A /* UIKit.framework */, 6003F5AF195388D20070C39A /* XCTest.framework */, - 75A6FE51C1A02DF38F62FAAD /* Pods_Firestore_Example.framework */, - 69F6A10DBD6187489481CD76 /* Pods_Firestore_Tests.framework */, - B2FA635DF5D116A67A7441CD /* Pods_Firestore_IntegrationTests.framework */, - 32AD40BF6B0E849B07FFD05E /* Pods_SwiftBuildTest.framework */, - 245812330F6A31632BB4B623 /* Pods_Firestore_Example_Firestore_SwiftTests_iOS.framework */, - 24A6BEC38BF31BC4BF0E9DA7 /* Pods_Firestore_Example_iOS.framework */, - 0E5F50EF80014608B1868944 /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */, - 8525646842C83F703237BAA4 /* Pods_Firestore_Tests_iOS.framework */, - B4EEE10E8E59CC91309335CA /* Pods_Firestore_IntegrationTests_iOS.framework */, + 5918805E993304321A05E82B /* Pods_Firestore_Example_iOS.framework */, + BB92EB03E3F92485023F64ED /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */, + 379B34A1536045869826D82A /* Pods_Firestore_Example_iOS_SwiftBuildTest.framework */, + ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */, + 2B50B3A0DF77100EEE887891 /* Pods_Firestore_Tests_iOS.framework */, ); name = Frameworks; sourceTree = "<group>"; }; - 6003F593195388D20070C39A /* Example for Firestore */ = { + 6003F593195388D20070C39A /* iOS */ = { isa = PBXGroup; children = ( 6003F59C195388D20070C39A /* FIRAppDelegate.h */, @@ -616,8 +677,7 @@ 6003F5A8195388D20070C39A /* Images.xcassets */, 6003F594195388D20070C39A /* Supporting Files */, ); - name = "Example for Firestore"; - path = Firestore; + path = iOS; sourceTree = "<group>"; }; 6003F594195388D20070C39A /* Supporting Files */ = { @@ -667,27 +727,19 @@ name = "Podspec Metadata"; sourceTree = "<group>"; }; - A47A1BF74A48BCAEAFBCBF1E /* Pods */ = { + AAEA2A72CFD1FA5AD34462F7 /* Pods */ = { isa = PBXGroup; children = ( - 9EF477AD4B2B643FD320867A /* Pods-Firestore_Example.debug.xcconfig */, - 4EBC5F5ABE1FD097EFE5E224 /* Pods-Firestore_Example.release.xcconfig */, - 9D52E67EE96AA7E5D6F69748 /* Pods-Firestore_IntegrationTests.debug.xcconfig */, - DB17FEDFB80770611A935A60 /* Pods-Firestore_IntegrationTests.release.xcconfig */, - CE00BABB5A3AAB44A4C209E2 /* Pods-Firestore_Tests.debug.xcconfig */, - 04DF37A117F88A9891379ED6 /* Pods-Firestore_Tests.release.xcconfig */, - 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */, - F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */, - 3F422FFBDA6E79396E2FB594 /* Pods-Firestore_Example-Firestore_SwiftTests_iOS.debug.xcconfig */, - C1D89E5405935366C88CC3E5 /* Pods-Firestore_Example-Firestore_SwiftTests_iOS.release.xcconfig */, - 555ACD6970390DC825A8F141 /* Pods-Firestore_Example_iOS.debug.xcconfig */, - D8CA124A5F9A704EA0E12785 /* Pods-Firestore_Example_iOS.release.xcconfig */, - 9837221D251B8D40E7D7B454 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */, - 80025E2E892B94823962D11D /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */, - BE88081EE627C46349C918EF /* Pods-Firestore_Tests_iOS.debug.xcconfig */, - 5A3E3BE5F322D66EE3D6CB65 /* Pods-Firestore_Tests_iOS.release.xcconfig */, - 3C7CE22C50805C4A854C73A1 /* Pods-Firestore_IntegrationTests_iOS.debug.xcconfig */, - 618AC3C38A174084B9420162 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */, + 3C81DE3772628FE297055662 /* Pods-Firestore_Example_iOS.debug.xcconfig */, + 3F0992A4B83C60841C52E960 /* Pods-Firestore_Example_iOS.release.xcconfig */, + 69E6C311558EC77729A16CF1 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */, + 11984BA0A99D7A7ABA5B0D90 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */, + 05C3D82261C3BE976889FF09 /* Pods-Firestore_Example_iOS-SwiftBuildTest.debug.xcconfig */, + 74ACEC3603BE58B57A7E8D4C /* Pods-Firestore_Example_iOS-SwiftBuildTest.release.xcconfig */, + 1277F98C20D2DF0867496976 /* Pods-Firestore_IntegrationTests_iOS.debug.xcconfig */, + F354C0FE92645B56A6C6FD44 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */, + E592181BFD7C53C305123739 /* Pods-Firestore_Tests_iOS.debug.xcconfig */, + B3F5B3AAE791A5911B9EAA82 /* Pods-Firestore_Tests_iOS.release.xcconfig */, ); name = Pods; sourceTree = "<group>"; @@ -695,28 +747,28 @@ AB356EF5200E9D1A0089B766 /* model */ = { isa = PBXGroup; children = ( + AB71064B201FA60300344F18 /* database_id_test.cc */, B6152AD5202A5385000E5744 /* document_key_test.cc */, AB6B908320322E4D00CC290A /* document_test.cc */, - B686F2B02024FFD70028D6BE /* resource_path_test.cc */, + 549CCA5320A36E1F00BCEB75 /* field_mask_test.cc */, B686F2AD2023DDB20028D6BE /* field_path_test.cc */, - AB71064B201FA60300344F18 /* database_id_test.cc */, AB356EF6200EA5EB0089B766 /* field_value_test.cc */, AB6B908520322E6D00CC290A /* maybe_document_test.cc */, AB6B908720322E8800CC290A /* no_document_test.cc */, + 549CCA5520A36E1F00BCEB75 /* precondition_test.cc */, + B686F2B02024FFD70028D6BE /* resource_path_test.cc */, ABA495B9202B7E79008A7851 /* snapshot_version_test.cc */, ); - name = model; - path = ../../core/test/firebase/firestore/model; + path = model; sourceTree = "<group>"; }; AB380CF7201937B800D97691 /* core */ = { isa = PBXGroup; children = ( - AB380CF82019382300D97691 /* target_id_generator_test.cc */, AB38D92E20235D22000A432D /* database_info_test.cc */, + AB380CF82019382300D97691 /* target_id_generator_test.cc */, ); - name = core; - path = ../../core/test/firebase/firestore/core; + path = core; sourceTree = "<group>"; }; AB38D9312023962A000A432D /* auth */ = { @@ -728,8 +780,7 @@ ABC1D7DF2023A3EF00BA84F0 /* token_test.cc */, AB38D93220239654000A432D /* user_test.cc */, ); - name = auth; - path = ../../core/test/firebase/firestore/auth; + path = auth; sourceTree = "<group>"; }; DE0761E51F2FE611003233AF /* SwiftBuildTest */ = { @@ -789,11 +840,13 @@ DE51B17B1F0D48AC0013853F /* Model */ = { isa = PBXGroup; children = ( + 54A0352320A3AEC3003E0143 /* field_transform_test.mm */, 5492E0B22021555000B64F25 /* FSTDocumentKeyTests.mm */, 5492E0B32021555100B64F25 /* FSTDocumentSetTests.mm */, 5492E0B62021555100B64F25 /* FSTDocumentTests.mm */, 5492E0B82021555100B64F25 /* FSTFieldValueTests.mm */, 5492E0B72021555100B64F25 /* FSTMutationTests.mm */, + 54A0352220A3AEC3003E0143 /* transform_operations_test.mm */, ); path = Model; sourceTree = "<group>"; @@ -934,11 +987,11 @@ isa = PBXNativeTarget; buildConfigurationList = 54C9EDFA2040E16300A969CD /* Build configuration list for PBXNativeTarget "Firestore_SwiftTests_iOS" */; buildPhases = ( - 6FB7F3A6D6ADAC64E4972A29 /* [CP] Check Pods Manifest.lock */, + D2D94DFA64939EF6DECDF908 /* [CP] Check Pods Manifest.lock */, 54C9EDED2040E16300A969CD /* Sources */, 54C9EDEE2040E16300A969CD /* Frameworks */, 54C9EDEF2040E16300A969CD /* Resources */, - 9E2D564AC55ADE2D52B7E951 /* [CP] Embed Pods Frameworks */, + EA424838F4A5DD7B337F57AB /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -954,11 +1007,11 @@ isa = PBXNativeTarget; buildConfigurationList = 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Example_iOS" */; buildPhases = ( - FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */, + 83F2AB95D08093BB076EE521 /* [CP] Check Pods Manifest.lock */, 6003F586195388D20070C39A /* Sources */, 6003F587195388D20070C39A /* Frameworks */, 6003F588195388D20070C39A /* Resources */, - 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */, + 1EE692C7509A98D7EB03CA51 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -973,11 +1026,11 @@ isa = PBXNativeTarget; buildConfigurationList = 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "Firestore_Tests_iOS" */; buildPhases = ( - 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */, + 8B469EB6DA9E6404589402E2 /* [CP] Check Pods Manifest.lock */, 6003F5AA195388D20070C39A /* Sources */, 6003F5AB195388D20070C39A /* Frameworks */, 6003F5AC195388D20070C39A /* Resources */, - BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */, + 329C25E418360CEF62F6CB2B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -993,11 +1046,11 @@ isa = PBXNativeTarget; buildConfigurationList = DE03B2E61F2149D600A30B9C /* Build configuration list for PBXNativeTarget "Firestore_IntegrationTests_iOS" */; buildPhases = ( - DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */, + A827A009A65B69DC1B80EAD4 /* [CP] Check Pods Manifest.lock */, DE03B2981F2149D600A30B9C /* Sources */, DE03B2D31F2149D600A30B9C /* Frameworks */, DE03B2D81F2149D600A30B9C /* Resources */, - DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */, + B7923D95031DB0DA112AAE9B /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1013,11 +1066,11 @@ isa = PBXNativeTarget; buildConfigurationList = DE0761F51F2FE611003233AF /* Build configuration list for PBXNativeTarget "SwiftBuildTest" */; buildPhases = ( - 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */, + 5504F81EEBBEF943CA61D32C /* [CP] Check Pods Manifest.lock */, DE0761E01F2FE611003233AF /* Sources */, DE0761E11F2FE611003233AF /* Frameworks */, DE0761E21F2FE611003233AF /* Resources */, - 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */, + 04404E0DCBB886A40E3C7175 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1142,13 +1195,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 125BDFEB177CFD41D7A40928 /* [CP] Embed Pods Frameworks */ = { + 04404E0DCBB886A40E3C7175 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh", + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-SwiftBuildTest/Pods-Firestore_Example_iOS-SwiftBuildTest-frameworks.sh", "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", @@ -1175,28 +1228,10 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-SwiftBuildTest/Pods-SwiftBuildTest-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 6FB7F3A6D6ADAC64E4972A29 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Firestore_Example_iOS-Firestore_SwiftTests_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"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-SwiftBuildTest/Pods-Firestore_Example_iOS-SwiftBuildTest-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 7C5123A9C345ECE100DA21BD /* [CP] Embed Pods Frameworks */ = { + 1EE692C7509A98D7EB03CA51 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1232,25 +1267,29 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS/Pods-Firestore_Example_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 8D94B6319191CD7344A4D1B9 /* [CP] Check Pods Manifest.lock */ = { + 329C25E418360CEF62F6CB2B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/GoogleTest/GoogleTest.framework", + "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Firestore_Tests_iOS-checkManifestLockResult.txt", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleTest.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", ); 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"; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 8F34C5E63ACEBD784CF82A45 /* [CP] Check Pods Manifest.lock */ = { + 5504F81EEBBEF943CA61D32C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1261,72 +1300,50 @@ ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-SwiftBuildTest-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Firestore_Example_iOS-SwiftBuildTest-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 9E2D564AC55ADE2D52B7E951 /* [CP] Embed Pods Frameworks */ = { + 83F2AB95D08093BB076EE521 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", - "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", - "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", - "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", - "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework", - "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", - "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework", - "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework", - "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", - "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Check Pods Manifest.lock"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + "$(DERIVED_FILE_DIR)/Pods-Firestore_Example_iOS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh\"\n"; + 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; }; - BB3FE78ABF533BFC38839A0E /* [CP] Embed Pods Frameworks */ = { + 8B469EB6DA9E6404589402E2 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", - "${BUILT_PRODUCTS_DIR}/GoogleTest/GoogleTest.framework", - "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Check Pods Manifest.lock"; 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", + "$(DERIVED_FILE_DIR)/Pods-Firestore_Tests_iOS-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS-frameworks.sh\"\n"; + 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; }; - DE03B2971F2149D600A30B9C /* [CP] Check Pods Manifest.lock */ = { + A827A009A65B69DC1B80EAD4 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1344,7 +1361,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - DE03B2E41F2149D600A30B9C /* [CP] Embed Pods Frameworks */ = { + B7923D95031DB0DA112AAE9B /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1362,7 +1379,7 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_IntegrationTests_iOS/Pods-Firestore_IntegrationTests_iOS-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - FAB3416C6DD87D45081EC3E8 /* [CP] Check Pods Manifest.lock */ = { + D2D94DFA64939EF6DECDF908 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -1373,13 +1390,49 @@ ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Firestore_Example_iOS-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-Firestore_Example_iOS-Firestore_SwiftTests_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; }; + EA424838F4A5DD7B337F57AB /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/BoringSSL/openssl.framework", + "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", + "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", + "${BUILT_PRODUCTS_DIR}/Protobuf/Protobuf.framework", + "${BUILT_PRODUCTS_DIR}/gRPC/GRPCClient.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-Core/grpc.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-ProtoRPC/ProtoRPC.framework", + "${BUILT_PRODUCTS_DIR}/gRPC-RxLibrary/RxLibrary.framework", + "${BUILT_PRODUCTS_DIR}/leveldb-library/leveldb.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/openssl.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Protobuf.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRPCClient.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/grpc.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProtoRPC.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RxLibrary.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/leveldb.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS/Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1409,6 +1462,7 @@ DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */, ABE6637A201FA81900ED349A /* database_id_test.cc in Sources */, 5492E0AF2021552D00B64F25 /* FSTReferenceSetTests.mm in Sources */, + 549CCA5120A36DBC00BCEB75 /* tree_sorted_map_test.cc in Sources */, 5492E09E2021552D00B64F25 /* FSTEagerGarbageCollectorTests.mm in Sources */, 5492E0C62021557E00B64F25 /* FSTWatchChange+Testing.mm in Sources */, 5492E064202154B900B64F25 /* FSTQueryListenerTests.mm in Sources */, @@ -1423,30 +1477,39 @@ 5492E058202154AB00B64F25 /* FSTAPIHelpers.mm in Sources */, AB380CFB2019388600D97691 /* target_id_generator_test.cc in Sources */, 5492E0A82021552D00B64F25 /* FSTLevelDBLocalStoreTests.mm in Sources */, + 549CCA5920A36E1F00BCEB75 /* precondition_test.cc in Sources */, ABC1D7DE2023A05300BA84F0 /* user_test.cc in Sources */, 5491BC721FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */, ABC1D7DD2023A04F00BA84F0 /* empty_credentials_provider_test.cc in Sources */, DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */, B686F2AF2023DDEE0028D6BE /* field_path_test.cc in Sources */, + 54A0352A20A3B3BD003E0143 /* testutil.cc in Sources */, 5492E03120213FFC00B64F25 /* FSTLevelDBSpecTests.mm in Sources */, + B6FB468F208F9BAE00554BA2 /* executor_std_test.cc in Sources */, 5492E0B12021552D00B64F25 /* FSTRemoteDocumentCacheTests.mm in Sources */, 5492E0BA2021555100B64F25 /* FSTDocumentSetTests.mm in Sources */, 54740A581FC914F000713A1A /* autoid_test.cc in Sources */, 548DB927200D590300E00ABC /* assert_test.cc in Sources */, 5492E0A62021552D00B64F25 /* FSTPersistenceTestHelpers.mm in Sources */, + B6FB468E208F9BAB00554BA2 /* executor_libdispatch_test.mm in Sources */, 5467FB01203E5717009C9584 /* FIRFirestoreTests.mm in Sources */, + B6FB4684208EA0EC00554BA2 /* async_queue_libdispatch_test.mm in Sources */, 5492E0A12021552D00B64F25 /* FSTMemoryLocalStoreTests.mm in Sources */, 5436F32420008FAD006E51E3 /* string_printf_test.cc in Sources */, 5492E067202154B900B64F25 /* FSTEventManagerTests.mm in Sources */, + 54A0353020A3B3D8003E0143 /* statusor_test.cc in Sources */, 5492E0BF2021555100B64F25 /* FSTFieldValueTests.mm in Sources */, + 54A0352620A3AED0003E0143 /* field_transform_test.mm in Sources */, 5492E055202154AB00B64F25 /* FIRDocumentSnapshotTests.mm in Sources */, 5492E03E2021401F00B64F25 /* FSTEventAccumulator.mm in Sources */, + 54A0352720A3AED0003E0143 /* transform_operations_test.mm in Sources */, DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */, 5492E0AA2021552D00B64F25 /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */, 5492E0AC2021552D00B64F25 /* FSTMutationQueueTests.mm in Sources */, 5492E056202154AB00B64F25 /* FIRFieldPathTests.mm in Sources */, 5492E03220213FFC00B64F25 /* FSTMockDatastore.mm in Sources */, ABC1D7E12023A40C00BA84F0 /* token_test.cc in Sources */, + 54A0353520A3D8CB003E0143 /* iterator_adaptors_test.cc in Sources */, AB356EF7200EA5EB0089B766 /* field_value_test.cc in Sources */, AB7BAB342012B519001E0872 /* geo_point_test.cc in Sources */, 5492E0AD2021552D00B64F25 /* FSTMemoryMutationQueueTests.mm in Sources */, @@ -1454,10 +1517,12 @@ 5492E054202154AB00B64F25 /* FIRFieldValueTests.mm in Sources */, AB6B908620322E6D00CC290A /* maybe_document_test.cc in Sources */, 5492E09F2021552D00B64F25 /* FSTLevelDBMigrationsTests.mm in Sources */, + 546854AA20A36867004BDBD5 /* datastore_test.cc in Sources */, 5492E053202154AB00B64F25 /* FIRDocumentReferenceTests.mm in Sources */, 5492E09D2021552D00B64F25 /* FSTLocalStoreTests.mm in Sources */, 5492E0A32021552D00B64F25 /* FSTLocalSerializerTests.mm in Sources */, 5492E0A72021552D00B64F25 /* FSTLevelDBKeyTests.mm in Sources */, + B6FB4685208EA0F000554BA2 /* async_queue_std_test.cc in Sources */, 5492E0A22021552D00B64F25 /* FSTQueryCacheTests.mm in Sources */, 5492E0A52021552D00B64F25 /* FSTMemoryRemoteDocumentCacheTests.mm in Sources */, AB6B908820322E8800CC290A /* no_document_test.cc in Sources */, @@ -1467,12 +1532,15 @@ 5492E0C82021557E00B64F25 /* FSTDatastoreTests.mm in Sources */, 54995F6F205B6E12004EFFA0 /* leveldb_key_test.cc in Sources */, 5492E065202154B900B64F25 /* FSTViewTests.mm in Sources */, + B6FB467D208E9D3C00554BA2 /* async_queue_test.cc in Sources */, + 54A0352F20A3B3D8003E0143 /* status_test.cc in Sources */, 5492E03C2021401F00B64F25 /* XCTestCase+Await.mm in Sources */, B6152AD7202A53CB000E5744 /* document_key_test.cc in Sources */, 5467FB08203E6A44009C9584 /* app_testing.mm in Sources */, 54764FAF1FAA21B90085E60A /* FSTGoogleTestTests.mm in Sources */, AB380D04201BC6E400D97691 /* ordered_code_test.cc in Sources */, 5492E03F2021401F00B64F25 /* FSTHelpers.mm in Sources */, + 549CCA5220A36DBC00BCEB75 /* sorted_map_test.cc in Sources */, 5492E068202154B900B64F25 /* FSTQueryTests.mm in Sources */, 5492E0AB2021552D00B64F25 /* StringViewTests.mm in Sources */, 5492E0C92021557E00B64F25 /* FSTRemoteEventTests.mm in Sources */, @@ -1481,6 +1549,7 @@ ABF6506C201131F8005F2C74 /* timestamp_test.cc in Sources */, 5492E0AE2021552D00B64F25 /* FSTLevelDBQueryCacheTests.mm in Sources */, ABC1D7DC2023A04B00BA84F0 /* credentials_provider_test.cc in Sources */, + 54511E8E209805F8005BD28F /* hashing_test.cc in Sources */, 5492E059202154AB00B64F25 /* FIRQuerySnapshotTests.mm in Sources */, 5492E050202154AA00B64F25 /* FIRCollectionReferenceTests.mm in Sources */, ABA495BB202B7E80008A7851 /* snapshot_version_test.cc in Sources */, @@ -1489,13 +1558,16 @@ 5492E03420213FFC00B64F25 /* FSTMemorySpecTests.mm in Sources */, AB380D02201BC69F00D97691 /* bits_test.cc in Sources */, 548DB929200D59F600E00ABC /* comparison_test.cc in Sources */, + 549CCA5720A36E1F00BCEB75 /* field_mask_test.cc in Sources */, 5492E03D2021401F00B64F25 /* FSTAssertTests.mm in Sources */, AB38D93020236E21000A432D /* database_info_test.cc in Sources */, 5492E052202154AB00B64F25 /* FIRGeoPointTests.mm in Sources */, 5492E0C72021557E00B64F25 /* FSTSerializerBetaTests.mm in Sources */, + B6FB4690208F9BB300554BA2 /* executor_test.cc in Sources */, 5492E03520213FFC00B64F25 /* FSTSpecTests.mm in Sources */, 5492E057202154AB00B64F25 /* FIRSnapshotMetadataTests.mm in Sources */, 54740A571FC914BA00713A1A /* secure_random_test.cc in Sources */, + 549CCA5020A36DBC00BCEB75 /* sorted_set_test.cc in Sources */, 5492E0BE2021555100B64F25 /* FSTMutationTests.mm in Sources */, 132E3E53179DE287D875F3F2 /* FSTLevelDBTransactionTests.mm in Sources */, ); @@ -1605,7 +1677,7 @@ /* Begin XCBuildConfiguration section */ 54C9EDF82040E16300A969CD /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9837221D251B8D40E7D7B454 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */; + baseConfigurationReference = 69E6C311558EC77729A16CF1 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; @@ -1644,7 +1716,7 @@ }; 54C9EDF92040E16300A969CD /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 80025E2E892B94823962D11D /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */; + baseConfigurationReference = 11984BA0A99D7A7ABA5B0D90 /* Pods-Firestore_Example_iOS-Firestore_SwiftTests_iOS.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; @@ -1758,7 +1830,7 @@ }; 6003F5C0195388D20070C39A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 555ACD6970390DC825A8F141 /* Pods-Firestore_Example_iOS.debug.xcconfig */; + baseConfigurationReference = 3C81DE3772628FE297055662 /* Pods-Firestore_Example_iOS.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -1768,8 +1840,12 @@ "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"", "\"${PODS_ROOT}/leveldb-library/include\"", ); - INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + INFOPLIST_FILE = "App/iOS/Firestore-Info.plist"; MODULE_NAME = ExampleApp; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; @@ -1778,7 +1854,7 @@ }; 6003F5C1195388D20070C39A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D8CA124A5F9A704EA0E12785 /* Pods-Firestore_Example_iOS.release.xcconfig */; + baseConfigurationReference = 3F0992A4B83C60841C52E960 /* Pods-Firestore_Example_iOS.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -1788,8 +1864,12 @@ "\"${PODS_ROOT}/Firebase/Firebase/Firebase\"", "\"${PODS_ROOT}/leveldb-library/include\"", ); - INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + INFOPLIST_FILE = "App/iOS/Firestore-Info.plist"; MODULE_NAME = ExampleApp; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; @@ -1798,7 +1878,7 @@ }; 6003F5C3195388D20070C39A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BE88081EE627C46349C918EF /* Pods-Firestore_Tests_iOS.debug.xcconfig */; + baseConfigurationReference = E592181BFD7C53C305123739 /* Pods-Firestore_Tests_iOS.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = EQHXZ8M8AV; @@ -1818,8 +1898,9 @@ "$(inherited)", "\"${PODS_ROOT}/../../..\"", "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", - "\"${PODS_ROOT}/leveldb-library/include\"", + "\"${PODS_ROOT}/GoogleTest/googlemock/include\"", "\"${PODS_ROOT}/GoogleTest/googletest/include\"", + "\"${PODS_ROOT}/leveldb-library/include\"", ); INFOPLIST_FILE = "Tests/Tests-Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; @@ -1831,7 +1912,7 @@ }; 6003F5C4195388D20070C39A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5A3E3BE5F322D66EE3D6CB65 /* Pods-Firestore_Tests_iOS.release.xcconfig */; + baseConfigurationReference = B3F5B3AAE791A5911B9EAA82 /* Pods-Firestore_Tests_iOS.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = EQHXZ8M8AV; @@ -1851,8 +1932,9 @@ "$(inherited)", "\"${PODS_ROOT}/../../..\"", "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", - "\"${PODS_ROOT}/leveldb-library/include\"", + "\"${PODS_ROOT}/GoogleTest/googlemock/include\"", "\"${PODS_ROOT}/GoogleTest/googletest/include\"", + "\"${PODS_ROOT}/leveldb-library/include\"", ); INFOPLIST_FILE = "Tests/Tests-Info.plist"; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; @@ -1864,7 +1946,7 @@ }; DE03B2E71F2149D600A30B9C /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 3C7CE22C50805C4A854C73A1 /* Pods-Firestore_IntegrationTests_iOS.debug.xcconfig */; + baseConfigurationReference = 1277F98C20D2DF0867496976 /* Pods-Firestore_IntegrationTests_iOS.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = EQHXZ8M8AV; @@ -1891,10 +1973,6 @@ OTHER_LDFLAGS = ( "$(inherited)", "-l\"c++\"", - "-framework", - "\"OCMock\"", - "-framework", - "\"leveldb\"", ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1905,7 +1983,7 @@ }; DE03B2E81F2149D600A30B9C /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 618AC3C38A174084B9420162 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */; + baseConfigurationReference = F354C0FE92645B56A6C6FD44 /* Pods-Firestore_IntegrationTests_iOS.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; DEVELOPMENT_TEAM = EQHXZ8M8AV; @@ -1932,10 +2010,6 @@ OTHER_LDFLAGS = ( "$(inherited)", "-l\"c++\"", - "-framework", - "\"OCMock\"", - "-framework", - "\"leveldb\"", ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1946,7 +2020,7 @@ }; DE0761F31F2FE611003233AF /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 42491D7DC8C8CD245CC22B93 /* Pods-SwiftBuildTest.debug.xcconfig */; + baseConfigurationReference = 05C3D82261C3BE976889FF09 /* Pods-Firestore_Example_iOS-SwiftBuildTest.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; @@ -1959,7 +2033,7 @@ DEVELOPMENT_TEAM = EQHXZ8M8AV; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; - INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + INFOPLIST_FILE = "App/iOS/Firestore-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = YES; @@ -1973,7 +2047,7 @@ }; DE0761F41F2FE611003233AF /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F23325524BEAF8D24F78AC88 /* Pods-SwiftBuildTest.release.xcconfig */; + baseConfigurationReference = 74ACEC3603BE58B57A7E8D4C /* Pods-Firestore_Example_iOS-SwiftBuildTest.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; @@ -1987,7 +2061,7 @@ DEVELOPMENT_TEAM = EQHXZ8M8AV; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; - INFOPLIST_FILE = "Firestore/Firestore-Info.plist"; + INFOPLIST_FILE = "App/iOS/Firestore-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 10.3; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; diff --git a/Firestore/Example/Podfile b/Firestore/Example/Podfile index a7fa097..7844066 100644 --- a/Firestore/Example/Podfile +++ b/Firestore/Example/Podfile @@ -2,21 +2,20 @@ #source 'sso://cpdc-internal/spec' #source 'https://github.com/CocoaPods/Specs.git' -# 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.0.1' - use_frameworks! -pod 'FirebaseAuth', :path => '../../' -pod 'FirebaseCore', :path => '../../' -pod 'FirebaseFirestore', :path => '../../' - target 'Firestore_Example_iOS' do platform :ios, '8.0' + # 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.0.1' + + pod 'FirebaseAuth', :path => '../../' + pod 'FirebaseCore', :path => '../../' + pod 'FirebaseFirestore', :path => '../../' + target 'Firestore_Tests_iOS' do inherit! :search_paths @@ -34,8 +33,8 @@ target 'Firestore_Example_iOS' do target 'Firestore_SwiftTests_iOS' do pod 'FirebaseFirestoreSwift', :path => '../../' end -end -target 'SwiftBuildTest' do - platform :ios, '8.0' + target 'SwiftBuildTest' do + platform :ios, '8.0' + end end diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm index 0454152..5629075 100644 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm +++ b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm @@ -15,6 +15,7 @@ */ #import <XCTest/XCTest.h> +#include <memory> #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" @@ -23,21 +24,29 @@ #import "Firestore/Source/Model/FSTDocumentSet.h" #import "Firestore/Source/Remote/FSTRemoteEvent.h" #import "Firestore/Source/Util/FSTAsyncQueryListener.h" -#import "Firestore/Source/Util/FSTDispatchQueue.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" +#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "absl/memory/memory.h" + +using firebase::firestore::util::internal::ExecutorLibdispatch; +using firebase::firestore::model::DocumentKeySet; + NS_ASSUME_NONNULL_BEGIN @interface FSTQueryListenerTests : XCTestCase -@property(nonatomic, strong, readonly) FSTDispatchQueue *asyncQueue; @end -@implementation FSTQueryListenerTests +@implementation FSTQueryListenerTests { + std::unique_ptr<ExecutorLibdispatch> _executor; +} - (void)setUp { - _asyncQueue = [FSTDispatchQueue - queueWith:dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)]; + // TODO(varconst): moving this test to C++, it should be possible to store Executor as a value, + // not a pointer, and initialize it in the constructor. + _executor = absl::make_unique<ExecutorLibdispatch>( + dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)); } - (void)testRaisesCollectionEvents { @@ -53,7 +62,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; FSTQueryListener *otherListener = [self listenToQuery:query accumulatingSnapshots:otherAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2prime ], nil); @@ -107,7 +116,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *listener = [self listenToQuery:query accumulatingSnapshots:accum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); FSTTargetChange *ackTarget = @@ -129,14 +138,14 @@ NS_ASSUME_NONNULL_BEGIN FSTDocument *doc1 = FSTTestDoc("rooms/Eros", 3, @{@"name" : @"Eros"}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/Eros", 4, @{@"name" : @"Eros2"}, NO); - __block FSTAsyncQueryListener *listener = [[FSTAsyncQueryListener alloc] - initWithDispatchQueue:self.asyncQueue - snapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { - [accum addObject:snapshot]; - [listener mute]; - }]; + __block FSTAsyncQueryListener *listener = + [[FSTAsyncQueryListener alloc] initWithExecutor:_executor.get() + snapshotHandler:^(FSTViewSnapshot *snapshot, NSError *error) { + [accum addObject:snapshot]; + [listener mute]; + }]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *viewSnapshot1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTViewSnapshot *viewSnapshot2 = FSTTestApplyChanges(view, @[ doc2 ], nil); @@ -146,9 +155,7 @@ NS_ASSUME_NONNULL_BEGIN // Drain queue XCTestExpectation *expectation = [self expectationWithDescription:@"Queue drained"]; - [self.asyncQueue dispatchAsync:^{ - [expectation fulfill]; - }]; + _executor->Execute([=] { [expectation fulfill]; }); [self waitForExpectationsWithTimeout:4.0 handler:^(NSError *_Nullable expectationError) { @@ -178,7 +185,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *fullListener = [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTTargetChange *ackTarget = @@ -218,7 +225,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *fullListener = [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); @@ -265,7 +272,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *fullListener = [self listenToQuery:query options:options accumulatingSnapshots:fullAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime ], nil); FSTViewSnapshot *snap3 = FSTTestApplyChanges(view, @[ doc3 ], nil); @@ -298,7 +305,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryListener *filteredListener = [self listenToQuery:query accumulatingSnapshots:filteredAccum]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1, doc2 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc1Prime, doc3 ], nil); @@ -331,7 +338,7 @@ NS_ASSUME_NONNULL_BEGIN waitForSyncWhenOnline:YES] accumulatingSnapshots:events]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); FSTViewSnapshot *snap3 = @@ -374,7 +381,7 @@ NS_ASSUME_NONNULL_BEGIN waitForSyncWhenOnline:YES] accumulatingSnapshots:events]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[ doc1 ], nil); FSTViewSnapshot *snap2 = FSTTestApplyChanges(view, @[ doc2 ], nil); @@ -415,7 +422,7 @@ NS_ASSUME_NONNULL_BEGIN options:[FSTListenOptions defaultOptions] accumulatingSnapshots:events]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); [listener applyChangedOnlineState:FSTOnlineStateOnline]; // no event @@ -441,7 +448,7 @@ NS_ASSUME_NONNULL_BEGIN options:[FSTListenOptions defaultOptions] accumulatingSnapshots:events]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snap1 = FSTTestApplyChanges(view, @[], nil); [listener applyChangedOnlineState:FSTOnlineStateOffline]; // no event diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm index 63ce711..ec62d82 100644 --- a/Firestore/Example/Tests/Core/FSTViewTests.mm +++ b/Firestore/Example/Tests/Core/FSTViewTests.mm @@ -34,6 +34,7 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::ResourcePath; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -49,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testAddsDocumentsBasedOnQuery { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); @@ -77,7 +78,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testRemovesDocuments { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); @@ -108,7 +109,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testReturnsNilIfThereAreNoChanges { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); @@ -123,7 +124,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testDoesNotReturnNilForFirstChanges { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewSnapshot *snapshot = FSTTestApplyChanges(view, @[], nil); XCTAssertNotNil(snapshot); @@ -137,7 +138,7 @@ NS_ASSUME_NONNULL_BEGIN value:[FSTDoubleValue doubleValue:2]]; query = [query queryByAddingFilter:filter]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{ @"sort" : @2 }, NO); FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/3", 0, @{ @"sort" : @3 }, NO); @@ -169,7 +170,7 @@ NS_ASSUME_NONNULL_BEGIN value:[FSTDoubleValue doubleValue:2]]; query = [query queryByAddingFilter:filter]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{ @"sort" : @1 }, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{ @"sort" : @3 }, NO); FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/3", 0, @{ @"sort" : @2 }, NO); @@ -205,7 +206,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testRemovesDocumentsForQueryWithLimit { FSTQuery *query = [self queryForMessages]; query = [query queryBySettingLimit:2]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{@"text" : @"msg1"}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{@"text" : @"msg2"}, NO); @@ -239,7 +240,7 @@ NS_ASSUME_NONNULL_BEGIN query = [query queryByAddingSortOrder:[FSTSortOrder sortOrderWithFieldPath:testutil::Field("num") ascending:YES]]; query = [query queryBySettingLimit:2]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/1", 0, @{ @"num" : @1 }, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/2", 0, @{ @"num" : @2 }, NO); @@ -283,7 +284,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testKeepsTrackOfLimboDocuments { FSTQuery *query = [self queryForMessages]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, NO); @@ -339,8 +340,8 @@ NS_ASSUME_NONNULL_BEGIN // Unlike other cases, here the view is initialized with a set of previously synced documents // which happens when listening to a previously listened-to query. - FSTView *view = [[FSTView alloc] initWithQuery:query - remoteDocuments:FSTTestDocKeySet(@[ doc1.key, doc2.key ])]; + FSTView *view = + [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{doc1.key, doc2.key}]; FSTTargetChange *markCurrent = [FSTTargetChange changeWithDocuments:@[] @@ -361,7 +362,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -394,7 +395,7 @@ NS_ASSUME_NONNULL_BEGIN FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{ @"order" : @1 }, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{ @"order" : @2 }, NO); FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -430,7 +431,7 @@ NS_ASSUME_NONNULL_BEGIN FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); FSTDocument *doc4 = FSTTestDoc("rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); FSTDocument *doc5 = FSTTestDoc("rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -460,7 +461,7 @@ NS_ASSUME_NONNULL_BEGIN FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/2", 0, @{ @"order" : @3 }, NO); FSTDocument *doc4 = FSTTestDoc("rooms/eros/messages/3", 0, @{ @"order" : @4 }, NO); FSTDocument *doc5 = FSTTestDoc("rooms/eros/messages/4", 0, @{ @"order" : @5 }, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -483,7 +484,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -506,7 +507,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQuery *query = [[self queryForMessages] queryBySettingLimit:20]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; @@ -528,7 +529,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQuery *query = [[self queryForMessages] queryBySettingLimit:2]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = @@ -551,69 +552,69 @@ NS_ASSUME_NONNULL_BEGIN FSTQuery *query = [self queryForMessages]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, NO); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); + XCTAssertEqual(changes.mutatedKeys, DocumentKeySet{}); FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/2", 0, @{}, YES); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc3.key ])); + XCTAssertEqual(changes.mutatedKeys, DocumentKeySet{doc3.key}); } - (void)testRemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges { FSTQuery *query = [self queryForMessages]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, YES); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key})); FSTDocument *doc2Prime = FSTTestDoc("rooms/eros/messages/1", 0, @{}, NO); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc2Prime ])]; [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[])); + XCTAssertEqual(changes.mutatedKeys, DocumentKeySet{}); } - (void)testRemembersLocalMutationsFromPreviousSnapshot { FSTQuery *query = [self queryForMessages]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, YES); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key})); FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/2", 0, @{}, NO); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ])]; [view applyChangesToDocuments:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key})); } - (void)testRemembersLocalMutationsFromPreviousCallToComputeChangesWithDocuments { FSTQuery *query = [self queryForMessages]; FSTDocument *doc1 = FSTTestDoc("rooms/eros/messages/0", 0, @{}, NO); FSTDocument *doc2 = FSTTestDoc("rooms/eros/messages/1", 0, @{}, YES); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:[FSTDocumentKeySet keySet]]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; // Start with a full view. FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc1, doc2 ])]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key})); FSTDocument *doc3 = FSTTestDoc("rooms/eros/messages/2", 0, @{}, NO); changes = [view computeChangesWithDocuments:FSTTestDocUpdates(@[ doc3 ]) previousChanges:changes]; - XCTAssertEqualObjects(changes.mutatedKeys, FSTTestDocKeySet(@[ doc2.key ])); + XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key})); } @end diff --git a/Firestore/Example/Tests/GoogleTest/GoogleTest.podspec b/Firestore/Example/Tests/GoogleTest/GoogleTest.podspec index 064fc59..0ecba3d 100644 --- a/Firestore/Example/Tests/GoogleTest/GoogleTest.podspec +++ b/Firestore/Example/Tests/GoogleTest/GoogleTest.podspec @@ -41,6 +41,8 @@ Google's C++ test framework. # (e.g. gtest.h). We don't need them because they're effectively empty: # they're compile-time hooks for third-party customization that we don't use. s.public_header_files = [ + 'googlemock/include/gmock/*.h', + 'googlemock/include/gmock/internal/*.h', 'googletest/include/gtest/*.h', 'googletest/include/gtest/internal/*.h' ] @@ -54,6 +56,9 @@ Google's C++ test framework. ] s.source_files = [ + 'googlemock/src/*.cc', + 'googlemock/include/gmock/*.h', + 'googlemock/include/gmock/internal/*.h', 'googletest/src/*.cc', 'googletest/include/gtest/*.h', 'googletest/include/gtest/internal/*.h' @@ -61,8 +66,11 @@ Google's C++ test framework. s.exclude_files = [ # A convenience wrapper for a simple command-line build. If included in - # this build, results in duplicate symbols + # this build, results in duplicate symbols. + 'googlemock/src/gmock-all.cc', 'googletest/src/gtest-all.cc', + # Both gmock and gtest define a main function but we only need one. + 'googletest/src/gtest_main.cc', ] s.library = 'c++' @@ -70,12 +78,17 @@ Google's C++ test framework. # When building this pod there are headers in googletest/src. s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => - '"${PODS_ROOT}/GoogleTest/googletest/include" "${PODS_ROOT}/GoogleTest/googletest"' + '"${PODS_ROOT}/GoogleTest/googlemock/include" ' + + '"${PODS_ROOT}/GoogleTest/googletest/include" ' + + '"${PODS_ROOT}/GoogleTest/googletest"' } s.prepare_command = <<-'CMD' # Remove includes of files in internal/custom sed -i.bak -e '/include.*internal\/custom/ d' \ + googlemock/include/gmock/gmock-matchers.h \ + googlemock/include/gmock/gmock-generated-actions.h \ + googlemock/include/gmock/internal/gmock-port.h \ googletest/include/gtest/gtest-printers.h \ googletest/include/gtest/internal/gtest-port.h \ googletest/src/gtest-death-test.cc \ diff --git a/Firestore/Example/Tests/Integration/API/FIRArrayTransformTests.mm b/Firestore/Example/Tests/Integration/API/FIRArrayTransformTests.mm index 1c82461..0a5b413 100644 --- a/Firestore/Example/Tests/Integration/API/FIRArrayTransformTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRArrayTransformTests.mm @@ -36,7 +36,7 @@ FIRDocumentReference *_docRef; // Accumulator used to capture events during the test. - FSTEventAccumulator *_accumulator; + FSTEventAccumulator<FIRDocumentSnapshot *> *_accumulator; // Listener registration for a listener maintained during the course of the test. id<FIRListenerRegistration> _listenerRegistration; @@ -64,29 +64,11 @@ #pragma mark - Test Helpers -/** Waits for a snapshot with local writes. */ -- (FIRDocumentSnapshot *)waitForLocalEvent { - FIRDocumentSnapshot *snapshot; - do { - snapshot = [_accumulator awaitEventWithName:@"Local event."]; - } while (!snapshot.metadata.hasPendingWrites); - return snapshot; -} - -/** Waits for a snapshot that has no pending writes */ -- (FIRDocumentSnapshot *)waitForRemoteEvent { - FIRDocumentSnapshot *snapshot; - do { - snapshot = [_accumulator awaitEventWithName:@"Remote event."]; - } while (snapshot.metadata.hasPendingWrites); - return snapshot; -} - /** Writes some initial data and consumes the events generated. */ - (void)writeInitialData:(NSDictionary<NSString *, id> *)data { [self writeDocumentRef:_docRef data:data]; - XCTAssertEqualObjects([self waitForLocalEvent].data, data); - XCTAssertEqualObjects([self waitForRemoteEvent].data, data); + XCTAssertEqualObjects([_accumulator awaitLocalEvent].data, data); + XCTAssertEqualObjects([_accumulator awaitRemoteEvent].data, data); } #pragma mark - Test Cases @@ -97,8 +79,8 @@ @"array" : [FIRFieldValue fieldValueForArrayUnion:@[ @1, @2 ]] }]; id expected = @{ @"array" : @[ @1, @2 ] }; - XCTAssertEqualObjects([self waitForLocalEvent].data, expected); - XCTAssertEqualObjects([self waitForRemoteEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitLocalEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitRemoteEvent].data, expected); } - (void)testAppendToArrayViaUpdate { @@ -110,8 +92,8 @@ }]; id expected = @{ @"array" : @[ @1, @3, @2, @4 ] }; - XCTAssertEqualObjects([self waitForLocalEvent].data, expected); - XCTAssertEqualObjects([self waitForRemoteEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitLocalEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitRemoteEvent].data, expected); } - (void)testAppendToArrayViaMergeSet { @@ -123,8 +105,8 @@ }]; id expected = @{ @"array" : @[ @1, @3, @2, @4 ] }; - XCTAssertEqualObjects([self waitForLocalEvent].data, expected); - XCTAssertEqualObjects([self waitForRemoteEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitLocalEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitRemoteEvent].data, expected); } - (void)testAppendObjectToArrayViaUpdate { @@ -137,8 +119,8 @@ }]; id expected = @{ @"array" : @[ @{@"a" : @"hi"}, @{@"a" : @"bye"} ] }; - XCTAssertEqualObjects([self waitForLocalEvent].data, expected); - XCTAssertEqualObjects([self waitForRemoteEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitLocalEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitRemoteEvent].data, expected); } - (void)testRemoveFromArrayViaUpdate { @@ -150,8 +132,8 @@ }]; id expected = @{ @"array" : @[ @3, @3 ] }; - XCTAssertEqualObjects([self waitForLocalEvent].data, expected); - XCTAssertEqualObjects([self waitForRemoteEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitLocalEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitRemoteEvent].data, expected); } - (void)testRemoveFromArrayViaMergeSet { @@ -163,8 +145,8 @@ }]; id expected = @{ @"array" : @[ @3, @3 ] }; - XCTAssertEqualObjects([self waitForLocalEvent].data, expected); - XCTAssertEqualObjects([self waitForRemoteEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitLocalEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitRemoteEvent].data, expected); } - (void)testRemoveObjectFromArrayViaUpdate { @@ -176,8 +158,8 @@ }]; id expected = @{ @"array" : @[ @{@"a" : @"bye"} ] }; - XCTAssertEqualObjects([self waitForLocalEvent].data, expected); - XCTAssertEqualObjects([self waitForRemoteEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitLocalEvent].data, expected); + XCTAssertEqualObjects([_accumulator awaitRemoteEvent].data, expected); } @end diff --git a/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm b/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm index 2188b8a..e444d7a 100644 --- a/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRCursorTests.mm @@ -216,32 +216,29 @@ FIRTimestamp *TimestampWithMicros(int64_t seconds, int32_t micros) { } - (void)testTimestampsCanBePassedToQueriesInWhereClause { - FIRTimestamp *currentTimestamp = [FIRTimestamp timestamp]; - int64_t seconds = currentTimestamp.seconds; - int32_t micros = currentTimestamp.nanoseconds / 1000; FIRCollectionReference *testCollection = [self collectionRefWithDocuments:@{ @"a" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros + 2), + @"timestamp" : TimestampWithMicros(100, 7), }, @"b" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros - 1), + @"timestamp" : TimestampWithMicros(100, 4), }, @"c" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros + 3), + @"timestamp" : TimestampWithMicros(100, 8), }, @"d" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros), + @"timestamp" : TimestampWithMicros(100, 5), }, @"e" : @{ - @"timestamp" : TimestampWithMicros(seconds, micros + 1), + @"timestamp" : TimestampWithMicros(100, 6), } }]; - FIRQuerySnapshot *querySnapshot = [self - readDocumentSetForRef:[[testCollection queryWhereField:@"timestamp" - isGreaterThanOrEqualTo:TimestampWithMicros(seconds, micros)] - queryWhereField:@"timestamp" - isLessThan:TimestampWithMicros(seconds, micros + 3)]]; + FIRQuerySnapshot *querySnapshot = + [self readDocumentSetForRef:[[testCollection queryWhereField:@"timestamp" + isGreaterThanOrEqualTo:TimestampWithMicros(100, 5)] + queryWhereField:@"timestamp" + isLessThan:TimestampWithMicros(100, 8)]]; XCTAssertEqualObjects(FIRQuerySnapshotGetIDs(querySnapshot), (@[ @"d", @"e", @"a" ])); } diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm index 9b6febe..f8091c0 100644 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm @@ -256,6 +256,155 @@ XCTAssertEqualObjects(document.data, finalData); } +- (void)testCannotSpecifyFieldMaskForMissingField { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + XCTAssertThrowsSpecific( + { [doc setData:@{} mergeFields:@[ @"foo" ]]; }, NSException, + @"Field 'foo' is specified in your field mask but missing from your input data."); +} + +- (void)testCanSetASubsetOfFieldsUsingMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{@"desc" : @"Description", @"owner" : @"Sebastian"}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : @"NewDescription", @"owner" : @"Sebastian"} + mergeFields:@[ @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testDoesNotApplyFieldDeleteOutsideOfMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{@"desc" : @"Description", @"owner" : @"Sebastian"}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : [FIRFieldValue fieldValueForDelete], @"owner" : @"Sebastian"} + mergeFields:@[ @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testDoesNotApplyFieldTransformOutsideOfMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{@"desc" : @"Description", @"owner" : @"Sebastian"}; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : [FIRFieldValue fieldValueForServerTimestamp], @"owner" : @"Sebastian"} + mergeFields:@[ @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testCanSetEmptyFieldMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = initialData; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{@"desc" : [FIRFieldValue fieldValueForServerTimestamp], @"owner" : @"Sebastian"} + mergeFields:@[] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + +- (void)testCanSpecifyFieldsMultipleTimesInFieldMask { + FIRDocumentReference *doc = [[self.db collectionWithPath:@"rooms"] documentWithAutoID]; + + NSDictionary<NSString *, id> *initialData = + @{ @"desc" : @"Description", + @"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"} }; + + NSDictionary<NSString *, id> *finalData = @{ + @"desc" : @"Description", + @"owner" : @{@"name" : @"Sebastian", @"email" : @"new@xyz.com"} + }; + + [self writeDocumentRef:doc data:initialData]; + + XCTestExpectation *completed = + [self expectationWithDescription:@"testCanSetASubsetOfFieldsUsingMask"]; + + [doc setData:@{ + @"desc" : @"NewDescription", + @"owner" : @{@"name" : @"Sebastian", @"email" : @"new@xyz.com"} + } + mergeFields:@[ @"owner.name", @"owner", @"owner" ] + completion:^(NSError *error) { + XCTAssertNil(error); + [completed fulfill]; + }]; + + [self awaitExpectations]; + + FIRDocumentSnapshot *document = [self readDocumentForRef:doc]; + XCTAssertEqualObjects(document.data, finalData); +} + - (void)testAddingToACollectionYieldsTheCorrectDocumentReference { FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; FIRDocumentReference *ref = [coll addDocumentWithData:@{ @"foo" : @1 }]; diff --git a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm index e1ad3d2..6a755c6 100644 --- a/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRServerTimestampTests.mm @@ -90,24 +90,6 @@ XCTAssertEqualObjects(initialDataSnap.data, _initialData); } -/** Waits for a snapshot with local writes. */ -- (FIRDocumentSnapshot *)waitForLocalEvent { - FIRDocumentSnapshot *snapshot; - do { - snapshot = [_accumulator awaitEventWithName:@"Local event."]; - } while (!snapshot.metadata.hasPendingWrites); - return snapshot; -} - -/** Waits for a snapshot that has no pending writes */ -- (FIRDocumentSnapshot *)waitForRemoteEvent { - FIRDocumentSnapshot *snapshot; - do { - snapshot = [_accumulator awaitEventWithName:@"Remote event."]; - } while (snapshot.metadata.hasPendingWrites); - return snapshot; -} - /** Verifies a snapshot containing _setData but with NSNull for the timestamps. */ - (void)verifyTimestampsAreNullInSnapshot:(FIRDocumentSnapshot *)snapshot { XCTAssertEqualObjects(snapshot.data, [self expectedDataWithTimestamp:[NSNull null]]); @@ -170,41 +152,42 @@ - (void)testServerTimestampsWorkViaSet { [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsAreNullInSnapshot:[self waitForLocalEvent]]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + [self verifyTimestampsAreNullInSnapshot:[_accumulator awaitLocalEvent]]; + [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; } - (void)testServerTimestampsWorkViaUpdate { [self writeInitialData]; [self updateDocumentRef:_docRef data:_updateData]; - [self verifyTimestampsAreNullInSnapshot:[self waitForLocalEvent]]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + [self verifyTimestampsAreNullInSnapshot:[_accumulator awaitLocalEvent]]; + [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; } - (void)testServerTimestampsWithEstimatedValue { [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsAreEstimatedInSnapshot:[self waitForLocalEvent]]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + [self verifyTimestampsAreEstimatedInSnapshot:[_accumulator awaitLocalEvent]]; + [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; } - (void)testServerTimestampsWithPreviousValue { [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; - FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + [self verifyTimestampsInSnapshot:[_accumulator awaitLocalEvent] fromPreviousSnapshot:nil]; + FIRDocumentSnapshot *remoteSnapshot = [_accumulator awaitRemoteEvent]; [_docRef updateData:_updateData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:remoteSnapshot]; + [self verifyTimestampsInSnapshot:[_accumulator awaitLocalEvent] + fromPreviousSnapshot:remoteSnapshot]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; } - (void)testServerTimestampsWithPreviousValueOfDifferentType { [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + [self verifyTimestampsInSnapshot:[_accumulator awaitLocalEvent] fromPreviousSnapshot:nil]; + [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + FIRDocumentSnapshot *localSnapshot = [_accumulator awaitLocalEvent]; XCTAssertEqualObjects([localSnapshot valueForField:@"a"], [NSNull null]); XCTAssertEqualObjects( [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], @@ -213,7 +196,7 @@ [[localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorEstimate] isKindOfClass:[FIRTimestamp class]]); - FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + FIRDocumentSnapshot *remoteSnapshot = [_accumulator awaitRemoteEvent]; XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[FIRTimestamp class]]); XCTAssertTrue([ [remoteSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious] @@ -225,55 +208,55 @@ - (void)testServerTimestampsWithConsecutiveUpdates { [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + [self verifyTimestampsInSnapshot:[_accumulator awaitLocalEvent] fromPreviousSnapshot:nil]; + [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; [self disableNetwork]; [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + FIRDocumentSnapshot *localSnapshot = [_accumulator awaitLocalEvent]; XCTAssertEqualObjects( [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], @42); [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - localSnapshot = [self waitForLocalEvent]; + localSnapshot = [_accumulator awaitLocalEvent]; XCTAssertEqualObjects( [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], @42); [self enableNetwork]; - FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + FIRDocumentSnapshot *remoteSnapshot = [_accumulator awaitRemoteEvent]; XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[FIRTimestamp class]]); } - (void)testServerTimestampsPreviousValueFromLocalMutation { [self writeDocumentRef:_docRef data:_setData]; - [self verifyTimestampsInSnapshot:[self waitForLocalEvent] fromPreviousSnapshot:nil]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + [self verifyTimestampsInSnapshot:[_accumulator awaitLocalEvent] fromPreviousSnapshot:nil]; + [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; [self disableNetwork]; [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - FIRDocumentSnapshot *localSnapshot = [self waitForLocalEvent]; + FIRDocumentSnapshot *localSnapshot = [_accumulator awaitLocalEvent]; XCTAssertEqualObjects( [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], @42); [_docRef updateData:@{ @"a" : @1337 }]; - localSnapshot = [self waitForLocalEvent]; + localSnapshot = [_accumulator awaitLocalEvent]; XCTAssertEqualObjects([localSnapshot valueForField:@"a"], @1337); [_docRef updateData:@{@"a" : [FIRFieldValue fieldValueForServerTimestamp]}]; - localSnapshot = [self waitForLocalEvent]; + localSnapshot = [_accumulator awaitLocalEvent]; XCTAssertEqualObjects( [localSnapshot valueForField:@"a" serverTimestampBehavior:FIRServerTimestampBehaviorPrevious], @1337); [self enableNetwork]; - FIRDocumentSnapshot *remoteSnapshot = [self waitForRemoteEvent]; + FIRDocumentSnapshot *remoteSnapshot = [_accumulator awaitRemoteEvent]; XCTAssertTrue([[remoteSnapshot valueForField:@"a"] isKindOfClass:[FIRTimestamp class]]); } @@ -282,7 +265,7 @@ [transaction setData:_setData forDocument:_docRef]; }]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; } - (void)testServerTimestampsWorkViaTransactionUpdate { @@ -290,7 +273,7 @@ [self runTransactionBlock:^(FIRTransaction *transaction) { [transaction updateData:_updateData forDocument:_docRef]; }]; - [self verifySnapshotWithResolvedTimestamps:[self waitForRemoteEvent]]; + [self verifySnapshotWithResolvedTimestamps:[_accumulator awaitRemoteEvent]]; } - (void)testServerTimestampsFailViaUpdateOnNonexistentDocument { diff --git a/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm b/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm index 6d10aba..8af8d15 100644 --- a/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRValidationTests.mm @@ -507,8 +507,8 @@ FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] isEqualTo:@1], reason); reason = - @"Invalid query. You can't do arrayContains queries on document ID since document IDs are " - @"not arrays."; + @"Invalid query. You can't perform arrayContains queries on document ID since document IDs " + "are not arrays."; FSTAssertThrows([collection queryWhereFieldPath:[FIRFieldPath documentID] arrayContains:@1], reason); } diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index ad911ce..920e3c5 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -25,7 +25,6 @@ #import "Firestore/Source/API/FSTUserDataConverter.h" #import "Firestore/Source/Core/FSTFirestoreClient.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocumentKey.h" #import "Firestore/Source/Model/FSTFieldValue.h" diff --git a/Firestore/Example/Tests/Integration/FSTStreamTests.mm b/Firestore/Example/Tests/Integration/FSTStreamTests.mm index 7e37913..2e5c9b6 100644 --- a/Firestore/Example/Tests/Integration/FSTStreamTests.mm +++ b/Firestore/Example/Tests/Integration/FSTStreamTests.mm @@ -29,12 +29,14 @@ #include "Firestore/core/src/firebase/firestore/auth/empty_credentials_provider.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; using firebase::firestore::auth::EmptyCredentialsProvider; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::SnapshotVersion; /** Exposes otherwise private methods for testing. */ @interface FSTStream (Testing) @@ -101,13 +103,13 @@ using firebase::firestore::model::DatabaseId; } - (void)watchStreamDidChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + snapshotVersion:(const SnapshotVersion &)snapshotVersion { [_states addObject:@"watchStreamDidChange"]; [_expectation fulfill]; _expectation = nil; } -- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)writeStreamDidReceiveResponseWithVersion:(const SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results { [_states addObject:@"writeStreamDidReceiveResponseWithVersion"]; [_expectation fulfill]; diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm index 362f46f..0e160c0 100644 --- a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm @@ -29,7 +29,6 @@ #import "Firestore/Protos/objc/google/firestore/v1beta1/Write.pbobjc.h" #import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentKey.h" @@ -50,6 +49,7 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::FieldMask; using firebase::firestore::model::Precondition; +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -125,7 +125,7 @@ NS_ASSUME_NONNULL_BEGIN XCTAssertEqual(decoded.batchID, model.batchID); XCTAssertEqualObjects(decoded.localWriteTime, model.localWriteTime); XCTAssertEqualObjects(decoded.mutations, model.mutations); - XCTAssertEqualObjects([decoded keys], [model keys]); + XCTAssertEqual([decoded keys], [model keys]); } - (void)testEncodesDocumentAsMaybeDocument { @@ -162,7 +162,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testEncodesQueryData { FSTQuery *query = FSTTestQuery("room"); FSTTargetID targetID = 42; - FSTSnapshotVersion *version = FSTTestVersion(1039); + SnapshotVersion version = testutil::Version(1039); NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1039); FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index 3565e2e..e10fb12 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -41,19 +41,15 @@ #import "Firestore/third_party/Immutable/Tests/FSTImmutableSortedSet+Testing.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +namespace testutil = firebase::firestore::testutil; using firebase::firestore::auth::User; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN -/** Creates a document version dictionary mapping the document in @a mutation to @a version. */ -FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, - FSTTestSnapshotVersion version) { - FSTDocumentVersionDictionary *result = [FSTDocumentVersionDictionary documentVersionDictionary]; - result = [result dictionaryBySettingObject:FSTTestVersion(version) forKey:mutation.key]; - return result; -} - @interface FSTLocalStoreTests () @property(nonatomic, strong, readwrite) id<FSTPersistence> localStorePersistence; @@ -140,7 +136,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTMutationBatch *batch = [self.batches firstObject]; [self.batches removeObjectAtIndex:0]; XCTAssertEqual(batch.mutations.count, 1, @"Acknowledging more than one mutation not supported."); - FSTSnapshotVersion *version = FSTTestVersion(documentVersion); + SnapshotVersion version = testutil::Version(documentVersion); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] initWithVersion:version transformResults:nil]; FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch @@ -227,8 +223,8 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1 localWriteTime:[FIRTimestamp timestamp] mutations:@[ set1, set2 ]]; - FSTDocumentKeySet *keys = [batch keys]; - XCTAssertEqual(keys.count, 2); + DocumentKeySet keys = [batch keys]; + XCTAssertEqual(keys.size(), 2u); } - (void)testHandlesSetMutation { @@ -805,6 +801,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, FSTQuery *query = FSTTestQuery("foo/bar"); FSTQueryData *queryData = [self.localStore allocateQuery:query]; + FSTListenSequenceNumber initialSequenceNumber = queryData.sequenceNumber; FSTBoxedTargetID *targetID = @(queryData.targetID); NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); @@ -818,7 +815,7 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingResponses = [NSMutableDictionary dictionary]; FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(1000) + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(1000) listenTargets:listens pendingTargetResponses:pendingResponses]; [aggregator addWatchChanges:@[ watchChange ]]; @@ -831,6 +828,10 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, // Should come back with the same resume token FSTQueryData *queryData2 = [self.localStore allocateQuery:query]; XCTAssertEqualObjects(queryData2.resumeToken, resumeToken); + + // The sequence number should have been bumped when we saved the new resume token. + FSTListenSequenceNumber newSequenceNumber = queryData2.sequenceNumber; + XCTAssertGreaterThan(newSequenceNumber, initialSequenceNumber); } - (void)testRemoteDocumentKeysForTarget { @@ -848,13 +849,14 @@ FSTDocumentVersionDictionary *FSTVersionDictionary(FSTMutation *mutation, [self.localStore locallyWriteMutations:@[ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) ]]; - FSTDocumentKeySet *keys = [self.localStore remoteDocumentKeysForTarget:2]; - FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); + DocumentKeySet keys = [self.localStore remoteDocumentKeysForTarget:2]; + DocumentKeySet expected{testutil::Key("foo/bar"), testutil::Key("foo/baz")}; + XCTAssertEqual(keys, expected); [self restartWithNoopGarbageCollector]; keys = [self.localStore remoteDocumentKeysForTarget:2]; - FSTAssertEqualSets(keys, (@[ FSTTestDocKey(@"foo/bar"), FSTTestDocKey(@"foo/baz") ])); + XCTAssertEqual(keys, (DocumentKeySet{testutil::Key("foo/bar"), testutil::Key("foo/baz")})); } @end diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm b/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm index 429a83a..44b49de 100644 --- a/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm @@ -19,7 +19,6 @@ #include <set> #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTEagerGarbageCollector.h" #import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Local/FSTQueryData.h" @@ -32,6 +31,8 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -130,9 +131,9 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryData *result = [self.queryCache queryDataForQuery:_queryRooms]; XCTAssertNotEqualObjects(queryData2.resumeToken, queryData1.resumeToken); - XCTAssertNotEqualObjects(queryData2.snapshotVersion, queryData1.snapshotVersion); + XCTAssertNotEqual(queryData2.snapshotVersion, queryData1.snapshotVersion); XCTAssertEqualObjects(result.resumeToken, queryData2.resumeToken); - XCTAssertEqualObjects(result.snapshotVersion, queryData2.snapshotVersion); + XCTAssertEqual(result.snapshotVersion, queryData2.snapshotVersion); }); } @@ -278,12 +279,12 @@ NS_ASSUME_NONNULL_BEGIN [self addMatchingKey:key2 forTargetID:1]; [self addMatchingKey:key3 forTargetID:2]; - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], @[ key3 ]); + XCTAssertEqual([self.queryCache matchingKeysForTargetID:1], (DocumentKeySet{key1, key2})); + XCTAssertEqual([self.queryCache matchingKeysForTargetID:2], (DocumentKeySet{key3})); [self addMatchingKey:key1 forTargetID:2]; - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:1], (@[ key1, key2 ])); - FSTAssertEqualSets([self.queryCache matchingKeysForTargetID:2], (@[ key1, key3 ])); + XCTAssertEqual([self.queryCache matchingKeysForTargetID:1], (DocumentKeySet{key1, key2})); + XCTAssertEqual([self.queryCache matchingKeysForTargetID:2], (DocumentKeySet{key1, key3})); }); } @@ -386,19 +387,18 @@ NS_ASSUME_NONNULL_BEGIN if ([self isTestBaseClass]) return; self.persistence.run("testLastRemoteSnapshotVersion", [&]() { - XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], - [FSTSnapshotVersion noVersion]); + XCTAssertEqual([self.queryCache lastRemoteSnapshotVersion], SnapshotVersion::None()); // Can set the snapshot version. - [self.queryCache setLastRemoteSnapshotVersion:FSTTestVersion(42)]; - XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); + [self.queryCache setLastRemoteSnapshotVersion:testutil::Version(42)]; + XCTAssertEqual([self.queryCache lastRemoteSnapshotVersion], testutil::Version(42)); }); // Snapshot version persists restarts. self.queryCache = [self.persistence queryCache]; self.persistence.run("testLastRemoteSnapshotVersion restart", [&]() { [self.queryCache start]; - XCTAssertEqualObjects([self.queryCache lastRemoteSnapshotVersion], FSTTestVersion(42)); + XCTAssertEqual([self.queryCache lastRemoteSnapshotVersion], testutil::Version(42)); }); } @@ -424,19 +424,17 @@ NS_ASSUME_NONNULL_BEGIN targetID:targetID listenSequenceNumber:sequenceNumber purpose:FSTQueryPurposeListen - snapshotVersion:FSTTestVersion(version) + snapshotVersion:testutil::Version(version) resumeToken:resumeToken]; } - (void)addMatchingKey:(const DocumentKey &)key forTargetID:(FSTTargetID)targetID { - FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; - keys = [keys setByAddingObject:key]; + DocumentKeySet keys{key}; [self.queryCache addMatchingKeys:keys forTargetID:targetID]; } - (void)removeMatchingKey:(const DocumentKey &)key forTargetID:(FSTTargetID)targetID { - FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; - keys = [keys setByAddingObject:key]; + DocumentKeySet keys{key}; [self.queryCache removeMatchingKeys:keys forTargetID:targetID]; } diff --git a/Firestore/Example/Tests/Model/FSTDocumentTests.mm b/Firestore/Example/Tests/Model/FSTDocumentTests.mm index 24858c5..4e3517c 100644 --- a/Firestore/Example/Tests/Model/FSTDocumentTests.mm +++ b/Firestore/Example/Tests/Model/FSTDocumentTests.mm @@ -18,7 +18,6 @@ #import <XCTest/XCTest.h> -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" @@ -28,6 +27,7 @@ namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -38,20 +38,20 @@ NS_ASSUME_NONNULL_BEGIN - (void)testConstructor { DocumentKey key = testutil::Key("messages/first"); - FSTSnapshotVersion *version = FSTTestVersion(1); + SnapshotVersion version = testutil::Version(1); FSTObjectValue *data = FSTTestObjectValue(@{ @"a" : @1 }); FSTDocument *doc = [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; XCTAssertEqualObjects(doc.key, FSTTestDocKey(@"messages/first")); - XCTAssertEqualObjects(doc.version, version); + XCTAssertEqual(doc.version, version); XCTAssertEqualObjects(doc.data, data); XCTAssertEqual(doc.hasLocalMutations, NO); } - (void)testExtractsFields { DocumentKey key = testutil::Key("rooms/eros"); - FSTSnapshotVersion *version = FSTTestVersion(1); + SnapshotVersion version = testutil::Version(1); FSTObjectValue *data = FSTTestObjectValue(@{ @"desc" : @"Discuss all the project related stuff", @"owner" : @{@"name" : @"Jonny", @"title" : @"scallywag"} diff --git a/Firestore/Example/Tests/Model/FSTMutationTests.mm b/Firestore/Example/Tests/Model/FSTMutationTests.mm index 936bd38..b9f98ce 100644 --- a/Firestore/Example/Tests/Model/FSTMutationTests.mm +++ b/Firestore/Example/Tests/Model/FSTMutationTests.mm @@ -135,7 +135,7 @@ using firebase::firestore::model::TransformOperation; FSTDocument *expectedDoc = [FSTDocument documentWithData:expectedData key:FSTTestDocKey(@"collection/key") - version:FSTTestVersion(0) + version:testutil::Version(0) hasLocalMutations:YES]; XCTAssertEqualObjects(transformedDoc, expectedDoc); @@ -301,7 +301,7 @@ using firebase::firestore::model::TransformOperation; FSTDocument *expectedDoc = [FSTDocument documentWithData:FSTTestObjectValue(expectedData) key:FSTTestDocKey(@"collection/key") - version:FSTTestVersion(0) + version:testutil::Version(0) hasLocalMutations:YES]; XCTAssertEqualObjects(transformedDoc, expectedDoc); @@ -315,7 +315,7 @@ using firebase::firestore::model::TransformOperation; @"collection/key", @{@"foo.bar" : [FIRFieldValue fieldValueForServerTimestamp]}); FSTMutationResult *mutationResult = [[FSTMutationResult alloc] - initWithVersion:FSTTestVersion(1) + initWithVersion:testutil::Version(1) transformResults:@[ [FSTTimestampValue timestampValue:_timestamp] ]]; FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc @@ -340,7 +340,7 @@ using firebase::firestore::model::TransformOperation; // Server just sends null transform results for array operations. FSTMutationResult *mutationResult = [[FSTMutationResult alloc] - initWithVersion:FSTTestVersion(1) + initWithVersion:testutil::Version(1) transformResults:@[ [FSTNullValue nullValue], [FSTNullValue nullValue] ]]; FSTMaybeDocument *transformedDoc = [transform applyTo:baseDoc @@ -368,7 +368,7 @@ using firebase::firestore::model::TransformOperation; FSTMutation *set = FSTTestSetMutation(@"collection/key", @{@"foo" : @"new-bar"}); FSTMutationResult *mutationResult = - [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + [[FSTMutationResult alloc] initWithVersion:testutil::Version(4) transformResults:nil]; FSTMaybeDocument *setDoc = [set applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp @@ -384,7 +384,7 @@ using firebase::firestore::model::TransformOperation; FSTMutation *patch = FSTTestPatchMutation("collection/key", @{@"foo" : @"new-bar"}, {}); FSTMutationResult *mutationResult = - [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(4) transformResults:nil]; + [[FSTMutationResult alloc] initWithVersion:testutil::Version(4) transformResults:nil]; FSTMaybeDocument *patchedDoc = [patch applyTo:baseDoc baseDocument:baseDoc localWriteTime:_timestamp @@ -394,15 +394,15 @@ using firebase::firestore::model::TransformOperation; XCTAssertEqualObjects(patchedDoc, FSTTestDoc("collection/key", 0, expectedData, NO)); } -#define ASSERT_VERSION_TRANSITION(mutation, base, expected) \ - do { \ - FSTMutationResult *mutationResult = \ - [[FSTMutationResult alloc] initWithVersion:FSTTestVersion(0) transformResults:nil]; \ - FSTMaybeDocument *actual = [mutation applyTo:base \ - baseDocument:base \ - localWriteTime:_timestamp \ - mutationResult:mutationResult]; \ - XCTAssertEqualObjects(actual, expected); \ +#define ASSERT_VERSION_TRANSITION(mutation, base, expected) \ + do { \ + FSTMutationResult *mutationResult = \ + [[FSTMutationResult alloc] initWithVersion:testutil::Version(0) transformResults:nil]; \ + FSTMaybeDocument *actual = [mutation applyTo:base \ + baseDocument:base \ + localWriteTime:_timestamp \ + mutationResult:mutationResult]; \ + XCTAssertEqualObjects(actual, expected); \ } while (0); /** diff --git a/Firestore/core/test/firebase/firestore/model/field_transform_test.cc b/Firestore/Example/Tests/Model/field_transform_test.mm index b66aeef..a22a0f3 100644 --- a/Firestore/core/test/firebase/firestore/model/field_transform_test.cc +++ b/Firestore/Example/Tests/Model/field_transform_test.mm @@ -26,9 +26,8 @@ namespace firestore { namespace model { TEST(FieldTransform, Getter) { - FieldTransform transform(testutil::Field("foo"), - absl::make_unique<ServerTimestampTransform>( - ServerTimestampTransform::Get())); + FieldTransform transform{testutil::Field("foo"), absl::make_unique<ServerTimestampTransform>( + ServerTimestampTransform::Get())}; EXPECT_EQ(testutil::Field("foo"), transform.path()); EXPECT_EQ(ServerTimestampTransform::Get(), transform.transformation()); diff --git a/Firestore/core/test/firebase/firestore/model/transform_operations_test.cc b/Firestore/Example/Tests/Model/transform_operations_test.mm index ec0882a..247ea13 100644 --- a/Firestore/core/test/firebase/firestore/model/transform_operations_test.cc +++ b/Firestore/Example/Tests/Model/transform_operations_test.mm @@ -31,9 +31,25 @@ class DummyOperation : public TransformOperation { return Type::Test; } + FSTFieldValue* ApplyToLocalView(FSTFieldValue* /* previousValue */, + FIRTimestamp* /* localWriteTime */) const override { + return nil; + } + + FSTFieldValue* ApplyToRemoteDocument(FSTFieldValue* /* previousValue */, + FSTFieldValue* /* transformResult */) const override { + return nil; + } + bool operator==(const TransformOperation& other) const override { return this == &other; } + + NSUInteger Hash() const override { + // arbitrary number, the same as used in ObjC implementation, since all + // instances are equal. + return 37; + } }; TEST(TransformOperations, ServerTimestamp) { diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm index 6ac2a6b..84d0fa1 100644 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -18,6 +18,7 @@ #import <XCTest/XCTest.h> +#import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentKey.h" @@ -28,7 +29,11 @@ #import "Firestore/Example/Tests/Remote/FSTWatchChange+Testing.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" + +namespace testutil = firebase::firestore::testutil; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -55,7 +60,7 @@ NS_ASSUME_NONNULL_BEGIN listens[targetID] = dummyQueryData; } FSTWatchChangeAggregator *aggregator = - [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:FSTTestVersion(3) + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:testutil::Version(3) listenTargets:listens pendingTargetResponses:outstanding]; [aggregator addWatchChanges:watchChanges]; @@ -81,7 +86,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + 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); @@ -143,7 +148,7 @@ NS_ASSUME_NONNULL_BEGIN outstanding:pendingResponses changes:@[ change1, change2, change3, change4 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + 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); @@ -171,7 +176,7 @@ NS_ASSUME_NONNULL_BEGIN [self aggregatorWithTargets:@[] outstanding:pendingResponses changes:@[ change1, change2 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); // doc1 is ignored because it was part of an inactive target XCTAssertEqual(event.documentUpdates.size(), 0); @@ -215,7 +220,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2, change3, change4, change5 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + 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); @@ -239,7 +244,7 @@ NS_ASSUME_NONNULL_BEGIN [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.count, 1); @@ -267,7 +272,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 1); XCTAssertEqualObjects(event.documentUpdates.at(doc1b.key), doc1b); @@ -291,7 +296,7 @@ NS_ASSUME_NONNULL_BEGIN [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.count, 1); FSTTargetChange *targetChange = event.targetChanges[@1]; @@ -333,7 +338,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2, change3, change4, change5, change6 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + 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); @@ -366,7 +371,7 @@ NS_ASSUME_NONNULL_BEGIN [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses changes:@[ change ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.count, 1); XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTUpdateMapping alloc] init]); @@ -388,7 +393,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2, change3 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 0); XCTAssertEqual(event.targetChanges.count, 0); XCTAssertEqual(aggregator.existenceFilters.count, 2); @@ -420,7 +425,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2, change3 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + 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); @@ -430,7 +435,7 @@ NS_ASSUME_NONNULL_BEGIN FSTUpdateMapping *mapping1 = [FSTUpdateMapping mappingWithAddedDocuments:@[ doc1, doc2 ] removedDocuments:@[]]; XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@1].snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.targetChanges[@1].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); XCTAssertEqualObjects(event.targetChanges[@1].resumeToken, _resumeToken1); @@ -439,7 +444,7 @@ NS_ASSUME_NONNULL_BEGIN // Mapping is reset XCTAssertEqualObjects(event.targetChanges[@1].mapping, [[FSTResetMapping alloc] init]); // Reset the resume snapshot - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(0)); + 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); @@ -448,7 +453,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)testDocumentUpdate { FSTDocument *doc1 = FSTTestDoc("docs/1", 1, @{ @"value" : @1 }, NO); FSTDeletedDocument *deletedDoc1 = - [FSTDeletedDocument documentWithKey:doc1.key version:FSTTestVersion(3)]; + [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); @@ -467,7 +472,7 @@ NS_ASSUME_NONNULL_BEGIN changes:@[ change1, change2 ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + 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); @@ -476,7 +481,7 @@ NS_ASSUME_NONNULL_BEGIN [event addDocumentUpdate:deletedDoc1]; [event addDocumentUpdate:doc3]; - XCTAssertEqualObjects(event.snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.documentUpdates.size(), 3); // doc1 is replaced XCTAssertEqualObjects(event.documentUpdates.at(doc1.key), deletedDoc1); @@ -511,12 +516,12 @@ NS_ASSUME_NONNULL_BEGIN FSTUpdateMapping *mapping1 = [FSTUpdateMapping mappingWithAddedDocuments:@[] removedDocuments:@[]]; XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + 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); - XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateMarkCurrent); XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken2); } @@ -544,12 +549,12 @@ NS_ASSUME_NONNULL_BEGIN FSTResetMapping *mapping1 = [FSTResetMapping mappingWithDocuments:@[]]; XCTAssertEqualObjects(event.targetChanges[@1].mapping, mapping1); - XCTAssertEqualObjects(event.targetChanges[@1].snapshotVersion, FSTTestVersion(3)); + 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); - XCTAssertEqualObjects(event.targetChanges[@2].snapshotVersion, FSTTestVersion(3)); + XCTAssertEqual(event.targetChanges[@2].snapshotVersion, testutil::Version(3)); XCTAssertEqual(event.targetChanges[@2].currentStatusUpdate, FSTCurrentStatusUpdateNone); XCTAssertEqualObjects(event.targetChanges[@2].resumeToken, resumeToken3); } @@ -557,20 +562,10 @@ NS_ASSUME_NONNULL_BEGIN - (void)testSynthesizeDeletes { FSTWatchChange *shouldSynthesize = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent targetIDs:@[ @1 ]]; - FSTWatchChange *wrongState = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @2 ]]; - 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:@[ @1, @2, @3 ] - outstanding:_noPendingResponses - changes:@[ shouldSynthesize, wrongState, hasDocument, docChange ]]; + FSTWatchChangeAggregator *aggregator = [self aggregatorWithTargets:@[ @1 ] + outstanding:_noPendingResponses + changes:@[ shouldSynthesize ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; DocumentKey synthesized = DocumentKey::FromPathString("docs/2"); @@ -581,14 +576,41 @@ NS_ASSUME_NONNULL_BEGIN FSTDeletedDocument *expected = [FSTDeletedDocument documentWithKey:synthesized version:event.snapshotVersion]; XCTAssertEqualObjects(expected, event.documentUpdates.at(synthesized)); + XCTAssertTrue(event.limboDocumentChanges.contains(synthesized)); +} + +- (void)testDoesntSynthesizeDeletesForWrongState { + FSTWatchChange *wrongState = + [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateNoChange targetIDs:@[ @2 ]]; - DocumentKey notSynthesized1 = DocumentKey::FromPathString("docs/no1"); - [event synthesizeDeleteForLimboTargetChange:event.targetChanges[@2] key:notSynthesized1]; - XCTAssertEqual(event.documentUpdates.find(notSynthesized1), event.documentUpdates.end()); + FSTWatchChangeAggregator *aggregator = + [self aggregatorWithTargets:@[ @2 ] outstanding:_noPendingResponses changes:@[ wrongState ]]; + FSTRemoteEvent *event = [aggregator remoteEvent]; + + 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)); +} + +- (void)testDoesntSynthesizeDeletesForExistingDoc { + 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)); } - (void)testFilterUpdates { @@ -599,44 +621,108 @@ NS_ASSUME_NONNULL_BEGIN documentKey:newDoc.key document:newDoc]; - FSTWatchTargetChange *resetTargetChange = - [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateReset - targetIDs:@[ @2 ] - resumeToken:_resumeToken1]; - FSTWatchChange *existingDocChange = - [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1, @2 ] + [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] removedTargetIDs:@[] documentKey:existingDoc.key document:existingDoc]; FSTWatchChangeAggregator *aggregator = - [self aggregatorWithTargets:@[ @1, @2 ] + [self aggregatorWithTargets:@[ @1 ] outstanding:_noPendingResponses - changes:@[ newDocChange, resetTargetChange, existingDocChange ]]; + changes:@[ newDocChange, existingDocChange ]]; FSTRemoteEvent *event = [aggregator remoteEvent]; - FSTDocumentKeySet *existingKeys = [[FSTDocumentKeySet keySet] setByAddingObject:existingDoc.key]; + 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 containsObject:existingDocKey]); + XCTAssertTrue(update.addedDocuments.contains(existingDocKey)); - [event filterUpdatesFromTargetChange:updateChange existingDocuments:existingKeys]; + [update filterUpdatesUsingExistingKeys:existingKeys]; // Now it's been filtered, since it already existed. - XCTAssertFalse([update.addedDocuments containsObject:existingDocKey]); - XCTAssertTrue([update.addedDocuments containsObject:newDocKey]); + 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 containsObject:existingDocKey]); + XCTAssertTrue(resetMapping.documents.contains(existingDocKey)); - [event filterUpdatesFromTargetChange:resetChange existingDocuments:existingKeys]; + [resetMapping filterUpdatesUsingExistingKeys:existingKeys]; // Document is still there, even though it already exists. Reset mappings don't get filtered. - XCTAssertTrue([resetMapping.documents containsObject:existingDocKey]); + XCTAssertTrue(resetMapping.documents.contains(existingDocKey)); +} + +- (void)testTracksLimboDocuments { + // 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 + document:doc1]; + + FSTWatchChange *docChange2 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @2 ] + removedTargetIDs:@[] + documentKey:doc2.key + document:doc2]; + + FSTWatchChange *docChange3 = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[ @1 ] + removedTargetIDs:@[] + documentKey:doc3.key + document:doc3]; + + 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 = [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)); + // Doc2 is only in the limbo target, so is tracked as a limbo document + XCTAssertTrue(limboDocChanges.contains(doc2.key)); + // Doc3 is only in the non-limbo target, therefore not tracked as limbo + XCTAssertFalse(limboDocChanges.contains(doc3.key)); } @end diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm index bbb3822..da47aaa 100644 --- a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm @@ -37,7 +37,6 @@ #import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" #import "Firestore/Source/API/FIRFieldValue+Internal.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentKey.h" @@ -58,10 +57,12 @@ namespace testutil = firebase::firestore::testutil; namespace util = firebase::firestore::util; +using firebase::Timestamp; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::FieldMask; using firebase::firestore::model::FieldTransform; using firebase::firestore::model::Precondition; +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -424,8 +425,7 @@ NS_ASSUME_NONNULL_BEGIN precondition:Precondition::UpdateTime(testutil::Version(4))]; GCFSWrite *proto = [GCFSWrite message]; proto.update = [self.serializer encodedDocumentWithFields:mutation.value key:mutation.key]; - proto.currentDocument.updateTime = - [self.serializer encodedTimestamp:[[FIRTimestamp alloc] initWithSeconds:0 nanoseconds:4000]]; + proto.currentDocument.updateTime = [self.serializer encodedTimestamp:Timestamp{0, 4000}]; [self assertRoundTripForMutation:mutation proto:proto]; } @@ -708,7 +708,7 @@ NS_ASSUME_NONNULL_BEGIN targetID:1 listenSequenceNumber:0 purpose:FSTQueryPurposeListen - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() resumeToken:FSTTestData(1, 2, 3, -1)]; GCFSTarget *expected = [GCFSTarget message]; @@ -729,7 +729,7 @@ NS_ASSUME_NONNULL_BEGIN targetID:1 listenSequenceNumber:0 purpose:FSTQueryPurposeListen - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() resumeToken:[NSData data]]; } diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h index e1ea2fb..6951f9c 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h @@ -18,6 +18,8 @@ #import "Firestore/Source/Remote/FSTDatastore.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + NS_ASSUME_NONNULL_BEGIN @interface FSTMockDatastore : FSTDatastore @@ -41,11 +43,13 @@ NS_ASSUME_NONNULL_BEGIN /** Injects an Added WatchChange that marks the given targetIDs current. */ - (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion: + (const firebase::firestore::model::SnapshotVersion &)snapshotVersion resumeToken:(NSData *)resumeToken; /** Injects a WatchChange as though it had come from the backend. */ -- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap; +- (void)writeWatchChange:(FSTWatchChange *)change + snapshotVersion:(const firebase::firestore::model::SnapshotVersion &)snap; /** Injects a stream failure as though it had come from the backend. */ - (void)failWatchStreamWithError:(NSError *)error; @@ -67,7 +71,7 @@ NS_ASSUME_NONNULL_BEGIN - (int)writesSent; /** Injects a write ack as though it had come from the backend in response to a write. */ -- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)ackWriteWithVersion:(const firebase::firestore::model::SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results; /** Injects a stream failure as though it had come from the backend. */ diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm index 6715b24..dd34556 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm @@ -16,7 +16,6 @@ #import "Firestore/Example/Tests/SpecTests/FSTMockDatastore.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" @@ -36,6 +35,7 @@ using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::EmptyCredentialsProvider; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::SnapshotVersion; @class GRPCProtoCall; @@ -120,9 +120,9 @@ NS_ASSUME_NONNULL_BEGIN FSTLog(@"watchQuery: %d: %@", query.targetID, query.query); self.datastore.watchStreamRequestCount += 1; // Snapshot version is ignored on the wire - FSTQueryData *sentQueryData = - [query queryDataByReplacingSnapshotVersion:[FSTSnapshotVersion noVersion] - resumeToken:query.resumeToken]; + FSTQueryData *sentQueryData = [query queryDataByReplacingSnapshotVersion:SnapshotVersion::None() + resumeToken:query.resumeToken + sequenceNumber:query.sequenceNumber]; self.activeTargets[@(query.targetID)] = sentQueryData; } @@ -138,7 +138,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Helper methods. -- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(const SnapshotVersion &)snap { if ([change isKindOfClass:[FSTWatchTargetChange class]]) { FSTWatchTargetChange *targetChange = (FSTWatchTargetChange *)change; if (targetChange.cause) { @@ -242,7 +242,7 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - Helper methods. /** Injects a write ack as though it had come from the backend in response to a write. */ -- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)ackWriteWithVersion:(const SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results { [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; } @@ -326,7 +326,7 @@ NS_ASSUME_NONNULL_BEGIN return [self.writeStream sentMutationsCount]; } -- (void)ackWriteWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)ackWriteWithVersion:(const SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results { [self.writeStream ackWriteWithVersion:commitVersion mutationResults:results]; } @@ -340,11 +340,11 @@ NS_ASSUME_NONNULL_BEGIN [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateAdded targetIDs:targetIDs cause:nil]; - [self writeWatchChange:change snapshotVersion:[FSTSnapshotVersion noVersion]]; + [self writeWatchChange:change snapshotVersion:SnapshotVersion::None()]; } - (void)writeWatchCurrentWithTargetIDs:(NSArray<FSTBoxedTargetID *> *)targetIDs - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion:(const SnapshotVersion &)snapshotVersion resumeToken:(NSData *)resumeToken { FSTWatchTargetChange *change = [FSTWatchTargetChange changeWithState:FSTWatchTargetChangeStateCurrent @@ -353,7 +353,7 @@ NS_ASSUME_NONNULL_BEGIN [self writeWatchChange:change snapshotVersion:snapshotVersion]; } -- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snap { +- (void)writeWatchChange:(FSTWatchChange *)change snapshotVersion:(const SnapshotVersion &)snap { [self.watchStream writeWatchChange:change snapshotVersion:snap]; } diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index 128f825..5a7cb72 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -24,7 +24,6 @@ #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTEagerGarbageCollector.h" #import "Firestore/Source/Local/FSTNoOpGarbageCollector.h" #import "Firestore/Source/Local/FSTPersistence.h" @@ -46,11 +45,15 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +namespace testutil = firebase::firestore::testutil; namespace util = firebase::firestore::util; using firebase::firestore::auth::User; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; NS_ASSUME_NONNULL_BEGIN @@ -145,8 +148,8 @@ static NSString *const kNoIOSTag = @"no-ios"; } } -- (FSTSnapshotVersion *)parseVersion:(NSNumber *_Nullable)version { - return FSTTestVersion(version.longLongValue); +- (SnapshotVersion)parseVersion:(NSNumber *_Nullable)version { + return testutil::Version(version.longLongValue); } - (FSTDocumentViewChange *)parseChange:(NSArray *)change ofType:(FSTDocumentViewChangeType)type { @@ -254,9 +257,11 @@ static NSString *const kNoIOSTag = @"no-ios"; NSArray *docSpec = watchEntity[@"doc"]; FSTDocumentKey *key = FSTTestDocKey(docSpec[0]); FSTObjectValue *value = FSTTestObjectValue(docSpec[2]); - FSTSnapshotVersion *version = [self parseVersion:docSpec[1]]; - FSTMaybeDocument *doc = - [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; + SnapshotVersion version = [self parseVersion:docSpec[1]]; + FSTMaybeDocument *doc = [FSTDocument documentWithData:value + key:key + version:std::move(version) + hasLocalMutations:NO]; FSTWatchChange *change = [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:watchEntity[@"targets"] removedTargetIDs:watchEntity[@"removedTargets"] @@ -309,7 +314,7 @@ static NSString *const kNoIOSTag = @"no-ios"; } - (void)doWriteAck:(NSDictionary *)spec { - FSTSnapshotVersion *version = [self parseVersion:spec[@"version"]]; + SnapshotVersion version = [self parseVersion:spec[@"version"]]; NSNumber *expectUserCallback = spec[@"expectUserCallback"]; FSTMutationResult *mutationResult = @@ -550,7 +555,7 @@ static NSString *const kNoIOSTag = @"no-ios"; targetID:targetID listenSequenceNumber:0 purpose:FSTQueryPurposeListen - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() resumeToken:resumeToken]; }]; self.driver.expectedActiveTargets = expectedActiveTargets; @@ -598,10 +603,13 @@ static NSString *const kNoIOSTag = @"no-ios"; // XCTAssertEqualObjects(actualTargets[targetID], queryData); FSTQueryData *actual = actualTargets[targetID]; - XCTAssertEqualObjects(actual.query, queryData.query); - XCTAssertEqual(actual.targetID, queryData.targetID); - XCTAssertEqualObjects(actual.snapshotVersion, queryData.snapshotVersion); - XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken); + XCTAssertNotNil(actual); + if (actual) { + XCTAssertEqualObjects(actual.query, queryData.query); + XCTAssertEqual(actual.targetID, queryData.targetID); + XCTAssertEqual(actual.snapshotVersion, queryData.snapshotVersion); + XCTAssertEqualObjects(actual.resumeToken, queryData.resumeToken); + } [actualTargets removeObjectForKey:targetID]; }]; diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index ac44cb5..cfd8974 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -25,13 +25,13 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTDocumentKey; @class FSTMutation; @class FSTMutationResult; @class FSTQuery; @class FSTQueryData; -@class FSTSnapshotVersion; @class FSTViewSnapshot; @class FSTWatchChange; @protocol FSTGarbageCollector; @@ -150,7 +150,7 @@ typedef std::unordered_map<firebase::firestore::auth::User, * simulating the server having sent a complete snapshot. */ - (void)receiveWatchChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot; + snapshotVersion:(const firebase::firestore::model::SnapshotVersion &)snapshot; /** * Delivers a watch stream error as if the Streaming Watch backend has generated some kind of error. @@ -195,7 +195,8 @@ typedef std::unordered_map<firebase::firestore::auth::User, * the mutation. Snapshot versions must be monotonically increasing. * @param mutationResults The mutation results for the write that is being acked. */ -- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion +- (FSTOutstandingWrite *)receiveWriteAckWithVersion: + (const firebase::firestore::model::SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)mutationResults; /** diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index f167ce5..2aa0e30 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -24,7 +24,6 @@ #import "Firestore/Source/Core/FSTEventManager.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Core/FSTSyncEngine.h" #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Local/FSTPersistence.h" @@ -49,6 +48,7 @@ using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; NS_ASSUME_NONNULL_BEGIN @@ -243,7 +243,7 @@ NS_ASSUME_NONNULL_BEGIN }]; } -- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(FSTSnapshotVersion *)commitVersion +- (FSTOutstandingWrite *)receiveWriteAckWithVersion:(const SnapshotVersion &)commitVersion mutationResults: (NSArray<FSTMutationResult *> *)mutationResults { FSTOutstandingWrite *write = [self currentOutstandingWrites].firstObject; @@ -333,7 +333,7 @@ NS_ASSUME_NONNULL_BEGIN } - (void)receiveWatchChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *_Nullable)snapshot { + snapshotVersion:(const SnapshotVersion &)snapshot { [self.dispatchQueue dispatchSync:^{ [self.datastore writeWatchChange:change snapshotVersion:snapshot]; }]; diff --git a/Firestore/Example/Tests/Util/FSTDispatchQueueTests.mm b/Firestore/Example/Tests/Util/FSTDispatchQueueTests.mm index 811fa34..1f49aa4 100644 --- a/Firestore/Example/Tests/Util/FSTDispatchQueueTests.mm +++ b/Firestore/Example/Tests/Util/FSTDispatchQueueTests.mm @@ -66,9 +66,10 @@ static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnectionBackoff; XCTAssertNotNil(caught); XCTAssertEqualObjects(caught.name, NSInternalInconsistencyException); - XCTAssertTrue( - [caught.reason hasPrefix:@"FIRESTORE INTERNAL ASSERTION FAILED: " - @"dispatchAsync called when we are already running on target"]); + XCTAssertTrue([caught.reason + hasPrefix: + @"FIRESTORE INTERNAL ASSERTION FAILED: " + @"Enqueue methods cannot be called when we are already running on target executor"]); } - (void)testDispatchAsyncAllowingSameQueueActuallyAllowsSameQueue { @@ -133,9 +134,10 @@ static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnectionBackoff; XCTAssertNotNil(caught); XCTAssertEqualObjects(caught.name, NSInternalInconsistencyException); - XCTAssertTrue( - [caught.reason hasPrefix:@"FIRESTORE INTERNAL ASSERTION FAILED: " - @"dispatchSync called when we are already running on target"]); + XCTAssertTrue([caught.reason + hasPrefix: + @"FIRESTORE INTERNAL ASSERTION FAILED: " + @"Enqueue methods cannot be called when we are already running on target executor"]); } - (void)testVerifyIsCurrentQueueActuallyRequiresCurrentQueue { @@ -150,7 +152,8 @@ static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnectionBackoff; } XCTAssertNotNil(caught); XCTAssertTrue([caught.reason hasPrefix:@"FIRESTORE INTERNAL ASSERTION FAILED: " - @"We are running on the wrong dispatch queue"]); + @"Expected to be called by the executor " + @"associated with this queue"]); } - (void)testVerifyIsCurrentQueueRequiresOperationIsInProgress { @@ -165,7 +168,7 @@ static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnectionBackoff; XCTAssertNotNil(caught); XCTAssertTrue( [caught.reason hasPrefix:@"FIRESTORE INTERNAL ASSERTION FAILED: " - @"verifyIsCurrentQueue called outside enterCheckedOperation"]); + @"VerifyIsCurrentQueue called when no operation is executing"]); } - (void)testVerifyIsCurrentQueueWorksWithOperationIsInProgress { @@ -194,9 +197,10 @@ static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnectionBackoff; }]; XCTAssertNil(problem); XCTAssertNotNil(caught); - XCTAssertTrue([caught.reason - hasPrefix:@"FIRESTORE INTERNAL ASSERTION FAILED: " - @"enterCheckedOperation may not be called when an operation is in progress"]); + XCTAssertTrue( + [caught.reason hasPrefix:@"FIRESTORE INTERNAL ASSERTION FAILED: " + @"ExecuteBlocking may not be called before the previous operation " + @"finishes executing"]); } /** @@ -217,8 +221,10 @@ static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnectionBackoff; _expectation = [self expectationWithDescription:@"Expected steps"]; _expectedSteps = @[ @1, @2, @3, @4 ]; [_queue dispatchAsync:[self blockForStep:1]]; - [_queue dispatchAfterDelay:0.005 timerID:timerID1 block:[self blockForStep:4]]; - [_queue dispatchAfterDelay:0.001 timerID:timerID2 block:[self blockForStep:3]]; + [_queue dispatchAsync:^{ + [_queue dispatchAfterDelay:0.005 timerID:timerID1 block:[self blockForStep:4]]; + [_queue dispatchAfterDelay:0.001 timerID:timerID2 block:[self blockForStep:3]]; + }]; [_queue dispatchAsync:[self blockForStep:2]]; [self awaitExpectations]; @@ -244,8 +250,10 @@ static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnectionBackoff; - (void)testCanManuallyDrainAllDelayedCallbacksForTesting { [_queue dispatchAsync:[self blockForStep:1]]; - [_queue dispatchAfterDelay:20 timerID:timerID1 block:[self blockForStep:4]]; - [_queue dispatchAfterDelay:10 timerID:timerID2 block:[self blockForStep:3]]; + [_queue dispatchAsync:^{ + [_queue dispatchAfterDelay:20 timerID:timerID1 block:[self blockForStep:4]]; + [_queue dispatchAfterDelay:10 timerID:timerID2 block:[self blockForStep:3]]; + }]; [_queue dispatchAsync:[self blockForStep:2]]; [_queue runDelayedCallbacksUntil:FSTTimerIDAll]; @@ -254,9 +262,11 @@ static const FSTTimerID timerID3 = FSTTimerIDWriteStreamConnectionBackoff; - (void)testCanManuallyDrainSpecificDelayedCallbacksForTesting { [_queue dispatchAsync:[self blockForStep:1]]; - [_queue dispatchAfterDelay:20 timerID:timerID1 block:[self blockForStep:5]]; - [_queue dispatchAfterDelay:10 timerID:timerID2 block:[self blockForStep:3]]; - [_queue dispatchAfterDelay:15 timerID:timerID3 block:[self blockForStep:4]]; + [_queue dispatchAsync:^{ + [_queue dispatchAfterDelay:20 timerID:timerID1 block:[self blockForStep:5]]; + [_queue dispatchAfterDelay:10 timerID:timerID2 block:[self blockForStep:3]]; + [_queue dispatchAfterDelay:15 timerID:timerID3 block:[self blockForStep:4]]; + }]; [_queue dispatchAsync:[self blockForStep:2]]; [_queue runDelayedCallbacksUntil:timerID3]; diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.h b/Firestore/Example/Tests/Util/FSTEventAccumulator.h index baa501b..58b802b 100644 --- a/Firestore/Example/Tests/Util/FSTEventAccumulator.h +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.h @@ -25,15 +25,21 @@ NS_ASSUME_NONNULL_BEGIN typedef void (^FSTValueEventHandler)(id _Nullable, NSError *_Nullable error); -@interface FSTEventAccumulator : NSObject +@interface FSTEventAccumulator <EventType> : NSObject + (instancetype)accumulatorForTest:(XCTestCase *)testCase; - (instancetype)init NS_UNAVAILABLE; -- (id)awaitEventWithName:(NSString *)name; +- (EventType)awaitEventWithName:(NSString *)name; -- (NSArray<id> *)awaitEvents:(NSUInteger)events name:(NSString *)name; +- (NSArray<EventType> *)awaitEvents:(NSUInteger)events name:(NSString *)name; + +/** Waits for a latency compensated local snapshot. */ +- (EventType)awaitLocalEvent; + +/** Waits for a snapshot that has no pending writes */ +- (EventType)awaitRemoteEvent; @property(nonatomic, strong, readonly) FSTValueEventHandler valueEventHandler; diff --git a/Firestore/Example/Tests/Util/FSTEventAccumulator.mm b/Firestore/Example/Tests/Util/FSTEventAccumulator.mm index 623ba2d..3ab6035 100644 --- a/Firestore/Example/Tests/Util/FSTEventAccumulator.mm +++ b/Firestore/Example/Tests/Util/FSTEventAccumulator.mm @@ -18,6 +18,9 @@ #import <XCTest/XCTest.h> +#import "Firestore/Source/Public/FIRDocumentSnapshot.h" +#import "Firestore/Source/Public/FIRQuerySnapshot.h" +#import "Firestore/Source/Public/FIRSnapshotMetadata.h" #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Example/Tests/Util/XCTestCase+Await.h" @@ -68,6 +71,31 @@ NS_ASSUME_NONNULL_BEGIN return events[0]; } +- (id)awaitLocalEvent { + id event; + do { + event = [self awaitEventWithName:@"Local Event"]; + } while (![self hasPendingWrites:event]); + return event; +} + +- (id)awaitRemoteEvent { + id event; + do { + event = [self awaitEventWithName:@"Remote Event"]; + } while ([self hasPendingWrites:event]); + return event; +} + +- (BOOL)hasPendingWrites:(id)event { + if ([event isKindOfClass:[FIRDocumentSnapshot class]]) { + return ((FIRDocumentSnapshot *)event).metadata.hasPendingWrites; + } else { + FSTAssert([event isKindOfClass:[FIRQuerySnapshot class]], @"Unexpected event: %@", event); + return ((FIRQuerySnapshot *)event).metadata.hasPendingWrites; + } +} + - (void (^)(id _Nullable, NSError *_Nullable))valueEventHandler { return ^void(id _Nullable value, NSError *_Nullable error) { // We can't store nil in the _events array, but these are still interesting to tests so store diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index 131da2d..ccc01ca 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -21,7 +21,6 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" @@ -40,7 +39,6 @@ @class FSTQuery; @class FSTRemoteEvent; @class FSTSetMutation; -@class FSTSnapshotVersion; @class FSTSortOrder; @class FSTTargetChange; @class FIRTimestamp; @@ -182,15 +180,9 @@ FSTObjectValue *FSTTestObjectValue(NSDictionary<NSString *, id> *data); /** A convenience method for creating document keys for tests. */ FSTDocumentKey *FSTTestDocKey(NSString *path); -/** A convenience method for creating a document key set for tests. */ -FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys); - /** Allow tests to just use an int literal for versions. */ typedef int64_t FSTTestSnapshotVersion; -/** A convenience method for creating snapshot versions for tests. */ -FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion version); - /** A convenience method for creating docs for tests. */ FSTDocument *FSTTestDoc(const absl::string_view path, FSTTestSnapshotVersion version, diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index 888a45f..dbd19aa 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -29,7 +29,6 @@ #import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Core/FSTView.h" #import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" @@ -45,6 +44,7 @@ #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/field_mask.h" #include "Firestore/core/src/firebase/firestore/model/field_transform.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" @@ -67,15 +67,13 @@ using firebase::firestore::model::Precondition; using firebase::firestore::model::ResourcePath; using firebase::firestore::model::ServerTimestampTransform; using firebase::firestore::model::TransformOperation; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN /** A string sentinel that can be used with FSTTestPatchMutation() to mark a field for deletion. */ static NSString *const kDeleteSentinel = @"<DELETE>"; -static const int kMicrosPerSec = 1000000; -static const int kMillisPerSec = 1000; - FIRTimestamp *FSTTestTimestamp(int year, int month, int day, int hour, int minute, int second) { NSDate *date = FSTTestDate(year, month, day, hour, minute, second); return [FIRTimestamp timestampWithDate:date]; @@ -150,22 +148,6 @@ FSTDocumentKey *FSTTestDocKey(NSString *path) { return [FSTDocumentKey keyWithPathString:path]; } -FSTDocumentKeySet *FSTTestDocKeySet(NSArray<FSTDocumentKey *> *keys) { - FSTDocumentKeySet *result = [FSTDocumentKeySet keySet]; - for (FSTDocumentKey *key in keys) { - result = [result setByAddingObject:key]; - } - return result; -} - -FSTSnapshotVersion *FSTTestVersion(FSTTestSnapshotVersion versionMicroseconds) { - int64_t seconds = versionMicroseconds / kMicrosPerSec; - int32_t nanos = (int32_t)(versionMicroseconds % kMicrosPerSec) * kMillisPerSec; - - FIRTimestamp *timestamp = [[FIRTimestamp alloc] initWithSeconds:seconds nanoseconds:nanos]; - return [FSTSnapshotVersion versionWithTimestamp:timestamp]; -} - FSTDocument *FSTTestDoc(const absl::string_view path, FSTTestSnapshotVersion version, NSDictionary<NSString *, id> *data, @@ -173,14 +155,14 @@ FSTDocument *FSTTestDoc(const absl::string_view path, DocumentKey key = testutil::Key(path); return [FSTDocument documentWithData:FSTTestObjectValue(data) key:key - version:FSTTestVersion(version) + version:testutil::Version(version) hasLocalMutations:hasMutations]; } FSTDeletedDocument *FSTTestDeletedDoc(const absl::string_view path, FSTTestSnapshotVersion version) { DocumentKey key = testutil::Key(path); - return [FSTDeletedDocument documentWithKey:key version:FSTTestVersion(version)]; + return [FSTDeletedDocument documentWithKey:key version:testutil::Version(version)]; } FSTDocumentKeyReference *FSTTestRef(const absl::string_view projectID, @@ -355,17 +337,17 @@ NSData *_Nullable FSTTestResumeTokenFromSnapshotVersion(FSTTestSnapshotVersion s FSTLocalViewChanges *FSTTestViewChanges(FSTQuery *query, NSArray<NSString *> *addedKeys, NSArray<NSString *> *removedKeys) { - FSTDocumentKeySet *added = [FSTDocumentKeySet keySet]; + DocumentKeySet added; for (NSString *keyPath in addedKeys) { - FSTDocumentKey *key = FSTTestDocKey(keyPath); - added = [added setByAddingObject:key]; + added = added.insert(testutil::Key(util::MakeStringView(keyPath))); } - FSTDocumentKeySet *removed = [FSTDocumentKeySet keySet]; + DocumentKeySet removed; for (NSString *keyPath in removedKeys) { - FSTDocumentKey *key = FSTTestDocKey(keyPath); - removed = [removed setByAddingObject:key]; + removed = removed.insert(testutil::Key(util::MakeStringView(keyPath))); } - return [FSTLocalViewChanges changesForQuery:query addedKeys:added removedKeys:removed]; + return [FSTLocalViewChanges changesForQuery:query + addedKeys:std::move(added) + removedKeys:std::move(removed)]; } NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm index 1817015..434b77b 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm @@ -17,7 +17,13 @@ #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" #import <FirebaseCore/FIRLogger.h> -#import <FirebaseFirestore/FirebaseFirestore-umbrella.h> +#import <FirebaseFirestore/FIRCollectionReference.h> +#import <FirebaseFirestore/FIRDocumentChange.h> +#import <FirebaseFirestore/FIRDocumentReference.h> +#import <FirebaseFirestore/FIRDocumentSnapshot.h> +#import <FirebaseFirestore/FIRFirestoreSettings.h> +#import <FirebaseFirestore/FIRQuerySnapshot.h> +#import <FirebaseFirestore/FIRSnapshotMetadata.h> #import <GRPCClient/GRPCCall+ChannelArg.h> #import <GRPCClient/GRPCCall+Tests.h> diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm index c2fc546..5ad606c 100644 --- a/Firestore/Source/API/FIRDocumentReference.mm +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -125,6 +125,11 @@ NS_ASSUME_NONNULL_BEGIN } - (void)setData:(NSDictionary<NSString *, id> *)documentData + mergeFields:(NSArray<id> *)mergeFields { + return [self setData:documentData mergeFields:mergeFields completion:nil]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData completion:(nullable void (^)(NSError *_Nullable error))completion { return [self setData:documentData merge:NO completion:completion]; } @@ -132,8 +137,19 @@ NS_ASSUME_NONNULL_BEGIN - (void)setData:(NSDictionary<NSString *, id> *)documentData merge:(BOOL)merge completion:(nullable void (^)(NSError *_Nullable error))completion { - FSTParsedSetData *parsed = merge ? [self.firestore.dataConverter parsedMergeData:documentData] - : [self.firestore.dataConverter parsedSetData:documentData]; + FSTParsedSetData *parsed = + merge ? [self.firestore.dataConverter parsedMergeData:documentData fieldMask:nil] + : [self.firestore.dataConverter parsedSetData:documentData]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key precondition:Precondition::None()] + completion:completion]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData + mergeFields:(NSArray<id> *)mergeFields + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedMergeData:documentData fieldMask:mergeFields]; return [self.firestore.client writeMutations:[parsed mutationsWithKey:self.key precondition:Precondition::None()] completion:completion]; @@ -264,8 +280,8 @@ addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions }; FSTAsyncQueryListener *asyncListener = - [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue - snapshotHandler:snapshotHandler]; + [[FSTAsyncQueryListener alloc] initWithExecutor:self.firestore.client.userExecutor + snapshotHandler:snapshotHandler]; FSTQueryListener *internalListener = [firestore.client listenToQuery:query diff --git a/Firestore/Source/API/FIRFieldPath.mm b/Firestore/Source/API/FIRFieldPath.mm index d0d8714..4fd0022 100644 --- a/Firestore/Source/API/FIRFieldPath.mm +++ b/Firestore/Source/API/FIRFieldPath.mm @@ -25,6 +25,7 @@ #import "Firestore/Source/Util/FSTUsageValidation.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; @@ -114,7 +115,7 @@ NS_ASSUME_NONNULL_BEGIN } - (NSUInteger)hash { - return _internalValue.Hash(); + return util::Hash(_internalValue); } - (const firebase::firestore::model::FieldPath &)internalValue { diff --git a/Firestore/Source/API/FIRFirestore.mm b/Firestore/Source/API/FIRFirestore.mm index fe461d6..e5f0c12 100644 --- a/Firestore/Source/API/FIRFirestore.mm +++ b/Firestore/Source/API/FIRFirestore.mm @@ -45,12 +45,16 @@ #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "absl/memory/memory.h" +#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" + namespace util = firebase::firestore::util; using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::FirebaseCredentialsProvider; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::ResourcePath; +using util::internal::Executor; +using util::internal::ExecutorLibdispatch; NS_ASSUME_NONNULL_BEGIN @@ -272,12 +276,13 @@ extern "C" NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; const DatabaseInfo database_info(*self.databaseID, util::MakeStringView(_persistenceKey), util::MakeStringView(_settings.host), _settings.sslEnabled); - FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; + std::unique_ptr<Executor> userExecutor = + absl::make_unique<ExecutorLibdispatch>(_settings.dispatchQueue); _client = [FSTFirestoreClient clientWithDatabaseInfo:database_info usePersistence:_settings.persistenceEnabled credentialsProvider:_credentialsProvider.get() - userDispatchQueue:userDispatchQueue + userExecutor:std::move(userExecutor) workerDispatchQueue:_workerDispatchQueue]; } } diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm index 32d8327..596f6ac 100644 --- a/Firestore/Source/API/FIRQuery.mm +++ b/Firestore/Source/API/FIRQuery.mm @@ -183,8 +183,8 @@ addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions }; FSTAsyncQueryListener *asyncListener = - [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue - snapshotHandler:snapshotHandler]; + [[FSTAsyncQueryListener alloc] initWithExecutor:self.firestore.client.userExecutor + snapshotHandler:snapshotHandler]; FSTQueryListener *internalListener = [firestore.client listenToQuery:query @@ -456,8 +456,8 @@ addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions if (fieldPath.IsKeyFieldPath()) { if (filterOperator == FSTRelationFilterOperatorArrayContains) { FSTThrowInvalidArgument( - @"Invalid query. You can't do arrayContains queries on document ID since document IDs " - @"are not arrays."); + @"Invalid query. You can't perform arrayContains queries on document ID since document " + "IDs are not arrays."); } if ([value isKindOfClass:[NSString class]]) { NSString *documentKey = (NSString *)value; diff --git a/Firestore/Source/API/FIRTransaction.mm b/Firestore/Source/API/FIRTransaction.mm index 668a359..b5bdefa 100644 --- a/Firestore/Source/API/FIRTransaction.mm +++ b/Firestore/Source/API/FIRTransaction.mm @@ -68,8 +68,19 @@ NS_ASSUME_NONNULL_BEGIN forDocument:(FIRDocumentReference *)document merge:(BOOL)merge { [self validateReference:document]; - FSTParsedSetData *parsed = merge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; + FSTParsedSetData *parsed = merge + ? [self.firestore.dataConverter parsedMergeData:data fieldMask:nil] + : [self.firestore.dataConverter parsedSetData:data]; + [self.internalTransaction setData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields { + [self validateReference:document]; + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedMergeData:data fieldMask:mergeFields]; [self.internalTransaction setData:parsed forDocument:document.key]; return self; } diff --git a/Firestore/Source/API/FIRWriteBatch.mm b/Firestore/Source/API/FIRWriteBatch.mm index 1185dae..366c708 100644 --- a/Firestore/Source/API/FIRWriteBatch.mm +++ b/Firestore/Source/API/FIRWriteBatch.mm @@ -70,8 +70,21 @@ NS_ASSUME_NONNULL_BEGIN merge:(BOOL)merge { [self verifyNotCommitted]; [self validateReference:document]; - FSTParsedSetData *parsed = merge ? [self.firestore.dataConverter parsedMergeData:data] - : [self.firestore.dataConverter parsedSetData:data]; + FSTParsedSetData *parsed = merge + ? [self.firestore.dataConverter parsedMergeData:data fieldMask:nil] + : [self.firestore.dataConverter parsedSetData:data]; + [self.mutations + addObjectsFromArray:[parsed mutationsWithKey:document.key precondition:Precondition::None()]]; + return self; +} + +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedMergeData:data fieldMask:mergeFields]; [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key precondition:Precondition::None()]]; return self; diff --git a/Firestore/Source/API/FSTUserDataConverter.h b/Firestore/Source/API/FSTUserDataConverter.h index 98a65ae..27a5f09 100644 --- a/Firestore/Source/API/FSTUserDataConverter.h +++ b/Firestore/Source/API/FSTUserDataConverter.h @@ -27,7 +27,6 @@ @class FSTObjectValue; @class FSTFieldValue; @class FSTMutation; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -130,7 +129,7 @@ typedef id _Nullable (^FSTPreConverterBlock)(id _Nullable); - (FSTParsedSetData *)parsedSetData:(id)input; /** Parse document data from a setData call with `merge:YES`. */ -- (FSTParsedSetData *)parsedMergeData:(id)input; +- (FSTParsedSetData *)parsedMergeData:(id)input fieldMask:(nullable NSArray<id> *)fieldMask; /** Parse update data from an updateData call. */ - (FSTParsedUpdateData *)parsedUpdateData:(id)input; diff --git a/Firestore/Source/API/FSTUserDataConverter.mm b/Firestore/Source/API/FSTUserDataConverter.mm index 2794398..6d01c75 100644 --- a/Firestore/Source/API/FSTUserDataConverter.mm +++ b/Firestore/Source/API/FSTUserDataConverter.mm @@ -412,7 +412,7 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { return self; } -- (FSTParsedSetData *)parsedMergeData:(id)input { +- (FSTParsedSetData *)parsedMergeData:(id)input fieldMask:(nullable NSArray<id> *)fieldMask { // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust // Obj-C to verify the type for us. if (![input isKindOfClass:[NSDictionary class]]) { @@ -424,9 +424,45 @@ typedef NS_ENUM(NSInteger, FSTUserDataSource) { path:absl::make_unique<FieldPath>(FieldPath::EmptyPath())]; FSTObjectValue *updateData = (FSTObjectValue *)[self parseData:input context:context]; + FieldMask convertedFieldMask; + std::vector<FieldTransform> convertedFieldTransform; + + if (fieldMask) { + __block std::vector<FieldPath> fieldMaskPaths{}; + [fieldMask enumerateObjectsUsingBlock:^(id fieldPath, NSUInteger idx, BOOL *stop) { + FieldPath path{}; + + if ([fieldPath isKindOfClass:[NSString class]]) { + path = [FIRFieldPath pathWithDotSeparatedString:fieldPath].internalValue; + } else if ([fieldPath isKindOfClass:[FIRFieldPath class]]) { + path = ((FIRFieldPath *)fieldPath).internalValue; + } else { + FSTThrowInvalidArgument( + @"All elements in mergeFields: must be NSStrings or FIRFieldPaths."); + } + + if ([updateData valueForPath:path] == nil) { + FSTThrowInvalidArgument( + @"Field '%s' is specified in your field mask but missing from your input data.", + path.CanonicalString().c_str()); + } + + fieldMaskPaths.push_back(path); + }]; + convertedFieldMask = FieldMask(fieldMaskPaths); + std::copy_if(context.fieldTransforms->begin(), context.fieldTransforms->end(), + std::back_inserter(convertedFieldTransform), + [&](const FieldTransform &fieldTransform) { + return convertedFieldMask.covers(fieldTransform.path()); + }); + } else { + convertedFieldMask = FieldMask{*context.fieldMask}; + convertedFieldTransform = *context.fieldTransforms; + } + return [[FSTParsedSetData alloc] initWithData:updateData - fieldMask:FieldMask{*context.fieldMask} - fieldTransforms:*context.fieldTransforms]; + fieldMask:convertedFieldMask + fieldTransforms:convertedFieldTransform]; } - (FSTParsedSetData *)parsedSetData:(id)input { diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h index 7285e65..94c2284 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.h +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -15,6 +15,7 @@ */ #import <Foundation/Foundation.h> +#include <memory> #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Core/FSTViewSnapshot.h" @@ -23,6 +24,7 @@ #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" @class FIRDocumentReference; @class FIRDocumentSnapshot; @@ -50,14 +52,15 @@ NS_ASSUME_NONNULL_BEGIN /** * Creates and returns a FSTFirestoreClient with the given parameters. * - * All callbacks and events will be triggered on the provided userDispatchQueue. + * All callbacks and events will be triggered on the provided userExecutor. */ -+ (instancetype)clientWithDatabaseInfo:(const firebase::firestore::core::DatabaseInfo &)databaseInfo - usePersistence:(BOOL)usePersistence - credentialsProvider:(firebase::firestore::auth::CredentialsProvider *) - credentialsProvider // no passing ownership - userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue - workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue; ++ (instancetype) +clientWithDatabaseInfo:(const firebase::firestore::core::DatabaseInfo &)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(firebase::firestore::auth::CredentialsProvider *) + credentialsProvider // no passing ownership + userExecutor:(std::unique_ptr<firebase::firestore::util::internal::Executor>)userExecutor + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue; - (instancetype)init __attribute__((unavailable("Use static constructor method."))); @@ -111,7 +114,7 @@ NS_ASSUME_NONNULL_BEGIN * Dispatch queue for user callbacks / events. This will often be the "Main Dispatch Queue" of the * app but the developer can configure it to a different queue if they so choose. */ -@property(nonatomic, strong, readonly) FSTDispatchQueue *userDispatchQueue; +- (firebase::firestore::util::internal::Executor *)userExecutor; @end diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm index 4f1a20b..cede958 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ b/Firestore/Source/Core/FSTFirestoreClient.mm @@ -18,6 +18,7 @@ #include <future> // NOLINT(build/c++11) #include <memory> +#include <utility> #import "FIRFirestoreErrors.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" @@ -56,6 +57,9 @@ using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::DocumentKeySet; + +using firebase::firestore::util::internal::Executor; NS_ASSUME_NONNULL_BEGIN @@ -67,7 +71,7 @@ NS_ASSUME_NONNULL_BEGIN usePersistence:(BOOL)usePersistence credentialsProvider: (CredentialsProvider *)credentialsProvider // no passing ownership - userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + userExecutor:(std::unique_ptr<Executor>)userExecutor workerDispatchQueue:(FSTDispatchQueue *)queue NS_DESIGNATED_INITIALIZER; @property(nonatomic, assign, readonly) const DatabaseInfo *databaseInfo; @@ -90,18 +94,24 @@ NS_ASSUME_NONNULL_BEGIN @end -@implementation FSTFirestoreClient +@implementation FSTFirestoreClient { + std::unique_ptr<Executor> _userExecutor; +} + +- (Executor *)userExecutor { + return _userExecutor.get(); +} + (instancetype)clientWithDatabaseInfo:(const DatabaseInfo &)databaseInfo usePersistence:(BOOL)usePersistence credentialsProvider: (CredentialsProvider *)credentialsProvider // no passing ownership - userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + userExecutor:(std::unique_ptr<Executor>)userExecutor workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { return [[FSTFirestoreClient alloc] initWithDatabaseInfo:databaseInfo usePersistence:usePersistence credentialsProvider:credentialsProvider - userDispatchQueue:userDispatchQueue + userExecutor:std::move(userExecutor) workerDispatchQueue:workerDispatchQueue]; } @@ -109,12 +119,12 @@ NS_ASSUME_NONNULL_BEGIN usePersistence:(BOOL)usePersistence credentialsProvider: (CredentialsProvider *)credentialsProvider // no passing ownership - userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + userExecutor:(std::unique_ptr<Executor>)userExecutor workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { if (self = [super init]) { _databaseInfo = databaseInfo; _credentialsProvider = credentialsProvider; - _userDispatchQueue = userDispatchQueue; + _userExecutor = std::move(userExecutor); _workerDispatchQueue = workerDispatchQueue; auto userPromise = std::make_shared<std::promise<User>>(); @@ -229,9 +239,7 @@ NS_ASSUME_NONNULL_BEGIN [self.workerDispatchQueue dispatchAsync:^{ [self.remoteStore disableNetwork]; if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(nil); - }]; + self->_userExecutor->Execute([=] { completion(nil); }); } }]; } @@ -240,9 +248,7 @@ NS_ASSUME_NONNULL_BEGIN [self.workerDispatchQueue dispatchAsync:^{ [self.remoteStore enableNetwork]; if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(nil); - }]; + self->_userExecutor->Execute([=] { completion(nil); }); } }]; } @@ -254,9 +260,7 @@ NS_ASSUME_NONNULL_BEGIN [self.remoteStore shutdown]; [self.persistence shutdown]; if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(nil); - }]; + self->_userExecutor->Execute([=] { completion(nil); }); } }]; } @@ -311,11 +315,9 @@ NS_ASSUME_NONNULL_BEGIN completion:(void (^)(FIRQuerySnapshot *_Nullable query, NSError *_Nullable error))completion { [self.workerDispatchQueue dispatchAsync:^{ - FSTDocumentDictionary *docs = [self.localStore executeQuery:query.query]; - FSTDocumentKeySet *remoteKeys = [FSTDocumentKeySet keySet]; - FSTView *view = [[FSTView alloc] initWithQuery:query.query remoteDocuments:remoteKeys]; + FSTView *view = [[FSTView alloc] initWithQuery:query.query remoteDocuments:DocumentKeySet{}]; FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs]; FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; FSTAssert(viewChange.limboChanges.count == 0, @@ -339,18 +341,14 @@ NS_ASSUME_NONNULL_BEGIN [self.workerDispatchQueue dispatchAsync:^{ if (mutations.count == 0) { if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(nil); - }]; + self->_userExecutor->Execute([=] { completion(nil); }); } } else { [self.syncEngine writeMutations:mutations completion:^(NSError *error) { // Dispatch the result back onto the user dispatch queue. if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(error); - }]; + self->_userExecutor->Execute([=] { completion(error); }); } }]; } @@ -361,17 +359,16 @@ NS_ASSUME_NONNULL_BEGIN updateBlock:(FSTTransactionBlock)updateBlock completion:(FSTVoidIDErrorBlock)completion { [self.workerDispatchQueue dispatchAsync:^{ - [self.syncEngine transactionWithRetries:retries - workerDispatchQueue:self.workerDispatchQueue - updateBlock:updateBlock - completion:^(id _Nullable result, NSError *_Nullable error) { - // Dispatch the result back onto the user dispatch queue. - if (completion) { - [self.userDispatchQueue dispatchAsync:^{ - completion(result, error); - }]; - } - }]; + [self.syncEngine + transactionWithRetries:retries + workerDispatchQueue:self.workerDispatchQueue + updateBlock:updateBlock + completion:^(id _Nullable result, NSError *_Nullable error) { + // Dispatch the result back onto the user dispatch queue. + if (completion) { + self->_userExecutor->Execute([=] { completion(result, error); }); + } + }]; }]; } diff --git a/Firestore/Source/Core/FSTQuery.mm b/Firestore/Source/Core/FSTQuery.mm index 0cd11e8..d3961e8 100644 --- a/Firestore/Source/Core/FSTQuery.mm +++ b/Firestore/Source/Core/FSTQuery.mm @@ -28,6 +28,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; @@ -259,7 +260,7 @@ NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOpe } - (NSUInteger)hash { - return _field.Hash(); + return util::Hash(_field); } @end @@ -305,7 +306,7 @@ NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOpe } - (NSUInteger)hash { - return _field.Hash(); + return util::Hash(_field); } @end diff --git a/Firestore/Source/Core/FSTSnapshotVersion.h b/Firestore/Source/Core/FSTSnapshotVersion.h deleted file mode 100644 index 8649d40..0000000 --- a/Firestore/Source/Core/FSTSnapshotVersion.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import <Foundation/Foundation.h> - -NS_ASSUME_NONNULL_BEGIN - -@class FIRTimestamp; - -/** - * A version of a document in Firestore. This corresponds to the version timestamp, such as - * update_time or read_time. - */ -@interface FSTSnapshotVersion : NSObject <NSCopying> - -/** Creates a new version that is smaller than all other versions. */ -+ (instancetype)noVersion; - -/** Creates a new version representing the given timestamp. */ -+ (instancetype)versionWithTimestamp:(FIRTimestamp *)timestamp; - -- (instancetype)init NS_UNAVAILABLE; - -- (NSComparisonResult)compare:(FSTSnapshotVersion *)other; - -@property(nonatomic, strong, readonly) FIRTimestamp *timestamp; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSnapshotVersion.mm b/Firestore/Source/Core/FSTSnapshotVersion.mm deleted file mode 100644 index 58b2be4..0000000 --- a/Firestore/Source/Core/FSTSnapshotVersion.mm +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/Core/FSTSnapshotVersion.h" - -#import "FIRTimestamp.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTSnapshotVersion - -+ (instancetype)noVersion { - static FSTSnapshotVersion *min; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - FIRTimestamp *timestamp = [[FIRTimestamp alloc] initWithSeconds:0 nanoseconds:0]; - min = [FSTSnapshotVersion versionWithTimestamp:timestamp]; - }); - return min; -} - -+ (instancetype)versionWithTimestamp:(FIRTimestamp *)timestamp { - return [[FSTSnapshotVersion alloc] initWithTimestamp:timestamp]; -} - -- (instancetype)initWithTimestamp:(FIRTimestamp *)timestamp { - self = [super init]; - if (self) { - _timestamp = timestamp; - } - return self; -} - -#pragma mark - NSObject methods - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } - if (![object isKindOfClass:[FSTSnapshotVersion class]]) { - return NO; - } - return [self.timestamp isEqual:((FSTSnapshotVersion *)object).timestamp]; -} - -- (NSUInteger)hash { - return self.timestamp.hash; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"<FSTSnapshotVersion: %@>", self.timestamp]; -} - -- (id)copyWithZone:(NSZone *_Nullable)zone { - // Implements NSCopying without actually copying because timestamps are immutable. - return self; -} - -#pragma mark - Public methods - -- (NSComparisonResult)compare:(FSTSnapshotVersion *)other { - return [self.timestamp compare:other.timestamp]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm index 138fb41..ed97d6c 100644 --- a/Firestore/Source/Core/FSTSyncEngine.mm +++ b/Firestore/Source/Core/FSTSyncEngine.mm @@ -21,10 +21,10 @@ #include <map> #include <set> #include <unordered_map> +#include <utility> #import "FIRFirestoreErrors.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Core/FSTTransaction.h" #import "Firestore/Source/Core/FSTView.h" #import "Firestore/Source/Core/FSTViewSnapshot.h" @@ -45,12 +45,15 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; using firebase::firestore::core::TargetIdGenerator; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -184,9 +187,9 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1; FSTQueryData *queryData = [self.localStore allocateQuery:query]; FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; - FSTDocumentKeySet *remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID]; + DocumentKeySet remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID]; - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:remoteKeys]; + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:std::move(remoteKeys)]; FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs]; FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; FSTAssert(viewChange.limboChanges.count == 0, @@ -301,8 +304,7 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1; if (iter == self->_limboKeysByTarget.end()) { FSTQueryView *qv = self.queryViewsByTarget[targetID]; FSTAssert(qv, @"Missing queryview for non-limbo query: %i", [targetID intValue]); - [remoteEvent filterUpdatesFromTargetChange:targetChange - existingDocuments:qv.view.syncedDocuments]; + [targetChange.mapping filterUpdatesUsingExistingKeys:qv.view.syncedDocuments]; } else { [remoteEvent synthesizeDeleteForLimboTargetChange:targetChange key:iter->second]; } @@ -346,10 +348,15 @@ static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1; NSMutableDictionary<NSNumber *, FSTTargetChange *> *targetChanges = [NSMutableDictionary dictionary]; FSTDeletedDocument *doc = - [FSTDeletedDocument documentWithKey:limboKey version:[FSTSnapshotVersion noVersion]]; - FSTRemoteEvent *event = [FSTRemoteEvent eventWithSnapshotVersion:[FSTSnapshotVersion noVersion] - targetChanges:targetChanges - documentUpdates:{{limboKey, doc}}]; + [FSTDeletedDocument documentWithKey:limboKey version:SnapshotVersion::None()]; + DocumentKeySet limboDocuments = DocumentKeySet{doc.key}; + FSTRemoteEvent *event = + [[FSTRemoteEvent alloc] initWithSnapshotVersion:SnapshotVersion::None() + targetChanges:targetChanges + documentUpdates:{ + { limboKey, doc } + } + limboDocuments:std::move(limboDocuments)]; [self applyRemoteEvent:event]; } else { FSTQueryView *queryView = self.queryViewsByTarget[@(targetID)]; diff --git a/Firestore/Source/Core/FSTTransaction.mm b/Firestore/Source/Core/FSTTransaction.mm index 4aabd5a..5c36b20 100644 --- a/Firestore/Source/Core/FSTTransaction.mm +++ b/Firestore/Source/Core/FSTTransaction.mm @@ -23,19 +23,21 @@ #import "FIRFirestoreErrors.h" #import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTDocument.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTDatastore.h" #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTUsageValidation.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" using firebase::firestore::model::DocumentKey; using firebase::firestore::model::Precondition; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -53,7 +55,7 @@ NS_ASSUME_NONNULL_BEGIN @end @implementation FSTTransaction { - std::map<DocumentKey, FSTSnapshotVersion *> _readVersions; + std::map<DocumentKey, SnapshotVersion> _readVersions; } + (instancetype)transactionWithDatastore:(FSTDatastore *)datastore { @@ -79,11 +81,11 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error { FSTAssert(error != nil, @"nil error parameter"); *error = nil; - FSTSnapshotVersion *docVersion = doc.version; + SnapshotVersion docVersion = doc.version; if ([doc isKindOfClass:[FSTDeletedDocument class]]) { // For deleted docs, we must record an explicit no version to build the right precondition // when writing. - docVersion = [FSTSnapshotVersion noVersion]; + docVersion = SnapshotVersion::None(); } if (_readVersions.find(doc.key) == _readVersions.end()) { _readVersions[doc.key] = docVersion; @@ -159,8 +161,8 @@ NS_ASSUME_NONNULL_BEGIN return Precondition::Exists(true); } - FSTSnapshotVersion *version = iter->second; - if ([version isEqual:[FSTSnapshotVersion noVersion]]) { + const SnapshotVersion &version = iter->second; + if (version == SnapshotVersion::None()) { // The document was read, but doesn't exist. // Return an error because the precondition is impossible if (error) { @@ -200,7 +202,7 @@ NS_ASSUME_NONNULL_BEGIN precondition:[self preconditionForDocumentKey:key]] ]]; // Since the delete will be applied before all following writes, we need to ensure that the // precondition for the next write will be exists without timestamp. - _readVersions[key] = [FSTSnapshotVersion noVersion]; + _readVersions[key] = SnapshotVersion::None(); } - (void)commitWithCompletion:(FSTVoidErrorBlock)completion { @@ -215,15 +217,15 @@ NS_ASSUME_NONNULL_BEGIN } // Make a list of read documents that haven't been written. - FSTDocumentKeySet *unwritten = [FSTDocumentKeySet keySet]; + DocumentKeySet unwritten; for (const auto &kv : _readVersions) { - unwritten = [unwritten setByAddingObject:kv.first]; + unwritten = unwritten.insert(kv.first); }; // For each mutation, note that the doc was written. for (FSTMutation *mutation in self.mutations) { - unwritten = [unwritten setByRemovingObject:mutation.key]; + unwritten = unwritten.erase(mutation.key); } - if (unwritten.count) { + if (!unwritten.empty()) { // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend. completion([NSError errorWithDomain:FIRFirestoreErrorDomain diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h index 431b863..fc6cead 100644 --- a/Firestore/Source/Core/FSTView.h +++ b/Firestore/Source/Core/FSTView.h @@ -18,9 +18,9 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @class FSTDocumentSet; @class FSTDocumentViewChangeSet; @@ -38,6 +38,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; +- (const firebase::firestore::model::DocumentKeySet &)mutatedKeys; + /** The new set of docs that should be in the view. */ @property(nonatomic, strong, readonly) FSTDocumentSet *documentSet; @@ -50,8 +52,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, assign, readonly) BOOL needsRefill; -@property(nonatomic, strong, readonly) FSTDocumentKeySet *mutatedKeys; - @end #pragma mark - FSTLimboDocumentChange @@ -97,7 +97,8 @@ typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithQuery:(FSTQuery *)query - remoteDocuments:(FSTDocumentKeySet *)remoteDocuments NS_DESIGNATED_INITIALIZER; + remoteDocuments:(firebase::firestore::model::DocumentKeySet)remoteDocuments + NS_DESIGNATED_INITIALIZER; /** * Iterates over a set of doc changes, applies the query limit, and computes what the new results @@ -152,7 +153,7 @@ typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { * The set of remote documents that the server has told us belongs to the target associated with * this view. */ -@property(nonatomic, strong, readonly) FSTDocumentKeySet *syncedDocuments; +- (const firebase::firestore::model::DocumentKeySet &)syncedDocuments; @end diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm index d87951a..d254a82 100644 --- a/Firestore/Source/Core/FSTView.mm +++ b/Firestore/Source/Core/FSTView.mm @@ -29,6 +29,7 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -40,26 +41,32 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet changeSet:(FSTDocumentViewChangeSet *)changeSet needsRefill:(BOOL)needsRefill - mutatedKeys:(FSTDocumentKeySet *)mutatedKeys NS_DESIGNATED_INITIALIZER; + mutatedKeys:(DocumentKeySet)mutatedKeys NS_DESIGNATED_INITIALIZER; @end -@implementation FSTViewDocumentChanges +@implementation FSTViewDocumentChanges { + DocumentKeySet _mutatedKeys; +} - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet changeSet:(FSTDocumentViewChangeSet *)changeSet needsRefill:(BOOL)needsRefill - mutatedKeys:(FSTDocumentKeySet *)mutatedKeys { + mutatedKeys:(DocumentKeySet)mutatedKeys { self = [super init]; if (self) { _documentSet = documentSet; _changeSet = changeSet; _needsRefill = needsRefill; - _mutatedKeys = mutatedKeys; + _mutatedKeys = std::move(mutatedKeys); } return self; } +- (const DocumentKeySet &)mutatedKeys { + return _mutatedKeys; +} + @end #pragma mark - FSTLimboDocumentChange @@ -165,32 +172,33 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang @property(nonatomic, strong) FSTDocumentSet *documentSet; -/** Documents included in the remote target. */ -@property(nonatomic, strong) FSTDocumentKeySet *syncedDocuments; - -/** Documents in the view but not in the remote target */ -@property(nonatomic, strong) FSTDocumentKeySet *limboDocuments; +@end -/** Document Keys that have local changes. */ -@property(nonatomic, strong) FSTDocumentKeySet *mutatedKeys; +@implementation FSTView { + /** Documents included in the remote target. */ + DocumentKeySet _syncedDocuments; -@end + /** Documents in the view but not in the remote target */ + DocumentKeySet _limboDocuments; -@implementation FSTView + /** Document Keys that have local changes. */ + DocumentKeySet _mutatedKeys; +} -- (instancetype)initWithQuery:(FSTQuery *)query - remoteDocuments:(nonnull FSTDocumentKeySet *)remoteDocuments { +- (instancetype)initWithQuery:(FSTQuery *)query remoteDocuments:(DocumentKeySet)remoteDocuments { self = [super init]; if (self) { _query = query; _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator]; - _syncedDocuments = remoteDocuments; - _limboDocuments = [FSTDocumentKeySet keySet]; - _mutatedKeys = [FSTDocumentKeySet keySet]; + _syncedDocuments = std::move(remoteDocuments); } return self; } +- (const DocumentKeySet &)syncedDocuments { + return _syncedDocuments; +} + - (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges { return [self computeChangesWithDocuments:docChanges previousChanges:nil]; } @@ -202,8 +210,8 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet]; FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet; - __block FSTDocumentKeySet *newMutatedKeys = - previousChanges ? previousChanges.mutatedKeys : self.mutatedKeys; + __block DocumentKeySet newMutatedKeys = + previousChanges ? previousChanges.mutatedKeys : _mutatedKeys; __block FSTDocumentSet *newDocumentSet = oldDocumentSet; __block BOOL needsRefill = NO; @@ -236,13 +244,13 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang if (newDoc) { newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc]; if (newDoc.hasLocalMutations) { - newMutatedKeys = [newMutatedKeys setByAddingObject:key]; + newMutatedKeys = newMutatedKeys.insert(key); } else { - newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + newMutatedKeys = newMutatedKeys.erase(key); } } else { newDocumentSet = [newDocumentSet documentSetByRemovingKey:key]; - newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + newMutatedKeys = newMutatedKeys.erase(key); } // Calculate change @@ -311,7 +319,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang FSTDocumentSet *oldDocuments = self.documentSet; self.documentSet = docChanges.documentSet; - self.mutatedKeys = docChanges.mutatedKeys; + _mutatedKeys = docChanges.mutatedKeys; // Sort changes based on type and query comparator. NSArray<FSTDocumentViewChange *> *changes = [docChanges.changeSet changes]; @@ -325,7 +333,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang }]; [self applyTargetChange:targetChange]; NSArray<FSTLimboDocumentChange *> *limboChanges = [self updateLimboDocuments]; - BOOL synced = self.limboDocuments.count == 0 && self.isCurrent; + BOOL synced = _limboDocuments.empty() && self.isCurrent; FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal; BOOL syncStateChanged = newSyncState != self.syncState; self.syncState = newSyncState; @@ -340,7 +348,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang oldDocuments:oldDocuments documentChanges:changes fromCache:newSyncState == FSTSyncStateLocal - hasPendingWrites:!docChanges.mutatedKeys.isEmpty + hasPendingWrites:!docChanges.mutatedKeys.empty() syncStateChanged:syncStateChanged]; return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges]; @@ -358,7 +366,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang initWithDocumentSet:self.documentSet changeSet:[FSTDocumentViewChangeSet changeSet] needsRefill:NO - mutatedKeys:self.mutatedKeys]]; + mutatedKeys:_mutatedKeys]]; } else { // No effect, just return a no-op FSTViewChange. return [[FSTViewChange alloc] initWithSnapshot:nil limboChanges:@[]]; @@ -370,7 +378,7 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang /** Returns whether the doc for the given key should be in limbo. */ - (BOOL)shouldBeLimboDocumentKey:(const DocumentKey &)key { // If the remote end says it's part of this query, it's not in limbo. - if ([self.syncedDocuments containsObject:key]) { + if (_syncedDocuments.contains(key)) { return NO; } // The local store doesn't think it's a result, so it shouldn't be in limbo. @@ -395,16 +403,14 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang if (targetChange) { FSTTargetMapping *targetMapping = targetChange.mapping; if ([targetMapping isKindOfClass:[FSTResetMapping class]]) { - self.syncedDocuments = ((FSTResetMapping *)targetMapping).documents; + _syncedDocuments = ((FSTResetMapping *)targetMapping).documents; } else if ([targetMapping isKindOfClass:[FSTUpdateMapping class]]) { - [((FSTUpdateMapping *)targetMapping).addedDocuments - enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - self.syncedDocuments = [self.syncedDocuments setByAddingObject:key]; - }]; - [((FSTUpdateMapping *)targetMapping).removedDocuments - enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - self.syncedDocuments = [self.syncedDocuments setByRemovingObject:key]; - }]; + for (const DocumentKey &key : ((FSTUpdateMapping *)targetMapping).addedDocuments) { + _syncedDocuments = _syncedDocuments.insert(key); + } + for (const DocumentKey &key : ((FSTUpdateMapping *)targetMapping).removedDocuments) { + _syncedDocuments = _syncedDocuments.erase(key); + } } switch (targetChange.currentStatusUpdate) { @@ -428,29 +434,29 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang } // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents. - FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments; - self.limboDocuments = [FSTDocumentKeySet keySet]; + DocumentKeySet oldLimboDocuments = std::move(_limboDocuments); + _limboDocuments = DocumentKeySet{}; for (FSTDocument *doc in self.documentSet.documentEnumerator) { if ([self shouldBeLimboDocumentKey:doc.key]) { - self.limboDocuments = [self.limboDocuments setByAddingObject:doc.key]; + _limboDocuments = _limboDocuments.insert(doc.key); } } // Diff the new limbo docs with the old limbo docs. NSMutableArray<FSTLimboDocumentChange *> *changes = - [NSMutableArray arrayWithCapacity:(oldLimboDocuments.count + self.limboDocuments.count)]; - [oldLimboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - if (![self.limboDocuments containsObject:key]) { + [NSMutableArray arrayWithCapacity:(oldLimboDocuments.size() + _limboDocuments.size())]; + for (const DocumentKey &key : oldLimboDocuments) { + if (!_limboDocuments.contains(key)) { [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved key:key]]; } - }]; - [self.limboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - if (![oldLimboDocuments containsObject:key]) { + } + for (const DocumentKey &key : _limboDocuments) { + if (!oldLimboDocuments.contains(key)) { [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded key:key]]; } - }]; + } return changes; } diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm index fae85e7..bc2f2eb 100644 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -243,6 +243,10 @@ using leveldb::WriteOptions; _ptr.reset(); } +- (_Nullable id<FSTReferenceDelegate>)referenceDelegate { + return nil; +} + #pragma mark - Error and Status + (nullable NSError *)errorWithStatus:(Status)status description:(NSString *)description, ... { diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm index 75c3cf6..2c9f68d 100644 --- a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm @@ -511,6 +511,7 @@ using leveldb::WriteOptions; documentKey:mutation.key batchID:batchID]; _db.currentTransaction->Delete(key); + [_db.referenceDelegate removeMutationReference:mutation.key]; [garbageCollector addPotentialGarbageKey:mutation.key]; } } diff --git a/Firestore/Source/Local/FSTLevelDBQueryCache.mm b/Firestore/Source/Local/FSTLevelDBQueryCache.mm index 5fde7d7..68b6f98 100644 --- a/Firestore/Source/Local/FSTLevelDBQueryCache.mm +++ b/Firestore/Source/Local/FSTLevelDBQueryCache.mm @@ -18,6 +18,7 @@ #include <memory> #include <string> +#include <utility> #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" #import "Firestore/Source/Core/FSTQuery.h" @@ -28,6 +29,7 @@ #import "Firestore/Source/Util/FSTAssert.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "absl/strings/match.h" NS_ASSUME_NONNULL_BEGIN @@ -35,9 +37,11 @@ NS_ASSUME_NONNULL_BEGIN using firebase::firestore::local::LevelDbTransaction; using Firestore::StringView; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; using leveldb::DB; using leveldb::Slice; using leveldb::Status; +using firebase::firestore::model::DocumentKeySet; @interface FSTLevelDBQueryCache () @@ -55,7 +59,7 @@ using leveldb::Status; * The last received snapshot version. This is part of `metadata` but we store it separately to * avoid extra conversion to/from GPBTimestamp. */ - FSTSnapshotVersion *_lastRemoteSnapshotVersion; + SnapshotVersion _lastRemoteSnapshotVersion; } + (nullable FSTPBTargetGlobal *)readTargetMetadataWithTransaction: @@ -135,13 +139,14 @@ using leveldb::Status; return self.metadata.highestListenSequenceNumber; } -- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { +- (const SnapshotVersion &)lastRemoteSnapshotVersion { return _lastRemoteSnapshotVersion; } -- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion { - _lastRemoteSnapshotVersion = snapshotVersion; - self.metadata.lastRemoteSnapshotVersion = [self.serializer encodedVersion:snapshotVersion]; +- (void)setLastRemoteSnapshotVersion:(SnapshotVersion)snapshotVersion { + _lastRemoteSnapshotVersion = std::move(snapshotVersion); + self.metadata.lastRemoteSnapshotVersion = + [self.serializer encodedVersion:_lastRemoteSnapshotVersion]; _db.currentTransaction->Put([FSTLevelDBTargetGlobalKey key], self.metadata); } @@ -278,30 +283,30 @@ using leveldb::Status; #pragma mark Matching Key tracking -- (void)addMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID { +- (void)addMatchingKeys:(const DocumentKeySet &)keys forTargetID:(FSTTargetID)targetID { // Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the // future if we wanted to store some other kind of value here, we can parse these empty values as // with some other protocol buffer (and the parser will see all default values). std::string emptyBuffer; - [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *documentKey, BOOL *stop) { + for (const DocumentKey &key : keys) { self->_db.currentTransaction->Put( - [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:documentKey], - emptyBuffer); + [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key], emptyBuffer); self->_db.currentTransaction->Put( - [FSTLevelDBDocumentTargetKey keyWithDocumentKey:documentKey targetID:targetID], - emptyBuffer); - }]; + [FSTLevelDBDocumentTargetKey keyWithDocumentKey:key targetID:targetID], emptyBuffer); + [self->_db.referenceDelegate addReference:key target:targetID]; + }; } -- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID { - [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { +- (void)removeMatchingKeys:(const DocumentKeySet &)keys forTargetID:(FSTTargetID)targetID { + for (const DocumentKey &key : keys) { self->_db.currentTransaction->Delete( [FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key]); self->_db.currentTransaction->Delete( [FSTLevelDBDocumentTargetKey keyWithDocumentKey:key targetID:targetID]); + [self->_db.referenceDelegate removeReference:key target:targetID]; [self.garbageCollector addPotentialGarbageKey:key]; - }]; + } } - (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID { @@ -327,12 +332,12 @@ using leveldb::Status; } } -- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { +- (DocumentKeySet)matchingKeysForTargetID:(FSTTargetID)targetID { std::string indexPrefix = [FSTLevelDBTargetDocumentKey keyPrefixWithTargetID:targetID]; auto indexIterator = _db.currentTransaction->NewIterator(); indexIterator->Seek(indexPrefix); - FSTDocumentKeySet *result = [FSTDocumentKeySet keySet]; + DocumentKeySet result; FSTLevelDBTargetDocumentKey *rowKey = [[FSTLevelDBTargetDocumentKey alloc] init]; for (; indexIterator->Valid(); indexIterator->Next()) { absl::string_view indexKey = indexIterator->key(); @@ -342,7 +347,7 @@ using leveldb::Status; break; } - result = [result setByAddingObject:rowKey.documentKey]; + result = result.insert(rowKey.documentKey); } return result; diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.h b/Firestore/Source/Local/FSTLocalDocumentsView.h index e75e0f3..bb5bb22 100644 --- a/Firestore/Source/Local/FSTLocalDocumentsView.h +++ b/Firestore/Source/Local/FSTLocalDocumentsView.h @@ -17,9 +17,9 @@ #import <Foundation/Foundation.h> #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @class FSTMaybeDocument; @class FSTQuery; @@ -53,7 +53,8 @@ NS_ASSUME_NONNULL_BEGIN * If we don't have cached state for a document in `keys`, a FSTDeletedDocument will be stored * for that key in the resulting set. */ -- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys; +- (FSTMaybeDocumentDictionary *)documentsForKeys: + (const firebase::firestore::model::DocumentKeySet &)keys; /** Performs a query against the local view of all documents. */ - (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query; diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.mm b/Firestore/Source/Local/FSTLocalDocumentsView.mm index e9b9423..471840a 100644 --- a/Firestore/Source/Local/FSTLocalDocumentsView.mm +++ b/Firestore/Source/Local/FSTLocalDocumentsView.mm @@ -17,7 +17,6 @@ #import "Firestore/Source/Local/FSTLocalDocumentsView.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTMutationQueue.h" #import "Firestore/Source/Local/FSTRemoteDocumentCache.h" #import "Firestore/Source/Model/FSTDocument.h" @@ -28,9 +27,12 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" using firebase::firestore::model::DocumentKey; using firebase::firestore::model::ResourcePath; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -64,14 +66,14 @@ NS_ASSUME_NONNULL_BEGIN return [self localDocument:remoteDoc key:key]; } -- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys { +- (FSTMaybeDocumentDictionary *)documentsForKeys:(const DocumentKeySet &)keys { FSTMaybeDocumentDictionary *results = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; - for (FSTDocumentKey *key in keys.objectEnumerator) { + for (const DocumentKey &key : keys) { // TODO(mikelehen): PERF: Consider fetching all remote documents at once rather than one-by-one. FSTMaybeDocument *maybeDoc = [self documentForKey:key]; // TODO(http://b/32275378): Don't conflate missing / deleted. if (!maybeDoc) { - maybeDoc = [FSTDeletedDocument documentWithKey:key version:[FSTSnapshotVersion noVersion]]; + maybeDoc = [FSTDeletedDocument documentWithKey:key version:SnapshotVersion::None()]; } results = [results dictionaryBySettingObject:maybeDoc forKey:key]; } @@ -105,7 +107,7 @@ NS_ASSUME_NONNULL_BEGIN // Now use the mutation queue to discover any other documents that may match the query after // applying mutations. - FSTDocumentKeySet *matchingKeys = [FSTDocumentKeySet keySet]; + DocumentKeySet matchingKeys; NSArray<FSTMutationBatch *> *matchingMutationBatches = [self.mutationQueue allMutationBatchesAffectingQuery:query]; for (FSTMutationBatch *batch in matchingMutationBatches) { @@ -114,13 +116,13 @@ NS_ASSUME_NONNULL_BEGIN // If the key is already in the results, we can skip it. if (![results containsKey:mutation.key]) { - matchingKeys = [matchingKeys setByAddingObject:mutation.key]; + matchingKeys = matchingKeys.insert(mutation.key); } } } // Now add in results for the matchingKeys. - for (FSTDocumentKey *key in matchingKeys.objectEnumerator) { + for (const DocumentKey &key : matchingKeys) { FSTMaybeDocument *doc = [self documentForKey:key]; if ([doc isKindOfClass:[FSTDocument class]]) { results = [results dictionaryBySettingObject:(FSTDocument *)doc forKey:key]; diff --git a/Firestore/Source/Local/FSTLocalSerializer.h b/Firestore/Source/Local/FSTLocalSerializer.h index 6ca7f01..b75f3e6 100644 --- a/Firestore/Source/Local/FSTLocalSerializer.h +++ b/Firestore/Source/Local/FSTLocalSerializer.h @@ -16,11 +16,12 @@ #import <Foundation/Foundation.h> +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + @class FSTMaybeDocument; @class FSTMutationBatch; @class FSTQueryData; @class FSTSerializerBeta; -@class FSTSnapshotVersion; @class FSTPBMaybeDocument; @class FSTPBTarget; @@ -61,11 +62,11 @@ NS_ASSUME_NONNULL_BEGIN /** Decodes an FSTPBTarget proto from local storage into an FSTQueryData model. */ - (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target; -/** Encodes an FSTSnapshotVersion model into a GPBTimestamp proto. */ -- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version; +/** Encodes a SnapshotVersion model into a GPBTimestamp proto. */ +- (GPBTimestamp *)encodedVersion:(const firebase::firestore::model::SnapshotVersion &)version; -/** Decodes a GPBTimestamp proto into a FSTSnapshotVersion model. */ -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version; +/** Decodes a GPBTimestamp proto into a SnapshotVersion model. */ +- (firebase::firestore::model::SnapshotVersion)decodedVersion:(GPBTimestamp *)version; @end diff --git a/Firestore/Source/Local/FSTLocalSerializer.mm b/Firestore/Source/Local/FSTLocalSerializer.mm index 61e173a..2c5ab4d 100644 --- a/Firestore/Source/Local/FSTLocalSerializer.mm +++ b/Firestore/Source/Local/FSTLocalSerializer.mm @@ -18,6 +18,7 @@ #include <cinttypes> +#import "FIRTimestamp.h" #import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" @@ -30,9 +31,13 @@ #import "Firestore/Source/Remote/FSTSerializerBeta.h" #import "Firestore/Source/Util/FSTAssert.h" +#include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +using firebase::Timestamp; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; @interface FSTLocalSerializer () @@ -99,8 +104,8 @@ using firebase::firestore::model::DocumentKey; FSTSerializerBeta *remoteSerializer = self.remoteSerializer; FSTObjectValue *data = [remoteSerializer decodedFields:document.fields]; - const DocumentKey key = [remoteSerializer decodedDocumentKey:document.name]; - FSTSnapshotVersion *version = [remoteSerializer decodedVersion:document.updateTime]; + DocumentKey key = [remoteSerializer decodedDocumentKey:document.name]; + SnapshotVersion version = [remoteSerializer decodedVersion:document.updateTime]; return [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; } @@ -118,8 +123,8 @@ using firebase::firestore::model::DocumentKey; - (FSTDeletedDocument *)decodedDeletedDocument:(FSTPBNoDocument *)proto { FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - const DocumentKey key = [remoteSerializer decodedDocumentKey:proto.name]; - FSTSnapshotVersion *version = [remoteSerializer decodedVersion:proto.readTime]; + DocumentKey key = [remoteSerializer decodedDocumentKey:proto.name]; + SnapshotVersion version = [remoteSerializer decodedVersion:proto.readTime]; return [FSTDeletedDocument documentWithKey:key version:version]; } @@ -128,7 +133,8 @@ using firebase::firestore::model::DocumentKey; FSTPBWriteBatch *proto = [FSTPBWriteBatch message]; proto.batchId = batch.batchID; - proto.localWriteTime = [remoteSerializer encodedTimestamp:batch.localWriteTime]; + proto.localWriteTime = [remoteSerializer + encodedTimestamp:Timestamp{batch.localWriteTime.seconds, batch.localWriteTime.nanoseconds}]; NSMutableArray<GCFSWrite *> *writes = proto.writesArray; for (FSTMutation *mutation in batch.mutations) { @@ -146,11 +152,13 @@ using firebase::firestore::model::DocumentKey; [mutations addObject:[remoteSerializer decodedMutation:write]]; } - FIRTimestamp *localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; + Timestamp localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; - return [[FSTMutationBatch alloc] initWithBatchID:batchID - localWriteTime:localWriteTime - mutations:mutations]; + return [[FSTMutationBatch alloc] + initWithBatchID:batchID + localWriteTime:[FIRTimestamp timestampWithSeconds:localWriteTime.seconds() + nanoseconds:localWriteTime.nanoseconds()] + mutations:mutations]; } - (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { @@ -181,7 +189,7 @@ using firebase::firestore::model::DocumentKey; FSTTargetID targetID = target.targetId; FSTListenSequenceNumber sequenceNumber = target.lastListenSequenceNumber; - FSTSnapshotVersion *version = [remoteSerializer decodedVersion:target.snapshotVersion]; + SnapshotVersion version = [remoteSerializer decodedVersion:target.snapshotVersion]; NSData *resumeToken = target.resumeToken; FSTQuery *query; @@ -206,11 +214,11 @@ using firebase::firestore::model::DocumentKey; resumeToken:resumeToken]; } -- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { +- (GPBTimestamp *)encodedVersion:(const SnapshotVersion &)version { return [self.remoteSerializer encodedVersion:version]; } -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { +- (SnapshotVersion)decodedVersion:(GPBTimestamp *)version { return [self.remoteSerializer decodedVersion:version]; } diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h index 82402e4..1f4146a 100644 --- a/Firestore/Source/Local/FSTLocalStore.h +++ b/Firestore/Source/Local/FSTLocalStore.h @@ -18,11 +18,11 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" -#import "Firestore/Source/Model/FSTDocumentVersionDictionary.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTLocalViewChanges; @class FSTLocalWriteResult; @@ -141,7 +141,7 @@ NS_ASSUME_NONNULL_BEGIN * Returns the last consistent snapshot processed (used by the RemoteStore to determine whether to * buffer incoming snapshots from the backend). */ -- (FSTSnapshotVersion *)lastRemoteSnapshotVersion; +- (const firebase::firestore::model::SnapshotVersion &)lastRemoteSnapshotVersion; /** * Updates the "ground-state" (remote) documents. We assume that the remote event reflects any @@ -156,7 +156,7 @@ NS_ASSUME_NONNULL_BEGIN * Returns the keys of the documents that are associated with the given targetID in the remote * table. */ -- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID; +- (firebase::firestore::model::DocumentKeySet)remoteDocumentKeysForTarget:(FSTTargetID)targetID; /** * Collects garbage if necessary. diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm index b5dfeec..0d6a785 100644 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ b/Firestore/Source/Local/FSTLocalStore.mm @@ -21,7 +21,6 @@ #import "FIRTimestamp.h" #import "Firestore/Source/Core/FSTListenSequence.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTGarbageCollector.h" #import "Firestore/Source/Local/FSTLocalDocumentsView.h" #import "Firestore/Source/Local/FSTLocalViewChanges.h" @@ -43,11 +42,14 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" using firebase::firestore::auth::User; -using firebase::firestore::model::DocumentKey; using firebase::firestore::core::TargetIdGenerator; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentVersionMap; NS_ASSUME_NONNULL_BEGIN @@ -112,6 +114,7 @@ NS_ASSUME_NONNULL_BEGIN _localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:_remoteDocumentCache mutationQueue:_mutationQueue]; _localViewReferences = [[FSTReferenceSet alloc] init]; + [_persistence.referenceDelegate addInMemoryPins:_localViewReferences]; _garbageCollector = garbageCollector; [_garbageCollector addGarbageSource:_queryCache]; @@ -186,11 +189,11 @@ NS_ASSUME_NONNULL_BEGIN mutationQueue:self.mutationQueue]; // Union the old/new changed keys. - FSTDocumentKeySet *changedKeys = [FSTDocumentKeySet keySet]; + DocumentKeySet changedKeys; for (NSArray<FSTMutationBatch *> *batches in @[ oldBatches, newBatches ]) { for (FSTMutationBatch *batch in batches) { for (FSTMutation *mutation in batch.mutations) { - changedKeys = [changedKeys setByAddingObject:mutation.key]; + changedKeys = changedKeys.insert(mutation.key); } } } @@ -205,7 +208,7 @@ NS_ASSUME_NONNULL_BEGIN FIRTimestamp *localWriteTime = [FIRTimestamp timestamp]; FSTMutationBatch *batch = [self.mutationQueue addMutationBatchWithWriteTime:localWriteTime mutations:mutations]; - FSTDocumentKeySet *keys = [batch keys]; + DocumentKeySet keys = [batch keys]; FSTMaybeDocumentDictionary *changedDocuments = [self.localDocuments documentsForKeys:keys]; return [FSTLocalWriteResult resultForBatchID:batch.batchID changes:changedDocuments]; }); @@ -217,10 +220,9 @@ NS_ASSUME_NONNULL_BEGIN [mutationQueue acknowledgeBatch:batchResult.batch streamToken:batchResult.streamToken]; - FSTDocumentKeySet *affected; + DocumentKeySet affected; if ([self shouldHoldBatchResultWithVersion:batchResult.commitVersion]) { [self.heldBatchResults addObject:batchResult]; - affected = [FSTDocumentKeySet keySet]; } else { affected = [self releaseBatchResults:@[ batchResult ]]; } @@ -239,7 +241,7 @@ NS_ASSUME_NONNULL_BEGIN FSTBatchID lastAcked = [self.mutationQueue highestAcknowledgedBatchID]; FSTAssert(batchID > lastAcked, @"Acknowledged batches can't be rejected."); - FSTDocumentKeySet *affected = [self removeMutationBatch:toReject]; + DocumentKeySet affected = [self removeMutationBatch:toReject]; [self.mutationQueue performConsistencyCheck]; @@ -256,12 +258,14 @@ NS_ASSUME_NONNULL_BEGIN [&]() { [self.mutationQueue setLastStreamToken:streamToken]; }); } -- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { +- (const SnapshotVersion &)lastRemoteSnapshotVersion { return [self.queryCache lastRemoteSnapshotVersion]; } - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { return self.persistence.run("Apply remote event", [&]() -> FSTMaybeDocumentDictionary * { + // TODO(gsoltis): move the sequence number into the reference delegate. + FSTListenSequenceNumber sequenceNumber = [self.listenSequence next]; id<FSTQueryCache> queryCache = self.queryCache; [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( @@ -274,6 +278,18 @@ NS_ASSUME_NONNULL_BEGIN return; } + // Update the resume token if the change includes one. Don't clear any preexisting value. + // Bump the sequence number as well, so that documents being removed now are ordered later + // than documents that were previously removed from this target. + NSData *resumeToken = change.resumeToken; + if (resumeToken.length > 0) { + queryData = [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion + resumeToken:resumeToken + sequenceNumber:sequenceNumber]; + self.targetIDs[targetIDNumber] = queryData; + [self.queryCache updateQueryData:queryData]; + } + FSTTargetMapping *mapping = change.mapping; if (mapping) { // First make sure that all references are deleted. @@ -291,61 +307,58 @@ NS_ASSUME_NONNULL_BEGIN FSTFail(@"Unknown mapping type: %@", mapping); } } - - // Update the resume token if the change includes one. Don't clear any preexisting value. - NSData *resumeToken = change.resumeToken; - if (resumeToken.length > 0) { - queryData = [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion - resumeToken:resumeToken]; - self.targetIDs[targetIDNumber] = queryData; - [self.queryCache updateQueryData:queryData]; - } }]; // TODO(klimt): This could probably be an NSMutableDictionary. - FSTDocumentKeySet *changedDocKeys = [FSTDocumentKeySet keySet]; + DocumentKeySet changedDocKeys; + const DocumentKeySet &limboDocuments = remoteEvent.limboDocumentChanges; for (const auto &kv : remoteEvent.documentUpdates) { const DocumentKey &key = kv.first; FSTMaybeDocument *doc = kv.second; - changedDocKeys = [changedDocKeys setByAddingObject:key]; + changedDocKeys = changedDocKeys.insert(key); FSTMaybeDocument *existingDoc = [self.remoteDocumentCache entryForKey:key]; // Make sure we don't apply an old document version to the remote cache, though we - // make an exception for [SnapshotVersion noVersion] which can happen for manufactured + // make an exception for SnapshotVersion::None() which can happen for manufactured // events (e.g. in the case of a limbo document resolution failing). - if (!existingDoc || [doc.version isEqual:[FSTSnapshotVersion noVersion]] || - [doc.version compare:existingDoc.version] != NSOrderedAscending) { + if (!existingDoc || SnapshotVersion{doc.version} == SnapshotVersion::None() || + SnapshotVersion{doc.version} >= SnapshotVersion{existingDoc.version}) { [self.remoteDocumentCache addEntry:doc]; } else { FSTLog( @"FSTLocalStore Ignoring outdated watch update for %s. " - "Current version: %@ Watch version: %@", - key.ToString().c_str(), existingDoc.version, doc.version); + "Current version: %s Watch version: %s", + key.ToString().c_str(), existingDoc.version.timestamp().ToString().c_str(), + doc.version.timestamp().ToString().c_str()); } // The document might be garbage because it was unreferenced by everything. // Make sure to mark it as garbage if it is... [self.garbageCollector addPotentialGarbageKey:key]; + if (limboDocuments.contains(key)) { + [self.persistence.referenceDelegate limboDocumentUpdated:key]; + } } // HACK: The only reason we allow omitting snapshot version is so we can synthesize remote // events when we get permission denied errors while trying to resolve the state of a locally // cached document that is in limbo. - FSTSnapshotVersion *lastRemoteVersion = [self.queryCache lastRemoteSnapshotVersion]; - FSTSnapshotVersion *remoteVersion = remoteEvent.snapshotVersion; - if (![remoteVersion isEqual:[FSTSnapshotVersion noVersion]]) { - FSTAssert([remoteVersion compare:lastRemoteVersion] != NSOrderedAscending, - @"Watch stream reverted to previous snapshot?? (%@ < %@)", remoteVersion, - lastRemoteVersion); + const SnapshotVersion &lastRemoteVersion = [self.queryCache lastRemoteSnapshotVersion]; + const SnapshotVersion &remoteVersion = remoteEvent.snapshotVersion; + if (remoteVersion != SnapshotVersion::None()) { + FSTAssert(remoteVersion >= lastRemoteVersion, + @"Watch stream reverted to previous snapshot?? (%s < %s)", + remoteVersion.timestamp().ToString().c_str(), + lastRemoteVersion.timestamp().ToString().c_str()); [self.queryCache setLastRemoteSnapshotVersion:remoteVersion]; } - FSTDocumentKeySet *releasedWriteKeys = [self releaseHeldBatchResults]; + DocumentKeySet releasedWriteKeys = [self releaseHeldBatchResults]; // Union the two key sets. - __block FSTDocumentKeySet *keysToRecalc = changedDocKeys; - [releasedWriteKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - keysToRecalc = [keysToRecalc setByAddingObject:key]; - }]; + DocumentKeySet keysToRecalc = changedDocKeys; + for (const DocumentKey &key : releasedWriteKeys) { + keysToRecalc = keysToRecalc.insert(key); + } return [self.localDocuments documentsForKeys:keysToRecalc]; }); @@ -358,6 +371,9 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryData *queryData = [self.queryCache queryDataForQuery:view.query]; FSTAssert(queryData, @"Local view changes contain unallocated query."); FSTTargetID targetID = queryData.targetID; + for (const DocumentKey &key : view.removedKeys) { + [self->_persistence.referenceDelegate removeReference:key target:targetID]; + } [localViewReferences addReferencesToKeys:view.addedKeys forID:targetID]; [localViewReferences removeReferencesToKeys:view.removedKeys forID:targetID]; } @@ -408,6 +424,7 @@ NS_ASSUME_NONNULL_BEGIN if (self.garbageCollector.isEager) { [self.queryCache removeQueryData:queryData]; } + [self.persistence.referenceDelegate removeTarget:queryData]; [self.targetIDs removeObjectForKey:@(queryData.targetID)]; // If this was the last watch target, then we won't get any more watch snapshots, so we should @@ -424,8 +441,8 @@ NS_ASSUME_NONNULL_BEGIN }); } -- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID { - return self.persistence.run("RemoteDocumentKeysForTarget", [&]() -> FSTDocumentKeySet * { +- (DocumentKeySet)remoteDocumentKeysForTarget:(FSTTargetID)targetID { + return self.persistence.run("RemoteDocumentKeysForTarget", [&]() -> DocumentKeySet { return [self.queryCache matchingKeysForTargetID:targetID]; }); } @@ -449,7 +466,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return the set of keys of docs that were modified by those writes. */ -- (FSTDocumentKeySet *)releaseHeldBatchResults { +- (DocumentKeySet)releaseHeldBatchResults { NSMutableArray<FSTMutationBatchResult *> *toRelease = [NSMutableArray array]; for (FSTMutationBatchResult *batchResult in self.heldBatchResults) { if (![self isRemoteUpToVersion:batchResult.commitVersion]) { @@ -459,25 +476,24 @@ NS_ASSUME_NONNULL_BEGIN } if (toRelease.count == 0) { - return [FSTDocumentKeySet keySet]; + return DocumentKeySet{}; } else { [self.heldBatchResults removeObjectsInRange:NSMakeRange(0, toRelease.count)]; return [self releaseBatchResults:toRelease]; } } -- (BOOL)isRemoteUpToVersion:(FSTSnapshotVersion *)version { +- (BOOL)isRemoteUpToVersion:(const SnapshotVersion &)version { // If there are no watch targets, then we won't get remote snapshots, and are always "up-to-date." - return [version compare:self.queryCache.lastRemoteSnapshotVersion] != NSOrderedDescending || - self.targetIDs.count == 0; + return version <= self.queryCache.lastRemoteSnapshotVersion || self.targetIDs.count == 0; } -- (BOOL)shouldHoldBatchResultWithVersion:(FSTSnapshotVersion *)version { +- (BOOL)shouldHoldBatchResultWithVersion:(const SnapshotVersion &)version { // Check if watcher isn't up to date or prior results are already held. return ![self isRemoteUpToVersion:version] || self.heldBatchResults.count > 0; } -- (FSTDocumentKeySet *)releaseBatchResults:(NSArray<FSTMutationBatchResult *> *)batchResults { +- (DocumentKeySet)releaseBatchResults:(NSArray<FSTMutationBatchResult *> *)batchResults { NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array]; for (FSTMutationBatchResult *batchResult in batchResults) { [self applyBatchResult:batchResult]; @@ -487,36 +503,37 @@ NS_ASSUME_NONNULL_BEGIN return [self removeMutationBatches:batches]; } -- (FSTDocumentKeySet *)removeMutationBatch:(FSTMutationBatch *)batch { +- (DocumentKeySet)removeMutationBatch:(FSTMutationBatch *)batch { return [self removeMutationBatches:@[ batch ]]; } /** Removes all the mutation batches named in the given array. */ -- (FSTDocumentKeySet *)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches { - // TODO(klimt): Could this be an NSMutableDictionary? - __block FSTDocumentKeySet *affectedDocs = [FSTDocumentKeySet keySet]; - +- (DocumentKeySet)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches { + DocumentKeySet affectedDocs; for (FSTMutationBatch *batch in batches) { for (FSTMutation *mutation in batch.mutations) { const DocumentKey &key = mutation.key; - affectedDocs = [affectedDocs setByAddingObject:key]; + affectedDocs = affectedDocs.insert(key); } } [self.mutationQueue removeMutationBatches:batches]; - return affectedDocs; } - (void)applyBatchResult:(FSTMutationBatchResult *)batchResult { FSTMutationBatch *batch = batchResult.batch; - FSTDocumentKeySet *docKeys = batch.keys; - [docKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *docKey, BOOL *stop) { + DocumentKeySet docKeys = batch.keys; + const DocumentVersionMap &versions = batchResult.docVersions; + for (const DocumentKey &docKey : docKeys) { FSTMaybeDocument *_Nullable remoteDoc = [self.remoteDocumentCache entryForKey:docKey]; FSTMaybeDocument *_Nullable doc = remoteDoc; - FSTSnapshotVersion *ackVersion = batchResult.docVersions[docKey]; - FSTAssert(ackVersion, @"docVersions should contain every doc in the write."); - if (!doc || [doc.version compare:ackVersion] == NSOrderedAscending) { + + auto ackVersionIter = versions.find(docKey); + FSTAssert(ackVersionIter != versions.end(), + @"docVersions should contain every doc in the write."); + const SnapshotVersion &ackVersion = ackVersionIter->second; + if (!doc || doc.version < ackVersion) { doc = [batch applyTo:doc documentKey:docKey mutationBatchResult:batchResult]; if (!doc) { FSTAssert(!remoteDoc, @"Mutation batch %@ applied to document %@ resulted in nil.", batch, @@ -525,7 +542,7 @@ NS_ASSUME_NONNULL_BEGIN [self.remoteDocumentCache addEntry:doc]; } } - }]; + } } @end diff --git a/Firestore/Source/Local/FSTLocalViewChanges.h b/Firestore/Source/Local/FSTLocalViewChanges.h index eb84642..143d010 100644 --- a/Firestore/Source/Local/FSTLocalViewChanges.h +++ b/Firestore/Source/Local/FSTLocalViewChanges.h @@ -16,7 +16,7 @@ #import <Foundation/Foundation.h> -#import "Firestore/Source/Model/FSTDocumentKeySet.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @class FSTDocumentSet; @class FSTMutation; @@ -34,16 +34,17 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTLocalViewChanges : NSObject + (instancetype)changesForQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys; + addedKeys:(firebase::firestore::model::DocumentKeySet)addedKeys + removedKeys:(firebase::firestore::model::DocumentKeySet)removedKeys; + (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot; - (id)init NS_UNAVAILABLE; @property(nonatomic, strong, readonly) FSTQuery *query; -@property(nonatomic, strong) FSTDocumentKeySet *addedKeys; -@property(nonatomic, strong) FSTDocumentKeySet *removedKeys; + +- (const firebase::firestore::model::DocumentKeySet &)addedKeys; +- (const firebase::firestore::model::DocumentKeySet &)removedKeys; @end diff --git a/Firestore/Source/Local/FSTLocalViewChanges.mm b/Firestore/Source/Local/FSTLocalViewChanges.mm index 9a7f445..eb6b259 100644 --- a/Firestore/Source/Local/FSTLocalViewChanges.mm +++ b/Firestore/Source/Local/FSTLocalViewChanges.mm @@ -16,31 +16,38 @@ #import "Firestore/Source/Local/FSTLocalViewChanges.h" +#include <utility> + #import "Firestore/Source/Core/FSTViewSnapshot.h" #import "Firestore/Source/Model/FSTDocument.h" +using firebase::firestore::model::DocumentKeySet; + NS_ASSUME_NONNULL_BEGIN @interface FSTLocalViewChanges () - (instancetype)initWithQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys NS_DESIGNATED_INITIALIZER; + addedKeys:(DocumentKeySet)addedKeys + removedKeys:(DocumentKeySet)removedKeys NS_DESIGNATED_INITIALIZER; @end -@implementation FSTLocalViewChanges +@implementation FSTLocalViewChanges { + DocumentKeySet _addedKeys; + DocumentKeySet _removedKeys; +} + (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot { - FSTDocumentKeySet *addedKeys = [FSTDocumentKeySet keySet]; - FSTDocumentKeySet *removedKeys = [FSTDocumentKeySet keySet]; + DocumentKeySet addedKeys; + DocumentKeySet removedKeys; for (FSTDocumentViewChange *docChange in viewSnapshot.documentChanges) { switch (docChange.type) { case FSTDocumentViewChangeTypeAdded: - addedKeys = [addedKeys setByAddingObject:docChange.document.key]; + addedKeys = addedKeys.insert(docChange.document.key); break; case FSTDocumentViewChangeTypeRemoved: - removedKeys = [removedKeys setByAddingObject:docChange.document.key]; + removedKeys = removedKeys.insert(docChange.document.key); break; default: @@ -49,28 +56,39 @@ NS_ASSUME_NONNULL_BEGIN } } - return [self changesForQuery:viewSnapshot.query addedKeys:addedKeys removedKeys:removedKeys]; + return [self changesForQuery:viewSnapshot.query + addedKeys:std::move(addedKeys) + removedKeys:std::move(removedKeys)]; } + (instancetype)changesForQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys { - return - [[FSTLocalViewChanges alloc] initWithQuery:query addedKeys:addedKeys removedKeys:removedKeys]; + addedKeys:(DocumentKeySet)addedKeys + removedKeys:(DocumentKeySet)removedKeys { + return [[FSTLocalViewChanges alloc] initWithQuery:query + addedKeys:std::move(addedKeys) + removedKeys:std::move(removedKeys)]; } - (instancetype)initWithQuery:(FSTQuery *)query - addedKeys:(FSTDocumentKeySet *)addedKeys - removedKeys:(FSTDocumentKeySet *)removedKeys { + addedKeys:(DocumentKeySet)addedKeys + removedKeys:(DocumentKeySet)removedKeys { self = [super init]; if (self) { _query = query; - _addedKeys = addedKeys; - _removedKeys = removedKeys; + _addedKeys = std::move(addedKeys); + _removedKeys = std::move(removedKeys); } return self; } +- (const DocumentKeySet &)addedKeys { + return _addedKeys; +} + +- (const DocumentKeySet &)removedKeys { + return _removedKeys; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.h b/Firestore/Source/Local/FSTMemoryMutationQueue.h index f0786cc..fd46a6e 100644 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.h +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.h @@ -16,6 +16,7 @@ #import <Foundation/Foundation.h> +#import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Local/FSTMutationQueue.h" @protocol FSTGarbageCollector; @@ -24,7 +25,9 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTMemoryMutationQueue : NSObject <FSTMutationQueue> -+ (instancetype)mutationQueue; +- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; /** The garbage collector to notify about potential garbage keys. */ @property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector; diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.mm b/Firestore/Source/Local/FSTMemoryMutationQueue.mm index 8028bb3..e05ee64 100644 --- a/Firestore/Source/Local/FSTMemoryMutationQueue.mm +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.mm @@ -18,9 +18,11 @@ #import "Firestore/Source/Core/FSTQuery.h" #import "Firestore/Source/Local/FSTDocumentReference.h" +#import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Source/Util/FSTAssert.h" +#import "Firestore/third_party/Immutable/FSTImmutableSortedSet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" @@ -72,14 +74,13 @@ static const NSComparator NumberComparator = ^NSComparisonResult(NSNumber *left, @end -@implementation FSTMemoryMutationQueue - -+ (instancetype)mutationQueue { - return [[FSTMemoryMutationQueue alloc] init]; +@implementation FSTMemoryMutationQueue { + FSTMemoryPersistence *_persistence; } -- (instancetype)init { +- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence { if (self = [super init]) { + _persistence = persistence; _queue = [NSMutableArray array]; _batchesByDocumentKey = [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; @@ -347,6 +348,7 @@ static const NSComparator NumberComparator = ^NSComparisonResult(NSNumber *left, for (FSTMutation *mutation in batch.mutations) { const DocumentKey &key = mutation.key; [garbageCollector addPotentialGarbageKey:key]; + [_persistence.referenceDelegate removeMutationReference:key]; FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:batchID]; references = [references setByRemovingObject:reference]; diff --git a/Firestore/Source/Local/FSTMemoryPersistence.mm b/Firestore/Source/Local/FSTMemoryPersistence.mm index 8d74881..3466f3e 100644 --- a/Firestore/Source/Local/FSTMemoryPersistence.mm +++ b/Firestore/Source/Local/FSTMemoryPersistence.mm @@ -59,7 +59,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init { if (self = [super init]) { - _queryCache = [[FSTMemoryQueryCache alloc] init]; + _queryCache = [[FSTMemoryQueryCache alloc] initWithPersistence:self]; _remoteDocumentCache = [[FSTMemoryRemoteDocumentCache alloc] init]; } return self; @@ -78,6 +78,10 @@ NS_ASSUME_NONNULL_BEGIN self.started = NO; } +- (_Nullable id<FSTReferenceDelegate>)referenceDelegate { + return nil; +} + - (const FSTTransactionRunner &)run { return _transactionRunner; } @@ -85,7 +89,7 @@ NS_ASSUME_NONNULL_BEGIN - (id<FSTMutationQueue>)mutationQueueForUser:(const User &)user { id<FSTMutationQueue> queue = _mutationQueues[user]; if (!queue) { - queue = [FSTMemoryMutationQueue mutationQueue]; + queue = [[FSTMemoryMutationQueue alloc] initWithPersistence:self]; _mutationQueues[user] = queue; } return queue; diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.h b/Firestore/Source/Local/FSTMemoryQueryCache.h index 98f0277..126ce59 100644 --- a/Firestore/Source/Local/FSTMemoryQueryCache.h +++ b/Firestore/Source/Local/FSTMemoryQueryCache.h @@ -20,11 +20,18 @@ NS_ASSUME_NONNULL_BEGIN +@class FSTMemoryPersistence; + /** * An implementation of the FSTQueryCache protocol that merely keeps queries in memory, suitable * for online only clients with persistence disabled. */ @interface FSTMemoryQueryCache : NSObject <FSTQueryCache> + +- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.mm b/Firestore/Source/Local/FSTMemoryQueryCache.mm index 18d14f2..2eba4f6 100644 --- a/Firestore/Source/Local/FSTMemoryQueryCache.mm +++ b/Firestore/Source/Local/FSTMemoryQueryCache.mm @@ -16,12 +16,19 @@ #import "Firestore/Source/Local/FSTMemoryQueryCache.h" +#include <utility> + #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Local/FSTReferenceSet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentKey; NS_ASSUME_NONNULL_BEGIN @@ -38,18 +45,20 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, assign) FSTListenSequenceNumber highestListenSequenceNumber; -/** The last received snapshot version. */ -@property(nonatomic, strong) FSTSnapshotVersion *lastRemoteSnapshotVersion; - @end -@implementation FSTMemoryQueryCache +@implementation FSTMemoryQueryCache { + FSTMemoryPersistence *_persistence; + /** The last received snapshot version. */ + SnapshotVersion _lastRemoteSnapshotVersion; +} -- (instancetype)init { +- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence { if (self = [super init]) { + _persistence = persistence; _queries = [NSMutableDictionary dictionary]; _references = [[FSTReferenceSet alloc] init]; - _lastRemoteSnapshotVersion = [FSTSnapshotVersion noVersion]; + _lastRemoteSnapshotVersion = SnapshotVersion::None(); } return self; } @@ -69,14 +78,13 @@ NS_ASSUME_NONNULL_BEGIN return _highestListenSequenceNumber; } -/*- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { +- (const SnapshotVersion &)lastRemoteSnapshotVersion { return _lastRemoteSnapshotVersion; } -- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - group:(FSTWriteGroup *)group { - _lastRemoteSnapshotVersion = snapshotVersion; -}*/ +- (void)setLastRemoteSnapshotVersion:(SnapshotVersion)snapshotVersion { + _lastRemoteSnapshotVersion = std::move(snapshotVersion); +} - (void)addQueryData:(FSTQueryData *)queryData { self.queries[queryData.query] = queryData; @@ -113,19 +121,25 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark Reference tracking -- (void)addMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID { +- (void)addMatchingKeys:(const DocumentKeySet &)keys forTargetID:(FSTTargetID)targetID { [self.references addReferencesToKeys:keys forID:targetID]; + for (const DocumentKey &key : keys) { + [_persistence.referenceDelegate addReference:key target:targetID]; + } } -- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID { +- (void)removeMatchingKeys:(const DocumentKeySet &)keys forTargetID:(FSTTargetID)targetID { [self.references removeReferencesToKeys:keys forID:targetID]; + for (const DocumentKey &key : keys) { + [_persistence.referenceDelegate removeReference:key target:targetID]; + } } - (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID { [self.references removeReferencesForID:targetID]; } -- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { +- (DocumentKeySet)matchingKeysForTargetID:(FSTTargetID)targetID { return [self.references referencedKeysForID:targetID]; } diff --git a/Firestore/Source/Local/FSTPersistence.h b/Firestore/Source/Local/FSTPersistence.h index 2294ef1..417ff3f 100644 --- a/Firestore/Source/Local/FSTPersistence.h +++ b/Firestore/Source/Local/FSTPersistence.h @@ -16,12 +16,16 @@ #import <Foundation/Foundation.h> +#import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Util/FSTAssert.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +@class FSTDocumentKey; @protocol FSTMutationQueue; @protocol FSTQueryCache; +@class FSTQueryData; @protocol FSTRemoteDocumentCache; +@class FSTReferenceSet; NS_ASSUME_NONNULL_BEGIN @@ -56,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN * of its reads and writes in order to avoid relying on being able to read back uncommitted writes. */ struct FSTTransactionRunner; +@protocol FSTReferenceDelegate; @protocol FSTPersistence <NSObject> /** @@ -87,6 +92,12 @@ struct FSTTransactionRunner; @property(nonatomic, readonly, assign) const FSTTransactionRunner &run; +/** + * This property provides access to hooks around the document reference lifecycle. It is initially + * nullable while being implemented, but the goal is to eventually have it be non-nil. + */ +@property(nonatomic, readonly, strong) _Nullable id<FSTReferenceDelegate> referenceDelegate; + @end @protocol FSTTransactional @@ -97,6 +108,52 @@ struct FSTTransactionRunner; @end +/** + * An FSTReferenceDelegate instance handles all of the hooks into the document-reference lifecycle. + * This includes being added to a target, being removed from a target, being subject to mutation, + * and being mutated by the user. + * + * Different implementations may do different things with each of these events. Not every + * implementation needs to do something with every lifecycle hook. + * + * Implementations that care about sequence numbers are responsible for generating them and making + * them available. + */ +@protocol FSTReferenceDelegate + +/** + * Registers an FSTReferenceSet of documents that should be considered 'referenced' and not eligible + * for removal during garbage collection. + */ +- (void)addInMemoryPins:(FSTReferenceSet *)set; + +/** + * Notify the delegate that a target was removed. + */ +- (void)removeTarget:(FSTQueryData *)queryData; + +/** + * Notify the delegate that the given document was added to the given target. + */ +- (void)addReference:(FSTDocumentKey *)key target:(FSTTargetID)targetID; + +/** + * Notify the delegate that the given document was removed from the given target. + */ +- (void)removeReference:(FSTDocumentKey *)key target:(FSTTargetID)targetID; + +/** + * Notify the delegate that a document is no longer being mutated by the user. + */ +- (void)removeMutationReference:(FSTDocumentKey *)key; + +/** + * Notify the delegate that a limbo document was updated. + */ +- (void)limboDocumentUpdated:(FSTDocumentKey *)key; + +@end + struct FSTTransactionRunner { // Intentionally disable nullability checking for this function. We cannot properly annotate // the function because this function can handle both pointer and non-pointer types. It is an error diff --git a/Firestore/Source/Local/FSTQueryCache.h b/Firestore/Source/Local/FSTQueryCache.h index d797d49..1ad46aa 100644 --- a/Firestore/Source/Local/FSTQueryCache.h +++ b/Firestore/Source/Local/FSTQueryCache.h @@ -18,13 +18,14 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Local/FSTGarbageCollector.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" + +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTDocumentSet; @class FSTMaybeDocument; @class FSTQuery; @class FSTQueryData; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -61,7 +62,7 @@ NS_ASSUME_NONNULL_BEGIN * * This is updated whenever our we get a TargetChange with a read_time and empty target_ids. */ -- (FSTSnapshotVersion *)lastRemoteSnapshotVersion; +- (const firebase::firestore::model::SnapshotVersion &)lastRemoteSnapshotVersion; /** * Set the snapshot version representing the last consistent snapshot received from the backend. @@ -69,7 +70,7 @@ NS_ASSUME_NONNULL_BEGIN * * @param snapshotVersion The new snapshot version. */ -- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion; +- (void)setLastRemoteSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion; /** * Adds an entry in the cache. @@ -104,15 +105,17 @@ NS_ASSUME_NONNULL_BEGIN - (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query; /** Adds the given document keys to cached query results of the given target ID. */ -- (void)addMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID; +- (void)addMatchingKeys:(const firebase::firestore::model::DocumentKeySet &)keys + forTargetID:(FSTTargetID)targetID; /** Removes the given document keys from the cached query results of the given target ID. */ -- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys forTargetID:(FSTTargetID)targetID; +- (void)removeMatchingKeys:(const firebase::firestore::model::DocumentKeySet &)keys + forTargetID:(FSTTargetID)targetID; /** Removes all the keys in the query results of the given target ID. */ - (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID; -- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID; +- (firebase::firestore::model::DocumentKeySet)matchingKeysForTargetID:(FSTTargetID)targetID; @end diff --git a/Firestore/Source/Local/FSTQueryData.h b/Firestore/Source/Local/FSTQueryData.h index 5db2de6..bde0a15 100644 --- a/Firestore/Source/Local/FSTQueryData.h +++ b/Firestore/Source/Local/FSTQueryData.h @@ -18,8 +18,9 @@ #import "Firestore/Source/Core/FSTTypes.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + @class FSTQuery; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -42,7 +43,7 @@ typedef NS_ENUM(NSInteger, FSTQueryPurpose) { targetID:(FSTTargetID)targetID listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber purpose:(FSTQueryPurpose)purpose - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion resumeToken:(NSData *)resumeToken NS_DESIGNATED_INITIALIZER; /** Convenience initializer for use when creating an FSTQueryData for the first time. */ @@ -53,9 +54,17 @@ typedef NS_ENUM(NSInteger, FSTQueryPurpose) { - (instancetype)init NS_UNAVAILABLE; -/** Creates a new query data instance with an updated snapshot version and resume token. */ -- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - resumeToken:(NSData *)resumeToken; +/** + * Creates a new query data instance with an updated snapshot version, resume token, and sequence + * number. + */ +- (instancetype)queryDataByReplacingSnapshotVersion: + (firebase::firestore::model::SnapshotVersion)snapshotVersion + resumeToken:(NSData *)resumeToken + sequenceNumber:(FSTListenSequenceNumber)sequenceNumber; + +/** The latest snapshot version seen for this target. */ +- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion; /** The query being listened to. */ @property(nonatomic, strong, readonly) FSTQuery *query; @@ -71,9 +80,6 @@ typedef NS_ENUM(NSInteger, FSTQueryPurpose) { /** The purpose of the query. */ @property(nonatomic, assign, readonly) FSTQueryPurpose purpose; -/** The latest snapshot version seen for this target. */ -@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; - /** * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting * without retransmitting all the data that matches the query. The resume token essentially diff --git a/Firestore/Source/Local/FSTQueryData.mm b/Firestore/Source/Local/FSTQueryData.mm index 6bb716a..16c3b2e 100644 --- a/Firestore/Source/Local/FSTQueryData.mm +++ b/Firestore/Source/Local/FSTQueryData.mm @@ -16,18 +16,25 @@ #import "Firestore/Source/Local/FSTQueryData.h" +#include <utility> + #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" + +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" + +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN -@implementation FSTQueryData +@implementation FSTQueryData { + SnapshotVersion _snapshotVersion; +} - (instancetype)initWithQuery:(FSTQuery *)query targetID:(FSTTargetID)targetID listenSequenceNumber:(FSTListenSequenceNumber)sequenceNumber purpose:(FSTQueryPurpose)purpose - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion:(SnapshotVersion)snapshotVersion resumeToken:(NSData *)resumeToken { self = [super init]; if (self) { @@ -35,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN _targetID = targetID; _sequenceNumber = sequenceNumber; _purpose = purpose; - _snapshotVersion = snapshotVersion; + _snapshotVersion = std::move(snapshotVersion); _resumeToken = [resumeToken copy]; } return self; @@ -49,10 +56,14 @@ NS_ASSUME_NONNULL_BEGIN targetID:targetID listenSequenceNumber:sequenceNumber purpose:purpose - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() resumeToken:[NSData data]]; } +- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion { + return _snapshotVersion; +} + - (BOOL)isEqual:(id)object { if (self == object) { return YES; @@ -63,7 +74,7 @@ NS_ASSUME_NONNULL_BEGIN FSTQueryData *other = (FSTQueryData *)object; return [self.query isEqual:other.query] && self.targetID == other.targetID && - self.purpose == other.purpose && [self.snapshotVersion isEqual:other.snapshotVersion] && + self.purpose == other.purpose && self.snapshotVersion == other.snapshotVersion && [self.resumeToken isEqual:other.resumeToken]; } @@ -71,25 +82,26 @@ NS_ASSUME_NONNULL_BEGIN NSUInteger result = [self.query hash]; result = result * 31 + self.targetID; result = result * 31 + self.purpose; - result = result * 31 + [self.snapshotVersion hash]; + result = result * 31 + self.snapshotVersion.Hash(); result = result * 31 + [self.resumeToken hash]; return result; } - (NSString *)description { return [NSString - stringWithFormat:@"<FSTQueryData: query:%@ target:%d purpose:%lu version:%@ resumeToken:%@)>", - self.query, self.targetID, (unsigned long)self.purpose, self.snapshotVersion, - self.resumeToken]; + stringWithFormat:@"<FSTQueryData: query:%@ target:%d purpose:%lu version:%s resumeToken:%@)>", + self.query, self.targetID, (unsigned long)self.purpose, + self.snapshotVersion.timestamp().ToString().c_str(), self.resumeToken]; } -- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - resumeToken:(NSData *)resumeToken { +- (instancetype)queryDataByReplacingSnapshotVersion:(SnapshotVersion)snapshotVersion + resumeToken:(NSData *)resumeToken + sequenceNumber:(FSTListenSequenceNumber)sequenceNumber { return [[FSTQueryData alloc] initWithQuery:self.query targetID:self.targetID - listenSequenceNumber:self.sequenceNumber + listenSequenceNumber:sequenceNumber purpose:self.purpose - snapshotVersion:snapshotVersion + snapshotVersion:std::move(snapshotVersion) resumeToken:resumeToken]; } diff --git a/Firestore/Source/Local/FSTReferenceSet.h b/Firestore/Source/Local/FSTReferenceSet.h index 9d842cb..9a90a40 100644 --- a/Firestore/Source/Local/FSTReferenceSet.h +++ b/Firestore/Source/Local/FSTReferenceSet.h @@ -18,7 +18,8 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Local/FSTGarbageCollector.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" + +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" NS_ASSUME_NONNULL_BEGIN @@ -47,13 +48,14 @@ NS_ASSUME_NONNULL_BEGIN - (void)addReferenceToKey:(const firebase::firestore::model::DocumentKey &)key forID:(int)ID; /** Add references to the given document keys for the given ID. */ -- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID; +- (void)addReferencesToKeys:(const firebase::firestore::model::DocumentKeySet &)keys forID:(int)ID; /** Removes a reference to the given document key for the given ID. */ - (void)removeReferenceToKey:(const firebase::firestore::model::DocumentKey &)key forID:(int)ID; /** Removes references to the given document keys for the given ID. */ -- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID; +- (void)removeReferencesToKeys:(const firebase::firestore::model::DocumentKeySet &)keys + forID:(int)ID; /** Clears all references with a given ID. Calls -removeReferenceToKey: for each key removed. */ - (void)removeReferencesForID:(int)ID; @@ -62,7 +64,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)removeAllReferences; /** Returns all of the document keys that have had references added for the given ID. */ -- (FSTDocumentKeySet *)referencedKeysForID:(int)ID; +- (firebase::firestore::model::DocumentKeySet)referencedKeysForID:(int)ID; @end diff --git a/Firestore/Source/Local/FSTReferenceSet.mm b/Firestore/Source/Local/FSTReferenceSet.mm index 14f5d47..6b34725 100644 --- a/Firestore/Source/Local/FSTReferenceSet.mm +++ b/Firestore/Source/Local/FSTReferenceSet.mm @@ -17,10 +17,12 @@ #import "Firestore/Source/Local/FSTReferenceSet.h" #import "Firestore/Source/Local/FSTDocumentReference.h" +#import "Firestore/third_party/Immutable/FSTImmutableSortedSet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -68,20 +70,20 @@ NS_ASSUME_NONNULL_BEGIN self.referencesByID = [self.referencesByID setByAddingObject:reference]; } -- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { - [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { +- (void)addReferencesToKeys:(const DocumentKeySet &)keys forID:(int)ID { + for (const DocumentKey &key : keys) { [self addReferenceToKey:key forID:ID]; - }]; + } } - (void)removeReferenceToKey:(const DocumentKey &)key forID:(int)ID { [self removeReference:[[FSTDocumentReference alloc] initWithKey:key ID:ID]]; } -- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { - [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { +- (void)removeReferencesToKeys:(const DocumentKeySet &)keys forID:(int)ID { + for (const DocumentKey &key : keys) { [self removeReferenceToKey:key forID:ID]; - }]; + } } - (void)removeReferencesForID:(int)ID { @@ -109,17 +111,17 @@ NS_ASSUME_NONNULL_BEGIN [self.garbageCollector addPotentialGarbageKey:reference.key]; } -- (FSTDocumentKeySet *)referencedKeysForID:(int)ID { +- (DocumentKeySet)referencedKeysForID:(int)ID { FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:DocumentKey::Empty() ID:ID]; FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:DocumentKey::Empty() ID:(ID + 1)]; - __block FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + __block DocumentKeySet keys; [self.referencesByID enumerateObjectsFrom:start to:end usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { - keys = [keys setByAddingObject:reference.key]; + keys = keys.insert(reference.key); }]; return keys; } diff --git a/Firestore/Source/Model/FSTDocument.h b/Firestore/Source/Model/FSTDocument.h index 47e4d28..0f8d4b3 100644 --- a/Firestore/Source/Model/FSTDocument.h +++ b/Firestore/Source/Model/FSTDocument.h @@ -18,10 +18,10 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTFieldValue; @class FSTObjectValue; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN @@ -32,14 +32,13 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTMaybeDocument : NSObject <NSCopying> - (id)init __attribute__((unavailable("Abstract base class"))); - (const firebase::firestore::model::DocumentKey &)key; - -@property(nonatomic, readonly) FSTSnapshotVersion *version; +- (const firebase::firestore::model::SnapshotVersion &)version; @end @interface FSTDocument : FSTMaybeDocument + (instancetype)documentWithData:(FSTObjectValue *)data key:(firebase::firestore::model::DocumentKey)key - version:(FSTSnapshotVersion *)version + version:(firebase::firestore::model::SnapshotVersion)version hasLocalMutations:(BOOL)mutations; - (nullable FSTFieldValue *)fieldForPath:(const firebase::firestore::model::FieldPath &)path; @@ -51,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTDeletedDocument : FSTMaybeDocument + (instancetype)documentWithKey:(firebase::firestore::model::DocumentKey)key - version:(FSTSnapshotVersion *)version; + version:(firebase::firestore::model::SnapshotVersion)version; @end /** An NSComparator suitable for comparing docs using only their keys. */ diff --git a/Firestore/Source/Model/FSTDocument.mm b/Firestore/Source/Model/FSTDocument.mm index 9898c2a..8c4c801 100644 --- a/Firestore/Source/Model/FSTDocument.mm +++ b/Firestore/Source/Model/FSTDocument.mm @@ -18,37 +18,38 @@ #include <utility> -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Util/FSTAssert.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldPath; +using firebase::firestore::model::SnapshotVersion; NS_ASSUME_NONNULL_BEGIN @interface FSTMaybeDocument () - (instancetype)initWithKey:(DocumentKey)key - version:(FSTSnapshotVersion *)version NS_DESIGNATED_INITIALIZER; + version:(SnapshotVersion)version NS_DESIGNATED_INITIALIZER; @end @implementation FSTMaybeDocument { DocumentKey _key; + SnapshotVersion _version; } -- (instancetype)initWithKey:(DocumentKey)key version:(FSTSnapshotVersion *)version { - FSTAssert(!!version, @"Version must not be nil."); +- (instancetype)initWithKey:(DocumentKey)key version:(SnapshotVersion)version { self = [super init]; if (self) { _key = std::move(key); - _version = version; + _version = std::move(version); } return self; } @@ -62,25 +63,29 @@ NS_ASSUME_NONNULL_BEGIN return _key; } +- (const SnapshotVersion &)version { + return _version; +} + @end @implementation FSTDocument + (instancetype)documentWithData:(FSTObjectValue *)data key:(DocumentKey)key - version:(FSTSnapshotVersion *)version + version:(SnapshotVersion)version hasLocalMutations:(BOOL)mutations { return [[FSTDocument alloc] initWithData:data key:std::move(key) - version:version + version:std::move(version) hasLocalMutations:mutations]; } - (instancetype)initWithData:(FSTObjectValue *)data key:(DocumentKey)key - version:(FSTSnapshotVersion *)version + version:(SnapshotVersion)version hasLocalMutations:(BOOL)mutations { - self = [super initWithKey:std::move(key) version:version]; + self = [super initWithKey:std::move(key) version:std::move(version)]; if (self) { _data = data; _localMutations = mutations; @@ -97,21 +102,22 @@ NS_ASSUME_NONNULL_BEGIN } FSTDocument *otherDoc = other; - return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version] && + return [self.key isEqual:otherDoc.key] && self.version == otherDoc.version && [self.data isEqual:otherDoc.data] && self.hasLocalMutations == otherDoc.hasLocalMutations; } - (NSUInteger)hash { NSUInteger result = [self.key hash]; - result = result * 31 + [self.version hash]; + result = result * 31 + self.version.Hash(); result = result * 31 + [self.data hash]; result = result * 31 + (self.hasLocalMutations ? 1 : 0); return result; } - (NSString *)description { - return [NSString stringWithFormat:@"<FSTDocument: key:%s version:%@ localMutations:%@ data:%@>", - self.key.ToString().c_str(), self.version, + return [NSString stringWithFormat:@"<FSTDocument: key:%s version:%s localMutations:%@ data:%@>", + self.key.ToString().c_str(), + self.version.timestamp().ToString().c_str(), self.localMutations ? @"YES" : @"NO", self.data]; } @@ -123,8 +129,8 @@ NS_ASSUME_NONNULL_BEGIN @implementation FSTDeletedDocument -+ (instancetype)documentWithKey:(DocumentKey)key version:(FSTSnapshotVersion *)version { - return [[FSTDeletedDocument alloc] initWithKey:std::move(key) version:version]; ++ (instancetype)documentWithKey:(DocumentKey)key version:(SnapshotVersion)version { + return [[FSTDeletedDocument alloc] initWithKey:std::move(key) version:std::move(version)]; } - (BOOL)isEqual:(id)other { @@ -136,12 +142,12 @@ NS_ASSUME_NONNULL_BEGIN } FSTDocument *otherDoc = other; - return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version]; + return [self.key isEqual:otherDoc.key] && self.version == otherDoc.version; } - (NSUInteger)hash { NSUInteger result = [self.key hash]; - result = result * 31 + [self.version hash]; + result = result * 31 + self.version.Hash(); return result; } diff --git a/Firestore/Source/Model/FSTDocumentKey.mm b/Firestore/Source/Model/FSTDocumentKey.mm index 679d7a6..d29df86 100644 --- a/Firestore/Source/Model/FSTDocumentKey.mm +++ b/Firestore/Source/Model/FSTDocumentKey.mm @@ -23,6 +23,7 @@ #import "Firestore/Source/Util/FSTAssert.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; @@ -72,7 +73,7 @@ NS_ASSUME_NONNULL_BEGIN } - (NSUInteger)hash { - return _path.Hash(); + return util::Hash(_path); } - (NSString *)description { diff --git a/Firestore/Source/Model/FSTDocumentKeySet.mm b/Firestore/Source/Model/FSTDocumentKeySet.mm deleted file mode 100644 index f07b785..0000000 --- a/Firestore/Source/Model/FSTDocumentKeySet.mm +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/Model/FSTDocumentKeySet.h" - -#import "Firestore/Source/Model/FSTDocumentKey.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTImmutableSortedSet (FSTDocumentKey) - -+ (instancetype)keySet { - return [FSTDocumentKeySet setWithComparator:FSTDocumentKeyComparator]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.h b/Firestore/Source/Model/FSTDocumentVersionDictionary.h deleted file mode 100644 index 674614e..0000000 --- a/Firestore/Source/Model/FSTDocumentVersionDictionary.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import <Foundation/Foundation.h> - -#import "Firestore/third_party/Immutable/FSTImmutableSortedDictionary.h" - -@class FSTDocumentKey; -@class FSTSnapshotVersion; - -NS_ASSUME_NONNULL_BEGIN - -/** A map of key to version number. */ -typedef FSTImmutableSortedDictionary<FSTDocumentKey *, FSTSnapshotVersion *> - FSTDocumentVersionDictionary; - -/** - * Extension to FSTImmutableSortedDictionary that allows natural construction of - * FSTDocumentVersionDictionary. - */ -@interface FSTImmutableSortedDictionary (FSTDocumentVersionDictionary) - -+ (instancetype)documentVersionDictionary; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.mm b/Firestore/Source/Model/FSTDocumentVersionDictionary.mm deleted file mode 100644 index 870e082..0000000 --- a/Firestore/Source/Model/FSTDocumentVersionDictionary.mm +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright 2017 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#import "Firestore/Source/Model/FSTDocumentVersionDictionary.h" - -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#import "Firestore/Source/Model/FSTDocumentKey.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTImmutableSortedDictionary (FSTDocumentVersionDictionary) - -+ (instancetype)documentVersionDictionary { - static FSTDocumentVersionDictionary *singleton; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - singleton = [FSTDocumentVersionDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; - }); - return singleton; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutation.h b/Firestore/Source/Model/FSTMutation.h index 7261f30..0acec15 100644 --- a/Firestore/Source/Model/FSTMutation.h +++ b/Firestore/Source/Model/FSTMutation.h @@ -24,13 +24,15 @@ #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/field_transform.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" +#include "absl/types/optional.h" + @class FSTDocument; @class FSTFieldValue; @class FSTMaybeDocument; @class FSTObjectValue; -@class FSTSnapshotVersion; @class FIRTimestamp; NS_ASSUME_NONNULL_BEGIN @@ -40,12 +42,12 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTMutationResult : NSObject - (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version +- (instancetype)initWithVersion:(absl::optional<firebase::firestore::model::SnapshotVersion>)version transformResults:(NSArray<FSTFieldValue *> *_Nullable)transformResults NS_DESIGNATED_INITIALIZER; /** The version at which the mutation was committed or null for a delete. */ -@property(nonatomic, strong, readonly, nullable) FSTSnapshotVersion *version; +- (const absl::optional<firebase::firestore::model::SnapshotVersion> &)version; /** * The resulting fields returned from the backend after a FSTTransformMutation has been committed. diff --git a/Firestore/Source/Model/FSTMutation.mm b/Firestore/Source/Model/FSTMutation.mm index 3432a7c..82a535e 100644 --- a/Firestore/Source/Model/FSTMutation.mm +++ b/Firestore/Source/Model/FSTMutation.mm @@ -23,7 +23,6 @@ #import "FIRTimestamp.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Util/FSTAssert.h" @@ -36,6 +35,8 @@ #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" +#include "absl/types/optional.h" + using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldMask; @@ -43,23 +44,30 @@ using firebase::firestore::model::FieldPath; using firebase::firestore::model::FieldTransform; using firebase::firestore::model::Precondition; using firebase::firestore::model::ServerTimestampTransform; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TransformOperation; NS_ASSUME_NONNULL_BEGIN #pragma mark - FSTMutationResult -@implementation FSTMutationResult +@implementation FSTMutationResult { + absl::optional<SnapshotVersion> _version; +} -- (instancetype)initWithVersion:(nullable FSTSnapshotVersion *)version +- (instancetype)initWithVersion:(absl::optional<SnapshotVersion>)version transformResults:(nullable NSArray<FSTFieldValue *> *)transformResults { if (self = [super init]) { - _version = version; + _version = std::move(version); _transformResults = transformResults; } return self; } +- (const absl::optional<SnapshotVersion> &)version { + return _version; +} + @end #pragma mark - FSTMutation @@ -157,7 +165,7 @@ NS_ASSUME_NONNULL_BEGIN // If the document didn't exist before, create it. return [FSTDocument documentWithData:self.value key:self.key - version:[FSTSnapshotVersion noVersion] + version:SnapshotVersion::None() hasLocalMutations:hasLocalMutations]; } @@ -239,10 +247,10 @@ NS_ASSUME_NONNULL_BEGIN if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { // Precondition applied, so create the document if necessary const DocumentKey &key = maybeDoc ? maybeDoc.key : self.key; - FSTSnapshotVersion *version = maybeDoc ? maybeDoc.version : [FSTSnapshotVersion noVersion]; + SnapshotVersion version = maybeDoc ? maybeDoc.version : SnapshotVersion::None(); maybeDoc = [FSTDocument documentWithData:[FSTObjectValue objectValue] key:key - version:version + version:std::move(version) hasLocalMutations:hasLocalMutations]; } @@ -384,30 +392,15 @@ serverTransformResultsWithBaseDocument:(nullable FSTMaybeDocument *)baseDocument for (NSUInteger i = 0; i < serverTransformResults.count; i++) { const FieldTransform &fieldTransform = self.fieldTransforms[i]; + const TransformOperation &transform = fieldTransform.transformation(); + FSTFieldValue *previousValue = nil; if ([baseDocument isMemberOfClass:[FSTDocument class]]) { previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path()]; } - FSTFieldValue *transformResult; - // The server just sends null as the transform result for array union / remove operations, so - // we have to calculate a result the same as we do for local applications. - if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayUnion) { - transformResult = [self - arrayUnionResultWithElements:ArrayTransform::Elements(fieldTransform.transformation()) - previousValue:previousValue]; - - } else if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayRemove) { - transformResult = [self - arrayRemoveResultWithElements:ArrayTransform::Elements(fieldTransform.transformation()) - previousValue:previousValue]; - - } else { - // Just use the server-supplied result. - transformResult = serverTransformResults[i]; - } - - [transformResults addObject:transformResult]; + [transformResults + addObject:transform.ApplyToRemoteDocument(previousValue, serverTransformResults[i])]; } return transformResults; } @@ -426,77 +419,18 @@ serverTransformResultsWithBaseDocument:(nullable FSTMaybeDocument *)baseDocument writeTime:(FIRTimestamp *)localWriteTime { NSMutableArray<FSTFieldValue *> *transformResults = [NSMutableArray array]; for (const FieldTransform &fieldTransform : self.fieldTransforms) { + const TransformOperation &transform = fieldTransform.transformation(); + FSTFieldValue *previousValue = nil; if ([baseDocument isMemberOfClass:[FSTDocument class]]) { previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path()]; } - FSTFieldValue *transformResult; - if (fieldTransform.transformation().type() == TransformOperation::Type::ServerTimestamp) { - transformResult = - [FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:localWriteTime - previousValue:previousValue]; - - } else if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayUnion) { - transformResult = [self - arrayUnionResultWithElements:ArrayTransform::Elements(fieldTransform.transformation()) - previousValue:previousValue]; - - } else if (fieldTransform.transformation().type() == TransformOperation::Type::ArrayRemove) { - transformResult = [self - arrayRemoveResultWithElements:ArrayTransform::Elements(fieldTransform.transformation()) - previousValue:previousValue]; - - } else { - FSTFail(@"Encountered unknown transform: %d type", fieldTransform.transformation().type()); - } - - [transformResults addObject:transformResult]; + [transformResults addObject:transform.ApplyToLocalView(previousValue, localWriteTime)]; } return transformResults; } -/** - * Transforms the provided `previousValue` via the provided `elements`. Used both for local - * application and after server acknowledgement. - */ -- (FSTFieldValue *)arrayUnionResultWithElements:(const std::vector<FSTFieldValue *> &)elements - previousValue:(FSTFieldValue *)previousValue { - NSMutableArray<FSTFieldValue *> *result = [self coercedFieldValuesArray:previousValue]; - for (FSTFieldValue *element : elements) { - if (![result containsObject:element]) { - [result addObject:element]; - } - } - return [[FSTArrayValue alloc] initWithValueNoCopy:result]; -} - -/** - * Transforms the provided `previousValue` via the provided `elements`. Used both for local - * application and after server acknowledgement. - */ -- (FSTFieldValue *)arrayRemoveResultWithElements:(const std::vector<FSTFieldValue *> &)elements - previousValue:(FSTFieldValue *)previousValue { - NSMutableArray<FSTFieldValue *> *result = [self coercedFieldValuesArray:previousValue]; - for (FSTFieldValue *element : elements) { - [result removeObject:element]; - } - return [[FSTArrayValue alloc] initWithValueNoCopy:result]; -} - -/** - * Inspects the provided value, returning a mutable copy of the internal array if it's an - * FSTArrayValue and an empty mutable array if it's nil or any other type of FSTFieldValue. - */ -- (NSMutableArray<FSTFieldValue *> *)coercedFieldValuesArray:(nullable FSTFieldValue *)value { - if ([value isMemberOfClass:[FSTArrayValue class]]) { - return [NSMutableArray arrayWithArray:((FSTArrayValue *)value).internalValue]; - } else { - // coerce to empty array. - return [NSMutableArray array]; - } -} - - (FSTObjectValue *)transformObject:(FSTObjectValue *)objectValue transformResults:(NSArray<FSTFieldValue *> *)transformResults { FSTAssert(transformResults.count == self.fieldTransforms.size(), @@ -556,7 +490,7 @@ serverTransformResultsWithBaseDocument:(nullable FSTMaybeDocument *)baseDocument FSTAssert([maybeDoc.key isEqual:self.key], @"Can only delete a document with the same key"); } - return [FSTDeletedDocument documentWithKey:self.key version:[FSTSnapshotVersion noVersion]]; + return [FSTDeletedDocument documentWithKey:self.key version:SnapshotVersion::None()]; } @end diff --git a/Firestore/Source/Model/FSTMutationBatch.h b/Firestore/Source/Model/FSTMutationBatch.h index 3c82338..761a885 100644 --- a/Firestore/Source/Model/FSTMutationBatch.h +++ b/Firestore/Source/Model/FSTMutationBatch.h @@ -16,17 +16,29 @@ #import <Foundation/Foundation.h> +#include <unordered_map> + #import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" -#import "Firestore/Source/Model/FSTDocumentVersionDictionary.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTMutation; @class FIRTimestamp; @class FSTMutationResult; @class FSTMutationBatchResult; -@class FSTSnapshotVersion; + +namespace firebase { +namespace firestore { +namespace model { + +// TODO(wilhuff): make this type a member of MutationBatchResult once that's a C++ class. +using DocumentVersionMap = std::unordered_map<DocumentKey, SnapshotVersion, DocumentKeyHash>; + +} // namespace model +} // namespace firestore +} // namespace firebase NS_ASSUME_NONNULL_BEGIN @@ -85,7 +97,7 @@ extern const FSTBatchID kFSTBatchIDUnknown; - (FSTMutationBatch *)toTombstone; /** Returns the set of unique keys referenced by all mutations in the batch. */ -- (FSTDocumentKeySet *)keys; +- (firebase::firestore::model::DocumentKeySet)keys; @property(nonatomic, assign, readonly) FSTBatchID batchID; @property(nonatomic, strong, readonly) FIRTimestamp *localWriteTime; @@ -106,15 +118,17 @@ extern const FSTBatchID kFSTBatchIDUnknown; * (as docVersions). */ + (instancetype)resultWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion + commitVersion:(firebase::firestore::model::SnapshotVersion)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)mutationResults streamToken:(nullable NSData *)streamToken; +- (const firebase::firestore::model::SnapshotVersion &)commitVersion; + @property(nonatomic, strong, readonly) FSTMutationBatch *batch; -@property(nonatomic, strong, readonly) FSTSnapshotVersion *commitVersion; @property(nonatomic, strong, readonly) NSArray<FSTMutationResult *> *mutationResults; @property(nonatomic, strong, readonly, nullable) NSData *streamToken; -@property(nonatomic, strong, readonly) FSTDocumentVersionDictionary *docVersions; + +- (const firebase::firestore::model::DocumentVersionMap &)docVersions; @end diff --git a/Firestore/Source/Model/FSTMutationBatch.mm b/Firestore/Source/Model/FSTMutationBatch.mm index e62a72c..1e9189c 100644 --- a/Firestore/Source/Model/FSTMutationBatch.mm +++ b/Firestore/Source/Model/FSTMutationBatch.mm @@ -16,16 +16,19 @@ #import "Firestore/Source/Model/FSTMutationBatch.h" +#include <utility> + #import "FIRTimestamp.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Util/FSTAssert.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" - using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::DocumentKeyHash; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; +using firebase::firestore::model::DocumentVersionMap; NS_ASSUME_NONNULL_BEGIN @@ -113,10 +116,10 @@ const FSTBatchID kFSTBatchIDUnknown = -1; } // TODO(klimt): This could use NSMutableDictionary instead. -- (FSTDocumentKeySet *)keys { - FSTDocumentKeySet *set = [FSTDocumentKeySet keySet]; +- (DocumentKeySet)keys { + DocumentKeySet set; for (FSTMutation *mutation in self.mutations) { - set = [set setByAddingObject:mutation.key]; + set = set.insert(mutation.key); } return set; } @@ -127,56 +130,66 @@ const FSTBatchID kFSTBatchIDUnknown = -1; @interface FSTMutationBatchResult () - (instancetype)initWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion + commitVersion:(SnapshotVersion)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)mutationResults streamToken:(nullable NSData *)streamToken - docVersions:(FSTDocumentVersionDictionary *)docVersions NS_DESIGNATED_INITIALIZER; + docVersions:(DocumentVersionMap)docVersions NS_DESIGNATED_INITIALIZER; @end -@implementation FSTMutationBatchResult +@implementation FSTMutationBatchResult { + SnapshotVersion _commitVersion; + DocumentVersionMap _docVersions; +} - (instancetype)initWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion + commitVersion:(SnapshotVersion)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)mutationResults streamToken:(nullable NSData *)streamToken - docVersions:(FSTDocumentVersionDictionary *)docVersions { + docVersions:(DocumentVersionMap)docVersions { if (self = [super init]) { _batch = batch; - _commitVersion = commitVersion; + _commitVersion = std::move(commitVersion); _mutationResults = mutationResults; _streamToken = streamToken; - _docVersions = docVersions; + _docVersions = std::move(docVersions); } return self; } +- (const SnapshotVersion &)commitVersion { + return _commitVersion; +} + +- (const DocumentVersionMap &)docVersions { + return _docVersions; +} + + (instancetype)resultWithBatch:(FSTMutationBatch *)batch - commitVersion:(FSTSnapshotVersion *)commitVersion + commitVersion:(SnapshotVersion)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)mutationResults streamToken:(nullable NSData *)streamToken { FSTAssert(batch.mutations.count == mutationResults.count, @"Mutations sent %lu must equal results received %lu", (unsigned long)batch.mutations.count, (unsigned long)mutationResults.count); - FSTDocumentVersionDictionary *docVersions = - [FSTDocumentVersionDictionary documentVersionDictionary]; + DocumentVersionMap docVersions; NSArray<FSTMutation *> *mutations = batch.mutations; for (NSUInteger i = 0; i < mutations.count; i++) { - FSTSnapshotVersion *_Nullable version = mutationResults[i].version; + absl::optional<SnapshotVersion> version = mutationResults[i].version; if (!version) { // deletes don't have a version, so we substitute the commitVersion // of the entire batch. version = commitVersion; } - docVersions = [docVersions dictionaryBySettingObject:version forKey:mutations[i].key]; + docVersions[mutations[i].key] = version.value(); } return [[FSTMutationBatchResult alloc] initWithBatch:batch - commitVersion:commitVersion + commitVersion:std::move(commitVersion) mutationResults:mutationResults streamToken:streamToken - docVersions:docVersions]; + docVersions:std::move(docVersions)]; } @end diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h index 4aa8c45..7baa30a 100644 --- a/Firestore/Source/Public/FIRDocumentReference.h +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -92,6 +92,23 @@ NS_SWIFT_NAME(DocumentReference) - (void)setData:(NSDictionary<NSString *, id> *)documentData merge:(BOOL)merge; /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData mergeFields:(NSArray<id> *)mergeFields; + +/** * Overwrites the document referred to by this `FIRDocumentReference`. If no document exists, it * is created. If a document already exists, it is overwritten. * @@ -121,6 +138,28 @@ NS_SWIFT_NAME(DocumentReference) completion:(nullable void (^)(NSError *_Nullable error))completion; /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + * @param completion A block to execute once the document has been successfully written to the + * server. This block will not be called while the client is offline, though local + * changes will be visible immediately. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData + mergeFields:(NSArray<id> *)mergeFields + completion:(nullable void (^)(NSError *_Nullable error))completion; + +/** * Updates fields in the document referred to by this `FIRDocumentReference`. * If the document does not exist, the update fails (specify a completion block to be notified). * diff --git a/Firestore/Source/Public/FIRTransaction.h b/Firestore/Source/Public/FIRTransaction.h index 2fa4430..e53414d 100644 --- a/Firestore/Source/Public/FIRTransaction.h +++ b/Firestore/Source/Public/FIRTransaction.h @@ -65,6 +65,30 @@ NS_SWIFT_NAME(Transaction) // clang-format on /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param data An `NSDictionary` containing the fields that make up the document + * to be written. + * @param document A reference to the document whose data should be overwritten. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields + NS_SWIFT_NAME(setData(_:forDocument:mergeFields:)); +// clang-format on + +/** * Updates fields in the document referred to by `document`. * If the document does not exist, the transaction will fail. * diff --git a/Firestore/Source/Public/FIRWriteBatch.h b/Firestore/Source/Public/FIRWriteBatch.h index 1568723..22d1b16 100644 --- a/Firestore/Source/Public/FIRWriteBatch.h +++ b/Firestore/Source/Public/FIRWriteBatch.h @@ -68,6 +68,29 @@ NS_SWIFT_NAME(WriteBatch) // clang-format on /** + * Writes to the document referred to by `document` and only replace the fields + * specified under `mergeFields`. Any field that is not specified in `mergeFields` + * is ignored and remains untouched. If the document doesn't yet exist, + * this method creates it and then sets the data. + * + * It is an error to include a field in `mergeFields` that does not have a corresponding + * value in the `data` dictionary. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @param mergeFields An `NSArray` that contains a list of `NSString` or `FIRFieldPath` elements + * specifying which fields to merge. Fields can contain dots to reference nested fields within + * the document. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + mergeFields:(NSArray<id> *)mergeFields + NS_SWIFT_NAME(setData(_:forDocument:mergeFields:)); +// clang-format on + +/** * Updates fields in the document referred to by `document`. * If document does not exist, the write batch will fail. * diff --git a/Firestore/Source/Remote/FSTDatastore.h b/Firestore/Source/Remote/FSTDatastore.h index b3ba46c..da14b6e 100644 --- a/Firestore/Source/Remote/FSTDatastore.h +++ b/Firestore/Source/Remote/FSTDatastore.h @@ -31,7 +31,6 @@ @class FSTMutationResult; @class FSTQueryData; @class FSTSerializerBeta; -@class FSTSnapshotVersion; @class FSTWatchChange; @class FSTWatchStream; @class FSTWriteStream; diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h index 0f6b6c7..c84e34d 100644 --- a/Firestore/Source/Remote/FSTRemoteEvent.h +++ b/Firestore/Source/Remote/FSTRemoteEvent.h @@ -20,14 +20,14 @@ #import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" -#import "Firestore/Source/Model/FSTDocumentKeySet.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTDocument; @class FSTExistenceFilter; @class FSTMaybeDocument; -@class FSTSnapshotVersion; @class FSTWatchChange; @class FSTQueryData; @@ -43,6 +43,15 @@ NS_ASSUME_NONNULL_BEGIN * base class. */ @interface FSTTargetMapping : NSObject + +/** + * Strips out mapping changes that aren't actually changes. That is, if the document already + * existed in the target, and is being added in the target, and this is not a reset, we can + * skip doing the work to associate the document with the target because it has already been done. + */ +- (void)filterUpdatesUsingExistingKeys: + (const firebase::firestore::model::DocumentKeySet &)existingKeys; + @end #pragma mark - FSTResetMapping @@ -57,7 +66,7 @@ NS_ASSUME_NONNULL_BEGIN + (FSTResetMapping *)mappingWithDocuments:(NSArray<FSTDocument *> *)documents; /** The new set of documents for the target. */ -@property(nonatomic, strong, readonly) FSTDocumentKeySet *documents; +- (const firebase::firestore::model::DocumentKeySet &)documents; @end #pragma mark - FSTUpdateMapping @@ -74,12 +83,13 @@ NS_ASSUME_NONNULL_BEGIN + (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added removedDocuments:(NSArray<FSTDocument *> *)removed; -- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys; +- (firebase::firestore::model::DocumentKeySet)applyTo: + (const firebase::firestore::model::DocumentKeySet &)keys; /** The documents added to the target. */ -@property(nonatomic, strong, readonly) FSTDocumentKeySet *addedDocuments; +- (const firebase::firestore::model::DocumentKeySet &)addedDocuments; /** The documents removed from the target. */ -@property(nonatomic, strong, readonly) FSTDocumentKeySet *removedDocuments; +- (const firebase::firestore::model::DocumentKeySet &)removedDocuments; @end #pragma mark - FSTTargetChange @@ -107,6 +117,12 @@ typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { @interface FSTTargetChange : NSObject /** + * Creates a new target change with the given SnapshotVersion. + */ +- (instancetype)initWithSnapshotVersion: + (firebase::firestore::model::SnapshotVersion)snapshotVersion; + +/** * Creates a new target change with the given documents. Instances of FSTDocument are considered * added. Instance of FSTDeletedDocument are considered removed. This is intended primarily for * testing. @@ -115,6 +131,12 @@ typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate; /** + * The snapshot version representing the last state at which this target received a consistent + * snapshot from the backend. + */ +- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion; + +/** * The new "current" (synced) status of this target. Set to CurrentStatusUpdateNone if the status * should not be updated. Note "current" has special meaning for in the RPC protocol that implies * that a target is both up-to-date and consistent with the rest of the watch stream. @@ -125,12 +147,6 @@ typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { @property(nonatomic, strong, readonly) FSTTargetMapping *mapping; /** - * The snapshot version representing the last state at which this target received a consistent - * snapshot from the backend. - */ -@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; - -/** * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting * without retransmitting all the data that matches the query. The resume token essentially * identifies a point in time from which the server should resume sending results. @@ -147,14 +163,15 @@ typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { */ @interface FSTRemoteEvent : NSObject -+ (instancetype) -eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges - documentUpdates: - (std::map<firebase::firestore::model::DocumentKey, FSTMaybeDocument *>)documentUpdates; +- (instancetype) +initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion + targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges + documentUpdates: + (std::map<firebase::firestore::model::DocumentKey, FSTMaybeDocument *>)documentUpdates + limboDocuments:(firebase::firestore::model::DocumentKeySet)limboDocuments; /** The snapshot version this event brings us up to. */ -@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; +- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion; /** A map from target to changes to the target. See TargetChange. */ @property(nonatomic, strong, readonly) @@ -166,6 +183,8 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion */ - (const std::map<firebase::firestore::model::DocumentKey, FSTMaybeDocument *> &)documentUpdates; +- (const firebase::firestore::model::DocumentKeySet &)limboDocumentChanges; + /** Adds a document update to this remote event */ - (void)addDocumentUpdate:(FSTMaybeDocument *)document; @@ -175,14 +194,6 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - (void)synthesizeDeleteForLimboTargetChange:(FSTTargetChange *)targetChange key:(const firebase::firestore::model::DocumentKey &)key; -/** - * Strips out mapping changes that aren't actually changes. That is, if the document already - * existed in the target, and is being added in the target, and this is not a reset, we can - * skip doing the work to associate the document with the target because it has already been done. - */ -- (void)filterUpdatesFromTargetChange:(FSTTargetChange *)targetChange - existingDocuments:(FSTDocumentKeySet *)existingDocuments; - @end #pragma mark - FSTWatchChangeAggregator @@ -194,7 +205,7 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion @interface FSTWatchChangeAggregator : NSObject - (instancetype) -initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion +initWithSnapshotVersion:(firebase::firestore::model::SnapshotVersion)snapshotVersion listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses NS_DESIGNATED_INITIALIZER; diff --git a/Firestore/Source/Remote/FSTRemoteEvent.mm b/Firestore/Source/Remote/FSTRemoteEvent.mm index 30aa0d3..438072e 100644 --- a/Firestore/Source/Remote/FSTRemoteEvent.mm +++ b/Firestore/Source/Remote/FSTRemoteEvent.mm @@ -19,16 +19,19 @@ #include <map> #include <utility> -#import "Firestore/Source/Core/FSTSnapshotVersion.h" +#import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Remote/FSTWatchChange.h" #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTClasses.h" #import "Firestore/Source/Util/FSTLogger.h" - #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::util::Hash; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -54,32 +57,38 @@ NS_ASSUME_NONNULL_BEGIN @throw FSTAbstractMethodException(); // NOLINT } +- (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys { + @throw FSTAbstractMethodException(); // NOLINT +} + @end #pragma mark - FSTResetMapping -@interface FSTResetMapping () -@property(nonatomic, strong) FSTDocumentKeySet *documents; -@end - -@implementation FSTResetMapping +@implementation FSTResetMapping { + DocumentKeySet _documents; +} + (instancetype)mappingWithDocuments:(NSArray<FSTDocument *> *)documents { - FSTResetMapping *mapping = [[FSTResetMapping alloc] init]; + DocumentKeySet keys; for (FSTDocument *doc in documents) { - mapping.documents = [mapping.documents setByAddingObject:doc.key]; + keys = keys.insert(doc.key); } - return mapping; + return [[FSTResetMapping alloc] initWithDocuments:std::move(keys)]; } -- (instancetype)init { +- (instancetype)initWithDocuments:(DocumentKeySet)documents { self = [super init]; if (self) { - _documents = [FSTDocumentKeySet keySet]; + _documents = std::move(documents); } return self; } +- (const DocumentKeySet &)documents { + return _documents; +} + - (BOOL)isEqual:(id)other { if (other == self) { return YES; @@ -89,53 +98,66 @@ NS_ASSUME_NONNULL_BEGIN } FSTResetMapping *otherMapping = (FSTResetMapping *)other; - return [self.documents isEqual:otherMapping.documents]; + return _documents == otherMapping.documents; } - (NSUInteger)hash { - return self.documents.hash; + return Hash(_documents); } - (void)addDocumentKey:(const DocumentKey &)documentKey { - self.documents = [self.documents setByAddingObject:documentKey]; + _documents = _documents.insert(documentKey); } - (void)removeDocumentKey:(const DocumentKey &)documentKey { - self.documents = [self.documents setByRemovingObject:documentKey]; + _documents = _documents.erase(documentKey); +} + +- (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys { + // No-op. Resets are not filtered. } @end #pragma mark - FSTUpdateMapping -@interface FSTUpdateMapping () -@property(nonatomic, strong) FSTDocumentKeySet *addedDocuments; -@property(nonatomic, strong) FSTDocumentKeySet *removedDocuments; -@end - -@implementation FSTUpdateMapping +@implementation FSTUpdateMapping { + DocumentKeySet _addedDocuments; + DocumentKeySet _removedDocuments; +} + (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added removedDocuments:(NSArray<FSTDocument *> *)removed { - FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + DocumentKeySet addedDocuments; + DocumentKeySet removedDocuments; for (FSTDocument *doc in added) { - mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + addedDocuments = addedDocuments.insert(doc.key); } for (FSTDocument *doc in removed) { - mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + removedDocuments = removedDocuments.insert(doc.key); } - return mapping; + return [[FSTUpdateMapping alloc] initWithAddedDocuments:std::move(addedDocuments) + removedDocuments:std::move(removedDocuments)]; } -- (instancetype)init { +- (instancetype)initWithAddedDocuments:(DocumentKeySet)addedDocuments + removedDocuments:(DocumentKeySet)removedDocuments { self = [super init]; if (self) { - _addedDocuments = [FSTDocumentKeySet keySet]; - _removedDocuments = [FSTDocumentKeySet keySet]; + _addedDocuments = std::move(addedDocuments); + _removedDocuments = std::move(removedDocuments); } return self; } +- (const DocumentKeySet &)addedDocuments { + return _addedDocuments; +} + +- (const DocumentKeySet &)removedDocuments { + return _removedDocuments; +} + - (BOOL)isEqual:(id)other { if (other == self) { return YES; @@ -145,33 +167,43 @@ NS_ASSUME_NONNULL_BEGIN } FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other; - return [self.addedDocuments isEqual:otherMapping.addedDocuments] && - [self.removedDocuments isEqual:otherMapping.removedDocuments]; + return _addedDocuments == otherMapping.addedDocuments && + _removedDocuments == otherMapping.removedDocuments; } - (NSUInteger)hash { - return self.addedDocuments.hash * 31 + self.removedDocuments.hash; + return Hash(_addedDocuments, _removedDocuments); } -- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys { - __block FSTDocumentKeySet *result = keys; - [self.addedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - result = [result setByAddingObject:key]; - }]; - [self.removedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { - result = [result setByRemovingObject:key]; - }]; +- (DocumentKeySet)applyTo:(const DocumentKeySet &)keys { + DocumentKeySet result = keys; + for (const DocumentKey &key : _addedDocuments) { + result = result.insert(key); + } + for (const DocumentKey &key : _removedDocuments) { + result = result.erase(key); + } return result; } - (void)addDocumentKey:(const DocumentKey &)documentKey { - self.addedDocuments = [self.addedDocuments setByAddingObject:documentKey]; - self.removedDocuments = [self.removedDocuments setByRemovingObject:documentKey]; + _addedDocuments = _addedDocuments.insert(documentKey); + _removedDocuments = _removedDocuments.erase(documentKey); } - (void)removeDocumentKey:(const DocumentKey &)documentKey { - self.addedDocuments = [self.addedDocuments setByRemovingObject:documentKey]; - self.removedDocuments = [self.removedDocuments setByAddingObject:documentKey]; + _addedDocuments = _addedDocuments.erase(documentKey); + _removedDocuments = _removedDocuments.insert(documentKey); +} + +- (void)filterUpdatesUsingExistingKeys:(const DocumentKeySet &)existingKeys { + DocumentKeySet result = _addedDocuments; + for (const DocumentKey &key : _addedDocuments) { + if (existingKeys.contains(key)) { + result = result.erase(key); + } + } + _addedDocuments = result; } @end @@ -181,11 +213,12 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTTargetChange () @property(nonatomic, assign) FSTCurrentStatusUpdate currentStatusUpdate; @property(nonatomic, strong, nullable) FSTTargetMapping *mapping; -@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; @property(nonatomic, strong) NSData *resumeToken; @end -@implementation FSTTargetChange +@implementation FSTTargetChange { + SnapshotVersion _snapshotVersion; +} - (instancetype)init { if (self = [super init]) { @@ -195,16 +228,32 @@ NS_ASSUME_NONNULL_BEGIN return self; } +- (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion { + if (self = [self init]) { + _snapshotVersion = std::move(snapshotVersion); + } + return self; +} + +- (const SnapshotVersion &)snapshotVersion { + return _snapshotVersion; +} + + (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { - FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + DocumentKeySet addedDocuments; + DocumentKeySet removedDocuments; for (FSTMaybeDocument *doc in docs) { if ([doc isKindOfClass:[FSTDeletedDocument class]]) { - mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + removedDocuments = removedDocuments.insert(doc.key); } else { - mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + addedDocuments = addedDocuments.insert(doc.key); } } + FSTUpdateMapping *mapping = + [[FSTUpdateMapping alloc] initWithAddedDocuments:std::move(addedDocuments) + removedDocuments:std::move(removedDocuments)]; + FSTTargetChange *change = [[FSTTargetChange alloc] init]; change.mapping = mapping; change.currentStatusUpdate = currentStatusUpdate; @@ -212,11 +261,11 @@ NS_ASSUME_NONNULL_BEGIN } + (instancetype)changeWithMapping:(FSTTargetMapping *)mapping - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + snapshotVersion:(SnapshotVersion)snapshotVersion currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { FSTTargetChange *change = [[FSTTargetChange alloc] init]; change.mapping = mapping; - change.snapshotVersion = snapshotVersion; + change->_snapshotVersion = std::move(snapshotVersion); change.currentStatusUpdate = currentStatusUpdate; return change; } @@ -243,57 +292,42 @@ NS_ASSUME_NONNULL_BEGIN #pragma mark - FSTRemoteEvent -@interface FSTRemoteEvent () { - NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *_targetChanges; -} - -- (instancetype) -initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - targetChanges:(NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges - documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates; - -@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; - -@end - @implementation FSTRemoteEvent { + SnapshotVersion _snapshotVersion; + NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *_targetChanges; std::map<DocumentKey, FSTMaybeDocument *> _documentUpdates; -} -+ (instancetype) -eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion - targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges - documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates { - return [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion - targetChanges:targetChanges - documentUpdates:std::move(documentUpdates)]; + DocumentKeySet _limboDocumentChanges; } -- (instancetype)initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion +- (instancetype)initWithSnapshotVersion:(SnapshotVersion)snapshotVersion targetChanges: (NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges - documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates { + documentUpdates:(std::map<DocumentKey, FSTMaybeDocument *>)documentUpdates + limboDocuments:(DocumentKeySet)limboDocuments { self = [super init]; if (self) { - _snapshotVersion = snapshotVersion; + _snapshotVersion = std::move(snapshotVersion); _targetChanges = targetChanges; _documentUpdates = std::move(documentUpdates); + _limboDocumentChanges = std::move(limboDocuments); } return self; } -- (void)filterUpdatesFromTargetChange:(FSTTargetChange *)targetChange - existingDocuments:(FSTDocumentKeySet *)existingDocuments { - if ([targetChange.mapping isKindOfClass:[FSTUpdateMapping class]]) { - FSTUpdateMapping *update = (FSTUpdateMapping *)targetChange.mapping; - FSTDocumentKeySet *added = update.addedDocuments; - __block FSTDocumentKeySet *result = added; - [added enumerateObjectsUsingBlock:^(FSTDocumentKey *docKey, BOOL *stop) { - if ([existingDocuments containsObject:docKey]) { - result = [result setByRemovingObject:docKey]; - } - }]; - update.addedDocuments = result; - } +- (NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges { + return _targetChanges; +} + +- (const DocumentKeySet &)limboDocumentChanges { + return _limboDocumentChanges; +} + +- (const std::map<DocumentKey, FSTMaybeDocument *> &)documentUpdates { + return _documentUpdates; +} + +- (const SnapshotVersion &)snapshotVersion { + return _snapshotVersion; } - (void)synthesizeDeleteForLimboTargetChange:(FSTTargetChange *)targetChange @@ -314,22 +348,15 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion // However, if the document doesn't exist and the current marker arrives, the document is // not present in the snapshot and our normal view handling would consider the document to // remain in limbo indefinitely because there are no updates to the document. To avoid this, - // we specially handle this just this case here: synthesizing a delete. + // we specially handle this case here: synthesizing a delete. // // TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an // explicit delete message and we could remove this special logic. _documentUpdates[key] = [FSTDeletedDocument documentWithKey:key version:_snapshotVersion]; + _limboDocumentChanges = _limboDocumentChanges.insert(key); } } -- (NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges { - return static_cast<NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *>(_targetChanges); -} - -- (const std::map<DocumentKey, FSTMaybeDocument *> &)documentUpdates { - return _documentUpdates; -} - /** Adds a document update to this remote event */ - (void)addDocumentUpdate:(FSTMaybeDocument *)document { _documentUpdates[document.key] = document; @@ -350,7 +377,7 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion // TODO(dimond): keep track of reset targets not to raise. FSTTargetChange *targetChange = [FSTTargetChange changeWithMapping:[[FSTResetMapping alloc] init] - snapshotVersion:[FSTSnapshotVersion noVersion] + snapshotVersion:SnapshotVersion::None() currentStatusUpdate:FSTCurrentStatusUpdateMarkNotCurrent]; _targetChanges[targetID] = targetChange; } @@ -361,9 +388,6 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion @interface FSTWatchChangeAggregator () -/** The snapshot version for every target change this creates. */ -@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; - /** Keeps track of the current target mappings */ @property(nonatomic, strong, readonly) NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges; @@ -381,35 +405,38 @@ eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion NSMutableDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *_existenceFilters; /** Keeps track of document to update */ std::map<DocumentKey, FSTMaybeDocument *> _documentUpdates; + + DocumentKeySet _limboDocuments; + /** The snapshot version for every target change this creates. */ + SnapshotVersion _snapshotVersion; } - (instancetype) -initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion +initWithSnapshotVersion:(SnapshotVersion)snapshotVersion listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses { self = [super init]; if (self) { - _snapshotVersion = snapshotVersion; + _snapshotVersion = std::move(snapshotVersion); _frozen = NO; _targetChanges = [NSMutableDictionary dictionary]; _listenTargets = listenTargets; _pendingTargetResponses = [NSMutableDictionary dictionaryWithDictionary:pendingTargetResponses]; - + _limboDocuments = DocumentKeySet{}; _existenceFilters = [NSMutableDictionary dictionary]; } return self; } - (NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *)existenceFilters { - return static_cast<NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *>(_existenceFilters); + return _existenceFilters; } - (FSTTargetChange *)targetChangeForTargetID:(FSTBoxedTargetID *)targetID { FSTTargetChange *change = self.targetChanges[targetID]; if (!change) { - change = [[FSTTargetChange alloc] init]; - change.snapshotVersion = self.snapshotVersion; + change = [[FSTTargetChange alloc] initWithSnapshotVersion:_snapshotVersion]; self.targetChanges[targetID] = change; } return change; @@ -435,20 +462,66 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion } } +/** + * Updates limbo document tracking for a given target-document mapping change. If the target is a + * limbo target, and the change for the document has only seen limbo targets so far, and we are not + * already tracking a change for this document, then consider this document a limbo document update. + * Otherwise, ensure that we don't consider this document a limbo document. Returns true if the + * change still has only seen limbo resolution changes. + */ +- (BOOL)updateLimboDocumentsForKey:(const DocumentKey &)documentKey + queryData:(FSTQueryData *)queryData + isOnlyLimbo:(BOOL)isOnlyLimbo { + if (!isOnlyLimbo) { + // It wasn't a limbo doc before, so it definitely isn't now. + return NO; + } + if (_documentUpdates.find(documentKey) == _documentUpdates.end()) { + // We haven't seen a document update for this key yet. + if (queryData.purpose == FSTQueryPurposeLimboResolution) { + // We haven't seen this document before, and this target is a limbo target. + _limboDocuments = _limboDocuments.insert(documentKey); + return YES; + } else { + // We haven't seen the document before, but this is a non-limbo target. + // Since we haven't seen it, we know it's not in our set of limbo docs. Return NO to ensure + // that this key is marked as non-limbo. + return NO; + } + } else if (queryData.purpose == FSTQueryPurposeLimboResolution) { + // We have only seen limbo targets so far for this document, and this is another limbo target. + return YES; + } else { + // We haven't marked this as non-limbo yet, but this target is not a limbo target. + // Mark the key as non-limbo and make sure it isn't in our set. + _limboDocuments = _limboDocuments.erase(documentKey); + return NO; + } +} + - (void)addDocumentChange:(FSTDocumentWatchChange *)docChange { BOOL relevant = NO; + BOOL isOnlyLimbo = YES; for (FSTBoxedTargetID *targetID in docChange.updatedTargetIDs) { - if ([self isActiveTarget:targetID]) { + FSTQueryData *queryData = [self queryDataForActiveTarget:targetID]; + if (queryData) { FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + isOnlyLimbo = [self updateLimboDocumentsForKey:docChange.documentKey + queryData:queryData + isOnlyLimbo:isOnlyLimbo]; [change.mapping addDocumentKey:docChange.documentKey]; relevant = YES; } } for (FSTBoxedTargetID *targetID in docChange.removedTargetIDs) { - if ([self isActiveTarget:targetID]) { + FSTQueryData *queryData = [self queryDataForActiveTarget:targetID]; + if (queryData) { FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + isOnlyLimbo = [self updateLimboDocumentsForKey:docChange.documentKey + queryData:queryData + isOnlyLimbo:isOnlyLimbo]; [change.mapping removeDocumentKey:docChange.documentKey]; relevant = YES; } @@ -473,7 +546,7 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion break; case FSTWatchTargetChangeStateAdded: [self recordResponseForTargetID:targetID]; - if (![self.pendingTargetResponses objectForKey:targetID]) { + if (!self.pendingTargetResponses[targetID]) { // We have a freshly added target, so we need to reset any state that we had previously // This can happen e.g. when remove and add back a target for existence filter // mismatches. @@ -514,12 +587,12 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion * responses that we have. */ - (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID { - NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; + NSNumber *count = self.pendingTargetResponses[targetID]; int newCount = count ? [count intValue] - 1 : -1; if (newCount == 0) { [self.pendingTargetResponses removeObjectForKey:targetID]; } else { - [self.pendingTargetResponses setObject:[NSNumber numberWithInt:newCount] forKey:targetID]; + self.pendingTargetResponses[targetID] = @(newCount); } } @@ -532,8 +605,12 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion * yet acknowledged the intended change in state. */ - (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID { - return [self.listenTargets objectForKey:targetID] && - ![self.pendingTargetResponses objectForKey:targetID]; + return [self queryDataForActiveTarget:targetID] != nil; +} + +- (FSTQueryData *_Nullable)queryDataForActiveTarget:(FSTBoxedTargetID *)targetID { + FSTQueryData *queryData = self.listenTargets[targetID]; + return (queryData && !self.pendingTargetResponses[targetID]) ? queryData : nil; } - (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange { @@ -559,9 +636,10 @@ initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion // Mark this aggregator as frozen so no further modifications are made. self.frozen = YES; - return [FSTRemoteEvent eventWithSnapshotVersion:self.snapshotVersion - targetChanges:targetChanges - documentUpdates:_documentUpdates]; + return [[FSTRemoteEvent alloc] initWithSnapshotVersion:_snapshotVersion + targetChanges:targetChanges + documentUpdates:_documentUpdates + limboDocuments:_limboDocuments]; } @end diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h index 09e1d32..9b01ce4 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.h +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -17,7 +17,6 @@ #import <Foundation/Foundation.h> #import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Model/FSTDocumentVersionDictionary.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" diff --git a/Firestore/Source/Remote/FSTRemoteStore.mm b/Firestore/Source/Remote/FSTRemoteStore.mm index 39d285a..0ea4887 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.mm +++ b/Firestore/Source/Remote/FSTRemoteStore.mm @@ -19,7 +19,6 @@ #include <cinttypes> #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Core/FSTTransaction.h" #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Local/FSTQueryData.h" @@ -37,11 +36,14 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; using firebase::firestore::auth::User; using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::model::DocumentKeySet; NS_ASSUME_NONNULL_BEGIN @@ -299,7 +301,7 @@ static const int kMaxPendingWrites = 10; } - (void)watchStreamDidChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + snapshotVersion:(const SnapshotVersion &)snapshotVersion { // Mark the connection as Online because we got a message from the server. [self.onlineStateTracker updateState:FSTOnlineStateOnline]; @@ -315,10 +317,8 @@ static const int kMaxPendingWrites = 10; // older than a previous snapshot we've processed (can happen after we resume a target // using a resume token). [self.accumulatedChanges addObject:change]; - FSTAssert(snapshotVersion, @"snapshotVersion must not be nil."); - if ([snapshotVersion isEqual:[FSTSnapshotVersion noVersion]] || - [snapshotVersion compare:[self.localStore lastRemoteSnapshotVersion]] == - NSOrderedAscending) { + if (snapshotVersion == SnapshotVersion::None() || + snapshotVersion < [self.localStore lastRemoteSnapshotVersion]) { return; } @@ -354,7 +354,7 @@ static const int kMaxPendingWrites = 10; * on to the SyncEngine. */ - (void)processBatchedWatchChanges:(NSArray<FSTWatchChange *> *)changes - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + snapshotVersion:(const SnapshotVersion &)snapshotVersion { FSTWatchChangeAggregator *aggregator = [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:snapshotVersion listenTargets:self.listenTargets @@ -394,7 +394,7 @@ static const int kMaxPendingWrites = 10; } else { // Not a document query. - FSTDocumentKeySet *trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID]; + DocumentKeySet trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID]; FSTTargetMapping *mapping = remoteEvent.targetChanges[target].mapping; if (mapping) { if ([mapping isKindOfClass:[FSTUpdateMapping class]]) { @@ -407,7 +407,7 @@ static const int kMaxPendingWrites = 10; } } - if (trackedRemote.count != (NSUInteger)filter.count) { + if (trackedRemote.size() != static_cast<size_t>(filter.count)) { FSTLog(@"Existence filter mismatch, resetting mapping"); // Make sure the mismatch is exposed in the remote event @@ -449,7 +449,8 @@ static const int kMaxPendingWrites = 10; if (queryData) { self->_listenTargets[target] = [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion - resumeToken:resumeToken]; + resumeToken:resumeToken + sequenceNumber:queryData.sequenceNumber]; } } }]; @@ -566,7 +567,7 @@ static const int kMaxPendingWrites = 10; } /** Handles a successful StreamingWriteResponse from the server that contains a mutation result. */ -- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)writeStreamDidReceiveResponseWithVersion:(const SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results { // This is a response to a write containing mutations and should be correlated to the first // pending write. diff --git a/Firestore/Source/Remote/FSTSerializerBeta.h b/Firestore/Source/Remote/FSTSerializerBeta.h index d96dbeb..cdf5d1f 100644 --- a/Firestore/Source/Remote/FSTSerializerBeta.h +++ b/Firestore/Source/Remote/FSTSerializerBeta.h @@ -16,8 +16,10 @@ #import <Foundation/Foundation.h> +#include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTFieldValue; @class FSTMaybeDocument; @@ -27,8 +29,6 @@ @class FSTObjectValue; @class FSTQuery; @class FSTQueryData; -@class FSTSnapshotVersion; -@class FIRTimestamp; @class FSTWatchChange; @class GCFSBatchGetDocumentsResponse; @@ -61,15 +61,19 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithDatabaseID:(const firebase::firestore::model::DatabaseId *)databaseID NS_DESIGNATED_INITIALIZER; -- (GPBTimestamp *)encodedTimestamp:(FIRTimestamp *)timestamp; -- (FIRTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp; +- (GPBTimestamp *)encodedTimestamp:(const firebase::Timestamp &)timestamp; +- (firebase::Timestamp)decodedTimestamp:(GPBTimestamp *)timestamp; -- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version; -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version; +- (GPBTimestamp *)encodedVersion:(const firebase::firestore::model::SnapshotVersion &)version; +- (firebase::firestore::model::SnapshotVersion)decodedVersion:(GPBTimestamp *)version; /** Returns the database ID, such as `projects/{project id}/databases/{database_id}`. */ - (NSString *)encodedDatabaseID; +/** + * Encodes the given document key as a fully qualified name. This includes the + * databaseId associated with this FSTSerializerBeta and the key path. + */ - (NSString *)encodedDocumentKey:(const firebase::firestore::model::DocumentKey &)key; - (firebase::firestore::model::DocumentKey)decodedDocumentKey:(NSString *)key; @@ -93,7 +97,8 @@ NS_ASSUME_NONNULL_BEGIN - (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target; - (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange; -- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange; +- (firebase::firestore::model::SnapshotVersion)versionFromListenResponse: + (GCFSListenResponse *)watchChange; - (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue key:(const firebase::firestore::model::DocumentKey &)key; diff --git a/Firestore/Source/Remote/FSTSerializerBeta.mm b/Firestore/Source/Remote/FSTSerializerBeta.mm index 5cbfecc..f862ec3 100644 --- a/Firestore/Source/Remote/FSTSerializerBeta.mm +++ b/Firestore/Source/Remote/FSTSerializerBeta.mm @@ -33,9 +33,7 @@ #import "FIRFirestoreErrors.h" #import "FIRGeoPoint.h" -#import "FIRTimestamp.h" #import "Firestore/Source/Core/FSTQuery.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTFieldValue.h" @@ -55,8 +53,10 @@ #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "absl/memory/memory.h" +#include "absl/types/optional.h" namespace util = firebase::firestore::util; +using firebase::Timestamp; using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; @@ -86,25 +86,25 @@ NS_ASSUME_NONNULL_BEGIN return self; } -#pragma mark - FSTSnapshotVersion <=> GPBTimestamp +#pragma mark - SnapshotVersion <=> GPBTimestamp -- (GPBTimestamp *)encodedTimestamp:(FIRTimestamp *)timestamp { +- (GPBTimestamp *)encodedTimestamp:(const Timestamp &)timestamp { GPBTimestamp *result = [GPBTimestamp message]; - result.seconds = timestamp.seconds; - result.nanos = timestamp.nanoseconds; + result.seconds = timestamp.seconds(); + result.nanos = timestamp.nanoseconds(); return result; } -- (FIRTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp { - return [[FIRTimestamp alloc] initWithSeconds:timestamp.seconds nanoseconds:timestamp.nanos]; +- (Timestamp)decodedTimestamp:(GPBTimestamp *)timestamp { + return Timestamp{timestamp.seconds, timestamp.nanos}; } -- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { - return [self encodedTimestamp:version.timestamp]; +- (GPBTimestamp *)encodedVersion:(const SnapshotVersion &)version { + return [self encodedTimestamp:version.timestamp()]; } -- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { - return [FSTSnapshotVersion versionWithTimestamp:[self decodedTimestamp:version]]; +- (SnapshotVersion)decodedVersion:(GPBTimestamp *)version { + return SnapshotVersion{[self decodedTimestamp:version]}; } #pragma mark - FIRGeoPoint <=> GTPLatLng @@ -206,8 +206,8 @@ NS_ASSUME_NONNULL_BEGIN return [self encodedString:[fieldValue value]]; } else if (fieldClass == [FSTTimestampValue class]) { - return [self encodedTimestampValue:[fieldValue value]]; - + FIRTimestamp *value = static_cast<FIRTimestamp *>([fieldValue value]); + return [self encodedTimestampValue:Timestamp{value.seconds, value.nanoseconds}]; } else if (fieldClass == [FSTGeoPointValue class]) { return [self encodedGeoPointValue:[fieldValue value]]; @@ -250,8 +250,12 @@ NS_ASSUME_NONNULL_BEGIN case GCFSValue_ValueType_OneOfCase_StringValue: return [FSTStringValue stringValue:valueProto.stringValue]; - case GCFSValue_ValueType_OneOfCase_TimestampValue: - return [FSTTimestampValue timestampValue:[self decodedTimestamp:valueProto.timestampValue]]; + case GCFSValue_ValueType_OneOfCase_TimestampValue: { + Timestamp value = [self decodedTimestamp:valueProto.timestampValue]; + return [FSTTimestampValue + timestampValue:[FIRTimestamp timestampWithSeconds:value.seconds() + nanoseconds:value.nanoseconds()]]; + } case GCFSValue_ValueType_OneOfCase_GeoPointValue: return [FSTGeoPointValue geoPointValue:[self decodedGeoPoint:valueProto.geoPointValue]]; @@ -303,7 +307,7 @@ NS_ASSUME_NONNULL_BEGIN return result; } -- (GCFSValue *)encodedTimestampValue:(FIRTimestamp *)value { +- (GCFSValue *)encodedTimestampValue:(const Timestamp &)value { GCFSValue *result = [GCFSValue message]; result.timestampValue = [self encodedTimestamp:value]; return result; @@ -429,8 +433,8 @@ NS_ASSUME_NONNULL_BEGIN FSTAssert(!!response.found, @"Tried to deserialize a found document from a deleted document."); const DocumentKey key = [self decodedDocumentKey:response.found.name]; FSTObjectValue *value = [self decodedFields:response.found.fields]; - FSTSnapshotVersion *version = [self decodedVersion:response.found.updateTime]; - FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + SnapshotVersion version = [self decodedVersion:response.found.updateTime]; + FSTAssert(version != SnapshotVersion::None(), @"Got a document response with no snapshot version"); return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; @@ -439,8 +443,8 @@ NS_ASSUME_NONNULL_BEGIN - (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response { FSTAssert(!!response.missing, @"Tried to deserialize a deleted document from a found document."); const DocumentKey key = [self decodedDocumentKey:response.missing]; - FSTSnapshotVersion *version = [self decodedVersion:response.readTime]; - FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + SnapshotVersion version = [self decodedVersion:response.readTime]; + FSTAssert(version != SnapshotVersion::None(), @"Got a no document response with no snapshot version"); return [FSTDeletedDocument documentWithKey:key version:version]; } @@ -668,8 +672,10 @@ NS_ASSUME_NONNULL_BEGIN - (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation { // NOTE: Deletes don't have an updateTime. - FSTSnapshotVersion *_Nullable version = - mutation.updateTime ? [self decodedVersion:mutation.updateTime] : nil; + absl::optional<SnapshotVersion> version; + if (mutation.updateTime) { + version = [self decodedVersion:mutation.updateTime]; + } NSMutableArray *_Nullable transformResults = nil; if (mutation.transformResultsArray.count > 0) { transformResults = [NSMutableArray array]; @@ -677,7 +683,8 @@ NS_ASSUME_NONNULL_BEGIN [transformResults addObject:[self decodedFieldValue:result]]; } } - return [[FSTMutationResult alloc] initWithVersion:version transformResults:transformResults]; + return [[FSTMutationResult alloc] initWithVersion:std::move(version) + transformResults:transformResults]; } #pragma mark - FSTQueryData => GCFSTarget proto @@ -1071,15 +1078,15 @@ NS_ASSUME_NONNULL_BEGIN } } -- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange { +- (SnapshotVersion)versionFromListenResponse:(GCFSListenResponse *)watchChange { // We have only reached a consistent snapshot for the entire stream if there is a read_time set // and it applies to all targets (i.e. the list of targets is empty). The backend is guaranteed to // send such responses. if (watchChange.responseTypeOneOfCase != GCFSListenResponse_ResponseType_OneOfCase_TargetChange) { - return [FSTSnapshotVersion noVersion]; + return SnapshotVersion::None(); } if (watchChange.targetChange.targetIdsArray.count != 0) { - return [FSTSnapshotVersion noVersion]; + return SnapshotVersion::None(); } return [self decodedVersion:watchChange.targetChange.readTime]; } @@ -1135,9 +1142,8 @@ NS_ASSUME_NONNULL_BEGIN - (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change { FSTObjectValue *value = [self decodedFields:change.document.fields]; const DocumentKey key = [self decodedDocumentKey:change.document.name]; - FSTSnapshotVersion *version = [self decodedVersion:change.document.updateTime]; - FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], - @"Got a document change with no snapshot version"); + SnapshotVersion version = [self decodedVersion:change.document.updateTime]; + FSTAssert(version != SnapshotVersion::None(), @"Got a document change with no snapshot version"); FSTMaybeDocument *document = [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; @@ -1152,8 +1158,8 @@ NS_ASSUME_NONNULL_BEGIN - (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change { const DocumentKey key = [self decodedDocumentKey:change.document]; - // Note that version might be unset in which case we use [FSTSnapshotVersion noVersion] - FSTSnapshotVersion *version = [self decodedVersion:change.readTime]; + // Note that version might be unset in which case we use SnapshotVersion::None() + SnapshotVersion version = [self decodedVersion:change.readTime]; FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version]; NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; diff --git a/Firestore/Source/Remote/FSTStream.h b/Firestore/Source/Remote/FSTStream.h index fba79d2..3bd8549 100644 --- a/Firestore/Source/Remote/FSTStream.h +++ b/Firestore/Source/Remote/FSTStream.h @@ -21,13 +21,13 @@ #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" @class FSTDispatchQueue; @class FSTMutation; @class FSTMutationResult; @class FSTQueryData; @class FSTSerializerBeta; -@class FSTSnapshotVersion; @class FSTWatchChange; @class FSTWatchStream; @class FSTWriteStream; @@ -179,7 +179,7 @@ NS_ASSUME_NONNULL_BEGIN * WatchChange responses sent back by the server. */ - (void)watchStreamDidChange:(FSTWatchChange *)change - snapshotVersion:(FSTSnapshotVersion *)snapshotVersion; + snapshotVersion:(const firebase::firestore::model::SnapshotVersion &)snapshotVersion; /** * Called by the FSTWatchStream when the underlying streaming RPC is interrupted for whatever @@ -250,7 +250,8 @@ NS_ASSUME_NONNULL_BEGIN * Called by the FSTWriteStream upon receiving a StreamingWriteResponse from the server that * contains mutation results. */ -- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion +- (void)writeStreamDidReceiveResponseWithVersion: + (const firebase::firestore::model::SnapshotVersion &)commitVersion mutationResults:(NSArray<FSTMutationResult *> *)results; /** diff --git a/Firestore/Source/Remote/FSTStream.mm b/Firestore/Source/Remote/FSTStream.mm index a96feae..f4ec675 100644 --- a/Firestore/Source/Remote/FSTStream.mm +++ b/Firestore/Source/Remote/FSTStream.mm @@ -36,6 +36,7 @@ #include "Firestore/core/src/firebase/firestore/auth/token.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" @@ -44,6 +45,7 @@ using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::Token; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::SnapshotVersion; /** * Initial backoff time in seconds after an error. @@ -691,7 +693,7 @@ static const NSTimeInterval kIdleTimeout = 60.0; [self.backoff reset]; FSTWatchChange *change = [_serializer decodedWatchChange:proto]; - FSTSnapshotVersion *snap = [_serializer versionFromListenResponse:proto]; + SnapshotVersion snap = [_serializer versionFromListenResponse:proto]; [self.delegate watchStreamDidChange:change snapshotVersion:snap]; } @@ -807,7 +809,7 @@ static const NSTimeInterval kIdleTimeout = 60.0; // might be causing an error we want to back off from. [self.backoff reset]; - FSTSnapshotVersion *commitVersion = [_serializer decodedVersion:response.commitTime]; + SnapshotVersion commitVersion = [_serializer decodedVersion:response.commitTime]; NSMutableArray<GCFSWriteResult *> *protos = response.writeResultsArray; NSMutableArray<FSTMutationResult *> *results = [NSMutableArray arrayWithCapacity:protos.count]; for (GCFSWriteResult *proto in protos) { diff --git a/Firestore/Source/Remote/FSTWatchChange.h b/Firestore/Source/Remote/FSTWatchChange.h index 8f730de..ed80e1a 100644 --- a/Firestore/Source/Remote/FSTWatchChange.h +++ b/Firestore/Source/Remote/FSTWatchChange.h @@ -22,7 +22,6 @@ @class FSTExistenceFilter; @class FSTMaybeDocument; -@class FSTSnapshotVersion; NS_ASSUME_NONNULL_BEGIN diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.h b/Firestore/Source/Util/FSTAsyncQueryListener.h index 4888268..06471d8 100644 --- a/Firestore/Source/Util/FSTAsyncQueryListener.h +++ b/Firestore/Source/Util/FSTAsyncQueryListener.h @@ -18,6 +18,8 @@ #import "Firestore/Source/Core/FSTViewSnapshot.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" + NS_ASSUME_NONNULL_BEGIN @class FSTDispatchQueue; @@ -28,9 +30,8 @@ NS_ASSUME_NONNULL_BEGIN */ @interface FSTAsyncQueryListener : NSObject -- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue - snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler - NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithExecutor:(firebase::firestore::util::internal::Executor*)executor + snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler NS_DESIGNATED_INITIALIZER; - (instancetype)init NS_UNAVAILABLE; diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.mm b/Firestore/Source/Util/FSTAsyncQueryListener.mm index b72ac57..81dd41f 100644 --- a/Firestore/Source/Util/FSTAsyncQueryListener.mm +++ b/Firestore/Source/Util/FSTAsyncQueryListener.mm @@ -18,16 +18,18 @@ #import "Firestore/Source/Util/FSTDispatchQueue.h" +using firebase::firestore::util::internal::Executor; + @implementation FSTAsyncQueryListener { volatile BOOL _muted; FSTViewSnapshotHandler _snapshotHandler; - FSTDispatchQueue *_dispatchQueue; + Executor *_executor; } -- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue - snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler { +- (instancetype)initWithExecutor:(Executor *)executor + snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler { if (self = [super init]) { - _dispatchQueue = dispatchQueue; + _executor = executor; _snapshotHandler = snapshotHandler; } return self; @@ -40,11 +42,11 @@ // users just want to turn on notifications "forever" and don't want to have // to keep track of our handle to keep them going. return ^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { - [self->_dispatchQueue dispatchAsync:^{ + self->_executor->Execute([self, snapshot, error] { if (!self->_muted) { self->_snapshotHandler(snapshot, error); } - }]; + }); }; } diff --git a/Firestore/Source/Util/FSTDispatchQueue.mm b/Firestore/Source/Util/FSTDispatchQueue.mm index 0974359..01b2732 100644 --- a/Firestore/Source/Util/FSTDispatchQueue.mm +++ b/Firestore/Source/Util/FSTDispatchQueue.mm @@ -16,154 +16,66 @@ #import <Foundation/Foundation.h> -#include <atomic> +#include <memory> +#include <utility> #import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTDispatchQueue.h" -NS_ASSUME_NONNULL_BEGIN - -/** - * removeDelayedCallback is used by FSTDelayedCallback and so we pre-declare it before the rest of - * the FSTDispatchQueue private interface. - */ -@interface FSTDispatchQueue () -- (void)removeDelayedCallback:(FSTDelayedCallback *)callback; -@end +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "absl/memory/memory.h" -#pragma mark - FSTDelayedCallback - -/** - * Represents a callback scheduled to be run in the future on an FSTDispatchQueue. - * - * It is created via [FSTDelayedCallback createAndScheduleWithQueue]. - * - * Supports cancellation (via cancel) and early execution (via skipDelay). - */ -@interface FSTDelayedCallback () +using firebase::firestore::util::AsyncQueue; +using firebase::firestore::util::DelayedOperation; +using firebase::firestore::util::TimerId; +using firebase::firestore::util::internal::Executor; +using firebase::firestore::util::internal::ExecutorLibdispatch; -@property(nonatomic, strong, readonly) FSTDispatchQueue *queue; -@property(nonatomic, assign, readonly) FSTTimerID timerID; -@property(nonatomic, assign, readonly) NSTimeInterval targetTime; -@property(nonatomic, copy) void (^callback)(); -/** YES if the callback has been run or canceled. */ -@property(nonatomic, getter=isDone) BOOL done; +NS_ASSUME_NONNULL_BEGIN -/** - * Creates and returns an FSTDelayedCallback that has been scheduled on the provided queue with the - * provided delay. - * - * @param queue The FSTDispatchQueue to run the callback on. - * @param timerID A FSTTimerID identifying the type of the delayed callback. - * @param delay The delay before the callback should be scheduled. - * @param callback The callback block to run. - * @return The created FSTDelayedCallback instance. - */ -+ (instancetype)createAndScheduleWithQueue:(FSTDispatchQueue *)queue - timerID:(FSTTimerID)timerID - delay:(NSTimeInterval)delay - callback:(void (^)(void))callback; +#pragma mark - FSTDelayedCallback -/** - * Queues the callback to run immediately (if it hasn't already been run or canceled). - */ -- (void)skipDelay; +@interface FSTDelayedCallback () { + DelayedOperation _impl; +} @end @implementation FSTDelayedCallback -- (instancetype)initWithQueue:(FSTDispatchQueue *)queue - timerID:(FSTTimerID)timerID - targetTime:(NSTimeInterval)targetTime - callback:(void (^)(void))callback { +- (instancetype)initWithImpl:(DelayedOperation &&)impl { if (self = [super init]) { - _queue = queue; - _timerID = timerID; - _targetTime = targetTime; - _callback = callback; - _done = NO; + _impl = std::move(impl); } return self; } -+ (instancetype)createAndScheduleWithQueue:(FSTDispatchQueue *)queue - timerID:(FSTTimerID)timerID - delay:(NSTimeInterval)delay - callback:(void (^)(void))callback { - NSTimeInterval targetTime = [[NSDate date] timeIntervalSince1970] + delay; - FSTDelayedCallback *delayedCallback = [[FSTDelayedCallback alloc] initWithQueue:queue - timerID:timerID - targetTime:targetTime - callback:callback]; - [delayedCallback startWithDelay:delay]; - return delayedCallback; -} - -/** - * Starts the timer. This is called immediately after construction by createAndScheduleWithQueue. - */ -- (void)startWithDelay:(NSTimeInterval)delay { - dispatch_time_t delayNs = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)); - dispatch_after(delayNs, self.queue.queue, ^{ - [self.queue enterCheckedOperation:^{ - [self delayDidElapse]; - }]; - }); -} - -- (void)skipDelay { - [self.queue dispatchAsyncAllowingSameQueue:^{ - [self delayDidElapse]; - }]; -} - - (void)cancel { - [self.queue verifyIsCurrentQueue]; - if (!self.isDone) { - // PORTING NOTE: There's no way to actually cancel the dispatched callback, but it'll be a no-op - // since we set done to YES. - [self markDone]; - } -} - -- (void)delayDidElapse { - [self.queue verifyIsCurrentQueue]; - if (!self.isDone) { - [self markDone]; - self.callback(); - } -} - -/** - * Marks this delayed callback as done, and notifies the FSTDispatchQueue that it should be removed. - */ -- (void)markDone { - self.done = YES; - [self.queue removeDelayedCallback:self]; + _impl.Cancel(); } @end #pragma mark - FSTDispatchQueue -@interface FSTDispatchQueue () -/** - * Callbacks scheduled to be queued in the future. Callbacks are automatically removed after they - * are run or canceled. - */ -@property(nonatomic, strong, readonly) NSMutableArray<FSTDelayedCallback *> *delayedCallbacks; - -- (instancetype)initWithQueue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; - -@end - @implementation FSTDispatchQueue { - /** - * Flag set while an FSTDispatchQueue operation is currently executing. Used for assertion - * sanity-checks. - */ - std::atomic<bool> _operationInProgress; + std::unique_ptr<AsyncQueue> _impl; +} + ++ (TimerId)convertTimerId:(FSTTimerID)objcTimerID { + const TimerId converted = static_cast<TimerId>(objcTimerID); + switch (converted) { + case TimerId::All: + case TimerId::ListenStreamIdle: + case TimerId::ListenStreamConnectionBackoff: + case TimerId::WriteStreamIdle: + case TimerId::WriteStreamConnectionBackoff: + case TimerId::OnlineStateTimeout: + return converted; + default: + FSTAssert(false, @"Unknown value of enum FSTTimerID."); + } } + (instancetype)queueWith:(dispatch_queue_t)dispatchQueue { @@ -172,141 +84,50 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithQueue:(dispatch_queue_t)queue { if (self = [super init]) { - _operationInProgress = false; _queue = queue; - _delayedCallbacks = [NSMutableArray array]; + auto executor = absl::make_unique<ExecutorLibdispatch>(queue); + _impl = absl::make_unique<AsyncQueue>(std::move(executor)); } return self; } - (void)verifyIsCurrentQueue { - FSTAssert([self onTargetQueue], - @"We are running on the wrong dispatch queue. Expected '%@' Actual: '%@'", - [self targetQueueLabel], [self currentQueueLabel]); - FSTAssert(_operationInProgress, - @"verifyIsCurrentQueue called outside enterCheckedOperation on queue '%@'", - [self currentQueueLabel]); + _impl->VerifyIsCurrentQueue(); } - (void)enterCheckedOperation:(void (^)(void))block { - FSTAssert(!_operationInProgress, - @"enterCheckedOperation may not be called when an operation is in progress"); - @try { - _operationInProgress = true; - [self verifyIsCurrentQueue]; - block(); - } @finally { - _operationInProgress = false; - } + _impl->ExecuteBlocking([block] { block(); }); } - (void)dispatchAsync:(void (^)(void))block { - FSTAssert(![self onTargetQueue] || !_operationInProgress, - @"dispatchAsync called when we are already running on target dispatch queue '%@'", - [self targetQueueLabel]); - - dispatch_async(self.queue, ^{ - [self enterCheckedOperation:block]; - }); + _impl->Enqueue([block] { block(); }); } - (void)dispatchAsyncAllowingSameQueue:(void (^)(void))block { - dispatch_async(self.queue, ^{ - [self enterCheckedOperation:block]; - }); + _impl->EnqueueRelaxed([block] { block(); }); } - (void)dispatchSync:(void (^)(void))block { - FSTAssert(![self onTargetQueue] || !_operationInProgress, - @"dispatchSync called when we are already running on target dispatch queue '%@'", - [self targetQueueLabel]); - - dispatch_sync(self.queue, ^{ - [self enterCheckedOperation:block]; - }); + _impl->EnqueueBlocking([block] { block(); }); } - (FSTDelayedCallback *)dispatchAfterDelay:(NSTimeInterval)delay timerID:(FSTTimerID)timerID block:(void (^)(void))block { - // While not necessarily harmful, we currently don't expect to have multiple callbacks with the - // same timerID in the queue, so defensively reject them. - FSTAssert(![self containsDelayedCallbackWithTimerID:timerID], - @"Attempted to schedule multiple callbacks with id %ld", (unsigned long)timerID); - FSTDelayedCallback *delayedCallback = [FSTDelayedCallback createAndScheduleWithQueue:self - timerID:timerID - delay:delay - callback:block]; - [self.delayedCallbacks addObject:delayedCallback]; - return delayedCallback; + const AsyncQueue::Milliseconds delayMs = + std::chrono::milliseconds(static_cast<long long>(delay * 1000)); + const TimerId convertedTimerId = [FSTDispatchQueue convertTimerId:timerID]; + DelayedOperation delayed_operation = + _impl->EnqueueAfterDelay(delayMs, convertedTimerId, [block] { block(); }); + return [[FSTDelayedCallback alloc] initWithImpl:std::move(delayed_operation)]; } - (BOOL)containsDelayedCallbackWithTimerID:(FSTTimerID)timerID { - NSUInteger matchIndex = [self.delayedCallbacks - indexOfObjectPassingTest:^BOOL(FSTDelayedCallback *obj, NSUInteger idx, BOOL *stop) { - return obj.timerID == timerID; - }]; - return matchIndex != NSNotFound; + return _impl->IsScheduled([FSTDispatchQueue convertTimerId:timerID]); } - (void)runDelayedCallbacksUntil:(FSTTimerID)lastTimerID { - dispatch_semaphore_t doneSemaphore = dispatch_semaphore_create(0); - - [self dispatchAsync:^{ - FSTAssert(lastTimerID == FSTTimerIDAll || [self containsDelayedCallbackWithTimerID:lastTimerID], - @"Attempted to run callbacks until missing timer ID: %ld", - (unsigned long)lastTimerID); - - [self sortDelayedCallbacks]; - for (FSTDelayedCallback *callback in self.delayedCallbacks) { - [callback skipDelay]; - if (lastTimerID != FSTTimerIDAll && callback.timerID == lastTimerID) { - break; - } - } - - // Now that the callbacks are queued, we want to enqueue an additional item to release the - // 'done' semaphore. - [self dispatchAsyncAllowingSameQueue:^{ - dispatch_semaphore_signal(doneSemaphore); - }]; - }]; - - dispatch_semaphore_wait(doneSemaphore, DISPATCH_TIME_FOREVER); -} - -// NOTE: For performance we could store the callbacks sorted (e.g. using std::priority_queue), -// but this sort only happens in tests (if runDelayedCallbacksUntil: is called), and the size -// is guaranteed to be small since we don't allow duplicate TimerIds (of which there are only 4). -- (void)sortDelayedCallbacks { - // We want to run callbacks in the same order they'd run if they ran naturally. - [self.delayedCallbacks - sortUsingComparator:^NSComparisonResult(FSTDelayedCallback *a, FSTDelayedCallback *b) { - return a.targetTime < b.targetTime - ? NSOrderedAscending - : a.targetTime > b.targetTime ? NSOrderedDescending : NSOrderedSame; - }]; -} - -/** Called by FSTDelayedCallback when a callback is run or canceled. */ -- (void)removeDelayedCallback:(FSTDelayedCallback *)callback { - NSUInteger index = [self.delayedCallbacks indexOfObject:callback]; - FSTAssert(index != NSNotFound, @"Delayed callback not found."); - [self.delayedCallbacks removeObjectAtIndex:index]; -} - -#pragma mark - Private Methods - -- (NSString *)currentQueueLabel { - return [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)]; -} - -- (NSString *)targetQueueLabel { - return [NSString stringWithUTF8String:dispatch_queue_get_label(self.queue)]; -} - -- (BOOL)onTargetQueue { - return [[self currentQueueLabel] isEqualToString:[self targetQueueLabel]]; + _impl->RunScheduledOperationsUntil([FSTDispatchQueue convertTimerId:lastTimerID]); } @end diff --git a/Firestore/core/CMakeLists.txt b/Firestore/core/CMakeLists.txt index 1a0c936..c6ae0f5 100644 --- a/Firestore/core/CMakeLists.txt +++ b/Firestore/core/CMakeLists.txt @@ -20,6 +20,7 @@ add_subdirectory(src/firebase/firestore/core) add_subdirectory(src/firebase/firestore/immutable) add_subdirectory(src/firebase/firestore/local) add_subdirectory(src/firebase/firestore/model) +add_subdirectory(src/firebase/firestore/nanopb) add_subdirectory(src/firebase/firestore/remote) add_subdirectory(src/firebase/firestore/util) diff --git a/Firestore/core/src/firebase/firestore/CMakeLists.txt b/Firestore/core/src/firebase/firestore/CMakeLists.txt index aad2ebb..9836b05 100644 --- a/Firestore/core/src/firebase/firestore/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/CMakeLists.txt @@ -18,6 +18,7 @@ cc_library( SOURCES geo_point.cc timestamp.cc + timestamp_internal.h DEPENDS firebase_firestore_util ) diff --git a/Firestore/core/src/firebase/firestore/immutable/CMakeLists.txt b/Firestore/core/src/firebase/firestore/immutable/CMakeLists.txt index 90ce204..af97b62 100644 --- a/Firestore/core/src/firebase/firestore/immutable/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/immutable/CMakeLists.txt @@ -24,6 +24,7 @@ cc_library( sorted_map_base.h sorted_map_base.cc sorted_map_iterator.h + sorted_set.h tree_sorted_map.h DEPENDS firebase_firestore_util diff --git a/Firestore/core/src/firebase/firestore/immutable/sorted_set.h b/Firestore/core/src/firebase/firestore/immutable/sorted_set.h new file mode 100644 index 0000000..d78fd61 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/immutable/sorted_set.h @@ -0,0 +1,157 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_SET_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_SET_H_ + +#include <algorithm> +#include <utility> + +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_map_base.h" +#include "Firestore/core/src/firebase/firestore/util/comparison.h" +#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" + +namespace firebase { +namespace firestore { +namespace immutable { + +namespace impl { + +// An empty value to associate with keys in the underlying map. +struct Empty { + friend bool operator==(Empty /* left */, Empty /* right */) { + return true; + } +}; + +} // namespace impl + +template <typename K, + typename V = impl::Empty, + typename C = util::Comparator<K>, + typename M = SortedMap<K, V, C>> +class SortedSet { + public: + using size_type = typename M::size_type; + using value_type = K; + + using const_iterator = typename M::const_key_iterator; + + explicit SortedSet(const C& comparator = C()) : map_{comparator} { + } + + explicit SortedSet(const M& map) : map_{map} { + } + + explicit SortedSet(M&& map) : map_{std::move(map)} { + } + + SortedSet(std::initializer_list<value_type> entries, const C& comparator = {}) + : map_{comparator} { + for (auto&& value : entries) { + map_ = map_.insert(value, {}); + } + } + + bool empty() const { + return map_.empty(); + } + + size_type size() const { + return map_.size(); + } + + SortedSet insert(const K& key) const { + return SortedSet{map_.insert(key, {})}; + } + + SortedSet erase(const K& key) const { + return SortedSet{map_.erase(key)}; + } + + bool contains(const K& key) const { + return map_.contains(key); + } + + const_iterator find(const K& key) const { + return const_iterator{map_.find(key)}; + } + + size_type find_index(const K& key) const { + return map_.find_index(key); + } + + const_iterator min() const { + return const_iterator{map_.min()}; + } + + const K& max() const { + return const_iterator{map_.max()}; + } + + const_iterator begin() const { + return const_iterator{map_.begin()}; + } + + const_iterator end() const { + return const_iterator{map_.end()}; + } + + /** + * Returns a view of this SortedSet containing just the keys that have been + * inserted that are greater than or equal to the given key. + */ + const util::range<const_iterator> values_from(const K& key) const { + return map_.keys_from(key); + } + + /** + * Returns a view of this SortedSet containing just the keys that have been + * inserted that are greater than or equal to the given start_key and less + * than the given end_key. + */ + const util::range<const_iterator> values_in(const K& start_key, + const K& end_key) const { + return map_.keys_in(start_key, end_key); + } + + friend bool operator==(const SortedSet& lhs, const SortedSet& rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + return std::equal(lhs.begin(), lhs.end(), rhs.begin()); + } + + friend bool operator!=(const SortedSet& lhs, const SortedSet& rhs) { + return !(lhs == rhs); + } + + private: + M map_; +}; + +template <typename K, typename V, typename C> +SortedSet<K, V, C> MakeSortedSet(const SortedMap<K, V, C>& map) { + return SortedSet<K, V, C>{map}; +} + +} // namespace immutable +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_IMMUTABLE_SORTED_SET_H_ diff --git a/Firestore/core/src/firebase/firestore/model/base_path.h b/Firestore/core/src/firebase/firestore/model/base_path.h index bc1f89d..58df6f0 100644 --- a/Firestore/core/src/firebase/firestore/model/base_path.h +++ b/Firestore/core/src/firebase/firestore/model/base_path.h @@ -25,6 +25,7 @@ #include <vector> #include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" namespace firebase { namespace firestore { @@ -159,19 +160,6 @@ class BasePath { return segments_ >= rhs.segments_; } -#if defined(__OBJC__) - // For Objective-C++ hash; to be removed after migration. - // Do NOT use in C++ code. - NSUInteger Hash() const { - std::hash<std::string> hash_fn; - NSUInteger hash_result = 0; - for (const std::string& segment : segments_) { - hash_result = hash_result * 31u + hash_fn(segment); - } - return hash_result; - } -#endif // defined(__OBJC__) - protected: BasePath() = default; template <typename IterT> diff --git a/Firestore/core/src/firebase/firestore/model/database_id.h b/Firestore/core/src/firebase/firestore/model/database_id.h index 0c0e0ec..c432b8f 100644 --- a/Firestore/core/src/firebase/firestore/model/database_id.h +++ b/Firestore/core/src/firebase/firestore/model/database_id.h @@ -20,6 +20,7 @@ #include <cstdint> #include <string> +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "absl/strings/string_view.h" namespace firebase { @@ -62,9 +63,8 @@ class DatabaseId { #if defined(__OBJC__) // For objective-c++ hash; to be removed after migration. // Do NOT use in C++ code. - NSUInteger Hash() const { - std::hash<std::string> hash_fn; - return hash_fn(project_id_) * 31u + hash_fn(database_id_); + size_t Hash() const { + return util::Hash(project_id_, database_id_); } #endif // defined(__OBJC__) diff --git a/Firestore/core/src/firebase/firestore/model/document_key.h b/Firestore/core/src/firebase/firestore/model/document_key.h index 4bdc04b..3f5f342 100644 --- a/Firestore/core/src/firebase/firestore/model/document_key.h +++ b/Firestore/core/src/firebase/firestore/model/document_key.h @@ -17,6 +17,7 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_H_ +#include <functional> #include <initializer_list> #include <memory> #include <string> @@ -26,6 +27,8 @@ #endif // defined(__OBJC__) #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/comparison.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "absl/strings/string_view.h" namespace firebase { @@ -56,15 +59,15 @@ class DocumentKey { return [FSTDocumentKey keyWithPath:path()]; } - std::string ToString() const { - return path().CanonicalString(); - } - NSUInteger Hash() const { - return std::hash<std::string>{}(ToString()); + return util::Hash(ToString()); } #endif + std::string ToString() const { + return path().CanonicalString(); + } + /** * Creates and returns a new document key using '/' to split the string into * segments. @@ -116,7 +119,20 @@ inline bool operator>=(const DocumentKey& lhs, const DocumentKey& rhs) { return lhs.path() >= rhs.path(); } +struct DocumentKeyHash { + size_t operator()(const DocumentKey& key) const { + return util::Hash(key.path()); + } +}; + } // namespace model + +namespace util { + +template <> +struct Comparator<model::DocumentKey> : public std::less<model::DocumentKey> {}; + +} // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/Source/Model/FSTDocumentKeySet.h b/Firestore/core/src/firebase/firestore/model/document_key_set.h index 80f6624..1301bf5 100644 --- a/Firestore/Source/Model/FSTDocumentKeySet.h +++ b/Firestore/core/src/firebase/firestore/model/document_key_set.h @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google + * 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. @@ -14,22 +14,21 @@ * limitations under the License. */ -#import <Foundation/Foundation.h> +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_SET_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_SET_H_ -#import "Firestore/third_party/Immutable/FSTImmutableSortedSet.h" +#include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" -@class FSTDocumentKey; - -NS_ASSUME_NONNULL_BEGIN +namespace firebase { +namespace firestore { +namespace model { /** Convenience type for a set of keys, since they are so common. */ -typedef FSTImmutableSortedSet<FSTDocumentKey *> FSTDocumentKeySet; - -@interface FSTImmutableSortedSet (FSTDocumentKey) - -/** Returns a new set using the DocumentKeyComparator. */ -+ (FSTDocumentKeySet *)keySet; +using DocumentKeySet = immutable::SortedSet<DocumentKey>; -@end +} // namespace model +} // namespace firestore +} // namespace firebase -NS_ASSUME_NONNULL_END +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DOCUMENT_KEY_SET_H_ diff --git a/Firestore/core/src/firebase/firestore/model/field_mask.h b/Firestore/core/src/firebase/firestore/model/field_mask.h index b895ab3..431e05a 100644 --- a/Firestore/core/src/firebase/firestore/model/field_mask.h +++ b/Firestore/core/src/firebase/firestore/model/field_mask.h @@ -23,6 +23,7 @@ #include <vector> #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" namespace firebase { namespace firestore { @@ -55,6 +56,22 @@ class FieldMask { return fields_.end(); } + /** + * Verifies that `fieldPath` is included by at least one field in this field + * mask. + * + * This is an O(n) operation, where `n` is the size of the field mask. + */ + bool covers(const FieldPath& fieldPath) const { + for (const FieldPath& fieldMaskPath : fields_) { + if (fieldMaskPath.IsPrefixOf(fieldPath)) { + return true; + } + } + + return false; + } + std::string ToString() const { // Ideally, one should use a string builder. Since this is only non-critical // code for logging and debugging, the logic is kept simple here. @@ -70,11 +87,7 @@ class FieldMask { } NSUInteger Hash() const { - NSUInteger hashResult = 0; - for (const FieldPath& field : fields_) { - hashResult = hashResult * 31u + field.Hash(); - } - return hashResult; + return util::Hash(fields_); } #endif diff --git a/Firestore/core/src/firebase/firestore/model/field_transform.h b/Firestore/core/src/firebase/firestore/model/field_transform.h index a1dd96c..1a7127a 100644 --- a/Firestore/core/src/firebase/firestore/model/field_transform.h +++ b/Firestore/core/src/firebase/firestore/model/field_transform.h @@ -22,6 +22,7 @@ #include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/transform_operations.h" +#include "Firestore/core/src/firebase/firestore/util/hashing.h" namespace firebase { namespace firestore { @@ -51,9 +52,7 @@ class FieldTransform { // For Objective-C++ hash; to be removed after migration. // Do NOT use in C++ code. NSUInteger Hash() const { - NSUInteger hash = path_.Hash(); - hash = hash * 31 + transformation_->Hash(); - return hash; + return util::Hash(path_, transformation_->Hash()); } #endif // defined(__OBJC__) diff --git a/Firestore/core/src/firebase/firestore/model/precondition.h b/Firestore/core/src/firebase/firestore/model/precondition.h index 4ab03c2..b98bb45 100644 --- a/Firestore/core/src/firebase/firestore/model/precondition.h +++ b/Firestore/core/src/firebase/firestore/model/precondition.h @@ -21,7 +21,6 @@ #if defined(__OBJC__) #import "FIRTimestamp.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" #import "Firestore/Source/Model/FSTDocument.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" #endif // defined(__OBJC__) diff --git a/Firestore/core/src/firebase/firestore/model/snapshot_version.h b/Firestore/core/src/firebase/firestore/model/snapshot_version.h index 1fbba1c..dbecea1 100644 --- a/Firestore/core/src/firebase/firestore/model/snapshot_version.h +++ b/Firestore/core/src/firebase/firestore/model/snapshot_version.h @@ -19,11 +19,6 @@ #include "Firestore/core/include/firebase/firestore/timestamp.h" -#if defined(__OBJC__) -#import "FIRTimestamp.h" -#import "Firestore/Source/Core/FSTSnapshotVersion.h" -#endif // defined(__OBJC__) - namespace firebase { namespace firestore { namespace model { @@ -34,6 +29,11 @@ namespace model { */ class SnapshotVersion { public: +#if __OBJC__ + SnapshotVersion() { + } +#endif // __OBJC__ + explicit SnapshotVersion(const Timestamp& timestamp); const Timestamp& timestamp() const { @@ -43,23 +43,11 @@ class SnapshotVersion { /** Creates a new version that is smaller than all other versions. */ static const SnapshotVersion& None(); -#if defined(__OBJC__) - SnapshotVersion(FSTSnapshotVersion* version) // NOLINT(runtime/explicit) - : timestamp_{version.timestamp.seconds, version.timestamp.nanoseconds} { - } - - operator FSTSnapshotVersion*() const { - if (timestamp_ == Timestamp{}) { - return [FSTSnapshotVersion noVersion]; - } else { - return [FSTSnapshotVersion - versionWithTimestamp:[FIRTimestamp - timestampWithSeconds:timestamp_.seconds() - nanoseconds:timestamp_ - .nanoseconds()]]; - } +#if __OBJC__ + size_t Hash() const { + return std::hash<Timestamp>{}(timestamp_); } -#endif // defined(__OBJC__) +#endif // __OBJC__ private: Timestamp timestamp_; diff --git a/Firestore/core/src/firebase/firestore/model/transform_operations.h b/Firestore/core/src/firebase/firestore/model/transform_operations.h index a1c2de0..8eed7ae 100644 --- a/Firestore/core/src/firebase/firestore/model/transform_operations.h +++ b/Firestore/core/src/firebase/firestore/model/transform_operations.h @@ -17,12 +17,16 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_TRANSFORM_OPERATIONS_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_TRANSFORM_OPERATIONS_H_ +#if !defined(__OBJC__) +#error "This header only supports Objective-C++." +#endif // !defined(__OBJC__) + #include <utility> #include <vector> -#if defined(__OBJC__) #import "Firestore/Source/Model/FSTFieldValue.h" -#endif + +#include "Firestore/core/include/firebase/firestore/timestamp.h" namespace firebase { namespace firestore { @@ -49,6 +53,20 @@ class TransformOperation { /** Returns the actual type. */ virtual Type type() const = 0; + /** + * Computes the local transform result against the provided `previousValue`, + * optionally using the provided localWriteTime. + */ + virtual FSTFieldValue* ApplyToLocalView( + FSTFieldValue* previousValue, FIRTimestamp* localWriteTime) const = 0; + + /** + * Computes a final transform result after the transform has been acknowledged + * by the server, potentially using the server-provided transformResult. + */ + virtual FSTFieldValue* ApplyToRemoteDocument( + FSTFieldValue* previousValue, FSTFieldValue* transformResult) const = 0; + /** Returns whether the two are equal. */ virtual bool operator==(const TransformOperation& other) const = 0; @@ -57,11 +75,9 @@ class TransformOperation { return !operator==(other); } -#if defined(__OBJC__) // For Objective-C++ hash; to be removed after migration. // Do NOT use in C++ code. virtual NSUInteger Hash() const = 0; -#endif // defined(__OBJC__) }; /** Transforms a value into a server-generated timestamp. */ @@ -74,6 +90,19 @@ class ServerTimestampTransform : public TransformOperation { return Type::ServerTimestamp; } + FSTFieldValue* ApplyToLocalView(FSTFieldValue* previousValue, + FIRTimestamp* localWriteTime) const override { + return [FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:localWriteTime + previousValue:previousValue]; + } + + FSTFieldValue* ApplyToRemoteDocument( + FSTFieldValue* /* previousValue */, + FSTFieldValue* transformResult) const override { + return transformResult; + } + bool operator==(const TransformOperation& other) const override { // All ServerTimestampTransform objects are equal. return other.type() == Type::ServerTimestamp; @@ -84,7 +113,6 @@ class ServerTimestampTransform : public TransformOperation { return shared_instance; } -#if defined(__OBJC__) // For Objective-C++ hash; to be removed after migration. // Do NOT use in C++ code. NSUInteger Hash() const override { @@ -92,16 +120,12 @@ class ServerTimestampTransform : public TransformOperation { // instances are equal. return 37; } -#endif // defined(__OBJC__) private: ServerTimestampTransform() { } }; -// TODO(mikelehen): ArrayTransform can only be used from Obj-C until we switch -// to using FieldValue instead of FSTFieldValue. -#if defined(__OBJC__) /** * Transforms an array via a union or remove operation (for convenience, we use * this class for both Type::ArrayUnion and Type::ArrayRemove). @@ -119,6 +143,21 @@ class ArrayTransform : public TransformOperation { return type_; } + FSTFieldValue* ApplyToLocalView( + FSTFieldValue* previousValue, + FIRTimestamp* /* localWriteTime */) const override { + return Apply(previousValue); + } + + FSTFieldValue* ApplyToRemoteDocument( + FSTFieldValue* previousValue, + FSTFieldValue* /* transformResult */) const override { + // The server just sends null as the transform result for array operations, + // so we have to calculate a result the same as we do for local + // applications. + return Apply(previousValue); + } + const std::vector<FSTFieldValue*>& elements() const { return elements_; } @@ -131,7 +170,7 @@ class ArrayTransform : public TransformOperation { if (array_transform.elements_.size() != elements_.size()) { return false; } - for (int i = 0; i < elements_.size(); i++) { + for (size_t i = 0; i < elements_.size(); i++) { if (![array_transform.elements_[i] isEqual:elements_[i]]) { return false; } @@ -139,7 +178,6 @@ class ArrayTransform : public TransformOperation { return true; } -#if defined(__OBJC__) // For Objective-C++ hash; to be removed after migration. // Do NOT use in C++ code. NSUInteger Hash() const override { @@ -150,7 +188,6 @@ class ArrayTransform : public TransformOperation { } return result; } -#endif // defined(__OBJC__) static const std::vector<FSTFieldValue*>& Elements( const TransformOperation& op) { @@ -162,8 +199,39 @@ class ArrayTransform : public TransformOperation { private: Type type_; std::vector<FSTFieldValue*> elements_; + + /** + * Inspects the provided value, returning a mutable copy of the internal array + * if it's an FSTArrayValue and an empty mutable array if it's nil or any + * other type of FSTFieldValue. + */ + static NSMutableArray<FSTFieldValue*>* CoercedFieldValuesArray( + FSTFieldValue* value) { + if ([value isMemberOfClass:[FSTArrayValue class]]) { + return [NSMutableArray + arrayWithArray:reinterpret_cast<FSTArrayValue*>(value).internalValue]; + } else { + // coerce to empty array. + return [NSMutableArray array]; + } + } + + FSTFieldValue* Apply(FSTFieldValue* previousValue) const { + NSMutableArray<FSTFieldValue*>* result = + ArrayTransform::CoercedFieldValuesArray(previousValue); + for (FSTFieldValue* element : elements_) { + if (type_ == Type::ArrayUnion) { + if (![result containsObject:element]) { + [result addObject:element]; + } + } else { + FIREBASE_ASSERT(type_ == Type::ArrayRemove); + [result removeObject:element]; + } + } + return [[FSTArrayValue alloc] initWithValueNoCopy:result]; + } }; -#endif } // namespace model } // namespace firestore diff --git a/Firestore/core/src/firebase/firestore/nanopb/CMakeLists.txt b/Firestore/core/src/firebase/firestore/nanopb/CMakeLists.txt new file mode 100644 index 0000000..82ffb65 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/nanopb/CMakeLists.txt @@ -0,0 +1,27 @@ +# 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. + +cc_library( + firebase_firestore_nanopb + SOURCES + tag.h + reader.h + reader.cc + writer.h + writer.cc + DEPENDS + firebase_firestore_util + firebase_firestore_protos_nanopb + nanopb +) diff --git a/Firestore/core/src/firebase/firestore/nanopb/reader.cc b/Firestore/core/src/firebase/firestore/nanopb/reader.cc new file mode 100644 index 0000000..86e38ac --- /dev/null +++ b/Firestore/core/src/firebase/firestore/nanopb/reader.cc @@ -0,0 +1,141 @@ +/* + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/nanopb/reader.h" + +#include "Firestore/Protos/nanopb/google/firestore/v1beta1/document.pb.h" + +namespace firebase { +namespace firestore { +namespace nanopb { + +using firebase::firestore::util::Status; +using std::int64_t; +using std::uint64_t; + +Reader Reader::Wrap(const uint8_t* bytes, size_t length) { + return Reader{pb_istream_from_buffer(bytes, length)}; +} + +Tag Reader::ReadTag() { + Tag tag; + if (!status_.ok()) return tag; + + bool eof; + if (!pb_decode_tag(&stream_, &tag.wire_type, &tag.field_number, &eof)) { + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + return tag; + } + + // nanopb code always returns a false status when setting eof. + FIREBASE_ASSERT_MESSAGE(!eof, "nanopb set both ok status and eof to true"); + + return tag; +} + +void Reader::ReadNanopbMessage(const pb_field_t fields[], void* dest_struct) { + if (!status_.ok()) return; + + if (!pb_decode(&stream_, fields, dest_struct)) { + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + } +} + +/** + * Note that (despite the return type) this works for bool, enum, int32, int64, + * uint32 and uint64 proto field types. + * + * Note: This is not expected to be called directly, but rather only via the + * other Decode* methods (i.e. DecodeBool, DecodeLong, etc) + * + * @return The decoded varint as a uint64_t. + */ +uint64_t Reader::ReadVarint() { + if (!status_.ok()) return 0; + + uint64_t varint_value = 0; + if (!pb_decode_varint(&stream_, &varint_value)) { + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + } + return varint_value; +} + +void Reader::ReadNull() { + uint64_t varint = ReadVarint(); + if (!status_.ok()) return; + + if (varint != google_protobuf_NullValue_NULL_VALUE) { + status_ = Status(FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (invalid null value)"); + } +} + +bool Reader::ReadBool() { + uint64_t varint = ReadVarint(); + if (!status_.ok()) return false; + + switch (varint) { + case 0: + return false; + case 1: + return true; + default: + status_ = + Status(FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (invalid bool value)"); + return false; + } +} + +int64_t Reader::ReadInteger() { + return ReadVarint(); +} + +std::string Reader::ReadString() { + if (!status_.ok()) return ""; + + pb_istream_t substream; + if (!pb_make_string_substream(&stream_, &substream)) { + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + pb_close_string_substream(&stream_, &substream); + return ""; + } + + std::string result(substream.bytes_left, '\0'); + if (!pb_read(&substream, reinterpret_cast<pb_byte_t*>(&result[0]), + substream.bytes_left)) { + status_ = Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + pb_close_string_substream(&stream_, &substream); + return ""; + } + + // NB: future versions of nanopb read the remaining characters out of the + // substream (and return false if that fails) as an additional safety + // check within pb_close_string_substream. Unfortunately, that's not present + // in the current version (0.38). We'll make a stronger assertion and check + // to make sure there *are* no remaining characters in the substream. + FIREBASE_ASSERT_MESSAGE( + substream.bytes_left == 0, + "Bytes remaining in substream after supposedly reading all of them."); + + pb_close_string_substream(&stream_, &substream); + + return result; +} + +} // namespace nanopb +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/nanopb/reader.h b/Firestore/core/src/firebase/firestore/nanopb/reader.h new file mode 100644 index 0000000..093e20b --- /dev/null +++ b/Firestore/core/src/firebase/firestore/nanopb/reader.h @@ -0,0 +1,175 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_READER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_READER_H_ + +#include <pb.h> +#include <pb_decode.h> + +#include <cstdint> +#include <functional> +#include <string> + +#include "Firestore/core/include/firebase/firestore/firestore_errors.h" +#include "Firestore/core/src/firebase/firestore/nanopb/tag.h" +#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" + +namespace firebase { +namespace firestore { +namespace nanopb { + +/** + * Docs TODO(rsgowman). But currently, this just wraps the underlying nanopb + * pb_istream_t. + */ +class Reader { + public: + /** + * Creates an input stream that reads from the specified bytes. Note that + * this reference must remain valid for the lifetime of this Reader. + * + * (This is roughly equivalent to the nanopb function + * pb_istream_from_buffer()) + * + * @param bytes where the input should be deserialized from. + */ + static Reader Wrap(const uint8_t* bytes, size_t length); + + /** + * Reads a message type from the input stream. + * + * This essentially wraps calls to nanopb's pb_decode_tag() method. + */ + Tag ReadTag(); + + /** + * Reads a nanopb message from the input stream. + * + * This essentially wraps calls to nanopb's pb_decode() method. If we didn't + * use `oneof`s in our protos, this would be the primary way of decoding + * messages. + */ + void ReadNanopbMessage(const pb_field_t fields[], void* dest_struct); + + void ReadNull(); + bool ReadBool(); + std::int64_t ReadInteger(); + + std::string ReadString(); + + /** + * Reads a message and its length. + * + * Analog to Writer::WriteNestedMessage(). See that methods docs for further + * details. + * + * Call this method when reading a nested message. Provide a function to read + * the message itself. + * + * @param read_message_fn Function to read the submessage. Note that this + * function is expected to check the Reader's status (via + * Reader::status().ok()) and if not ok, to return a placeholder/invalid + * value. + */ + template <typename T> + T ReadNestedMessage(const std::function<T(Reader*)>& read_message_fn); + + size_t bytes_left() const { + return stream_.bytes_left; + } + + util::Status status() const { + return status_; + } + + void set_status(util::Status status) { + status_ = status; + } + + private: + /** + * Creates a new Reader, based on the given nanopb pb_istream_t. Note that + * a shallow copy will be taken. (Non-null pointers within this struct must + * remain valid for the lifetime of this Reader.) + */ + explicit Reader(pb_istream_t stream) : stream_(stream) { + } + + /** + * Reads a "varint" from the input stream. + * + * This essentially wraps calls to nanopb's pb_decode_varint() method. + * + * Note that (despite the return type) this works for bool, enum, int32, + * int64, uint32 and uint64 proto field types. + * + * Note: This is not expected to be called direclty, but rather only via the + * other Decode* methods (i.e. DecodeBool, DecodeLong, etc) + * + * @return The decoded varint as a uint64_t. + */ + std::uint64_t ReadVarint(); + + util::Status status_ = util::Status::OK(); + + pb_istream_t stream_; +}; + +template <typename T> +T Reader::ReadNestedMessage(const std::function<T(Reader*)>& read_message_fn) { + // Implementation note: This is roughly modeled on pb_decode_delimited, + // adjusted to account for the oneof in FieldValue. + + if (!status_.ok()) return read_message_fn(this); + + pb_istream_t raw_substream; + if (!pb_make_string_substream(&stream_, &raw_substream)) { + status_ = + util::Status(FirestoreErrorCode::DataLoss, PB_GET_ERROR(&stream_)); + pb_close_string_substream(&stream_, &raw_substream); + return read_message_fn(this); + } + Reader substream(raw_substream); + + // If this fails, we *won't* return right away so that we can cleanup the + // substream (although technically, that turns out not to matter; no resource + // leaks occur if we don't do this.) + // TODO(rsgowman): Consider RAII here. (Watch out for Reader class which also + // wraps streams.) + T message = read_message_fn(&substream); + status_ = substream.status(); + + // NB: future versions of nanopb read the remaining characters out of the + // substream (and return false if that fails) as an additional safety + // check within pb_close_string_substream. Unfortunately, that's not present + // in the current version (0.38). We'll make a stronger assertion and check + // to make sure there *are* no remaining characters in the substream. + FIREBASE_ASSERT_MESSAGE( + substream.bytes_left() == 0, + "Bytes remaining in substream after supposedly reading all of them."); + + pb_close_string_substream(&stream_, &substream.stream_); + + return message; +} + +} // namespace nanopb +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_READER_H_ diff --git a/Firestore/core/src/firebase/firestore/nanopb/tag.h b/Firestore/core/src/firebase/firestore/nanopb/tag.h new file mode 100644 index 0000000..455ef0c --- /dev/null +++ b/Firestore/core/src/firebase/firestore/nanopb/tag.h @@ -0,0 +1,43 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_TAG_H__ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_TAG_H__ + +#include <pb.h> + +namespace firebase { +namespace firestore { +namespace nanopb { + +/** + * Represents a nanopb tag. + * + * field_number is one of the field tags that nanopb generates based off of + * the proto messages. They're typically named in the format: + * <parentNameSpace>_<childNameSpace>_<message>_<field>_tag, e.g. + * google_firestore_v1beta1_Document_name_tag. + */ +struct Tag { + pb_wire_type_t wire_type; + uint32_t field_number; +}; + +} // namespace nanopb +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_TAG_H_ diff --git a/Firestore/core/src/firebase/firestore/nanopb/writer.cc b/Firestore/core/src/firebase/firestore/nanopb/writer.cc new file mode 100644 index 0000000..cbee989 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/nanopb/writer.cc @@ -0,0 +1,165 @@ +/* + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/nanopb/writer.h" + +#include "Firestore/Protos/nanopb/google/firestore/v1beta1/document.pb.h" + +namespace firebase { +namespace firestore { +namespace nanopb { + +using firebase::firestore::util::Status; +using std::int64_t; +using std::int8_t; +using std::uint64_t; + +Writer Writer::Wrap(std::vector<uint8_t>* out_bytes) { + // TODO(rsgowman): find a better home for this constant. + // A document is defined to have a max size of 1MiB - 4 bytes. + static const size_t kMaxDocumentSize = 1 * 1024 * 1024 - 4; + + // Construct a nanopb output stream. + // + // Set the max_size to be the max document size (as an upper bound; one would + // expect individual FieldValue's to be smaller than this). + // + // bytes_written is (always) initialized to 0. (NB: nanopb does not know or + // care about the underlying output vector, so where we are in the vector + // itself is irrelevant. i.e. don't use out_bytes->size()) + pb_ostream_t raw_stream = { + /*callback=*/[](pb_ostream_t* stream, const pb_byte_t* buf, + size_t count) -> bool { + auto* out_bytes = static_cast<std::vector<uint8_t>*>(stream->state); + out_bytes->insert(out_bytes->end(), buf, buf + count); + return true; + }, + /*state=*/out_bytes, + /*max_size=*/kMaxDocumentSize, + /*bytes_written=*/0, + /*errmsg=*/nullptr}; + return Writer(raw_stream); +} + +void Writer::WriteTag(Tag tag) { + if (!status_.ok()) return; + + if (!pb_encode_tag(&stream_, tag.wire_type, tag.field_number)) { + FIREBASE_ASSERT_MESSAGE(false, PB_GET_ERROR(&stream_)); + } +} + +void Writer::WriteNanopbMessage(const pb_field_t fields[], + const void* src_struct) { + if (!status_.ok()) return; + + if (!pb_encode(&stream_, fields, src_struct)) { + FIREBASE_ASSERT_MESSAGE(false, PB_GET_ERROR(&stream_)); + } +} + +void Writer::WriteSize(size_t size) { + return WriteVarint(size); +} + +void Writer::WriteVarint(uint64_t value) { + if (!status_.ok()) return; + + if (!pb_encode_varint(&stream_, value)) { + FIREBASE_ASSERT_MESSAGE(false, PB_GET_ERROR(&stream_)); + } +} + +void Writer::WriteNull() { + return WriteVarint(google_protobuf_NullValue_NULL_VALUE); +} + +void Writer::WriteBool(bool bool_value) { + return WriteVarint(bool_value); +} + +void Writer::WriteInteger(int64_t integer_value) { + return WriteVarint(integer_value); +} + +void Writer::WriteString(const std::string& string_value) { + if (!status_.ok()) return; + + if (!pb_encode_string( + &stream_, reinterpret_cast<const pb_byte_t*>(string_value.c_str()), + string_value.length())) { + FIREBASE_ASSERT_MESSAGE(false, PB_GET_ERROR(&stream_)); + } +} + +void Writer::WriteNestedMessage( + const std::function<void(Writer*)>& write_message_fn) { + if (!status_.ok()) return; + + // First calculate the message size using a non-writing substream. + Writer sizer = Writer::Sizing(); + write_message_fn(&sizer); + status_ = sizer.status(); + if (!status_.ok()) return; + size_t size = sizer.bytes_written(); + + // Write out the size to the output writer. + WriteSize(size); + if (!status_.ok()) return; + + // If this stream is itself a sizing stream, then we don't need to actually + // parse field_value a second time; just update the bytes_written via a call + // to pb_write. (If we try to write the contents into a sizing stream, it'll + // fail since sizing streams don't actually have any buffer space.) + if (stream_.callback == nullptr) { + if (!pb_write(&stream_, nullptr, size)) { + FIREBASE_ASSERT_MESSAGE(false, PB_GET_ERROR(&stream_)); + } + return; + } + + // Ensure the output stream has enough space + if (stream_.bytes_written + size > stream_.max_size) { + FIREBASE_ASSERT_MESSAGE( + false, + "Insufficient space in the output stream to write the given message"); + } + + // Use a substream to verify that a callback doesn't write more than what it + // did the first time. (Use an initializer rather than setting fields + // individually like nanopb does. This gives us a *chance* of noticing if + // nanopb adds new fields.) + Writer writer({stream_.callback, stream_.state, + /*max_size=*/size, /*bytes_written=*/0, + /*errmsg=*/nullptr}); + write_message_fn(&writer); + status_ = writer.status(); + if (!status_.ok()) return; + + stream_.bytes_written += writer.stream_.bytes_written; + stream_.state = writer.stream_.state; + stream_.errmsg = writer.stream_.errmsg; + + if (writer.bytes_written() != size) { + // submsg size changed + FIREBASE_ASSERT_MESSAGE( + false, "Parsing the nested message twice yielded different sizes"); + } +} + +} // namespace nanopb +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/nanopb/writer.h b/Firestore/core/src/firebase/firestore/nanopb/writer.h new file mode 100644 index 0000000..e428826 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/nanopb/writer.h @@ -0,0 +1,140 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_WRITER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_WRITER_H_ + +#include <pb.h> +#include <pb_encode.h> + +#include <cstdint> +#include <functional> +#include <string> +#include <vector> + +#include "Firestore/core/src/firebase/firestore/nanopb/tag.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" + +namespace firebase { +namespace firestore { +namespace nanopb { + +/** + * Docs TODO(rsgowman). But currently, this just wraps the underlying nanopb + * pb_ostream_t. Also doc how to check status. + */ +class Writer { + public: + /** + * Creates an output stream that writes to the specified vector. Note that + * this vector pointer must remain valid for the lifetime of this Writer. + * + * (This is roughly equivalent to the nanopb function + * pb_ostream_from_buffer()) + * + * @param out_bytes where the output should be serialized to. + */ + static Writer Wrap(std::vector<std::uint8_t>* out_bytes); + + /** + * Creates a non-writing output stream used to calculate the size of + * the serialized output. + */ + static Writer Sizing() { + return Writer(PB_OSTREAM_SIZING); + } + + /** + * Writes a message type to the output stream. + * + * This essentially wraps calls to nanopb's pb_encode_tag() method. + */ + void WriteTag(Tag tag); + + /** + * Writes a nanopb message to the output stream. + * + * This essentially wraps calls to nanopb's `pb_encode()` method. If we didn't + * use `oneof`s in our protos, this would be the primary way of encoding + * messages. + */ + void WriteNanopbMessage(const pb_field_t fields[], const void* src_struct); + + void WriteSize(size_t size); + void WriteNull(); + void WriteBool(bool bool_value); + void WriteInteger(std::int64_t integer_value); + + void WriteString(const std::string& string_value); + + /** + * Writes a message and its length. + * + * When writing a top level message, protobuf doesn't include the length + * (since you can get that already from the length of the binary output.) But + * when writing a sub/nested message, you must include the length in the + * serialization. + * + * Call this method when writing a nested message. Provide a function to + * write the message itself. This method will calculate the size of the + * written message (using the provided function with a non-writing sizing + * stream), write out the size (and perform sanity checks), and then serialize + * the message by calling the provided function a second time. + */ + void WriteNestedMessage(const std::function<void(Writer*)>& write_message_fn); + + size_t bytes_written() const { + return stream_.bytes_written; + } + + util::Status status() const { + return status_; + } + + private: + util::Status status_ = util::Status::OK(); + + /** + * Creates a new Writer, based on the given nanopb pb_ostream_t. Note that + * a shallow copy will be taken. (Non-null pointers within this struct must + * remain valid for the lifetime of this Writer.) + */ + explicit Writer(const pb_ostream_t& stream) : stream_(stream) { + } + + /** + * Writes a "varint" to the output stream. + * + * This essentially wraps calls to nanopb's pb_encode_varint() method. + * + * Note that (despite the value parameter type) this works for bool, enum, + * int32, int64, uint32 and uint64 proto field types. + * + * Note: This is not expected to be called directly, but rather only + * via the other Write* methods (i.e. WriteBool, WriteLong, etc) + * + * @param value The value to write, represented as a uint64_t. + */ + void WriteVarint(std::uint64_t value); + + pb_ostream_t stream_; +}; + +} // namespace nanopb +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_NANOPB_WRITER_H_ diff --git a/Firestore/core/src/firebase/firestore/remote/CMakeLists.txt b/Firestore/core/src/firebase/firestore/remote/CMakeLists.txt index 7f528fb..fc51b37 100644 --- a/Firestore/core/src/firebase/firestore/remote/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/remote/CMakeLists.txt @@ -21,7 +21,9 @@ cc_library( serializer.cc DEPENDS firebase_firestore_model + firebase_firestore_nanopb firebase_firestore_protos_nanopb + firebase_firestore_util grpc::grpc nanopb ) diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.cc b/Firestore/core/src/firebase/firestore/remote/serializer.cc index b5a0720..6240c21 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.cc +++ b/Firestore/core/src/firebase/firestore/remote/serializer.cc @@ -25,364 +25,84 @@ #include <utility> #include "Firestore/Protos/nanopb/google/firestore/v1beta1/document.pb.h" +#include "Firestore/Protos/nanopb/google/firestore/v1beta1/firestore.pb.h" +#include "Firestore/core/include/firebase/firestore/firestore_errors.h" +#include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/src/firebase/firestore/model/document.h" +#include "Firestore/core/src/firebase/firestore/model/no_document.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/nanopb/reader.h" +#include "Firestore/core/src/firebase/firestore/nanopb/tag.h" +#include "Firestore/core/src/firebase/firestore/nanopb/writer.h" +#include "Firestore/core/src/firebase/firestore/timestamp_internal.h" #include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "absl/memory/memory.h" namespace firebase { namespace firestore { namespace remote { +using firebase::Timestamp; +using firebase::TimestampInternal; +using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::Document; +using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldValue; +using firebase::firestore::model::MaybeDocument; +using firebase::firestore::model::NoDocument; using firebase::firestore::model::ObjectValue; +using firebase::firestore::model::ResourcePath; +using firebase::firestore::model::SnapshotVersion; +using firebase::firestore::nanopb::Reader; +using firebase::firestore::nanopb::Tag; +using firebase::firestore::nanopb::Writer; using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; namespace { -class Writer; - -class Reader; - void EncodeObject(Writer* writer, const ObjectValue& object_value); -ObjectValue DecodeObject(Reader* reader); - -/** - * Represents a nanopb tag. - * - * field_number is one of the field tags that nanopb generates based off of - * the proto messages. They're typically named in the format: - * <parentNameSpace>_<childNameSpace>_<message>_<field>_tag, e.g. - * google_firestore_v1beta1_Document_name_tag. - */ -struct Tag { - pb_wire_type_t wire_type; - uint32_t field_number; -}; +ObjectValue::Map DecodeObject(Reader* reader); -/** - * Docs TODO(rsgowman). But currently, this just wraps the underlying nanopb - * pb_ostream_t. Also doc how to check status. - */ -class Writer { - public: - /** - * Creates an output stream that writes to the specified vector. Note that - * this vector pointer must remain valid for the lifetime of this Writer. - * - * (This is roughly equivalent to the nanopb function - * pb_ostream_from_buffer()) - * - * @param out_bytes where the output should be serialized to. - */ - static Writer Wrap(std::vector<uint8_t>* out_bytes); - - /** - * Creates a non-writing output stream used to calculate the size of - * the serialized output. - */ - static Writer Sizing() { - return Writer(PB_OSTREAM_SIZING); - } - - /** - * Writes a message type to the output stream. - * - * This essentially wraps calls to nanopb's pb_encode_tag() method. - */ - void WriteTag(Tag tag); - - void WriteSize(size_t size); - void WriteNull(); - void WriteBool(bool bool_value); - void WriteInteger(int64_t integer_value); - - void WriteString(const std::string& string_value); - - /** - * Writes a message and its length. - * - * When writing a top level message, protobuf doesn't include the length - * (since you can get that already from the length of the binary output.) But - * when writing a sub/nested message, you must include the length in the - * serialization. - * - * Call this method when writing a nested message. Provide a function to - * write the message itself. This method will calculate the size of the - * written message (using the provided function with a non-writing sizing - * stream), write out the size (and perform sanity checks), and then serialize - * the message by calling the provided function a second time. - */ - void WriteNestedMessage(const std::function<void(Writer*)>& write_message_fn); - - size_t bytes_written() const { - return stream_.bytes_written; - } - - Status status() const { - return status_; - } - - private: - Status status_ = Status::OK(); - - /** - * Creates a new Writer, based on the given nanopb pb_ostream_t. Note that - * a shallow copy will be taken. (Non-null pointers within this struct must - * remain valid for the lifetime of this Writer.) - */ - explicit Writer(const pb_ostream_t& stream) : stream_(stream) { - } - - /** - * Writes a "varint" to the output stream. - * - * This essentially wraps calls to nanopb's pb_encode_varint() method. - * - * Note that (despite the value parameter type) this works for bool, enum, - * int32, int64, uint32 and uint64 proto field types. - * - * Note: This is not expected to be called directly, but rather only - * via the other Write* methods (i.e. WriteBool, WriteLong, etc) - * - * @param value The value to write, represented as a uint64_t. - */ - void WriteVarint(uint64_t value); - - pb_ostream_t stream_; -}; - -/** - * Docs TODO(rsgowman). But currently, this just wraps the underlying nanopb - * pb_istream_t. - */ -class Reader { - public: - /** - * Creates an input stream that reads from the specified bytes. Note that - * this reference must remain valid for the lifetime of this Reader. - * - * (This is roughly equivalent to the nanopb function - * pb_istream_from_buffer()) - * - * @param bytes where the input should be deserialized from. - */ - static Reader Wrap(const uint8_t* bytes, size_t length); - - /** - * Reads a message type from the input stream. - * - * This essentially wraps calls to nanopb's pb_decode_tag() method. - */ - Tag ReadTag(); - - void ReadNull(); - bool ReadBool(); - int64_t ReadInteger(); - - std::string ReadString(); - - /** - * Reads a message and its length. - * - * Analog to Writer::WriteNestedMessage(). See that methods docs for further - * details. - * - * Call this method when reading a nested message. Provide a function to read - * the message itself. - */ - template <typename T> - T ReadNestedMessage(const std::function<T(Reader*)>& read_message_fn); - - size_t bytes_left() const { - return stream_.bytes_left; - } - - private: - /** - * Creates a new Reader, based on the given nanopb pb_istream_t. Note that - * a shallow copy will be taken. (Non-null pointers within this struct must - * remain valid for the lifetime of this Reader.) - */ - explicit Reader(pb_istream_t stream) : stream_(stream) { - } - - /** - * Reads a "varint" from the input stream. - * - * This essentially wraps calls to nanopb's pb_decode_varint() method. - * - * Note that (despite the return type) this works for bool, enum, int32, - * int64, uint32 and uint64 proto field types. - * - * Note: This is not expected to be called direclty, but rather only via the - * other Decode* methods (i.e. DecodeBool, DecodeLong, etc) - * - * @return The decoded varint as a uint64_t. - */ - uint64_t ReadVarint(); - - pb_istream_t stream_; -}; - -Writer Writer::Wrap(std::vector<uint8_t>* out_bytes) { - // TODO(rsgowman): find a better home for this constant. - // A document is defined to have a max size of 1MiB - 4 bytes. - static const size_t kMaxDocumentSize = 1 * 1024 * 1024 - 4; - - // Construct a nanopb output stream. - // - // Set the max_size to be the max document size (as an upper bound; one would - // expect individual FieldValue's to be smaller than this). - // - // bytes_written is (always) initialized to 0. (NB: nanopb does not know or - // care about the underlying output vector, so where we are in the vector - // itself is irrelevant. i.e. don't use out_bytes->size()) - pb_ostream_t raw_stream = { - /*callback=*/[](pb_ostream_t* stream, const pb_byte_t* buf, - size_t count) -> bool { - auto* out_bytes = static_cast<std::vector<uint8_t>*>(stream->state); - out_bytes->insert(out_bytes->end(), buf, buf + count); - return true; - }, - /*state=*/out_bytes, - /*max_size=*/kMaxDocumentSize, - /*bytes_written=*/0, - /*errmsg=*/nullptr}; - return Writer(raw_stream); -} - -Reader Reader::Wrap(const uint8_t* bytes, size_t length) { - return Reader{pb_istream_from_buffer(bytes, length)}; -} - -// TODO(rsgowman): I've left the methods as near as possible to where they were -// before, which implies that the Writer methods are interspersed with the -// Reader methods. This should make it a bit easier to review. Refactor these to -// group the related methods together (probably within their own file rather -// than here). - -void Writer::WriteTag(Tag tag) { - if (!status_.ok()) return; - - if (!pb_encode_tag(&stream_, tag.wire_type, tag.field_number)) { - FIREBASE_ASSERT_MESSAGE(false, PB_GET_ERROR(&stream_)); - } +void EncodeTimestamp(Writer* writer, const Timestamp& timestamp_value) { + google_protobuf_Timestamp timestamp_proto = + google_protobuf_Timestamp_init_zero; + timestamp_proto.seconds = timestamp_value.seconds(); + timestamp_proto.nanos = timestamp_value.nanoseconds(); + writer->WriteNanopbMessage(google_protobuf_Timestamp_fields, + ×tamp_proto); } -Tag Reader::ReadTag() { - Tag tag; - bool eof; - bool ok = pb_decode_tag(&stream_, &tag.wire_type, &tag.field_number, &eof); - if (!ok || eof) { - // TODO(rsgowman): figure out error handling - abort(); +Timestamp DecodeTimestamp(Reader* reader) { + if (!reader->status().ok()) return {}; + + google_protobuf_Timestamp timestamp_proto = + google_protobuf_Timestamp_init_zero; + reader->ReadNanopbMessage(google_protobuf_Timestamp_fields, ×tamp_proto); + + // The Timestamp ctor will assert if we provide values outside the valid + // range. However, since we're decoding, a single corrupt byte could cause + // this to occur, so we'll verify the ranges before passing them in since we'd + // rather not abort in these situations. + if (timestamp_proto.seconds < TimestampInternal::Min().seconds()) { + reader->set_status(Status( + FirestoreErrorCode::DataLoss, + "Invalid message: timestamp beyond the earliest supported date")); + return {}; + } else if (TimestampInternal::Max().seconds() < timestamp_proto.seconds) { + reader->set_status( + Status(FirestoreErrorCode::DataLoss, + "Invalid message: timestamp behond the latest supported date")); + return {}; + } else if (timestamp_proto.nanos < 0 || timestamp_proto.nanos > 999999999) { + reader->set_status(Status( + FirestoreErrorCode::DataLoss, + "Invalid message: timestamp nanos must be between 0 and 999999999")); + return {}; } - return tag; -} - -void Writer::WriteSize(size_t size) { - return WriteVarint(size); -} - -void Writer::WriteVarint(uint64_t value) { - if (!status_.ok()) return; - - if (!pb_encode_varint(&stream_, value)) { - FIREBASE_ASSERT_MESSAGE(false, PB_GET_ERROR(&stream_)); - } -} - -/** - * Note that (despite the return type) this works for bool, enum, int32, int64, - * uint32 and uint64 proto field types. - * - * Note: This is not expected to be called directly, but rather only via the - * other Decode* methods (i.e. DecodeBool, DecodeLong, etc) - * - * @return The decoded varint as a uint64_t. - */ -uint64_t Reader::ReadVarint() { - uint64_t varint_value; - if (!pb_decode_varint(&stream_, &varint_value)) { - // TODO(rsgowman): figure out error handling - abort(); - } - return varint_value; -} - -void Writer::WriteNull() { - return WriteVarint(google_protobuf_NullValue_NULL_VALUE); -} - -void Reader::ReadNull() { - uint64_t varint = ReadVarint(); - if (varint != google_protobuf_NullValue_NULL_VALUE) { - // TODO(rsgowman): figure out error handling - abort(); - } -} - -void Writer::WriteBool(bool bool_value) { - return WriteVarint(bool_value); -} - -bool Reader::ReadBool() { - uint64_t varint = ReadVarint(); - switch (varint) { - case 0: - return false; - case 1: - return true; - default: - // TODO(rsgowman): figure out error handling - abort(); - } -} - -void Writer::WriteInteger(int64_t integer_value) { - return WriteVarint(integer_value); -} - -int64_t Reader::ReadInteger() { - return ReadVarint(); -} - -void Writer::WriteString(const std::string& string_value) { - if (!status_.ok()) return; - - if (!pb_encode_string( - &stream_, reinterpret_cast<const pb_byte_t*>(string_value.c_str()), - string_value.length())) { - FIREBASE_ASSERT_MESSAGE(false, PB_GET_ERROR(&stream_)); - } -} - -std::string Reader::ReadString() { - pb_istream_t substream; - if (!pb_make_string_substream(&stream_, &substream)) { - // TODO(rsgowman): figure out error handling - abort(); - } - - std::string result(substream.bytes_left, '\0'); - if (!pb_read(&substream, reinterpret_cast<pb_byte_t*>(&result[0]), - substream.bytes_left)) { - // TODO(rsgowman): figure out error handling - abort(); - } - - // NB: future versions of nanopb read the remaining characters out of the - // substream (and return false if that fails) as an additional safety - // check within pb_close_string_substream. Unfortunately, that's not present - // in the current version (0.38). We'll make a stronger assertion and check - // to make sure there *are* no remaining characters in the substream. - if (substream.bytes_left != 0) { - // TODO(rsgowman): figure out error handling - abort(); - } - - pb_close_string_substream(&stream_, &substream); - - return result; + return Timestamp{timestamp_proto.seconds, timestamp_proto.nanos}; } // Named '..Impl' so as to not conflict with Serializer::EncodeFieldValue. @@ -417,6 +137,14 @@ void EncodeFieldValueImpl(Writer* writer, const FieldValue& field_value) { writer->WriteString(field_value.string_value()); break; + case FieldValue::Type::Timestamp: + writer->WriteTag( + {PB_WT_STRING, google_firestore_v1beta1_Value_timestamp_value_tag}); + writer->WriteNestedMessage([&field_value](Writer* writer) { + EncodeTimestamp(writer, field_value.timestamp_value()); + }); + break; + case FieldValue::Type::Object: writer->WriteTag( {PB_WT_STRING, google_firestore_v1beta1_Value_map_value_tag}); @@ -430,30 +158,56 @@ void EncodeFieldValueImpl(Writer* writer, const FieldValue& field_value) { } FieldValue DecodeFieldValueImpl(Reader* reader) { + if (!reader->status().ok()) return FieldValue::NullValue(); + Tag tag = reader->ReadTag(); + if (!reader->status().ok()) return FieldValue::NullValue(); // Ensure the tag matches the wire type - // TODO(rsgowman): figure out error handling switch (tag.field_number) { case google_firestore_v1beta1_Value_null_value_tag: case google_firestore_v1beta1_Value_boolean_value_tag: case google_firestore_v1beta1_Value_integer_value_tag: if (tag.wire_type != PB_WT_VARINT) { - abort(); + reader->set_status( + Status(FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (mismatch between " + "the wiretype and the field number (tag))")); } break; case google_firestore_v1beta1_Value_string_value_tag: + case google_firestore_v1beta1_Value_timestamp_value_tag: case google_firestore_v1beta1_Value_map_value_tag: if (tag.wire_type != PB_WT_STRING) { - abort(); + reader->set_status( + Status(FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (mismatch between " + "the wiretype and the field number (tag))")); } break; default: - abort(); + // We could get here for one of two reasons; either because the input + // bytes are corrupt, or because we're attempting to parse a tag that we + // haven't implemented yet. Long term, the latter reason should become + // less likely (especially in production), so we'll assume former. + + // TODO(rsgowman): While still in development, we'll contradict the above + // and assume the latter. Remove the following assertion when we're + // confident that we're handling all the tags in the protos. + FIREBASE_ASSERT_MESSAGE( + false, + "Unhandled message field number (tag): %i. (Or possibly " + "corrupt input bytes)", + tag.field_number); + reader->set_status(Status( + FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (invalid field number (tag))")); } + if (!reader->status().ok()) return FieldValue::NullValue(); + switch (tag.field_number) { case google_firestore_v1beta1_Value_null_value_tag: reader->ReadNull(); @@ -464,98 +218,22 @@ FieldValue DecodeFieldValueImpl(Reader* reader) { return FieldValue::IntegerValue(reader->ReadInteger()); case google_firestore_v1beta1_Value_string_value_tag: return FieldValue::StringValue(reader->ReadString()); + case google_firestore_v1beta1_Value_timestamp_value_tag: + return FieldValue::TimestampValue( + reader->ReadNestedMessage<Timestamp>(DecodeTimestamp)); case google_firestore_v1beta1_Value_map_value_tag: - return FieldValue::ObjectValueFromMap( - DecodeObject(reader).internal_value); + return FieldValue::ObjectValueFromMap(DecodeObject(reader)); default: - // TODO(rsgowman): figure out error handling - abort(); + // This indicates an internal error as we've already ensured that this is + // a valid field_number. + FIREBASE_ASSERT_MESSAGE( + false, + "Somehow got an unexpected field number (tag) after verifying that " + "the field number was expected."); } } -void Writer::WriteNestedMessage( - const std::function<void(Writer*)>& write_message_fn) { - if (!status_.ok()) return; - - // First calculate the message size using a non-writing substream. - Writer sizer = Writer::Sizing(); - write_message_fn(&sizer); - status_ = sizer.status(); - if (!status_.ok()) return; - size_t size = sizer.bytes_written(); - - // Write out the size to the output writer. - WriteSize(size); - if (!status_.ok()) return; - - // If this stream is itself a sizing stream, then we don't need to actually - // parse field_value a second time; just update the bytes_written via a call - // to pb_write. (If we try to write the contents into a sizing stream, it'll - // fail since sizing streams don't actually have any buffer space.) - if (stream_.callback == nullptr) { - if (!pb_write(&stream_, nullptr, size)) { - FIREBASE_ASSERT_MESSAGE(false, PB_GET_ERROR(&stream_)); - } - return; - } - - // Ensure the output stream has enough space - if (stream_.bytes_written + size > stream_.max_size) { - FIREBASE_ASSERT_MESSAGE( - false, - "Insufficient space in the output stream to write the given message"); - } - - // Use a substream to verify that a callback doesn't write more than what it - // did the first time. (Use an initializer rather than setting fields - // individually like nanopb does. This gives us a *chance* of noticing if - // nanopb adds new fields.) - Writer writer({stream_.callback, stream_.state, - /*max_size=*/size, /*bytes_written=*/0, - /*errmsg=*/nullptr}); - write_message_fn(&writer); - status_ = writer.status(); - if (!status_.ok()) return; - - stream_.bytes_written += writer.stream_.bytes_written; - stream_.state = writer.stream_.state; - stream_.errmsg = writer.stream_.errmsg; - - if (writer.bytes_written() != size) { - // submsg size changed - FIREBASE_ASSERT_MESSAGE( - false, "Parsing the nested message twice yielded different sizes"); - } -} - -template <typename T> -T Reader::ReadNestedMessage(const std::function<T(Reader*)>& read_message_fn) { - // Implementation note: This is roughly modeled on pb_decode_delimited, - // adjusted to account for the oneof in FieldValue. - pb_istream_t raw_substream; - if (!pb_make_string_substream(&stream_, &raw_substream)) { - // TODO(rsgowman): figure out error handling - abort(); - } - Reader substream(raw_substream); - - T message = read_message_fn(&substream); - - // NB: future versions of nanopb read the remaining characters out of the - // substream (and return false if that fails) as an additional safety - // check within pb_close_string_substream. Unfortunately, that's not present - // in the current version (0.38). We'll make a stronger assertion and check - // to make sure there *are* no remaining characters in the substream. - if (substream.bytes_left() != 0) { - // TODO(rsgowman): figure out error handling - abort(); - } - pb_close_string_substream(&stream_, &substream.stream_); - - return message; -} - /** * Encodes a 'FieldsEntry' object, within a FieldValue's map_value type. * @@ -590,7 +268,10 @@ void EncodeFieldsEntry(Writer* writer, const ObjectValue::Map::value_type& kv) { } ObjectValue::Map::value_type DecodeFieldsEntry(Reader* reader) { + if (!reader->status().ok()) return {}; + Tag tag = reader->ReadTag(); + if (!reader->status().ok()) return {}; // TODO(rsgowman): figure out error handling: We can do better than a failed // assertion. @@ -600,6 +281,7 @@ ObjectValue::Map::value_type DecodeFieldsEntry(Reader* reader) { std::string key = reader->ReadString(); tag = reader->ReadTag(); + if (!reader->status().ok()) return {}; FIREBASE_ASSERT(tag.field_number == google_firestore_v1beta1_MapValue_FieldsEntry_value_tag); FIREBASE_ASSERT(tag.wire_type == PB_WT_STRING); @@ -607,27 +289,32 @@ ObjectValue::Map::value_type DecodeFieldsEntry(Reader* reader) { FieldValue value = reader->ReadNestedMessage<FieldValue>(DecodeFieldValueImpl); - return {key, value}; + return ObjectValue::Map::value_type{key, value}; } void EncodeObject(Writer* writer, const ObjectValue& object_value) { return writer->WriteNestedMessage([&object_value](Writer* writer) { // Write each FieldsEntry (i.e. key-value pair.) for (const auto& kv : object_value.internal_value) { - writer->WriteTag({PB_WT_STRING, - google_firestore_v1beta1_MapValue_FieldsEntry_key_tag}); + writer->WriteTag( + {PB_WT_STRING, google_firestore_v1beta1_MapValue_fields_tag}); writer->WriteNestedMessage( [&kv](Writer* writer) { return EncodeFieldsEntry(writer, kv); }); } }); } -ObjectValue DecodeObject(Reader* reader) { - ObjectValue::Map internal_value = reader->ReadNestedMessage<ObjectValue::Map>( +ObjectValue::Map DecodeObject(Reader* reader) { + if (!reader->status().ok()) return ObjectValue::Map(); + + return reader->ReadNestedMessage<ObjectValue::Map>( [](Reader* reader) -> ObjectValue::Map { ObjectValue::Map result; + if (!reader->status().ok()) return result; + while (reader->bytes_left()) { Tag tag = reader->ReadTag(); + if (!reader->status().ok()) return result; FIREBASE_ASSERT(tag.field_number == google_firestore_v1beta1_MapValue_fields_tag); FIREBASE_ASSERT(tag.wire_type == PB_WT_STRING); @@ -640,6 +327,7 @@ ObjectValue DecodeObject(Reader* reader) { // map. // TODO(rsgowman): figure out error handling: We can do better than a // failed assertion. + if (!reader->status().ok()) return result; FIREBASE_ASSERT(result.find(fv.first) == result.end()); // Add this key,fieldvalue to the results map. @@ -647,7 +335,64 @@ ObjectValue DecodeObject(Reader* reader) { } return result; }); - return ObjectValue{internal_value}; +} + +/** + * Creates the prefix for a fully qualified resource path, without a local path + * on the end. + */ +ResourcePath EncodeDatabaseId(const DatabaseId& database_id) { + return ResourcePath{"projects", database_id.project_id(), "databases", + database_id.database_id()}; +} + +/** + * Encodes a databaseId and resource path into the following form: + * /projects/$projectId/database/$databaseId/documents/$path + */ +std::string EncodeResourceName(const DatabaseId& database_id, + const ResourcePath& path) { + return EncodeDatabaseId(database_id) + .Append("documents") + .Append(path) + .CanonicalString(); +} + +/** + * Validates that a path has a prefix that looks like a valid encoded + * databaseId. + */ +bool IsValidResourceName(const ResourcePath& path) { + // Resource names have at least 4 components (project ID, database ID) + // and commonly the (root) resource type, e.g. documents + return path.size() >= 4 && path[0] == "projects" && path[2] == "databases"; +} + +/** + * Decodes a fully qualified resource name into a resource path and validates + * that there is a project and database encoded in the path. There are no + * guarantees that a local path is also encoded in this resource name. + */ +ResourcePath DecodeResourceName(absl::string_view encoded) { + ResourcePath resource = ResourcePath::FromString(encoded); + FIREBASE_ASSERT_MESSAGE(IsValidResourceName(resource), + "Tried to deserialize invalid key %s", + resource.CanonicalString().c_str()); + return resource; +} + +/** + * Decodes a fully qualified resource name into a resource path and validates + * that there is a project and database encoded in the path along with a local + * path. + */ +ResourcePath ExtractLocalPathFromResourceName( + const ResourcePath& resource_name) { + FIREBASE_ASSERT_MESSAGE( + resource_name.size() > 4 && resource_name[4] == "documents", + "Tried to deserialize invalid key %s", + resource_name.CanonicalString().c_str()); + return resource_name.PopFirst(5); } } // namespace @@ -659,9 +404,157 @@ Status Serializer::EncodeFieldValue(const FieldValue& field_value, return writer.status(); } -FieldValue Serializer::DecodeFieldValue(const uint8_t* bytes, size_t length) { +StatusOr<FieldValue> Serializer::DecodeFieldValue(const uint8_t* bytes, + size_t length) { Reader reader = Reader::Wrap(bytes, length); - return DecodeFieldValueImpl(&reader); + FieldValue fv = DecodeFieldValueImpl(&reader); + if (reader.status().ok()) { + return fv; + } else { + return reader.status(); + } +} + +std::string Serializer::EncodeKey(const DocumentKey& key) const { + return EncodeResourceName(database_id_, key.path()); +} + +DocumentKey Serializer::DecodeKey(absl::string_view name) const { + ResourcePath resource = DecodeResourceName(name); + FIREBASE_ASSERT_MESSAGE(resource[1] == database_id_.project_id(), + "Tried to deserialize key from different project."); + FIREBASE_ASSERT_MESSAGE(resource[3] == database_id_.database_id(), + "Tried to deserialize key from different database."); + return DocumentKey{ExtractLocalPathFromResourceName(resource)}; +} + +util::Status Serializer::EncodeDocument(const DocumentKey& key, + const ObjectValue& value, + std::vector<uint8_t>* out_bytes) const { + Writer writer = Writer::Wrap(out_bytes); + EncodeDocument(&writer, key, value); + return writer.status(); +} + +void Serializer::EncodeDocument(Writer* writer, + const DocumentKey& key, + const ObjectValue& object_value) const { + // Encode Document.name + writer->WriteTag({PB_WT_STRING, google_firestore_v1beta1_Document_name_tag}); + writer->WriteString(EncodeKey(key)); + + // Encode Document.fields (unless it's empty) + if (!object_value.internal_value.empty()) { + writer->WriteTag( + {PB_WT_STRING, google_firestore_v1beta1_Document_fields_tag}); + EncodeObject(writer, object_value); + } + + // Skip Document.create_time and Document.update_time, since they're + // output-only fields. +} + +util::StatusOr<std::unique_ptr<model::MaybeDocument>> +Serializer::DecodeMaybeDocument(const uint8_t* bytes, size_t length) const { + Reader reader = Reader::Wrap(bytes, length); + std::unique_ptr<MaybeDocument> maybeDoc = + DecodeBatchGetDocumentsResponse(&reader); + + if (reader.status().ok()) { + return maybeDoc; + } else { + return reader.status(); + } +} + +std::unique_ptr<MaybeDocument> Serializer::DecodeBatchGetDocumentsResponse( + Reader* reader) const { + Tag tag = reader->ReadTag(); + if (!reader->status().ok()) return nullptr; + + // Ensure the tag matches the wire type + switch (tag.field_number) { + case google_firestore_v1beta1_BatchGetDocumentsResponse_found_tag: + case google_firestore_v1beta1_BatchGetDocumentsResponse_missing_tag: + if (tag.wire_type != PB_WT_STRING) { + reader->set_status( + Status(FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (mismatch between " + "the wiretype and the field number (tag))")); + } + break; + + default: + reader->set_status(Status( + FirestoreErrorCode::DataLoss, + "Input proto bytes cannot be parsed (invalid field number (tag))")); + } + + if (!reader->status().ok()) return nullptr; + + switch (tag.field_number) { + case google_firestore_v1beta1_BatchGetDocumentsResponse_found_tag: + return reader->ReadNestedMessage<std::unique_ptr<MaybeDocument>>( + [this](Reader* reader) -> std::unique_ptr<MaybeDocument> { + return DecodeDocument(reader); + }); + case google_firestore_v1beta1_BatchGetDocumentsResponse_missing_tag: + // TODO(rsgowman): Right now, we only support Document (and don't support + // NoDocument). That should change in the next PR or so. + abort(); + default: + // This indicates an internal error as we've already ensured that this is + // a valid field_number. + FIREBASE_ASSERT_MESSAGE( + false, + "Somehow got an unexpected field number (tag) after verifying that " + "the field number was expected."); + } +} + +std::unique_ptr<Document> Serializer::DecodeDocument(Reader* reader) const { + if (!reader->status().ok()) return nullptr; + + std::string name; + FieldValue fields = FieldValue::ObjectValueFromMap({}); + SnapshotVersion version = SnapshotVersion::None(); + + while (reader->bytes_left()) { + Tag tag = reader->ReadTag(); + if (!reader->status().ok()) return nullptr; + FIREBASE_ASSERT(tag.wire_type == PB_WT_STRING); + switch (tag.field_number) { + case google_firestore_v1beta1_Document_name_tag: + name = reader->ReadString(); + break; + case google_firestore_v1beta1_Document_fields_tag: + // TODO(rsgowman): Rather than overwriting, we should instead merge with + // the existing FieldValue (if any). + fields = DecodeFieldValueImpl(reader); + break; + case google_firestore_v1beta1_Document_create_time_tag: + // This field is ignored by the client sdk, but we still need to extract + // it. + reader->ReadNestedMessage<Timestamp>(DecodeTimestamp); + break; + case google_firestore_v1beta1_Document_update_time_tag: + // TODO(rsgowman): Rather than overwriting, we should instead merge with + // the existing SnapshotVersion (if any). Less relevant here, since it's + // just two numbers which are both expected to be present, but if the + // proto evolves that might change. + version = SnapshotVersion{ + reader->ReadNestedMessage<Timestamp>(DecodeTimestamp)}; + break; + default: + // TODO(rsgowman): Error handling. (Invalid tags should fail to decode, + // but shouldn't cause a crash.) + abort(); + } + } + + return absl::make_unique<Document>(std::move(fields), DecodeKey(name), + version, + /*has_local_modifications=*/false); } } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.h b/Firestore/core/src/firebase/firestore/remote/serializer.h index 7f08f7d..e79c238 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.h +++ b/Firestore/core/src/firebase/firestore/remote/serializer.h @@ -19,12 +19,22 @@ #include <cstdint> #include <cstdlib> +#include <memory> +#include <string> #include <vector> +#include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/document.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" +#include "Firestore/core/src/firebase/firestore/model/maybe_document.h" +#include "Firestore/core/src/firebase/firestore/nanopb/reader.h" +#include "Firestore/core/src/firebase/firestore/nanopb/writer.h" #include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" #include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "absl/base/attributes.h" +#include "absl/strings/string_view.h" namespace firebase { namespace firestore { @@ -45,29 +55,23 @@ namespace remote { // interpret." Adjust for C++. class Serializer { public: - Serializer() { + /** + * @param database_id Must remain valid for the lifetime of this Serializer + * object. + */ + explicit Serializer(const firebase::firestore::model::DatabaseId& database_id) + : database_id_(database_id) { } - // TODO(rsgowman): We eventually need the DatabaseId, but can't add it just - // yet since it's not used yet (which travis complains about). So for now, - // we'll create a parameterless ctor (above) that likely won't exist in the - // final version of this class. - ///** - // * @param database_id Must remain valid for the lifetime of this Serializer - // * object. - // */ - // explicit Serializer(const firebase::firestore::model::DatabaseId& - // database_id) - // : database_id_(database_id) { - //} /** - * Converts the FieldValue model passed into bytes. + * @brief Converts the FieldValue model passed into bytes. * * @param field_value the model to convert. * @param[out] out_bytes A buffer to place the output. The bytes will be * appended to this vector. + * @return A Status, which if not ok(), indicates what went wrong. Note that + * errors during encoding generally indicate a serious/fatal error. */ - // TODO(rsgowman): error handling, incl return code. // TODO(rsgowman): If we never support any output except to a vector, it may // make sense to have Serializer own the vector and provide an accessor rather // than asking the user to create it first. @@ -80,26 +84,89 @@ class Serializer { * * @param bytes The bytes to convert. It's assumed that exactly all of the * bytes will be used by this conversion. - * @return The model equivalent of the bytes. + * @return The model equivalent of the bytes or a Status indicating + * what went wrong. */ - // TODO(rsgowman): error handling. - model::FieldValue DecodeFieldValue(const uint8_t* bytes, size_t length); + util::StatusOr<model::FieldValue> DecodeFieldValue(const uint8_t* bytes, + size_t length); /** * @brief Converts from bytes to the model FieldValue format. * * @param bytes The bytes to convert. It's assumed that exactly all of the * bytes will be used by this conversion. - * @return The model equivalent of the bytes. + * @return The model equivalent of the bytes or a Status indicating + * what went wrong. */ - // TODO(rsgowman): error handling. - model::FieldValue DecodeFieldValue(const std::vector<uint8_t>& bytes) { + util::StatusOr<model::FieldValue> DecodeFieldValue( + const std::vector<uint8_t>& bytes) { return DecodeFieldValue(bytes.data(), bytes.size()); } + /** + * Encodes the given document key as a fully qualified name. This includes the + * databaseId associated with this Serializer and the key path. + */ + std::string EncodeKey( + const firebase::firestore::model::DocumentKey& key) const; + + /** + * Decodes the given document key from a fully qualified name. + */ + firebase::firestore::model::DocumentKey DecodeKey( + absl::string_view name) const; + + /** + * @brief Converts the Document (i.e. key/value) into bytes. + * + * @param[out] out_bytes A buffer to place the output. The bytes will be + * appended to this vector. + * @return A Status, which if not ok(), indicates what went wrong. Note that + * errors during encoding generally indicate a serious/fatal error. + */ + // TODO(rsgowman): Similar to above, if we never support any output except to + // a vector, it may make sense to have Serializer own the vector and provide + // an accessor rather than asking the user to create it first. + util::Status EncodeDocument( + const firebase::firestore::model::DocumentKey& key, + const firebase::firestore::model::ObjectValue& value, + std::vector<uint8_t>* out_bytes) const; + + /** + * @brief Converts from bytes to the model Document format. + * + * @param bytes The bytes to convert. These bytes must represent a + * BatchGetDocumentsResponse. It's assumed that exactly all of the bytes will + * be used by this conversion. + * @return The model equivalent of the bytes or a Status indicating + * what went wrong. + */ + util::StatusOr<std::unique_ptr<model::MaybeDocument>> DecodeMaybeDocument( + const uint8_t* bytes, size_t length) const; + + /** + * @brief Converts from bytes to the model Document format. + * + * @param bytes The bytes to convert. These bytes must represent a + * BatchGetDocumentsResponse. It's assumed that exactly all of the bytes will + * be used by this conversion. + * @return The model equivalent of the bytes or a Status indicating + * what went wrong. + */ + util::StatusOr<std::unique_ptr<model::MaybeDocument>> DecodeMaybeDocument( + const std::vector<uint8_t>& bytes) const { + return DecodeMaybeDocument(bytes.data(), bytes.size()); + } + private: - // TODO(rsgowman): We don't need the database_id_ yet (but will eventually). - // model::DatabaseId* database_id_; + void EncodeDocument(nanopb::Writer* writer, + const model::DocumentKey& key, + const model::ObjectValue& object_value) const; + std::unique_ptr<model::MaybeDocument> DecodeBatchGetDocumentsResponse( + nanopb::Reader* reader) const; + std::unique_ptr<model::Document> DecodeDocument(nanopb::Reader* reader) const; + + const firebase::firestore::model::DatabaseId& database_id_; }; } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/timestamp_internal.h b/Firestore/core/src/firebase/firestore/timestamp_internal.h new file mode 100644 index 0000000..3516176 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/timestamp_internal.h @@ -0,0 +1,49 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_TIMESTAMP_INTERNAL_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_TIMESTAMP_INTERNAL_H_ + +#include "Firestore/core/include/firebase/firestore/timestamp.h" + +namespace firebase { + +/** + * Details about the Timestamp class which are useful internally, but we don't + * want to expose publicly. + */ +class TimestampInternal { + public: + /** + * Represents the maximum allowable time that the Timestamp class handles, + * specifically 9999-12-31T23:59:59.999999999Z. + */ + static firebase::Timestamp Max() { + return {253402300800L - 1, 999999999}; + } + + /** + * Represents the minimum allowable time that the Timestamp class handles, + * specifically 0001-01-01T00:00:00Z. + */ + static firebase::Timestamp Min() { + return {-62135596800L, 0}; + } +}; + +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_TIMESTAMP_INTERNAL_H_ diff --git a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt index 3afead1..b2b015b 100644 --- a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt @@ -109,6 +109,63 @@ else() endif() +## async queue + +check_symbol_exists(dispatch_async_f dispatch/dispatch.h HAVE_LIBDISPATCH) + +cc_library( + firebase_firestore_util_executor_std + SOURCES + executor_std.cc + executor_std.h + executor.h + DEPENDS + absl_bad_optional_access + absl_optional + ${FIREBASE_FIRESTORE_UTIL_LOG} + EXCLUDE_FROM_ALL +) + +if(HAVE_LIBDISPATCH) +cc_library( + firebase_firestore_util_executor_libdispatch + SOURCES + executor_libdispatch.mm + executor_libdispatch.h + executor.h + DEPENDS + absl_bad_optional_access + absl_optional + absl_strings + ${FIREBASE_FIRESTORE_UTIL_LOG} + EXCLUDE_FROM_ALL +) +endif() + +if(HAVE_LIBDISPATCH) + set( + FIREBASE_FIRESTORE_UTIL_EXECUTOR + firebase_firestore_util_executor_libdispatch + ) + +else() + set( + FIREBASE_FIRESTORE_UTIL_EXECUTOR + firebase_firestore_util_executor_std + ) + +endif() + +cc_library( + firebase_firestore_util_async_queue + SOURCES + async_queue.cc + async_queue.h + DEPENDS + ${FIREBASE_FIRESTORE_UTIL_EXECUTOR} + ${FIREBASE_FIRESTORE_UTIL_LOG} + EXCLUDE_FROM_ALL +) ## main library @@ -128,6 +185,7 @@ cc_library( comparison.cc comparison.h config.h + hashing.h iterator_adaptors.h ordered_code.cc ordered_code.h @@ -143,6 +201,7 @@ cc_library( DEPENDS absl_base firebase_firestore_util_base + firebase_firestore_util_async_queue ${FIREBASE_FIRESTORE_UTIL_LOG} ${FIREBASE_FIRESTORE_UTIL_RANDOM} ) diff --git a/Firestore/core/src/firebase/firestore/util/async_queue.cc b/Firestore/core/src/firebase/firestore/util/async_queue.cc new file mode 100644 index 0000000..81aac7c --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/async_queue.cc @@ -0,0 +1,142 @@ +/* + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" + +#include <utility> + +#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "absl/memory/memory.h" + +namespace firebase { +namespace firestore { +namespace util { + +using internal::Executor; + +AsyncQueue::AsyncQueue(std::unique_ptr<Executor> executor) + : executor_{std::move(executor)} { + is_operation_in_progress_ = false; +} + +// TODO(varconst): assert in destructor that the queue is empty. + +void AsyncQueue::VerifyIsCurrentExecutor() const { + FIREBASE_ASSERT_MESSAGE( + executor_->IsCurrentExecutor(), + "Expected to be called by the executor associated with this queue " + "(expected executor: '%s', actual executor: '%s')", + executor_->Name().c_str(), executor_->CurrentExecutorName().c_str()); +} + +void AsyncQueue::VerifyIsCurrentQueue() const { + VerifyIsCurrentExecutor(); + FIREBASE_ASSERT_MESSAGE( + is_operation_in_progress_, + "VerifyIsCurrentQueue called when no operation is executing " + "(expected executor: '%s', actual executor: '%s')", + executor_->Name().c_str(), executor_->CurrentExecutorName().c_str()); +} + +void AsyncQueue::ExecuteBlocking(const Operation& operation) { + VerifyIsCurrentExecutor(); + FIREBASE_ASSERT_MESSAGE(!is_operation_in_progress_, + "ExecuteBlocking may not be called " + "before the previous operation finishes executing"); + + is_operation_in_progress_ = true; + operation(); + is_operation_in_progress_ = false; +} + +void AsyncQueue::Enqueue(const Operation& operation) { + VerifySequentialOrder(); + EnqueueRelaxed(operation); +} + +void AsyncQueue::EnqueueRelaxed(const Operation& operation) { + executor_->Execute(Wrap(operation)); +} + +DelayedOperation AsyncQueue::EnqueueAfterDelay(const Milliseconds delay, + const TimerId timer_id, + const Operation& operation) { + VerifyIsCurrentExecutor(); + + // While not necessarily harmful, we currently don't expect to have multiple + // callbacks with the same timer_id in the queue, so defensively reject + // them. + FIREBASE_ASSERT_MESSAGE( + !IsScheduled(timer_id), + "Attempted to schedule multiple operations with id %d", timer_id); + + Executor::TaggedOperation tagged{static_cast<int>(timer_id), Wrap(operation)}; + return executor_->Schedule(delay, std::move(tagged)); +} + +AsyncQueue::Operation AsyncQueue::Wrap(const Operation& operation) { + // Decorator pattern: wrap `operation` into a call to `ExecuteBlocking` to + // ensure that it doesn't spawn any nested operations. + + // Note: can't move `operation` into lambda until C++14. + return [this, operation] { ExecuteBlocking(operation); }; +} + +void AsyncQueue::VerifySequentialOrder() const { + // This is the inverse of `VerifyIsCurrentQueue`. + FIREBASE_ASSERT_MESSAGE( + !is_operation_in_progress_ || !executor_->IsCurrentExecutor(), + "Enqueue methods cannot be called when we are already running on " + "target executor" + "(this queue's executor: '%s', current executor: '%s')", + executor_->Name().c_str(), executor_->CurrentExecutorName().c_str()); +} + +// Test-only functions + +void AsyncQueue::EnqueueBlocking(const Operation& operation) { + VerifySequentialOrder(); + executor_->ExecuteBlocking(Wrap(operation)); +} + +bool AsyncQueue::IsScheduled(const TimerId timer_id) const { + return executor_->IsScheduled(static_cast<int>(timer_id)); +} + +void AsyncQueue::RunScheduledOperationsUntil(const TimerId last_timer_id) { + FIREBASE_ASSERT_MESSAGE( + !executor_->IsCurrentExecutor(), + "RunScheduledOperationsUntil must not be called on the queue"); + + executor_->ExecuteBlocking([this, last_timer_id] { + FIREBASE_ASSERT_MESSAGE( + last_timer_id == TimerId::All || IsScheduled(last_timer_id), + "Attempted to run scheduled operations until missing timer id: %d", + last_timer_id); + + for (auto next = executor_->PopFromSchedule(); next.has_value(); + next = executor_->PopFromSchedule()) { + next->operation(); + if (next->tag == static_cast<int>(last_timer_id)) { + break; + } + } + }); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/async_queue.h b/Firestore/core/src/firebase/firestore/util/async_queue.h new file mode 100644 index 0000000..e2df387 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/async_queue.h @@ -0,0 +1,164 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_H_ + +#include <atomic> +#include <chrono> // NOLINT(build/c++11) +#include <functional> +#include <memory> + +#include "Firestore/core/src/firebase/firestore/util/executor.h" + +namespace firebase { +namespace firestore { +namespace util { + +/** + * Well-known "timer" ids used when scheduling delayed operations on the + * AsyncQueue. These ids can then be used from tests to check for the + * presence of delayed operations or to run them early. + */ +enum class TimerId { + /** All can be used with `RunDelayedOperationsUntil` to run all timers. */ + All, + + /** + * The following 4 timers are used in `Stream` for the listen and write + * streams. The "Idle" timer is used to close the stream due to inactivity. + * The "ConnectionBackoff" timer is used to restart a stream once the + * appropriate backoff delay has elapsed. + */ + ListenStreamIdle, + ListenStreamConnectionBackoff, + WriteStreamIdle, + WriteStreamConnectionBackoff, + + /** + * A timer used in `OnlineStateTracker` to transition from + * `OnlineStateUnknown` to `Offline` after a set timeout, rather than waiting + * indefinitely for success or failure. + */ + OnlineStateTimeout, +}; + +// A serial queue that executes given operations asynchronously, one at a time. +// Operations may be scheduled to be executed as soon as possible or in the +// future. Operations scheduled for the same time are FIFO-ordered. +// +// `AsyncQueue` wraps a platform-specific executor, adding checks that enforce +// sequential ordering of operations: an enqueued operation, while being run, +// normally cannot enqueue other operations for immediate execution (but see +// `EnqueueRelaxed`). +// +// `AsyncQueue` methods have particular expectations about whether they must be +// invoked on the queue or not; check "preconditions" section in comments on +// each method. +// +// A significant portion of `AsyncQueue` interface only exists for test purposes +// and must *not* be used in regular code. +class AsyncQueue { + public: + using Operation = internal::Executor::Operation; + using Milliseconds = internal::Executor::Milliseconds; + + explicit AsyncQueue(std::unique_ptr<internal::Executor> executor); + + // Asserts for the caller that it is being invoked as part of an operation on + // the `AsyncQueue`. + void VerifyIsCurrentQueue() const; + + // Enqueue methods + + // Puts the `operation` on the queue to be executed as soon as possible, while + // maintaining FIFO order. + // + // Precondition: `Enqueue` calls cannot be nested; that is, `Enqueue` may not + // be called by a previously enqueued operation when it is run (as a special + // case, destructors invoked when an enqueued operation has run and is being + // destroyed may invoke `Enqueue`). + void Enqueue(const Operation& operation); + + // Like `Enqueue`, but without applying any prerequisite checks. + void EnqueueRelaxed(const Operation& operation); + + // Puts the `operation` on the queue to be executed `delay` milliseconds from + // now, and returns a handle that allows to cancel the operation (provided it + // hasn't run already). + // + // `operation` is tagged by a `timer_id` which allows to identify the caller. + // Only one operation tagged with any given `timer_id` may be on the queue at + // any time; an attempt to put another such operation will result in an + // assertion failure. In tests, these tags also allow to check for presence of + // certain operations and to run certain operations in advance. + // + // Precondition: `EnqueueAfterDelay` is being invoked asynchronously on the + // queue. + DelayedOperation EnqueueAfterDelay(Milliseconds delay, + TimerId timer_id, + const Operation& operation); + + // Direct execution + + // Immediately executes the `operation` on the queue. + // + // This is largely a workaround to allow other classes (GRPC) to directly + // access the underlying dispatch queue without getting `AsyncQueue` into an + // inconsistent state. + // + // Precondition: no other operation is being executed on the queue at the + // moment of the call (i.e., `ExecuteBlocking` cannot call `ExecuteBlocking`). + // + // Precondition: `ExecuteBlocking` is being invoked asynchronously on the + // queue. + void ExecuteBlocking(const Operation& operation); + + // Test-only interface follows + // TODO(varconst): move the test-only interface into a helper object that is + // a friend of AsyncQueue and delegates its public methods to private methods + // on AsyncQueue. + + // Like `Enqueue`, but blocks until the `operation` is complete. + void EnqueueBlocking(const Operation& operation); + + // Checks whether an operation tagged with `timer_id` is currently scheduled + // for execution in the future. + bool IsScheduled(TimerId timer_id) const; + + // Force runs operations scheduled for future execution, in scheduled order, + // up to *and including* the operation tagged with `last_timer_id`. + // + // Precondition: `RunScheduledOperationsUntil` is *not* being invoked on the + // queue. + void RunScheduledOperationsUntil(TimerId last_timer_id); + + private: + Operation Wrap(const Operation& operation); + + // Asserts that the current invocation happens asynchronously on the queue. + void VerifyIsCurrentExecutor() const; + void VerifySequentialOrder() const; + + std::atomic<bool> is_operation_in_progress_; + std::unique_ptr<internal::Executor> executor_; +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_H_ diff --git a/Firestore/core/src/firebase/firestore/util/executor.h b/Firestore/core/src/firebase/firestore/util/executor.h new file mode 100644 index 0000000..df8b0b5 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor.h @@ -0,0 +1,129 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_H_ + +#include <chrono> // NOLINT(build/c++11) +#include <functional> +#include <string> +#include <utility> + +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace util { + +// A handle to an operation scheduled for future execution. The handle may +// outlive the operation, but it *cannot* outlive the executor that created it. +class DelayedOperation { + public: + DelayedOperation() { + } + + // If the operation has not been run yet, cancels the operation. Otherwise, + // this function is a no-op. + void Cancel() { + cancel_func_(); + } + + // Internal use only. + explicit DelayedOperation(std::function<void()>&& cancel_func) + : cancel_func_{std::move(cancel_func)} { + } + + private: + std::function<void()> cancel_func_; +}; + +namespace internal { + +// An interface to a platform-specific executor of asynchronous operations +// (called tasks on other platforms). +// +// Operations may be scheduled for immediate or delayed execution. Operations +// delayed until the exact same time are scheduled in FIFO order. +// +// The operations are executed sequentially; only a single operation is executed +// at any given time. +// +// Delayed operations may be canceled if they have not already been run. +class Executor { + public: + using Tag = int; + using Operation = std::function<void()>; + using Milliseconds = std::chrono::milliseconds; + + // Operations scheduled for future execution have an opaque tag. The value of + // the tag is ignored by the executor but can be used to find operations with + // a given tag after they are scheduled. + struct TaggedOperation { + TaggedOperation() { + } + TaggedOperation(const Tag tag, Operation&& operation) + : tag{tag}, operation{std::move(operation)} { + } + Tag tag = 0; + Operation operation; + }; + + virtual ~Executor() { + } + + // Schedules the `operation` to be asynchronously executed as soon as + // possible, in FIFO order. + virtual void Execute(Operation&& operation) = 0; + // Like `Execute`, but blocks until the `operation` finishes, consequently + // draining immediate operations from the executor. + virtual void ExecuteBlocking(Operation&& operation) = 0; + // Scheduled the given `operation` to be executed after `delay` milliseconds + // from now, and returns a handle that allows to cancel the operation + // (provided it hasn't been run already). The operation is tagged to allow + // retrieving it later. + // + // `delay` must be non-negative; use `Execute` to schedule operations for + // immediate execution. + virtual DelayedOperation Schedule(Milliseconds delay, + TaggedOperation&& operation) = 0; + + // Checks for the caller whether it is being invoked by this executor. + virtual bool IsCurrentExecutor() const = 0; + // Returns some sort of an identifier for the current execution context. The + // only guarantee is that it will return different values depending on whether + // this function is invoked by this executor or not. + virtual std::string CurrentExecutorName() const = 0; + // Like `CurrentExecutorName`, but returns an identifier for this executor, + // whether the caller code currently runs on this executor or not. + virtual std::string Name() const = 0; + + // Checks whether an operation tagged with the given `tag` is currently + // scheduled for future execution. + virtual bool IsScheduled(Tag tag) const = 0; + // Removes the nearest due scheduled operation from the schedule and returns + // it to the caller. This function may be used to reschedule operations. + // Immediate operations don't count; only operations scheduled for delayed + // execution may be removed. If no such operations are currently scheduled, an + // empty `optional` is returned. + virtual absl::optional<TaggedOperation> PopFromSchedule() = 0; +}; + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_H_ diff --git a/Firestore/core/src/firebase/firestore/util/executor_libdispatch.h b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.h new file mode 100644 index 0000000..f913fe5 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.h @@ -0,0 +1,91 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_LIBDISPATCH_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_LIBDISPATCH_H_ + +#include <chrono> // NOLINT(build/c++11) +#include <functional> +#include <memory> +#include <string> +#include <utility> +#include <vector> +#include "dispatch/dispatch.h" + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "absl/strings/string_view.h" + +#if !defined(__OBJC__) +// `dispatch_queue_t` gets defined to different types when compiled in C++ or +// Objective-C mode. Source files including this header should all be compiled +// in the same mode to avoid linker errors. +#error "This header only supports Objective-C++ (see comment for more info)." +#endif // !defined(__OBJC__) + +namespace firebase { +namespace firestore { +namespace util { + +namespace internal { + +// Generic wrapper over `dispatch_async_f`, providing `dispatch_async`-like +// interface: accepts an arbitrary invocable object in place of an Objective-C +// block. +void DispatchAsync(dispatch_queue_t queue, std::function<void()>&& work); + +// Similar to `DispatchAsync` but wraps `dispatch_sync_f`. +void DispatchSync(dispatch_queue_t queue, std::function<void()> work); + +class TimeSlot; + +// A serial queue built on top of libdispatch. The operations are run on +// a dedicated serial dispatch queue. +class ExecutorLibdispatch : public Executor { + public: + explicit ExecutorLibdispatch(dispatch_queue_t dispatch_queue); + + bool IsCurrentExecutor() const override; + std::string CurrentExecutorName() const override; + std::string Name() const override; + + void Execute(Operation&& operation) override; + void ExecuteBlocking(Operation&& operation) override; + DelayedOperation Schedule(Milliseconds delay, + TaggedOperation&& operation) override; + + void RemoveFromSchedule(const TimeSlot* to_remove); + + bool IsScheduled(Tag tag) const override; + absl::optional<TaggedOperation> PopFromSchedule() override; + + dispatch_queue_t dispatch_queue() const { + return dispatch_queue_; + } + + private: + dispatch_queue_t dispatch_queue_; + // Stores non-owned pointers to `TimeSlot`s. + // Invariant: if a `TimeSlot` is in `schedule_`, it's a valid pointer. + std::vector<TimeSlot*> schedule_; +}; + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_LIBDISPATCH_H_ diff --git a/Firestore/core/src/firebase/firestore/util/executor_libdispatch.mm b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.mm new file mode 100644 index 0000000..597d450 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.mm @@ -0,0 +1,284 @@ +/* + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" + +namespace firebase { +namespace firestore { +namespace util { +namespace internal { + +namespace { + +absl::string_view StringViewFromDispatchLabel(const char* const label) { + // Make sure string_view's data is not null, because it's used for logging. + return label ? absl::string_view{label} : absl::string_view{""}; +} + +// GetLabel functions are guaranteed to never return a "null" string_view +// (i.e. data() != nullptr). +absl::string_view GetQueueLabel(const dispatch_queue_t queue) { + return StringViewFromDispatchLabel(dispatch_queue_get_label(queue)); +} +absl::string_view GetCurrentQueueLabel() { + // Note: dispatch_queue_get_label may return nullptr if the queue wasn't + // initialized with a label. + return StringViewFromDispatchLabel( + dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)); +} + +} // namespace + +void DispatchAsync(const dispatch_queue_t queue, std::function<void()>&& work) { + // Dynamically allocate the function to make sure the object is valid by the + // time libdispatch gets to it. + const auto wrap = new std::function<void()>{std::move(work)}; + + dispatch_async_f(queue, wrap, [](void* const raw_work) { + const auto unwrap = static_cast<std::function<void()>*>(raw_work); + (*unwrap)(); + delete unwrap; + }); +} + +void DispatchSync(const dispatch_queue_t queue, std::function<void()> work) { + FIREBASE_ASSERT_MESSAGE( + GetCurrentQueueLabel() != GetQueueLabel(queue), + "Calling DispatchSync on the current queue will lead to a deadlock."); + + // Unlike dispatch_async_f, dispatch_sync_f blocks until the work passed to it + // is done, so passing a reference to a local variable is okay. + dispatch_sync_f(queue, &work, [](void* const raw_work) { + const auto unwrap = static_cast<std::function<void()>*>(raw_work); + (*unwrap)(); + }); +} + +namespace { + +template <typename Work> +void RunSynchronized(const ExecutorLibdispatch* const executor, Work&& work) { + if (executor->IsCurrentExecutor()) { + work(); + } else { + DispatchSync(executor->dispatch_queue(), std::forward<Work>(work)); + } +} + +} // namespace + +// Represents a "busy" time slot on the schedule. +// +// Since libdispatch doesn't provide a way to cancel a scheduled operation, once +// a slot is created, it will always stay in the schedule until the time is +// past. Consequently, it is more useful to think of a time slot than +// a particular scheduled operation -- by the time the slot comes, operation may +// or may not be there (imagine getting to a meeting and finding out it's been +// canceled). +// +// Precondition: all member functions, including the constructor, are *only* +// invoked on the Firestore queue. +// +// Ownership: +// +// - `TimeSlot` is exclusively owned by libdispatch; +// - `ExecutorLibdispatch` contains non-owning pointers to `TimeSlot`s; +// - invariant: if the executor contains a pointer to a `TimeSlot`, it is +// a valid object. It is achieved because when libdispatch invokes +// a `TimeSlot`, it always removes it from the executor before deleting it. +// The reverse is not true: a canceled time slot is removed from the executor, +// but won't be destroyed until its original due time is past. + +class TimeSlot { + public: + TimeSlot(ExecutorLibdispatch* executor, + Executor::Milliseconds delay, + Executor::TaggedOperation&& operation); + + // Returns the operation that was scheduled for this time slot and turns the + // slot into a no-op. + Executor::TaggedOperation Unschedule(); + + bool operator<(const TimeSlot& rhs) const { + return target_time_ < rhs.target_time_; + } + bool operator==(const Executor::Tag tag) const { + return tagged_.tag == tag; + } + + void MarkDone() { + done_ = true; + } + + static void InvokedByLibdispatch(void* const raw_self); + + private: + void Execute(); + void RemoveFromSchedule(); + + using TimePoint = std::chrono::time_point<std::chrono::steady_clock, + Executor::Milliseconds>; + + ExecutorLibdispatch* const executor_; + const TimePoint target_time_; // Used for sorting + Executor::TaggedOperation tagged_; + + // True if the operation has either been run or canceled. + // + // Note on thread-safety: because the precondition is that all member + // functions of this class are executed on the dispatch queue, no + // synchronization is required for `done_`. + bool done_ = false; +}; + +TimeSlot::TimeSlot(ExecutorLibdispatch* const executor, + const Executor::Milliseconds delay, + Executor::TaggedOperation&& operation) + : executor_{executor}, + target_time_{std::chrono::time_point_cast<Executor::Milliseconds>( + std::chrono::steady_clock::now()) + + delay}, + tagged_{std::move(operation)} { +} + +Executor::TaggedOperation TimeSlot::Unschedule() { + if (!done_) { + RemoveFromSchedule(); + } + return std::move(tagged_); +} + +void TimeSlot::InvokedByLibdispatch(void* const raw_self) { + auto const self = static_cast<TimeSlot*>(raw_self); + self->Execute(); + delete self; +} + +void TimeSlot::Execute() { + if (done_) { + // `done_` might mean that the executor is already destroyed, so don't call + // `RemoveFromSchedule`. + return; + } + + RemoveFromSchedule(); + + FIREBASE_ASSERT_MESSAGE(tagged_.operation, + "TimeSlot contains an invalid function object"); + tagged_.operation(); +} + +void TimeSlot::RemoveFromSchedule() { + executor_->RemoveFromSchedule(this); +} + +// ExecutorLibdispatch + +ExecutorLibdispatch::ExecutorLibdispatch(const dispatch_queue_t dispatch_queue) + : dispatch_queue_{dispatch_queue} { +} + +bool ExecutorLibdispatch::IsCurrentExecutor() const { + return GetCurrentQueueLabel() == GetQueueLabel(dispatch_queue()); +} +std::string ExecutorLibdispatch::CurrentExecutorName() const { + return GetCurrentQueueLabel().data(); +} +std::string ExecutorLibdispatch::Name() const { + return GetQueueLabel(dispatch_queue()).data(); +} + +void ExecutorLibdispatch::Execute(Operation&& operation) { + DispatchAsync(dispatch_queue(), std::move(operation)); +} +void ExecutorLibdispatch::ExecuteBlocking(Operation&& operation) { + DispatchSync(dispatch_queue(), std::move(operation)); +} + +DelayedOperation ExecutorLibdispatch::Schedule(const Milliseconds delay, + TaggedOperation&& operation) { + namespace chr = std::chrono; + const dispatch_time_t delay_ns = dispatch_time( + DISPATCH_TIME_NOW, chr::duration_cast<chr::nanoseconds>(delay).count()); + + // Ownership is fully transferred to libdispatch -- because it's impossible + // to truly cancel work after it's been dispatched, libdispatch is + // guaranteed to outlive the executor, and it's possible for work to be + // invoked by libdispatch after the executor is destroyed. Executor only + // stores an observer pointer to the operation. + + auto const time_slot = new TimeSlot{this, delay, std::move(operation)}; + dispatch_after_f(delay_ns, dispatch_queue(), time_slot, + TimeSlot::InvokedByLibdispatch); + RunSynchronized(this, [this, time_slot] { schedule_.push_back(time_slot); }); + return DelayedOperation{[this, time_slot] { + // `time_slot` might be destroyed by the time cancellation function runs. + // Therefore, don't access any methods on `time_slot`, only use it as + // a handle to remove from `schedule_`. + RemoveFromSchedule(time_slot); + }}; +} + +void ExecutorLibdispatch::RemoveFromSchedule(const TimeSlot* const to_remove) { + RunSynchronized(this, [this, to_remove] { + const auto found = std::find_if( + schedule_.begin(), schedule_.end(), + [to_remove](const TimeSlot* op) { return op == to_remove; }); + // It's possible for the operation to be missing if libdispatch gets to run + // it after it was force-run, for example. + if (found != schedule_.end()) { + (*found)->MarkDone(); + schedule_.erase(found); + } + }); +} + +// Test-only methods + +bool ExecutorLibdispatch::IsScheduled(const Tag tag) const { + bool result = false; + RunSynchronized(this, [this, tag, &result] { + result = std::any_of( + schedule_.begin(), schedule_.end(), + [&tag](const TimeSlot* const operation) { return *operation == tag; }); + }); + return result; +} + +absl::optional<Executor::TaggedOperation> +ExecutorLibdispatch::PopFromSchedule() { + absl::optional<Executor::TaggedOperation> result; + + RunSynchronized(this, [this, &result] { + if (schedule_.empty()) { + return; + } + // Sorting upon each call to `PopFromSchedule` is inefficient, which is + // consciously ignored because this function is only ever called from tests. + std::sort( + schedule_.begin(), schedule_.end(), + [](const TimeSlot* lhs, const TimeSlot* rhs) { return *lhs < *rhs; }); + const auto nearest = schedule_.begin(); + result = (*nearest)->Unschedule(); + }); + + return result; +} + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/executor_std.cc b/Firestore/core/src/firebase/firestore/util/executor_std.cc new file mode 100644 index 0000000..f03a712 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor_std.cc @@ -0,0 +1,155 @@ +/* + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/util/executor_std.h" + +#include <future> // NOLINT(build/c++11) +#include <sstream> + +namespace firebase { +namespace firestore { +namespace util { +namespace internal { + +namespace { + +// The only guarantee is that different `thread_id`s will produce different +// values. +std::string ThreadIdToString(const std::thread::id thread_id) { + std::ostringstream stream; + stream << thread_id; + return stream.str(); +} + +} // namespace + +ExecutorStd::ExecutorStd() { + // Somewhat counter-intuitively, constructor of `std::atomic` assigns the + // value non-atomically, so the atomic initialization must be provided here, + // before the worker thread is started. + // See [this thread](https://stackoverflow.com/questions/25609858) for context + // on the constructor. + current_id_ = 0; + shutting_down_ = false; + worker_thread_ = std::thread{&ExecutorStd::PollingThread, this}; +} + +ExecutorStd::~ExecutorStd() { + shutting_down_ = true; + // Make sure the worker thread is not blocked, so that the call to `join` + // doesn't hang. + UnblockQueue(); + worker_thread_.join(); +} + +void ExecutorStd::Execute(Operation&& operation) { + PushOnSchedule(std::move(operation), Immediate()); +} + +DelayedOperation ExecutorStd::Schedule(const Milliseconds delay, + TaggedOperation&& tagged) { + // While negative delay can be interpreted as a request for immediate + // execution, supporting it would provide a hacky way to modify FIFO ordering + // of immediate operations. + FIREBASE_ASSERT_MESSAGE(delay.count() >= 0, + "Schedule: delay cannot be negative"); + + namespace chr = std::chrono; + const auto now = chr::time_point_cast<Milliseconds>(chr::steady_clock::now()); + const auto id = + PushOnSchedule(std::move(tagged.operation), now + delay, tagged.tag); + + return DelayedOperation{[this, id] { TryCancel(id); }}; +} + +void ExecutorStd::TryCancel(const Id operation_id) { + schedule_.RemoveIf( + [operation_id](const Entry& e) { return e.id == operation_id; }); +} + +ExecutorStd::Id ExecutorStd::PushOnSchedule(Operation&& operation, + const TimePoint when, + const Tag tag) { + // Note: operations scheduled for immediate execution don't actually need an + // id. This could be tweaked to reuse the same id for all such operations. + const auto id = NextId(); + schedule_.Push(Entry{std::move(operation), id, tag}, when); + return id; +} + +void ExecutorStd::PollingThread() { + while (!shutting_down_) { + Entry entry = schedule_.PopBlocking(); + if (entry.tagged.operation) { + entry.tagged.operation(); + } + } +} + +void ExecutorStd::UnblockQueue() { + // Put a no-op for immediate execution on the queue to ensure that + // `schedule_.PopBlocking` returns, and worker thread can notice that shutdown + // is in progress. + schedule_.Push(Entry{[] {}, /*id=*/0}, Immediate()); +} + +ExecutorStd::Id ExecutorStd::NextId() { + // The wrap around after ~4 billion operations is explicitly ignored. Even if + // an instance of `ExecutorStd` runs long enough to get `current_id_` to + // overflow, it's extremely unlikely that any object still holds a reference + // that is old enough to cause a conflict. + return current_id_++; +} + +bool ExecutorStd::IsCurrentExecutor() const { + return std::this_thread::get_id() == worker_thread_.get_id(); +} + +std::string ExecutorStd::CurrentExecutorName() const { + return ThreadIdToString(std::this_thread::get_id()); +} + +std::string ExecutorStd::Name() const { + return ThreadIdToString(worker_thread_.get_id()); +} + +void ExecutorStd::ExecuteBlocking(Operation&& operation) { + std::promise<void> signal_finished; + Execute([&] { + operation(); + signal_finished.set_value(); + }); + signal_finished.get_future().wait(); +} + +bool ExecutorStd::IsScheduled(const Tag tag) const { + return schedule_.Contains( + [&tag](const Entry& e) { return e.tagged.tag == tag; }); +} + +absl::optional<Executor::TaggedOperation> ExecutorStd::PopFromSchedule() { + auto removed = + schedule_.RemoveIf([](const Entry& e) { return !e.IsImmediate(); }); + if (!removed.has_value()) { + return {}; + } + return {std::move(removed.value().tagged)}; +} + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/executor_std.h b/Firestore/core/src/firebase/firestore/util/executor_std.h new file mode 100644 index 0000000..8f2be02 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/executor_std.h @@ -0,0 +1,280 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_STD_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_STD_H_ + +#include <algorithm> +#include <atomic> +#include <condition_variable> // NOLINT(build/c++11) +#include <deque> +#include <mutex> // NOLINT(build/c++11) +#include <string> +#include <thread> // NOLINT(build/c++11) +#include <utility> + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/src/firebase/firestore/util/firebase_assert.h" +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace async { + +// A thread-safe class similar to a priority queue where the entries are +// prioritized by the time for which they're scheduled. Entries scheduled for +// the exact same time are prioritized in FIFO order. +// +// The main function of `Schedule` is `PopBlocking`, which sleeps until an entry +// becomes available. It correctly handles entries being asynchonously added or +// removed from the schedule. +// +// The details of time management are completely concealed within the class. +// Once an entry is scheduled, there is no way to reschedule or even retrieve +// the time. +template <typename T> +class Schedule { + // Internal invariants: + // - entries are always in sorted order, leftmost entry is always the most + // due; + // - each operation modifying the queue notifies the condition variable `cv_`. + public: + using Duration = std::chrono::milliseconds; + using Clock = std::chrono::steady_clock; + // Entries are scheduled using absolute time. + using TimePoint = std::chrono::time_point<Clock, Duration>; + + // Schedules an entry for the specified time due. `due` may be in the past. + void Push(const T& value, const TimePoint due) { + InsertPreservingOrder(Entry{value, due}); + } + void Push(T&& value, const TimePoint due) { + InsertPreservingOrder(Entry{std::move(value), due}); + } + + // If the queue contains at least one entry for which the scheduled time is + // due now (according to the system clock), removes the entry which is the + // most overdue from the queue and returns it. If no entry is due, returns an + // empty `optional`. + absl::optional<T> PopIfDue() { + std::lock_guard<std::mutex> lock{mutex_}; + + if (HasDueLocked()) { + return ExtractLocked(scheduled_.begin()); + } + return {}; + } + + // Blocks until at least one entry is available for which the scheduled time + // is due now (according to the system clock), removes the entry which is the + // most overdue from the queue and returns it. The function will + // attempt to minimize both the waiting time and busy waiting. + T PopBlocking() { + std::unique_lock<std::mutex> lock{mutex_}; + + while (true) { + cv_.wait(lock, [this] { return !scheduled_.empty(); }); + + // To minimize busy waiting, sleep until either the nearest entry in the + // future either changes, or else becomes due. + const auto until = scheduled_.front().due; + cv_.wait_until(lock, until, + [this, until] { return scheduled_.front().due != until; }); + // There are 3 possibilities why `wait_until` has returned: + // - `wait_until` has timed out, in which case the current time is at + // least `until`, so there must be an overdue entry; + // - a new entry has been added which comes before `until`. It must be + // either overdue (in which case `HasDueLocked` will break the cycle), + // or else `until` must be reevaluated (on the next iteration of the + // loop); + // - `until` entry has been removed. This means `until` has to be + // reevaluated, similar to #2. + + if (HasDueLocked()) { + return ExtractLocked(scheduled_.begin()); + } + } + } + + bool empty() const { + std::lock_guard<std::mutex> lock{mutex_}; + return scheduled_.empty(); + } + + size_t size() const { + std::lock_guard<std::mutex> lock{mutex_}; + return scheduled_.size(); + } + + // Removes the first entry satisfying predicate from the queue and returns it. + // If no such entry exists, returns an empty `optional`. Predicate is applied + // to entries in order according to their scheduled time. + // + // Note that this function doesn't take into account whether the removed entry + // is past its due time. + template <typename Pred> + absl::optional<T> RemoveIf(const Pred pred) { + std::lock_guard<std::mutex> lock{mutex_}; + + for (auto iter = scheduled_.begin(), end = scheduled_.end(); iter != end; + ++iter) { + if (pred(iter->value)) { + return ExtractLocked(iter); + } + } + return {}; + } + + // Checks whether the queue contains an entry satisfying the given predicate. + template <typename Pred> + bool Contains(const Pred pred) const { + std::lock_guard<std::mutex> lock{mutex_}; + return std::any_of(scheduled_.begin(), scheduled_.end(), + [&pred](const Entry& s) { return pred(s.value); }); + } + + private: + struct Entry { + bool operator<(const Entry& rhs) const { + return due < rhs.due; + } + + T value; + TimePoint due; + }; + // All removals are on the front, but most insertions are expected to be on + // the back. + using Container = std::deque<Entry>; + using Iterator = typename Container::iterator; + + void InsertPreservingOrder(Entry&& new_entry) { + std::lock_guard<std::mutex> lock{mutex_}; + + const auto insertion_point = + std::upper_bound(scheduled_.begin(), scheduled_.end(), new_entry); + scheduled_.insert(insertion_point, std::move(new_entry)); + + cv_.notify_one(); + } + + // This function expects the mutex to be already locked. + bool HasDueLocked() const { + namespace chr = std::chrono; + const auto now = chr::time_point_cast<Duration>(Clock::now()); + return !scheduled_.empty() && now >= scheduled_.front().due; + } + + // This function expects the mutex to be already locked. + T ExtractLocked(const Iterator where) { + FIREBASE_ASSERT_MESSAGE(!scheduled_.empty(), + "Trying to pop an entry from an empty queue."); + + T result = std::move(where->value); + scheduled_.erase(where); + cv_.notify_one(); + + return result; + } + + mutable std::mutex mutex_; + std::condition_variable cv_; + Container scheduled_; +}; + +} // namespace async + +namespace internal { + +// A serial queue that executes provided operations on a dedicated background +// thread, using C++11 standard library functionality. +class ExecutorStd : public Executor { + public: + ExecutorStd(); + ~ExecutorStd(); + + void Execute(Operation&& operation) override; + void ExecuteBlocking(Operation&& operation) override; + + DelayedOperation Schedule(Milliseconds delay, + TaggedOperation&& tagged) override; + + bool IsCurrentExecutor() const override; + std::string CurrentExecutorName() const override; + std::string Name() const override; + + bool IsScheduled(Tag tag) const override; + absl::optional<TaggedOperation> PopFromSchedule() override; + + using TimePoint = async::Schedule<Operation>::TimePoint; + // To allow canceling operations, each scheduled operation is assigned + // a monotonically increasing identifier. + using Id = unsigned int; + + // If the operation hasn't yet been run, it will be removed from the queue. + // Otherwise, this function is a no-op. + void TryCancel(Id operation_id); + + Id PushOnSchedule(Operation&& operation, TimePoint when, Tag tag = -1); + + void PollingThread(); + void UnblockQueue(); + Id NextId(); + + // As a convention, assign the epoch time to all operations scheduled for + // immediate execution. Note that it means that an immediate operation is + // always scheduled before any delayed operation, even in the corner case when + // the immediate operation was scheduled after a delayed operation was due + // (but hasn't yet run). + static TimePoint Immediate() { + return TimePoint{}; + } + + struct Entry { + Entry() { + } + Entry(Operation&& operation, + const ExecutorStd::Id id, + const ExecutorStd::Tag tag = kNoTag) + : tagged{tag, std::move(operation)}, id{id} { + } + + bool IsImmediate() const { + return tagged.tag == kNoTag; + } + + static constexpr Tag kNoTag = -1; + TaggedOperation tagged; + Id id = 0; + }; + // Operations scheduled for immediate execution are also put on the schedule + // (with due time set to `Immediate`). + async::Schedule<Entry> schedule_; + + std::thread worker_thread_; + // Used to stop the worker thread. + std::atomic<bool> shutting_down_{false}; + + std::atomic<Id> current_id_{0}; +}; + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EXECUTOR_STD_H_ diff --git a/Firestore/core/src/firebase/firestore/util/hashing.h b/Firestore/core/src/firebase/firestore/util/hashing.h new file mode 100644 index 0000000..d8058c8 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/hashing.h @@ -0,0 +1,151 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_HASHING_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_HASHING_H_ + +#include <iterator> +#include <type_traits> + +namespace firebase { +namespace firestore { +namespace util { + +// This is a pretty terrible hash implementation for lack of a better one being +// readily available. It exists as a portability crutch between our existing +// Objective-C code where overriding `-isEqual:` also requires `-hash` and C++ +// where `operator==()` can be defined without defining a hash code. +// +// It's based on the recommendation in Effective Java, Item 9, wherein you +// implement composite hashes like so: +// +// size_t result = first_; +// result = 31 * result + second_; +// result = 31 * result + third_; +// // ... +// return result; +// +// This is the basis of this implementation because that's what the existing +// Objective-C code mostly does by hand. Using this implementation gets the +// same result by calling +// +// return util::Hash(first_, second_, /* ..., */ third_); +// +// TODO(wilhuff): Replace this with whatever Abseil releases. + +namespace impl { + +/** + * Combines a hash_value with whatever accumulated state there is so far. + */ +inline size_t Combine(size_t state, size_t hash_value) { + return 31 * state + hash_value; +} + +/** + * Explicit ordering of hashers, allowing SFINAE without all the enable_if + * cruft. + * + * In order we try: + * * A Hash() member, if defined and the return type is an integral type + * * A std::hash specialization, if available + * * A range-based specialization, valid if either of the above hold on the + * members of the range. + * + * Explicit ordering resolves the ambiguity of the case where a std::hash + * specialization is available, but the type is also a range for whose members + * std::hash is also available, e.g. with std::string. + * + * HashChoice is a recursive type, defined such that HashChoice<0> is the most + * specific type with HashChoice<1> and beyond being progressively less + * specific. This causes the compiler to prioritize the overloads with + * lower-numbered HashChoice types, allowing compilation to succeed even if + * multiple specializations match. + */ +template <int I> +struct HashChoice : HashChoice<I + 1> {}; + +template <> +struct HashChoice<2> {}; + +template <typename K> +size_t InvokeHash(const K& value); + +/** + * Hashes the given value if it defines a Hash() member. + * + * @return The result of `value.Hash()`. + */ +template <typename K> +auto RankedInvokeHash(const K& value, HashChoice<0>) -> decltype(value.Hash()) { + return value.Hash(); +} + +/** + * Hashes the given value if it has a specialization of std::hash. + * + * @return The result of `std::hash<K>{}(value)` + */ +template <typename K> +auto RankedInvokeHash(const K& value, HashChoice<1>) + -> decltype(std::hash<K>{}(value)) { + return std::hash<K>{}(value); +} + +/** + * Hashes the contents of the given range of values if the value_type of the + * range can be hashed. + */ +template <typename Range> +auto RankedInvokeHash(const Range& range, HashChoice<2>) + -> decltype(impl::InvokeHash(*std::begin(range))) { + size_t result = 0; + size_t size = 0; + for (auto&& element : range) { + ++size; + result = Combine(result, InvokeHash(element)); + } + result = Combine(result, size); + return result; +} + +template <typename K> +size_t InvokeHash(const K& value) { + return RankedInvokeHash(value, HashChoice<0>{}); +} + +inline size_t HashInternal(size_t state) { + return state; +} + +template <typename T, typename... Ts> +size_t HashInternal(size_t state, const T& value, const Ts&... rest) { + state = Combine(state, InvokeHash(value)); + return HashInternal(state, rest...); +} + +} // namespace impl + +template <typename... Ts> +size_t Hash(const Ts&... values) { + return impl::HashInternal(0u, values...); +} + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_HASHING_H_ diff --git a/Firestore/core/test/firebase/firestore/immutable/CMakeLists.txt b/Firestore/core/test/firebase/firestore/immutable/CMakeLists.txt index 753e2d0..aa8643b 100644 --- a/Firestore/core/test/firebase/firestore/immutable/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/immutable/CMakeLists.txt @@ -18,6 +18,7 @@ cc_test( array_sorted_map_test.cc testing.h sorted_map_test.cc + sorted_set_test.cc tree_sorted_map_test.cc DEPENDS firebase_firestore_immutable diff --git a/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc b/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc index bcacb50..75353d9 100644 --- a/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc +++ b/Firestore/core/test/firebase/firestore/immutable/sorted_map_test.cc @@ -18,6 +18,7 @@ #include <numeric> #include <random> +#include <type_traits> #include <unordered_set> #include <utility> @@ -297,6 +298,9 @@ TYPED_TEST(SortedMapTest, MinMax) { } TYPED_TEST(SortedMapTest, IteratorsAreDefaultConstructible) { + ASSERT_TRUE( + std::is_default_constructible<typename TypeParam::const_iterator>::value); + // If this compiles the test has succeeded typename TypeParam::const_iterator iter; (void)iter; diff --git a/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc b/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc new file mode 100644 index 0000000..a4b337c --- /dev/null +++ b/Firestore/core/test/firebase/firestore/immutable/sorted_set_test.cc @@ -0,0 +1,182 @@ +/* + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/immutable/sorted_set.h" + +#include <random> +#include <unordered_set> + +#include "Firestore/core/test/firebase/firestore/immutable/testing.h" + +using firebase::firestore::immutable::impl::SortedMapBase; +using SizeType = SortedMapBase::size_type; + +namespace firebase { +namespace firestore { +namespace immutable { + +template <typename K> +SortedSet<K> ToSet(const std::vector<K>& container) { + SortedSet<K> result; + for (auto&& entry : container) { + result = result.insert(entry); + } + return result; +} + +static const int kLargeNumber = 100; + +TEST(SortedSetTest, EmptyBehavior) { + SortedSet<int> set; + + EXPECT_TRUE(set.empty()); + EXPECT_EQ(0u, set.size()); + + EXPECT_TRUE(NotFound(set, 1)); +} + +TEST(SortedSetTest, Size) { + std::mt19937 rand; + std::uniform_int_distribution<int> dist(0, 999); + + std::unordered_set<int> expected; + + SortedSet<int> set; + for (int i = 0; i < kLargeNumber; ++i) { + int value = dist(rand); + + // The random number sequence can generate duplicates, so the expected size + // won't necessarily depend upon `i`. + expected.insert(value); + + set = set.insert(value); + EXPECT_EQ(expected.size(), set.size()); + } + + for (int i = 0; i < kLargeNumber; ++i) { + int value = dist(rand); + + // The random number sequence can generate duplicates, so the expected size + // won't necessarily depend upon `i`. + expected.erase(value); + + set = set.erase(value); + EXPECT_EQ(expected.size(), set.size()); + } +} + +TEST(SortedSetSet, Find) { + SortedSet<int> set = SortedSet<int>{}.insert(1).insert(2).insert(4); + + EXPECT_TRUE(NotFound(set, 0)); + EXPECT_TRUE(Found(set, 1)); + EXPECT_TRUE(Found(set, 2)); + EXPECT_TRUE(NotFound(set, 3)); + EXPECT_TRUE(Found(set, 4)); + EXPECT_TRUE(NotFound(set, 5)); +} + +TEST(SortedSetTest, IteratorsAreDefaultConstructible) { + static_assert( + std::is_default_constructible<SortedSet<int>::const_iterator>::value, + "is default constructible"); +} + +TEST(SortedSetTest, CanBeConstructedFromSortedMap) { + using Map = SortedMap<int, int>; + + Map map = Map{}.insert(1, 2).insert(3, 4); + auto set = MakeSortedSet(map); + + ASSERT_TRUE(Found(set, 1)); + ASSERT_TRUE(NotFound(set, 2)); + + // Set insertion does not modify the underlying map + set = set.insert(2); + ASSERT_TRUE(Found(set, 2)); + ASSERT_TRUE(NotFound(map, 2)); +} + +TEST(SortedSetTest, Iterator) { + std::vector<int> all = Sequence(kLargeNumber); + SortedSet<int> set = ToSet(Shuffled(all)); + + auto begin = set.begin(); + ASSERT_EQ(0, *begin); + + auto end = set.end(); + ASSERT_EQ(all.size(), static_cast<size_t>(std::distance(begin, end))); + + ASSERT_SEQ_EQ(all, set); +} + +TEST(SortedSetTest, ValuesFrom) { + std::vector<int> all = Sequence(2, 42, 2); + SortedSet<int> set = ToSet(Shuffled(all)); + ASSERT_EQ(20u, set.size()); + + // Test from before keys. + ASSERT_SEQ_EQ(all, set.values_from(0)); + + // Test from after keys. + ASSERT_SEQ_EQ(Empty(), set.values_from(100)); + + // Test from a key in the set: should start at that key. + ASSERT_SEQ_EQ(Sequence(10, 42, 2), set.values_from(10)); + + // Test from in between keys: should start just after that key. + ASSERT_SEQ_EQ(Sequence(12, 42, 2), set.values_from(11)); +} + +TEST(SortedSetTest, ValuesIn) { + std::vector<int> all = Sequence(2, 42, 2); + SortedSet<int> set = ToSet(Shuffled(all)); + ASSERT_EQ(20u, set.size()); + + // Constructs a sequence from `start` up to but not including `end` by 2. + auto Seq = [](int start, int end) { return Sequence(start, end, 2); }; + + ASSERT_SEQ_EQ(Empty(), set.values_in(0, 1)); // before to before + ASSERT_SEQ_EQ(all, set.values_in(0, 100)) // before to after + ASSERT_SEQ_EQ(Seq(2, 6), set.values_in(0, 6)) // before to in set + ASSERT_SEQ_EQ(Seq(2, 8), set.values_in(0, 7)) // before to in between + + ASSERT_SEQ_EQ(Empty(), set.values_in(100, 0)); // after to before + ASSERT_SEQ_EQ(Empty(), set.values_in(100, 110)); // after to after + ASSERT_SEQ_EQ(Empty(), set.values_in(100, 6)); // after to in set + ASSERT_SEQ_EQ(Empty(), set.values_in(100, 7)); // after to in between + + ASSERT_SEQ_EQ(Empty(), set.values_in(6, 0)); // in set to before + ASSERT_SEQ_EQ(Seq(6, 42), set.values_in(6, 100)); // in set to after + ASSERT_SEQ_EQ(Seq(6, 10), set.values_in(6, 10)); // in set to in set + ASSERT_SEQ_EQ(Seq(6, 12), set.values_in(6, 11)); // in set to in between + + ASSERT_SEQ_EQ(Empty(), set.values_in(7, 0)); // in between to before + ASSERT_SEQ_EQ(Seq(8, 42), set.values_in(7, 100)); // in between to after + ASSERT_SEQ_EQ(Seq(8, 10), set.values_in(7, 10)); // in between to key in set + ASSERT_SEQ_EQ(Seq(8, 14), set.values_in(7, 13)); // in between to in between +} + +TEST(SortedSetTest, HashesStdHashable) { + SortedSet<int> set; + + size_t result = util::Hash(set); + (void)result; +} + +} // namespace immutable +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/immutable/testing.h b/Firestore/core/test/firebase/firestore/immutable/testing.h index 9e839c6..8e496dd 100644 --- a/Firestore/core/test/firebase/firestore/immutable/testing.h +++ b/Firestore/core/test/firebase/firestore/immutable/testing.h @@ -18,16 +18,33 @@ #define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_IMMUTABLE_TESTING_H_ #include <algorithm> +#include <string> +#include <type_traits> #include <utility> #include <vector> #include "Firestore/core/src/firebase/firestore/util/secure_random.h" +#include "absl/strings/str_cat.h" #include "gtest/gtest.h" namespace firebase { namespace firestore { namespace immutable { +template <typename K, typename V> +std::string Describe(const std::pair<K, V>& pair) { + return absl::StrCat("(", pair.first, ", ", pair.second, ")"); +} + +// Describes the given item by its std::to_string implementation (if +// std::to_string is defined for V). The return type is not defined directly +// in terms of std::string in order to allow specialization failure to select +// a different overload. +template <typename V> +auto Describe(const V& item) -> decltype(std::to_string(item)) { + return std::to_string(item); +} + template <typename Container, typename K> testing::AssertionResult NotFound(const Container& map, const K& key) { if (map.contains(key)) { @@ -40,11 +57,15 @@ testing::AssertionResult NotFound(const Container& map, const K& key) { return testing::AssertionSuccess(); } else { return testing::AssertionFailure() - << "Should not have found (" << found->first << ", " << found->second - << ")"; + << "Should not have found " << Describe(*found); } } +/** + * Asserts that the given key is found in the given container and that it maps + * to the given value. This only works with map-type containers where value_type + * is `std::pair<K, V>`. + */ template <typename Container, typename K, typename V> testing::AssertionResult Found(const Container& map, const K& key, @@ -67,6 +88,31 @@ testing::AssertionResult Found(const Container& map, } } +/** + * Asserts that the given key is found in the given container without + * necessarily checking that the key maps to any value. This also makes + * this compatible with non-mapped containers where K is the value_type. + */ +template <typename Container, typename K> +testing::AssertionResult Found(const Container& container, const K& key) { + if (!container.contains(key)) { + return testing::AssertionFailure() + << "Did not find key " << key << " using contains()"; + } + + auto found = container.find(key); + if (found == container.end()) { + return testing::AssertionFailure() + << "Did not find key " << key << " using find()"; + } + if (*found == key) { + return testing::AssertionSuccess(); + } else { + return testing::AssertionFailure() + << "Found entry was " << Describe(*found); + } +} + /** Creates an empty vector (for readability). */ inline std::vector<int> Empty() { return {}; diff --git a/Firestore/core/test/firebase/firestore/model/CMakeLists.txt b/Firestore/core/test/firebase/firestore/model/CMakeLists.txt index 9c94677..b38d658 100644 --- a/Firestore/core/test/firebase/firestore/model/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/model/CMakeLists.txt @@ -20,14 +20,12 @@ cc_test( document_test.cc field_mask_test.cc field_path_test.cc - field_transform_test.cc field_value_test.cc maybe_document_test.cc no_document_test.cc precondition_test.cc resource_path_test.cc snapshot_version_test.cc - transform_operations_test.cc DEPENDS firebase_firestore_model ) diff --git a/Firestore/core/test/firebase/firestore/model/document_key_test.cc b/Firestore/core/test/firebase/firestore/model/document_key_test.cc index 619ee7f..71b78d1 100644 --- a/Firestore/core/test/firebase/firestore/model/document_key_test.cc +++ b/Firestore/core/test/firebase/firestore/model/document_key_test.cc @@ -21,8 +21,11 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "gtest/gtest.h" +using firebase::firestore::testutil::Key; + namespace firebase { namespace firestore { namespace model { @@ -112,16 +115,16 @@ TEST(DocumentKey, IsDocumentKey) { } TEST(DocumentKey, Comparison) { - const DocumentKey abcd({"a", "b", "c", "d"}); - const DocumentKey abcd_too({"a", "b", "c", "d"}); - const DocumentKey xyzw({"x", "y", "z", "w"}); + DocumentKey abcd = Key("a/b/c/d"); + DocumentKey abcd_too = Key("a/b/c/d"); + DocumentKey xyzw = Key("x/y/z/w"); EXPECT_EQ(abcd, abcd_too); EXPECT_NE(abcd, xyzw); - const DocumentKey empty; - const DocumentKey a({"a", "a"}); - const DocumentKey b({"b", "b"}); - const DocumentKey ab({"a", "a", "b", "b"}); + DocumentKey empty; + DocumentKey a = Key("a/a"); + DocumentKey b = Key("b/b"); + DocumentKey ab = Key("a/a/b/b"); EXPECT_FALSE(empty < empty); EXPECT_TRUE(empty <= empty); @@ -148,6 +151,12 @@ TEST(DocumentKey, Comparison) { EXPECT_TRUE(ab >= a); } +TEST(DocumentKey, Comparator) { + DocumentKey abcd = Key("a/b/c/d"); + DocumentKey xyzw = Key("x/y/z/w"); + EXPECT_TRUE(util::Comparator<DocumentKey>{}(abcd, xyzw)); +} + } // namespace model } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc index 96125f7..a147309 100644 --- a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc @@ -33,18 +33,41 @@ #include <vector> #include "Firestore/Protos/cpp/google/firestore/v1beta1/document.pb.h" +#include "Firestore/Protos/cpp/google/firestore/v1beta1/firestore.pb.h" +#include "Firestore/core/include/firebase/firestore/firestore_errors.h" +#include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" +#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/timestamp_internal.h" #include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "google/protobuf/stubs/common.h" #include "google/protobuf/util/message_differencer.h" #include "gtest/gtest.h" +using firebase::Timestamp; +using firebase::TimestampInternal; +using firebase::firestore::FirestoreErrorCode; +using firebase::firestore::model::DatabaseId; +using firebase::firestore::model::Document; +using firebase::firestore::model::DocumentKey; using firebase::firestore::model::FieldValue; +using firebase::firestore::model::MaybeDocument; +using firebase::firestore::model::NoDocument; using firebase::firestore::model::ObjectValue; +using firebase::firestore::model::SnapshotVersion; using firebase::firestore::remote::Serializer; +using firebase::firestore::testutil::Key; using firebase::firestore::util::Status; +using firebase::firestore::util::StatusOr; using google::protobuf::util::MessageDifferencer; +#define ASSERT_OK(status) ASSERT_TRUE(StatusOk(status)) +#define ASSERT_NOT_OK(status) ASSERT_FALSE(StatusOk(status)) +#define EXPECT_OK(status) EXPECT_TRUE(StatusOk(status)) +#define EXPECT_NOT_OK(status) EXPECT_FALSE(StatusOk(status)) + TEST(Serializer, CanLinkToNanopb) { // This test doesn't actually do anything interesting as far as actually using // nanopb is concerned but that it can run at all is proof that all the @@ -56,8 +79,11 @@ TEST(Serializer, CanLinkToNanopb) { // Fixture for running serializer tests. class SerializerTest : public ::testing::Test { public: - SerializerTest() : serializer(/*DatabaseId("p", "d")*/) { + SerializerTest() : serializer(kDatabaseId) { + msg_diff.ReportDifferencesToString(&message_differences); } + + const DatabaseId kDatabaseId{"p", "d"}; Serializer serializer; void ExpectRoundTrip(const FieldValue& model, @@ -74,22 +100,88 @@ class SerializerTest : public ::testing::Test { ExpectDeserializationRoundTrip(model, proto, type); } + void ExpectRoundTrip( + const DocumentKey& key, + const FieldValue& value, + const SnapshotVersion& update_time, + const google::firestore::v1beta1::BatchGetDocumentsResponse& proto) { + ExpectSerializationRoundTrip(key, value, update_time, proto); + ExpectDeserializationRoundTrip(key, value, update_time, proto); + } + + /** + * Checks the status. Don't use directly; use one of the relevant macros + * instead. eg: + * + * Status good_status = ...; + * ASSERT_OK(good_status); + * + * Status bad_status = ...; + * EXPECT_NOT_OK(bad_status); + */ + testing::AssertionResult StatusOk(const Status& status) { + if (!status.ok()) { + return testing::AssertionFailure() + << "Status should have been ok, but instead contained " + << status.ToString(); + } + return testing::AssertionSuccess(); + } + + template <typename T> + testing::AssertionResult StatusOk(const StatusOr<T>& status) { + return StatusOk(status.status()); + } + + /** + * Ensures that decoding fails with the given status. + * + * @param status the expected (failed) status. Only the code() is verified. + */ + void ExpectFailedStatusDuringDecode(Status status, + const std::vector<uint8_t>& bytes) { + StatusOr<FieldValue> bad_status = serializer.DecodeFieldValue(bytes); + ASSERT_NOT_OK(bad_status); + EXPECT_EQ(status.code(), bad_status.status().code()); + } + google::firestore::v1beta1::Value ValueProto(nullptr_t) { - std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::NullValue(), &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); return proto; } - google::firestore::v1beta1::Value ValueProto(bool b) { + std::vector<uint8_t> EncodeFieldValue(Serializer* serializer, + const FieldValue& fv) { + std::vector<uint8_t> bytes; + Status status = serializer->EncodeFieldValue(fv, &bytes); + EXPECT_OK(status); + return bytes; + } + + std::vector<uint8_t> EncodeDocument(Serializer* serializer, + const DocumentKey& key, + const FieldValue& value) { std::vector<uint8_t> bytes; Status status = - serializer.EncodeFieldValue(FieldValue::BooleanValue(b), &bytes); - EXPECT_TRUE(status.ok()); + serializer->EncodeDocument(key, value.object_value(), &bytes); + EXPECT_OK(status); + return bytes; + } + + void Mutate(uint8_t* byte, + uint8_t expected_initial_value, + uint8_t new_value) { + ASSERT_EQ(*byte, expected_initial_value); + *byte = new_value; + } + + google::firestore::v1beta1::Value ValueProto(bool b) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::BooleanValue(b)); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -97,10 +189,8 @@ class SerializerTest : public ::testing::Test { } google::firestore::v1beta1::Value ValueProto(int64_t i) { - std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::IntegerValue(i), &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::IntegerValue(i)); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -112,10 +202,17 @@ class SerializerTest : public ::testing::Test { } google::firestore::v1beta1::Value ValueProto(const std::string& s) { - std::vector<uint8_t> bytes; - Status status = - serializer.EncodeFieldValue(FieldValue::StringValue(s), &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::StringValue(s)); + google::firestore::v1beta1::Value proto; + bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); + EXPECT_TRUE(ok); + return proto; + } + + google::firestore::v1beta1::Value ValueProto(const Timestamp& ts) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::TimestampValue(ts)); google::firestore::v1beta1::Value proto; bool ok = proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); @@ -128,13 +225,11 @@ class SerializerTest : public ::testing::Test { const google::firestore::v1beta1::Value& proto, FieldValue::Type type) { EXPECT_EQ(type, model.type()); - std::vector<uint8_t> bytes; - Status status = serializer.EncodeFieldValue(model, &bytes); - EXPECT_TRUE(status.ok()); + std::vector<uint8_t> bytes = EncodeFieldValue(&serializer, model); google::firestore::v1beta1::Value actual_proto; bool ok = actual_proto.ParseFromArray(bytes.data(), bytes.size()); EXPECT_TRUE(ok); - EXPECT_TRUE(MessageDifferencer::Equals(proto, actual_proto)); + EXPECT_TRUE(msg_diff.Compare(proto, actual_proto)) << message_differences; } void ExpectDeserializationRoundTrip( @@ -145,28 +240,87 @@ class SerializerTest : public ::testing::Test { std::vector<uint8_t> bytes(size); bool status = proto.SerializeToArray(bytes.data(), size); EXPECT_TRUE(status); - FieldValue actual_model = serializer.DecodeFieldValue(bytes); + StatusOr<FieldValue> actual_model_status = + serializer.DecodeFieldValue(bytes); + EXPECT_OK(actual_model_status); + FieldValue actual_model = actual_model_status.ValueOrDie(); EXPECT_EQ(type, actual_model.type()); EXPECT_EQ(model, actual_model); } -}; -// TODO(rsgowman): whoops! A previous commit performed approx s/Encodes/Writes/, -// but should not have done so here. Change it back in this file. + void ExpectSerializationRoundTrip( + const DocumentKey& key, + const FieldValue& value, + const SnapshotVersion& update_time, + const google::firestore::v1beta1::BatchGetDocumentsResponse& proto) { + std::vector<uint8_t> bytes = EncodeDocument(&serializer, key, value); + google::firestore::v1beta1::Document actual_proto; + bool ok = actual_proto.ParseFromArray(bytes.data(), bytes.size()); + EXPECT_TRUE(ok); + + // TODO(rsgowman): Right now, we only support Document (and don't support + // NoDocument). That should change in the next PR or so. + EXPECT_TRUE(proto.has_found()); + + // Slight weirdness: When we *encode* a document for sending it to the + // backend, we don't encode the update_time (or create_time). But when we + // *decode* a document, we *do* decode the update_time (though we still + // ignore the create_time). Therefore, we'll verify the update_time + // independently, and then strip it out before comparing the rest. + EXPECT_FALSE(actual_proto.has_create_time()); + EXPECT_EQ(update_time.timestamp().seconds(), + proto.found().update_time().seconds()); + EXPECT_EQ(update_time.timestamp().nanoseconds(), + proto.found().update_time().nanos()); + google::firestore::v1beta1::BatchGetDocumentsResponse proto_copy{proto}; + proto_copy.mutable_found()->clear_update_time(); + proto_copy.mutable_found()->clear_create_time(); + + EXPECT_TRUE(msg_diff.Compare(proto_copy.found(), actual_proto)) + << message_differences; + } + + void ExpectDeserializationRoundTrip( + const DocumentKey& key, + const FieldValue& value, + const SnapshotVersion& update_time, + const google::firestore::v1beta1::BatchGetDocumentsResponse& proto) { + size_t size = proto.ByteSizeLong(); + std::vector<uint8_t> bytes(size); + bool status = proto.SerializeToArray(bytes.data(), size); + EXPECT_TRUE(status); + StatusOr<std::unique_ptr<MaybeDocument>> actual_model_status = + serializer.DecodeMaybeDocument(bytes); + EXPECT_OK(actual_model_status); + std::unique_ptr<MaybeDocument> actual_model = + std::move(actual_model_status).ValueOrDie(); + + // TODO(rsgowman): Right now, we only support Document (and don't support + // NoDocument). That should change in the next PR or so. + EXPECT_EQ(MaybeDocument::Type::Document, actual_model->type()); + Document* actual_doc_model = static_cast<Document*>(actual_model.get()); + EXPECT_EQ(key, actual_model->key()); + EXPECT_EQ(value, actual_doc_model->data()); + EXPECT_EQ(update_time, actual_model->version()); + } + + std::string message_differences; + MessageDifferencer msg_diff; +}; -TEST_F(SerializerTest, WritesNull) { +TEST_F(SerializerTest, EncodesNull) { FieldValue model = FieldValue::NullValue(); ExpectRoundTrip(model, ValueProto(nullptr), FieldValue::Type::Null); } -TEST_F(SerializerTest, WritesBool) { +TEST_F(SerializerTest, EncodesBool) { for (bool bool_value : {true, false}) { FieldValue model = FieldValue::BooleanValue(bool_value); ExpectRoundTrip(model, ValueProto(bool_value), FieldValue::Type::Boolean); } } -TEST_F(SerializerTest, WritesIntegers) { +TEST_F(SerializerTest, EncodesIntegers) { std::vector<int64_t> cases{0, 1, -1, @@ -181,7 +335,7 @@ TEST_F(SerializerTest, WritesIntegers) { } } -TEST_F(SerializerTest, WritesString) { +TEST_F(SerializerTest, EncodesString) { std::vector<std::string> cases{ "", "a", @@ -204,7 +358,24 @@ TEST_F(SerializerTest, WritesString) { } } -TEST_F(SerializerTest, WritesEmptyMap) { +TEST_F(SerializerTest, EncodesTimestamps) { + std::vector<Timestamp> cases{ + {}, // epoch + {1234, 0}, + {1234, 999999999}, + {-1234, 0}, + {-1234, 999999999}, + TimestampInternal::Max(), + TimestampInternal::Min(), + }; + + for (const Timestamp& ts_value : cases) { + FieldValue model = FieldValue::TimestampValue(ts_value); + ExpectRoundTrip(model, ValueProto(ts_value), FieldValue::Type::Timestamp); + } +} + +TEST_F(SerializerTest, EncodesEmptyMap) { FieldValue model = FieldValue::ObjectValueFromMap({}); google::firestore::v1beta1::Value proto; @@ -213,7 +384,7 @@ TEST_F(SerializerTest, WritesEmptyMap) { ExpectRoundTrip(model, proto, FieldValue::Type::Object); } -TEST_F(SerializerTest, WritesNestedObjects) { +TEST_F(SerializerTest, EncodesNestedObjects) { FieldValue model = FieldValue::ObjectValueFromMap({ {"b", FieldValue::TrueValue()}, // TODO(rsgowman): add doubles (once they're supported) @@ -258,7 +429,206 @@ TEST_F(SerializerTest, WritesNestedObjects) { ExpectRoundTrip(model, proto, FieldValue::Type::Object); } +TEST_F(SerializerTest, BadNullValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); + + // Alter the null value from 0 to 1. + Mutate(&bytes[1], /*expected_initial_value=*/0, /*new_value=*/1); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadBoolValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::BooleanValue(true)); + + // Alter the bool value from 1 to 2. (Value values are 0,1) + Mutate(&bytes[1], /*expected_initial_value=*/1, /*new_value=*/2); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadIntegerValue) { + // Encode 'maxint'. This should result in 9 0xff bytes, followed by a 1. + std::vector<uint8_t> bytes = EncodeFieldValue( + &serializer, + FieldValue::IntegerValue(std::numeric_limits<uint64_t>::max())); + ASSERT_EQ(11u, bytes.size()); + for (size_t i = 1; i < bytes.size() - 1; i++) { + ASSERT_EQ(0xff, bytes[i]); + } + + // make the number a bit bigger + Mutate(&bytes[10], /*expected_initial_value=*/1, /*new_value=*/0xff); + bytes.resize(12); + bytes[11] = 0x7f; + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadStringValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::StringValue("a")); + + // Claim that the string length is 5 instead of 1. (The first two bytes are + // used by the encoded tag.) + Mutate(&bytes[2], /*expected_initial_value=*/1, /*new_value=*/5); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadTimestampValue_TooLarge) { + std::vector<uint8_t> bytes = EncodeFieldValue( + &serializer, FieldValue::TimestampValue(TimestampInternal::Max())); + + // Add some time, which should push us above the maximum allowed timestamp. + Mutate(&bytes[4], 0x82, 0x83); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadTimestampValue_TooSmall) { + std::vector<uint8_t> bytes = EncodeFieldValue( + &serializer, FieldValue::TimestampValue(TimestampInternal::Min())); + + // Remove some time, which should push us below the minimum allowed timestamp. + Mutate(&bytes[4], 0x92, 0x91); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, BadTag) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); + + // The google::firestore::v1beta1::Value value_type oneof currently has tags + // up to 18. For this test, we'll pick a tag that's unlikely to be added in + // the near term but still fits within a uint8_t even when encoded. + // Specifically 31. 0xf8 represents field number 31 encoded as a varint. + Mutate(&bytes[0], /*expected_initial_value=*/0x58, /*new_value=*/0xf8); + + // TODO(rsgowman): The behaviour is *temporarily* slightly different during + // development; this will cause a failed assertion rather than a failed + // status. Remove this EXPECT_ANY_THROW statement (and reenable the + // following commented out statement) once the corresponding assert has been + // removed from serializer.cc. + EXPECT_ANY_THROW(ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes)); + // ExpectFailedStatusDuringDecode( + // Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, TagVarintWiretypeStringMismatch) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::BooleanValue(true)); + + // 0x0a represents a bool value encoded as a string. (We're using a + // boolean_value tag here, but any tag that would be represented by a varint + // would do.) + Mutate(&bytes[0], /*expected_initial_value=*/0x08, /*new_value=*/0x0a); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, TagStringWiretypeVarintMismatch) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::StringValue("foo")); + + // 0x88 represents a string value encoded as a varint. + Mutate(&bytes[0], /*expected_initial_value=*/0x8a, /*new_value=*/0x88); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, IncompleteFieldValue) { + std::vector<uint8_t> bytes = + EncodeFieldValue(&serializer, FieldValue::NullValue()); + ASSERT_EQ(2u, bytes.size()); + + // Remove the (null) payload + ASSERT_EQ(0x00, bytes[1]); + bytes.pop_back(); + + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, IncompleteTag) { + std::vector<uint8_t> bytes; + ExpectFailedStatusDuringDecode( + Status(FirestoreErrorCode::DataLoss, "ignored"), bytes); +} + +TEST_F(SerializerTest, EncodesKey) { + EXPECT_EQ("projects/p/databases/d/documents", serializer.EncodeKey(Key(""))); + EXPECT_EQ("projects/p/databases/d/documents/one/two/three/four", + serializer.EncodeKey(Key("one/two/three/four"))); +} + +TEST_F(SerializerTest, DecodesKey) { + EXPECT_EQ(Key(""), serializer.DecodeKey("projects/p/databases/d/documents")); + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + "projects/p/databases/d/documents/one/two/three/four")); + // Same, but with a leading slash + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + "/projects/p/databases/d/documents/one/two/three/four")); +} + +TEST_F(SerializerTest, BadKey) { + std::vector<std::string> bad_cases{ + "", // empty (and too short) + "projects/p", // too short + "projects/p/databases/d", // too short + "projects/p/databases/d/documents/odd_number_of_local_elements", + "projects_spelled_wrong/p/databases/d/documents", + "projects/p/databases_spelled_wrong/d/documents", + "projects/not_project_p/databases/d/documents", + "projects/p/databases/not_database_d/documents", + "projects/p/databases/d/not_documents", + }; + + for (const std::string& bad_key : bad_cases) { + EXPECT_ANY_THROW(serializer.DecodeKey(bad_key)); + } +} + +TEST_F(SerializerTest, EncodesEmptyDocument) { + DocumentKey key = DocumentKey::FromPathString("path/to/the/doc"); + FieldValue empty_value = FieldValue::ObjectValueFromMap({}); + SnapshotVersion update_time = SnapshotVersion{{1234, 5678}}; + + google::firestore::v1beta1::BatchGetDocumentsResponse proto; + google::firestore::v1beta1::Document* doc_proto = proto.mutable_found(); + doc_proto->set_name(serializer.EncodeKey(key)); + doc_proto->mutable_fields(); + + google::protobuf::Timestamp* update_time_proto = + doc_proto->mutable_update_time(); + update_time_proto->set_seconds(1234); + update_time_proto->set_nanos(5678); + + // Note that we ignore create time in our serializer. We never set it, and + // never read it (other than to throw it away). But the server could (and + // probably does) set it, so we need to be able to discard it properly. The + // ExpectRoundTrip deals with this asymmetry. + google::protobuf::Timestamp* create_time_proto = + doc_proto->mutable_create_time(); + create_time_proto->set_seconds(8765); + create_time_proto->set_nanos(4321); + + ExpectRoundTrip(key, empty_value, update_time, proto); +} + // TODO(rsgowman): Test [en|de]coding multiple protos into the same output // vector. - -// TODO(rsgowman): Death test for decoding invalid bytes. diff --git a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt index e5dbec5..ea80ea2 100644 --- a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt @@ -61,6 +61,60 @@ if(HAVE_OPENSSL_RAND_H) ) endif() +## executors + +cc_test( + firebase_firestore_util_executor_std_test + SOURCES + executor_test.h + executor_test.cc + executor_std_test.cc + async_tests_util.h + DEPENDS + firebase_firestore_util_executor_std +) + +if(HAVE_LIBDISPATCH) + cc_test( + firebase_firestore_util_executor_libdispatch_test + SOURCES + executor_test.h + executor_test.cc + executor_libdispatch_test.mm + async_tests_util.h + DEPENDS + firebase_firestore_util_executor_libdispatch + ) +endif() + +## async queue + +cc_test( + firebase_firestore_util_async_queue_std_test + SOURCES + async_queue_test.h + async_queue_test.cc + async_queue_std_test.cc + async_tests_util.h + DEPENDS + firebase_firestore_util_executor_std + firebase_firestore_util_async_queue +) + +if(HAVE_LIBDISPATCH) + cc_test( + firebase_firestore_util_async_queue_libdispatch_test + SOURCES + async_queue_test.h + async_queue_test.cc + async_queue_libdispatch_test.mm + async_tests_util.h + DEPENDS + firebase_firestore_util_executor_libdispatch + firebase_firestore_util_async_queue + ) +endif() + ## main library cc_test( @@ -69,6 +123,7 @@ cc_test( autoid_test.cc bits_test.cc comparison_test.cc + hashing_test.cc iterator_adaptors_test.cc ordered_code_test.cc status_test.cc diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_libdispatch_test.mm b/Firestore/core/test/firebase/firestore/util/async_queue_libdispatch_test.mm new file mode 100644 index 0000000..f1ff394 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_queue_libdispatch_test.mm @@ -0,0 +1,87 @@ +/* + * 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. + */ + +#include "Firestore/core/test/firebase/firestore/util/async_queue_test.h" + +#include <memory> + +#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace { + +dispatch_queue_t CreateDispatchQueue() { + return dispatch_queue_create("AsyncQueueTests", DISPATCH_QUEUE_SERIAL); +} + +std::unique_ptr<internal::Executor> CreateExecutorFromQueue( + const dispatch_queue_t queue) { + return absl::make_unique<internal::ExecutorLibdispatch>(queue); +} + +std::unique_ptr<internal::Executor> CreateExecutorLibdispatch() { + return CreateExecutorFromQueue(CreateDispatchQueue()); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(AsyncQueueLibdispatch, + AsyncQueueTest, + ::testing::Values(CreateExecutorLibdispatch)); + +class AsyncQueueTestLibdispatchOnly : public TestWithTimeoutMixin, + public ::testing::Test { + public: + AsyncQueueTestLibdispatchOnly() + : underlying_queue{CreateDispatchQueue()}, + queue{CreateExecutorFromQueue(underlying_queue)} { + } + + dispatch_queue_t underlying_queue; + AsyncQueue queue; +}; + +// Additional tests to see how libdispatch-based version of `AsyncQueue` +// interacts with raw usage of libdispatch. + +TEST_F(AsyncQueueTestLibdispatchOnly, SameQueueIsAllowedForUnownedActions) { + internal::DispatchAsync(underlying_queue, [this] { + queue.Enqueue([this] { signal_finished(); }); + }); + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_F(AsyncQueueTestLibdispatchOnly, + VerifyIsCurrentQueueRequiresOperationInProgress) { + internal::DispatchSync(underlying_queue, [this] { + EXPECT_ANY_THROW(queue.VerifyIsCurrentQueue()); + }); +} + +TEST_F(AsyncQueueTestLibdispatchOnly, + VerifyIsCurrentQueueRequiresBeingCalledOnTheQueue) { + ASSERT_NE(underlying_queue, dispatch_get_main_queue()); + EXPECT_ANY_THROW(queue.VerifyIsCurrentQueue()); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_std_test.cc b/Firestore/core/test/firebase/firestore/util/async_queue_std_test.cc new file mode 100644 index 0000000..9e69ad0 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_queue_std_test.cc @@ -0,0 +1,41 @@ +/* + * 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. + */ + +#include "Firestore/core/test/firebase/firestore/util/async_queue_test.h" + +#include "Firestore/core/src/firebase/firestore/util/executor_std.h" + +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace { + +std::unique_ptr<internal::Executor> ExecutorFactory() { + return absl::make_unique<internal::ExecutorStd>(); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(AsyncQueueStd, + AsyncQueueTest, + ::testing::Values(ExecutorFactory)); +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_test.cc b/Firestore/core/test/firebase/firestore/util/async_queue_test.cc new file mode 100644 index 0000000..4247584 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_queue_test.cc @@ -0,0 +1,188 @@ +/* + * 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. + */ + +#include "Firestore/core/test/firebase/firestore/util/async_queue_test.h" + +#include <chrono> // NOLINT(build/c++11) +#include <future> // NOLINT(build/c++11) +#include <string> + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace { + +// In these generic tests the specific timer ids don't matter. +const TimerId kTimerId1 = TimerId::ListenStreamConnectionBackoff; +const TimerId kTimerId2 = TimerId::ListenStreamIdle; +const TimerId kTimerId3 = TimerId::WriteStreamConnectionBackoff; + +} // namespace + +TEST_P(AsyncQueueTest, Enqueue) { + queue.Enqueue([&] { signal_finished(); }); + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_P(AsyncQueueTest, EnqueueDisallowsNesting) { + queue.Enqueue([&] { // clang-format off + // clang-format on + EXPECT_ANY_THROW(queue.Enqueue([] {})); + signal_finished(); + }); + + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_P(AsyncQueueTest, EnqueueRelaxedWorksFromWithinEnqueue) { + queue.Enqueue([&] { // clang-format off + queue.EnqueueRelaxed([&] { signal_finished(); }); + // clang-format on + }); + + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_P(AsyncQueueTest, EnqueueBlocking) { + bool finished = false; + queue.EnqueueBlocking([&] { finished = true; }); + EXPECT_TRUE(finished); +} + +TEST_P(AsyncQueueTest, EnqueueBlockingDisallowsNesting) { + queue.EnqueueBlocking([&] { // clang-format off + EXPECT_ANY_THROW(queue.EnqueueBlocking([] {});); + // clang-format on + }); +} + +TEST_P(AsyncQueueTest, ExecuteBlockingDisallowsNesting) { + queue.EnqueueBlocking( + [&] { EXPECT_ANY_THROW(queue.ExecuteBlocking([] {});); }); +} + +TEST_P(AsyncQueueTest, VerifyIsCurrentQueueWorksWithOperationInProgress) { + queue.EnqueueBlocking([&] { EXPECT_NO_THROW(queue.VerifyIsCurrentQueue()); }); +} + +// TODO(varconst): this test is inherently flaky because it can't be guaranteed +// that the enqueued asynchronous operation didn't finish before the code has +// a chance to even enqueue the next operation. Delays are chosen so that the +// test is unlikely to fail in practice. Need to revisit this. +TEST_P(AsyncQueueTest, CanScheduleOperationsInTheFuture) { + std::string steps; + + queue.Enqueue([&steps] { steps += '1'; }); + queue.Enqueue([&] { + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(20), kTimerId1, [&] { + steps += '4'; + signal_finished(); + }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(10), kTimerId2, + [&steps] { steps += '3'; }); + queue.EnqueueRelaxed([&steps] { steps += '2'; }); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_EQ(steps, "1234"); +} + +TEST_P(AsyncQueueTest, CanCancelDelayedOperations) { + std::string steps; + + queue.Enqueue([&] { + // Queue everything from the queue to ensure nothing completes before we + // cancel. + + queue.EnqueueRelaxed([&steps] { steps += '1'; }); + + DelayedOperation delayed_operation = queue.EnqueueAfterDelay( + AsyncQueue::Milliseconds(1), kTimerId1, [&steps] { steps += '2'; }); + + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(5), kTimerId2, [&] { + steps += '3'; + signal_finished(); + }); + + EXPECT_TRUE(queue.IsScheduled(kTimerId1)); + delayed_operation.Cancel(); + EXPECT_FALSE(queue.IsScheduled(kTimerId1)); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_EQ(steps, "13"); + EXPECT_FALSE(queue.IsScheduled(kTimerId1)); +} + +TEST_P(AsyncQueueTest, CanCallCancelOnDelayedOperationAfterTheOperationHasRun) { + DelayedOperation delayed_operation; + queue.Enqueue([&] { + delayed_operation = queue.EnqueueAfterDelay( + AsyncQueue::Milliseconds(10), kTimerId1, [&] { signal_finished(); }); + EXPECT_TRUE(queue.IsScheduled(kTimerId1)); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_FALSE(queue.IsScheduled(kTimerId1)); + EXPECT_NO_THROW(delayed_operation.Cancel()); +} + +TEST_P(AsyncQueueTest, CanManuallyDrainAllDelayedOperationsForTesting) { + std::string steps; + + queue.Enqueue([&] { + queue.EnqueueRelaxed([&steps] { steps += '1'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(20000), kTimerId1, + [&] { steps += '4'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(10000), kTimerId2, + [&steps] { steps += '3'; }); + queue.EnqueueRelaxed([&steps] { steps += '2'; }); + signal_finished(); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + queue.RunScheduledOperationsUntil(TimerId::All); + EXPECT_EQ(steps, "1234"); +} + +TEST_P(AsyncQueueTest, CanManuallyDrainSpecificDelayedOperationsForTesting) { + std::string steps; + + queue.Enqueue([&] { + queue.EnqueueRelaxed([&] { steps += '1'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(20000), kTimerId1, + [&steps] { steps += '5'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(10000), kTimerId2, + [&steps] { steps += '3'; }); + queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(15000), kTimerId3, + [&steps] { steps += '4'; }); + queue.EnqueueRelaxed([&] { steps += '2'; }); + signal_finished(); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + queue.RunScheduledOperationsUntil(kTimerId3); + EXPECT_EQ(steps, "1234"); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_test.h b/Firestore/core/test/firebase/firestore/util/async_queue_test.h new file mode 100644 index 0000000..61c7ab6 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_queue_test.h @@ -0,0 +1,47 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_TEST_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_TEST_H_ + +#include <memory> + +#include "gtest/gtest.h" + +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/test/firebase/firestore/util/async_tests_util.h" + +namespace firebase { +namespace firestore { +namespace util { + +using FactoryFunc = std::unique_ptr<internal::Executor> (*)(); + +class AsyncQueueTest : public TestWithTimeoutMixin, + public ::testing::TestWithParam<FactoryFunc> { + public: + // `GetParam()` must return a factory function. + AsyncQueueTest() : queue{GetParam()()} { + } + + AsyncQueue queue; +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_QUEUE_TEST_H_ diff --git a/Firestore/core/test/firebase/firestore/util/async_tests_util.h b/Firestore/core/test/firebase/firestore/util/async_tests_util.h new file mode 100644 index 0000000..f953d66 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/async_tests_util.h @@ -0,0 +1,90 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_TESTS_UTIL_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_TESTS_UTIL_H_ + +#include <chrono> // NOLINT(build/c++11) +#include <cstdlib> +#include <future> // NOLINT(build/c++11) + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +inline std::chrono::time_point<std::chrono::steady_clock, + std::chrono::milliseconds> +now() { + return std::chrono::time_point_cast<std::chrono::milliseconds>( + std::chrono::steady_clock::now()); +} + +constexpr auto kTimeout = std::chrono::seconds(5); + +// Waits for the future to become ready and returns whether it timed out. +inline bool Await(const std::future<void>& future, + const std::chrono::milliseconds timeout = kTimeout) { + return future.wait_for(timeout) == std::future_status::ready; +} + +// Unfortunately, the future returned from std::async blocks in its destructor +// until the async call is finished. If the function called from std::async is +// buggy and hangs forever, the future's destructor will also hang forever. To +// avoid all tests freezing, the only thing to do is to abort (which skips +// destructors). +inline void Abort() { + ADD_FAILURE(); + std::abort(); +} + +// Calls std::abort if the future times out. +inline void AbortOnTimeout(const std::future<void>& future) { + if (!Await(future, kTimeout)) { + Abort(); + } +} + +// The macro calls AbortOnTimeout, but preserves stack trace. +#define ABORT_ON_TIMEOUT(future) \ + do { \ + SCOPED_TRACE("Async operation timed out, aborting..."); \ + AbortOnTimeout(future); \ + } while (0) + +class TestWithTimeoutMixin { + public: + TestWithTimeoutMixin() : signal_finished{[] {}} { + } + + // Googletest doesn't contain built-in functionality to block until an async + // operation completes, and there is no timeout by default. Work around both + // by resolving a packaged_task in the async operation and blocking on the + // associated future (with timeout). + bool WaitForTestToFinish(const std::chrono::seconds timeout = kTimeout) { + return signal_finished.get_future().wait_for(timeout) == + std::future_status::ready; + } + + std::packaged_task<void()> signal_finished; +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_TESTS_UTIL_H_ diff --git a/Firestore/core/test/firebase/firestore/util/executor_libdispatch_test.mm b/Firestore/core/test/firebase/firestore/util/executor_libdispatch_test.mm new file mode 100644 index 0000000..330c8fc --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/executor_libdispatch_test.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. + */ + +#include "Firestore/core/test/firebase/firestore/util/executor_test.h" + +#include <memory> + +#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace { + +std::unique_ptr<internal::Executor> ExecutorFactory() { + return absl::make_unique<internal::ExecutorLibdispatch>( + dispatch_queue_create("ExecutorLibdispatchTests", DISPATCH_QUEUE_SERIAL)); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(ExecutorTestLibdispatch, + ExecutorTest, + ::testing::Values(ExecutorFactory)); + +namespace internal { +class ExecutorLibdispatchOnlyTests : public TestWithTimeoutMixin, + public ::testing::Test { + public: + ExecutorLibdispatchOnlyTests() : executor{ExecutorFactory()} { + } + + std::unique_ptr<Executor> executor; +}; + +TEST_F(ExecutorLibdispatchOnlyTests, NameReturnsLabelOfTheQueue) { + EXPECT_EQ(executor->Name(), "ExecutorLibdispatchTests"); + executor->Execute([&] { + EXPECT_EQ(executor->CurrentExecutorName(), "ExecutorLibdispatchTests"); + signal_finished(); + }); + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_F(ExecutorLibdispatchOnlyTests, + ExecuteBlockingOnTheCurrentQueueIsNotAllowed) { + EXPECT_NO_THROW(executor->ExecuteBlocking([] {})); + executor->Execute([&] { + EXPECT_ANY_THROW(executor->ExecuteBlocking([] {})); + signal_finished(); + }); + EXPECT_TRUE(WaitForTestToFinish()); +} + +} // namespace internal +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/executor_std_test.cc b/Firestore/core/test/firebase/firestore/util/executor_std_test.cc new file mode 100644 index 0000000..59c3c32 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/executor_std_test.cc @@ -0,0 +1,244 @@ +/* + * 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. + */ + +#include "Firestore/core/test/firebase/firestore/util/executor_test.h" + +#include <chrono> // NOLINT(build/c++11) +#include <cstdlib> +#include <future> // NOLINT(build/c++11) +#include <string> +#include <thread> // NOLINT(build/c++11) + +#include "Firestore/core/src/firebase/firestore/util/executor_std.h" +#include "Firestore/core/test/firebase/firestore/util/async_tests_util.h" +#include "absl/memory/memory.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace chr = std::chrono; +using async::Schedule; + +class ScheduleTest : public ::testing::Test { + public: + ScheduleTest() : start_time{now()} { + } + + using ScheduleT = Schedule<int>; + + ScheduleT schedule; + ScheduleT::TimePoint start_time; +}; + +// Schedule tests + +TEST_F(ScheduleTest, PopIfDue_Immediate) { + EXPECT_FALSE(schedule.PopIfDue().has_value()); + + // Push values in a deliberately non-sorted order. + schedule.Push(3, start_time); + schedule.Push(1, start_time); + schedule.Push(2, start_time); + EXPECT_FALSE(schedule.empty()); + EXPECT_EQ(schedule.size(), 3u); + + EXPECT_EQ(schedule.PopIfDue().value(), 3); + EXPECT_EQ(schedule.PopIfDue().value(), 1); + EXPECT_EQ(schedule.PopIfDue().value(), 2); + EXPECT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_TRUE(schedule.empty()); + EXPECT_EQ(schedule.size(), 0u); +} + +TEST_F(ScheduleTest, PopIfDue_Delayed) { + schedule.Push(1, start_time + chr::milliseconds(5)); + schedule.Push(2, start_time + chr::milliseconds(3)); + schedule.Push(3, start_time + chr::milliseconds(1)); + + std::this_thread::sleep_for(chr::milliseconds(5)); + + EXPECT_EQ(schedule.PopIfDue().value(), 3); + EXPECT_EQ(schedule.PopIfDue().value(), 2); + EXPECT_EQ(schedule.PopIfDue().value(), 1); + EXPECT_TRUE(schedule.empty()); +} + +TEST_F(ScheduleTest, PopBlocking) { + schedule.Push(1, start_time + chr::milliseconds(3)); + EXPECT_FALSE(schedule.PopIfDue().has_value()); + + EXPECT_EQ(schedule.PopBlocking(), 1); + EXPECT_GE(now(), start_time + chr::milliseconds(3)); + EXPECT_TRUE(schedule.empty()); +} + +TEST_F(ScheduleTest, RemoveIf) { + schedule.Push(1, start_time); + schedule.Push(2, now() + chr::minutes(1)); + + auto maybe_removed = schedule.RemoveIf([](const int v) { return v == 1; }); + EXPECT_TRUE(maybe_removed.has_value()); + EXPECT_EQ(maybe_removed.value(), 1); + + // Non-existent value. + maybe_removed = schedule.RemoveIf([](const int v) { return v == 1; }); + EXPECT_FALSE(maybe_removed.has_value()); + + maybe_removed = schedule.RemoveIf([](const int v) { return v == 2; }); + EXPECT_TRUE(maybe_removed.has_value()); + EXPECT_EQ(maybe_removed.value(), 2); + EXPECT_TRUE(schedule.empty()); +} + +TEST_F(ScheduleTest, Ordering) { + schedule.Push(11, start_time + chr::milliseconds(5)); + schedule.Push(1, start_time); + schedule.Push(2, start_time); + schedule.Push(9, start_time + chr::milliseconds(2)); + schedule.Push(3, start_time); + schedule.Push(10, start_time + chr::milliseconds(3)); + schedule.Push(12, start_time + chr::milliseconds(5)); + schedule.Push(4, start_time); + schedule.Push(5, start_time); + schedule.Push(6, start_time); + schedule.Push(8, start_time + chr::milliseconds(1)); + schedule.Push(7, start_time); + + std::vector<int> values; + while (!schedule.empty()) { + values.push_back(schedule.PopBlocking()); + } + const std::vector<int> expected = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + EXPECT_EQ(values, expected); +} + +TEST_F(ScheduleTest, AddingEntryUnblocksEmptyQueue) { + const auto future = std::async(std::launch::async, [&] { + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 1); + }); + + std::this_thread::sleep_for(chr::milliseconds(5)); + schedule.Push(1, start_time); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingUnblocksOnNewPastDueEntries) { + const auto far_away = start_time + chr::seconds(10); + schedule.Push(5, far_away); + + const auto future = std::async(std::launch::async, [&] { + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 3); + }); + + std::this_thread::sleep_for(chr::milliseconds(5)); + schedule.Push(3, start_time); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingAdjustsWaitTimeOnNewSoonerEntries) { + const auto far_away = start_time + chr::seconds(10); + schedule.Push(5, far_away); + + const auto future = std::async(std::launch::async, [&] { + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 3); + // Make sure schedule hasn't been waiting longer than necessary. + EXPECT_LT(now(), far_away); + }); + + std::this_thread::sleep_for(chr::milliseconds(5)); + schedule.Push(3, start_time + chr::milliseconds(100)); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingCanReadjustTimeIfSeveralElementsAreAdded) { + const auto far_away = start_time + chr::seconds(5); + const auto very_far_away = start_time + chr::seconds(10); + schedule.Push(3, very_far_away); + + const auto future = std::async(std::launch::async, [&] { + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 1); + EXPECT_LT(now(), far_away); + }); + + std::this_thread::sleep_for(chr::milliseconds(5)); + schedule.Push(2, far_away); + std::this_thread::sleep_for(chr::milliseconds(1)); + schedule.Push(1, start_time + chr::milliseconds(100)); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingNoticesRemovals) { + const auto future = std::async(std::launch::async, [&] { + schedule.Push(1, start_time + chr::milliseconds(50)); + schedule.Push(2, start_time + chr::milliseconds(100)); + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 2); + }); + + while (schedule.empty()) { + std::this_thread::sleep_for(chr::milliseconds(1)); + } + const auto maybe_removed = + schedule.RemoveIf([](const int v) { return v == 1; }); + EXPECT_EQ(maybe_removed.value(), 1); + ABORT_ON_TIMEOUT(future); +} + +TEST_F(ScheduleTest, PopBlockingIsNotAffectedByIrrelevantRemovals) { + const auto future = std::async(std::launch::async, [&] { + schedule.Push(1, start_time + chr::milliseconds(50)); + schedule.Push(2, start_time + chr::seconds(10)); + ASSERT_FALSE(schedule.PopIfDue().has_value()); + EXPECT_EQ(schedule.PopBlocking(), 1); + }); + + // Wait (with timeout) for both values to appear in the schedule. + while (schedule.size() != 2) { + if (now() - start_time >= kTimeout) { + Abort(); + } + std::this_thread::sleep_for(chr::milliseconds(1)); + } + const auto maybe_removed = + schedule.RemoveIf([](const int v) { return v == 2; }); + ASSERT_TRUE(maybe_removed.has_value()); + EXPECT_EQ(maybe_removed.value(), 2); + ABORT_ON_TIMEOUT(future); +} + +// ExecutorStd tests + +namespace { + +inline std::unique_ptr<internal::Executor> ExecutorFactory() { + return absl::make_unique<internal::ExecutorStd>(); +} + +} // namespace + +INSTANTIATE_TEST_CASE_P(ExecutorTestStd, + ExecutorTest, + ::testing::Values(ExecutorFactory)); + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/executor_test.cc b/Firestore/core/test/firebase/firestore/util/executor_test.cc new file mode 100644 index 0000000..99bddce --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/executor_test.cc @@ -0,0 +1,218 @@ +/* + * 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. + */ + +#include "Firestore/core/test/firebase/firestore/util/executor_test.h" + +#include <chrono> // NOLINT(build/c++11) +#include <cstdlib> +#include <future> // NOLINT(build/c++11) +#include <string> +#include <thread> // NOLINT(build/c++11) + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +namespace chr = std::chrono; +using internal::Executor; + +namespace { + +DelayedOperation Schedule(Executor* const executor, + const Executor::Milliseconds delay, + Executor::Operation&& operation) { + const Executor::Tag no_tag = -1; + return executor->Schedule( + delay, Executor::TaggedOperation{no_tag, std::move(operation)}); +} + +} // namespace + +TEST_P(ExecutorTest, Execute) { + executor->Execute([&] { signal_finished(); }); + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_P(ExecutorTest, ExecuteBlocking) { + bool finished = false; + executor->ExecuteBlocking([&] { finished = true; }); + EXPECT_TRUE(finished); +} + +TEST_P(ExecutorTest, DestructorDoesNotBlockIfThereArePendingTasks) { + const auto future = std::async(std::launch::async, [&] { + auto another_executor = GetParam()(); + Schedule(another_executor.get(), chr::minutes(5), [] {}); + Schedule(another_executor.get(), chr::minutes(10), [] {}); + // Destructor shouldn't block waiting for the 5/10-minute-away operations. + }); + + ABORT_ON_TIMEOUT(future); +} + +// TODO(varconst): this test is inherently flaky because it can't be guaranteed +// that the enqueued asynchronous operation didn't finish before the code has +// a chance to even enqueue the next operation. Delays are chosen so that the +// test is unlikely to fail in practice. Need to revisit this. +TEST_P(ExecutorTest, CanScheduleOperationsInTheFuture) { + std::string steps; + + executor->Execute([&steps] { steps += '1'; }); + Schedule(executor.get(), Executor::Milliseconds(20), [&] { + steps += '4'; + signal_finished(); + }); + Schedule(executor.get(), Executor::Milliseconds(10), + [&steps] { steps += '3'; }); + executor->Execute([&steps] { steps += '2'; }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_EQ(steps, "1234"); +} + +TEST_P(ExecutorTest, CanCancelDelayedOperations) { + std::string steps; + + executor->Execute([&] { + executor->Execute([&steps] { steps += '1'; }); + + DelayedOperation delayed_operation = Schedule( + executor.get(), Executor::Milliseconds(1), [&steps] { steps += '2'; }); + + Schedule(executor.get(), Executor::Milliseconds(5), [&] { + steps += '3'; + signal_finished(); + }); + + delayed_operation.Cancel(); + }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_EQ(steps, "13"); +} + +TEST_P(ExecutorTest, DelayedOperationIsValidAfterTheOperationHasRun) { + DelayedOperation delayed_operation = Schedule( + executor.get(), Executor::Milliseconds(1), [&] { signal_finished(); }); + + EXPECT_TRUE(WaitForTestToFinish()); + EXPECT_NO_THROW(delayed_operation.Cancel()); +} + +TEST_P(ExecutorTest, IsCurrentExecutor) { + EXPECT_FALSE(executor->IsCurrentExecutor()); + EXPECT_NE(executor->Name(), executor->CurrentExecutorName()); + + executor->ExecuteBlocking([&] { + EXPECT_TRUE(executor->IsCurrentExecutor()); + EXPECT_EQ(executor->Name(), executor->CurrentExecutorName()); + }); + + executor->Execute([&] { + EXPECT_TRUE(executor->IsCurrentExecutor()); + EXPECT_EQ(executor->Name(), executor->CurrentExecutorName()); + }); + + Schedule(executor.get(), Executor::Milliseconds(1), [&] { + EXPECT_TRUE(executor->IsCurrentExecutor()); + EXPECT_EQ(executor->Name(), executor->CurrentExecutorName()); + signal_finished(); + }); + + EXPECT_TRUE(WaitForTestToFinish()); +} + +TEST_P(ExecutorTest, OperationsCanBeRemovedFromScheduleBeforeTheyRun) { + const Executor::Tag tag_foo = 1; + const Executor::Tag tag_bar = 2; + + // Make sure the schedule is empty. + EXPECT_FALSE(executor->IsScheduled(tag_foo)); + EXPECT_FALSE(executor->IsScheduled(tag_bar)); + EXPECT_FALSE(executor->PopFromSchedule().has_value()); + + // Add two operations to the schedule with different tags. + + // The exact delay doesn't matter as long as it's too far away to be executed + // during the test. + const auto far_away = chr::seconds(1); + executor->Schedule(far_away, {tag_foo, [] {}}); + // Scheduled operations can be distinguished by their tag. + EXPECT_TRUE(executor->IsScheduled(tag_foo)); + EXPECT_FALSE(executor->IsScheduled(tag_bar)); + + // This operation will be scheduled after the previous one (operations + // scheduled with the same delay are FIFO ordered). + executor->Schedule(far_away, {tag_bar, [] {}}); + EXPECT_TRUE(executor->IsScheduled(tag_foo)); + EXPECT_TRUE(executor->IsScheduled(tag_bar)); + + // Now pop the operations one by one without waiting for them to be executed, + // check that operations are popped in the order they are scheduled and + // preserve tags. Schedule should become empty as a result. + + auto maybe_operation = executor->PopFromSchedule(); + ASSERT_TRUE(maybe_operation.has_value()); + EXPECT_EQ(maybe_operation->tag, tag_foo); + EXPECT_FALSE(executor->IsScheduled(tag_foo)); + EXPECT_TRUE(executor->IsScheduled(tag_bar)); + + maybe_operation = executor->PopFromSchedule(); + ASSERT_TRUE(maybe_operation.has_value()); + EXPECT_EQ(maybe_operation->tag, tag_bar); + EXPECT_FALSE(executor->IsScheduled(tag_bar)); + + // Schedule should now be empty. + EXPECT_FALSE(executor->PopFromSchedule().has_value()); +} + +TEST_P(ExecutorTest, DuplicateTagsOnOperationsAreAllowed) { + const Executor::Tag tag_foo = 1; + std::string steps; + + // Add two operations with the same tag to the schedule to verify that + // duplicate tags are allowed. + + const auto far_away = chr::seconds(1); + executor->Schedule(far_away, {tag_foo, [&steps] { steps += '1'; }}); + executor->Schedule(far_away, {tag_foo, [&steps] { steps += '2'; }}); + EXPECT_TRUE(executor->IsScheduled(tag_foo)); + + auto maybe_operation = executor->PopFromSchedule(); + ASSERT_TRUE(maybe_operation.has_value()); + EXPECT_EQ(maybe_operation->tag, tag_foo); + // There's still another operation with the same tag in the schedule. + EXPECT_TRUE(executor->IsScheduled(tag_foo)); + + maybe_operation->operation(); + + maybe_operation = executor->PopFromSchedule(); + ASSERT_TRUE(maybe_operation.has_value()); + EXPECT_EQ(maybe_operation->tag, tag_foo); + EXPECT_FALSE(executor->IsScheduled(tag_foo)); + + maybe_operation->operation(); + // Despite having the same tag, the operations should have been ordered + // according to their scheduled time and preserved their identity. + EXPECT_EQ(steps, "12"); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/executor_test.h b/Firestore/core/test/firebase/firestore/util/executor_test.h new file mode 100644 index 0000000..8b78d50 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/executor_test.h @@ -0,0 +1,46 @@ +/* + * 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. + */ + +#ifndef FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_EXECUTOR_TEST_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_EXECUTOR_TEST_H_ + +#include <memory> + +#include "gtest/gtest.h" + +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/test/firebase/firestore/util/async_tests_util.h" + +namespace firebase { +namespace firestore { +namespace util { + +using FactoryFunc = std::unique_ptr<internal::Executor> (*)(); + +class ExecutorTest : public TestWithTimeoutMixin, + public ::testing::TestWithParam<FactoryFunc> { + public: + // `GetParam()` must return a factory function. + ExecutorTest() : executor{GetParam()()} { + } + + std::unique_ptr<internal::Executor> executor; +}; +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_EXECUTOR_TEST_H_ diff --git a/Firestore/core/test/firebase/firestore/util/hashing_test.cc b/Firestore/core/test/firebase/firestore/util/hashing_test.cc new file mode 100644 index 0000000..e5d9ff8 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/util/hashing_test.cc @@ -0,0 +1,105 @@ +/* + * 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. + */ + +#include "Firestore/core/src/firebase/firestore/util/hashing.h" + +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +struct HasHashMember { + size_t Hash() const { + return 42; + } +}; + +TEST(HashingTest, Int) { + ASSERT_EQ(std::hash<int>{}(0), Hash(0)); +} + +TEST(HashingTest, Float) { + ASSERT_EQ(std::hash<double>{}(1.0), Hash(1.0)); +} + +TEST(HashingTest, String) { + ASSERT_EQ(std::hash<std::string>{}("foobar"), Hash(std::string{"foobar"})); +} + +TEST(HashingTest, StringView) { + // For StringView we expect the range-based hasher to kick in. This is + // basically terrible, but no worse than Java's `String.hashCode()`. Another + // possibility would be just to create a temporary std::string and std::hash + // that, but that requires an explicit specialization. Since we're only + // defining this for compatibility with Objective-C and not really sensitive + // to performance or hash quality here, this is good enough. + size_t expected = 'a'; + expected = 31u * expected + 1; + ASSERT_EQ(expected, Hash(absl::string_view{"a"})); +} + +TEST(HashingTest, SizeT) { + ASSERT_EQ(42u, Hash(size_t{42u})); +} + +TEST(HashingTest, Array) { + int values[] = {0, 1, 2}; + + size_t expected = 0; + expected = 31 * expected + 1; + expected = 31 * expected + 2; + expected = 31 * expected + 3; // length of array + ASSERT_EQ(expected, Hash(values)); +} + +TEST(HashingTest, HasHashMember) { + ASSERT_EQ(static_cast<size_t>(42), Hash(HasHashMember{})); +} + +TEST(HashingTest, RangeOfStdHashable) { + std::vector<int> values{42}; + ASSERT_EQ(31u * 42u + 1, Hash(values)); + + std::vector<int> values_leading_zero{0, 42}; + std::vector<int> values_trailing_zero{42, 0}; + + EXPECT_NE(Hash(values), Hash(values_leading_zero)); + EXPECT_NE(Hash(values), Hash(values_trailing_zero)); + EXPECT_NE(Hash(values_leading_zero), Hash(values_trailing_zero)); +} + +TEST(HashingTest, RangeOfHashMember) { + std::vector<HasHashMember> values{HasHashMember{}}; + ASSERT_EQ(31u * 42u + 1, Hash(values)); +} + +TEST(HashingTest, Composite) { + // Verify the result ends up as if hand-rolled + EXPECT_EQ(1u, Hash(1)); + EXPECT_EQ(31u, Hash(1, 0)); + EXPECT_EQ(31u * 31u, Hash(1, 0, 0)); + + size_t expected = Hash(1); + expected = 31 * expected + Hash(2); + expected = 31 * expected + Hash(3); + EXPECT_EQ(expected, Hash(1, 2, 3)); +} + +} // namespace util +} // namespace firestore +} // namespace firebase diff --git a/Functions/Example/FirebaseFunctions.xcodeproj/project.pbxproj b/Functions/Example/FirebaseFunctions.xcodeproj/project.pbxproj index d70f4b5..e018981 100644 --- a/Functions/Example/FirebaseFunctions.xcodeproj/project.pbxproj +++ b/Functions/Example/FirebaseFunctions.xcodeproj/project.pbxproj @@ -20,16 +20,12 @@ 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; 6003F5BC195388D20070C39A /* FIRFunctionsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5BB195388D20070C39A /* FIRFunctionsTests.m */; }; - 633D50991C0FDF68085030B5 /* Pods_FirebaseFunctions_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B954281766FFD7C7C9970E9B /* Pods_FirebaseFunctions_Example.framework */; }; - 6358342A9F1AF395C6C52471 /* Pods_FirebaseFunctions_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0A4C1781DC97515D32B33DBD /* Pods_FirebaseFunctions_IntegrationTests.framework */; }; 71719F9F1E33DC2100824A3D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 71719F9D1E33DC2100824A3D /* LaunchScreen.storyboard */; }; - 78F883E44C88CC18414F3DAF /* Pods_FirebaseFunctions_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A19679D6C21E3206B4A13697 /* Pods_FirebaseFunctions_Tests.framework */; }; 7C58B03A1F1441F0005ED954 /* FUNSerializerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7C58B0391F1441F0005ED954 /* FUNSerializerTests.m */; }; 7CBFAA82205702AB00A65866 /* FIRIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7C9BFD3F1F10A12F001A19ED /* FIRIntegrationTests.m */; }; 7CBFAA86205702AB00A65866 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F5AF195388D20070C39A /* XCTest.framework */; }; 7CBFAA87205702AB00A65866 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 7CBFAA88205702AB00A65866 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; - 7CBFAA89205702AB00A65866 /* Pods_FirebaseFunctions_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A19679D6C21E3206B4A13697 /* Pods_FirebaseFunctions_Tests.framework */; }; 7CBFAA8B205702AB00A65866 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; 7CF3BEC21F97EE2F00B16B6E /* FUNFakeInstanceID.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CF3BEC01F97EE2F00B16B6E /* FUNFakeInstanceID.m */; }; 7CF563091F1FE70600FEE1F4 /* FUNFakeApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CF563081F1FE70600FEE1F4 /* FUNFakeApp.m */; }; @@ -54,10 +50,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 0A4C1781DC97515D32B33DBD /* Pods_FirebaseFunctions_IntegrationTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FirebaseFunctions_IntegrationTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 501CFEC7487BD4C12FCEAB03 /* Pods-FirebaseFunctions_IntegrationTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseFunctions_IntegrationTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FirebaseFunctions_IntegrationTests/Pods-FirebaseFunctions_IntegrationTests.debug.xcconfig"; sourceTree = "<group>"; }; - 59519EC771AF4770F85B6534 /* Pods-FirebaseFunctions_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseFunctions_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FirebaseFunctions_Example/Pods-FirebaseFunctions_Example.debug.xcconfig"; sourceTree = "<group>"; }; - 5B43C813C653DE3455952E9B /* Pods-FirebaseFunctions_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseFunctions_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-FirebaseFunctions_Tests/Pods-FirebaseFunctions_Tests.release.xcconfig"; sourceTree = "<group>"; }; 6003F58A195388D20070C39A /* FirebaseFunctions_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FirebaseFunctions_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -86,13 +78,8 @@ 7CF563081F1FE70600FEE1F4 /* FUNFakeApp.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FUNFakeApp.m; sourceTree = "<group>"; }; 7CF5630A1F1FE76700FEE1F4 /* FUNFakeApp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FUNFakeApp.h; sourceTree = "<group>"; }; 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; - A19679D6C21E3206B4A13697 /* Pods_FirebaseFunctions_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FirebaseFunctions_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B954281766FFD7C7C9970E9B /* Pods_FirebaseFunctions_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FirebaseFunctions_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E0A8D570636E99E7C3396DF8 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; }; - E2E6249788F1618471335BE7 /* Pods-FirebaseFunctions_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseFunctions_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-FirebaseFunctions_Example/Pods-FirebaseFunctions_Example.release.xcconfig"; sourceTree = "<group>"; }; F1F2A7C03C10A3A03F9502B8 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = "<group>"; }; - F840F9EDCB950E7509527AB7 /* Pods-FirebaseFunctions_IntegrationTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseFunctions_IntegrationTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-FirebaseFunctions_IntegrationTests/Pods-FirebaseFunctions_IntegrationTests.release.xcconfig"; sourceTree = "<group>"; }; - FFE894EDD87F1EF24F0ED8DD /* Pods-FirebaseFunctions_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FirebaseFunctions_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FirebaseFunctions_Tests/Pods-FirebaseFunctions_Tests.debug.xcconfig"; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -103,7 +90,6 @@ 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */, 6003F592195388D20070C39A /* UIKit.framework in Frameworks */, 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */, - 633D50991C0FDF68085030B5 /* Pods_FirebaseFunctions_Example.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -114,7 +100,6 @@ 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */, 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */, 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */, - 78F883E44C88CC18414F3DAF /* Pods_FirebaseFunctions_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,27 +110,12 @@ 7CBFAA86205702AB00A65866 /* XCTest.framework in Frameworks */, 7CBFAA87205702AB00A65866 /* UIKit.framework in Frameworks */, 7CBFAA88205702AB00A65866 /* Foundation.framework in Frameworks */, - 7CBFAA89205702AB00A65866 /* Pods_FirebaseFunctions_Tests.framework in Frameworks */, - 6358342A9F1AF395C6C52471 /* Pods_FirebaseFunctions_IntegrationTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 004DC60D1A62294B06E83453 /* Pods */ = { - isa = PBXGroup; - children = ( - 59519EC771AF4770F85B6534 /* Pods-FirebaseFunctions_Example.debug.xcconfig */, - E2E6249788F1618471335BE7 /* Pods-FirebaseFunctions_Example.release.xcconfig */, - FFE894EDD87F1EF24F0ED8DD /* Pods-FirebaseFunctions_Tests.debug.xcconfig */, - 5B43C813C653DE3455952E9B /* Pods-FirebaseFunctions_Tests.release.xcconfig */, - 501CFEC7487BD4C12FCEAB03 /* Pods-FirebaseFunctions_IntegrationTests.debug.xcconfig */, - F840F9EDCB950E7509527AB7 /* Pods-FirebaseFunctions_IntegrationTests.release.xcconfig */, - ); - name = Pods; - sourceTree = "<group>"; - }; 6003F581195388D10070C39A = { isa = PBXGroup; children = ( @@ -156,7 +126,6 @@ 6003F5B5195388D20070C39A /* Tests */, 6003F58C195388D20070C39A /* Frameworks */, 6003F58B195388D20070C39A /* Products */, - 004DC60D1A62294B06E83453 /* Pods */, 7CBFAA92205702AC00A65866 /* FirebaseFunctions_Tests copy-Info.plist */, ); sourceTree = "<group>"; @@ -178,9 +147,6 @@ 6003F58F195388D20070C39A /* CoreGraphics.framework */, 6003F591195388D20070C39A /* UIKit.framework */, 6003F5AF195388D20070C39A /* XCTest.framework */, - B954281766FFD7C7C9970E9B /* Pods_FirebaseFunctions_Example.framework */, - A19679D6C21E3206B4A13697 /* Pods_FirebaseFunctions_Tests.framework */, - 0A4C1781DC97515D32B33DBD /* Pods_FirebaseFunctions_IntegrationTests.framework */, ); name = Frameworks; sourceTree = "<group>"; @@ -266,12 +232,9 @@ isa = PBXNativeTarget; buildConfigurationList = 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "FirebaseFunctions_Example" */; buildPhases = ( - C1B1E870DA1D732EC2F1F946 /* [CP] Check Pods Manifest.lock */, 6003F586195388D20070C39A /* Sources */, 6003F587195388D20070C39A /* Frameworks */, 6003F588195388D20070C39A /* Resources */, - BB7A7FC40453827CC7541BB7 /* [CP] Embed Pods Frameworks */, - 0409BC62B2C369D14416E480 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -286,12 +249,9 @@ isa = PBXNativeTarget; buildConfigurationList = 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "FirebaseFunctions_Tests" */; buildPhases = ( - D9E332DA9E27ECA02C2B45D2 /* [CP] Check Pods Manifest.lock */, 6003F5AA195388D20070C39A /* Sources */, 6003F5AB195388D20070C39A /* Frameworks */, 6003F5AC195388D20070C39A /* Resources */, - 3B7914C40161367B84BC9E52 /* [CP] Embed Pods Frameworks */, - F0A35EE9B43049BA133AD88F /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -307,12 +267,9 @@ isa = PBXNativeTarget; buildConfigurationList = 7CBFAA8E205702AB00A65866 /* Build configuration list for PBXNativeTarget "FirebaseFunctions_IntegrationTests" */; buildPhases = ( - 7CBFAA80205702AB00A65866 /* [CP] Check Pods Manifest.lock */, 7CBFAA81205702AB00A65866 /* Sources */, 7CBFAA85205702AB00A65866 /* Frameworks */, 7CBFAA8A205702AB00A65866 /* Resources */, - 7CBFAA8C205702AB00A65866 /* [CP] Embed Pods Frameworks */, - 7CBFAA8D205702AB00A65866 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -389,158 +346,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 0409BC62B2C369D14416E480 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FirebaseFunctions_Example/Pods-FirebaseFunctions_Example-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 3B7914C40161367B84BC9E52 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FirebaseFunctions_Tests/Pods-FirebaseFunctions_Tests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 7CBFAA80205702AB00A65866 /* [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-FirebaseFunctions_IntegrationTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 7CBFAA8C205702AB00A65866 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FirebaseFunctions_IntegrationTests/Pods-FirebaseFunctions_IntegrationTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 7CBFAA8D205702AB00A65866 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FirebaseFunctions_IntegrationTests/Pods-FirebaseFunctions_IntegrationTests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - BB7A7FC40453827CC7541BB7 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-FirebaseFunctions_Example/Pods-FirebaseFunctions_Example-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework", - "${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FirebaseFunctions_Example/Pods-FirebaseFunctions_Example-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - C1B1E870DA1D732EC2F1F946 /* [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-FirebaseFunctions_Example-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - D9E332DA9E27ECA02C2B45D2 /* [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-FirebaseFunctions_Tests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - F0A35EE9B43049BA133AD88F /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FirebaseFunctions_Tests/Pods-FirebaseFunctions_Tests-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 6003F586195388D20070C39A /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -689,12 +494,15 @@ }; 6003F5C0195388D20070C39A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 59519EC771AF4770F85B6534 /* Pods-FirebaseFunctions_Example.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; GCC_PRECOMPILE_PREFIX_HEADER = NO; INFOPLIST_FILE = "FirebaseFunctions/FirebaseFunctions-Info.plist"; MODULE_NAME = ExampleApp; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; @@ -703,12 +511,15 @@ }; 6003F5C1195388D20070C39A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E2E6249788F1618471335BE7 /* Pods-FirebaseFunctions_Example.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; GCC_PRECOMPILE_PREFIX_HEADER = NO; INFOPLIST_FILE = "FirebaseFunctions/FirebaseFunctions-Info.plist"; MODULE_NAME = ExampleApp; + OTHER_LDFLAGS = ( + "$(inherited)", + "-all_load", + ); PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME)"; WRAPPER_EXTENSION = app; @@ -717,7 +528,6 @@ }; 6003F5C3195388D20070C39A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FFE894EDD87F1EF24F0ED8DD /* Pods-FirebaseFunctions_Tests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( @@ -741,7 +551,6 @@ }; 6003F5C4195388D20070C39A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5B43C813C653DE3455952E9B /* Pods-FirebaseFunctions_Tests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( @@ -761,7 +570,6 @@ }; 7CBFAA8F205702AB00A65866 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 501CFEC7487BD4C12FCEAB03 /* Pods-FirebaseFunctions_IntegrationTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( @@ -785,7 +593,6 @@ }; 7CBFAA90205702AB00A65866 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F840F9EDCB950E7509527AB7 /* Pods-FirebaseFunctions_IntegrationTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( diff --git a/Functions/README.md b/Functions/README.md index f0b8d62..b35f507 100644 --- a/Functions/README.md +++ b/Functions/README.md @@ -1,10 +1,22 @@ # Cloud Functions for Firebase iOS SDK -## To run unit tests +## Development + +Follow the subsequent instructions to develop, debug, unit test, and +integration test FirebaseFunctions: + +``` +$ git clone git@github.com:firebase/firebase-ios-sdk.git +$ cd firebase-ios-sdk/Functions/Example +$ pod update +$ open FirebaseFunctions.xcworkspace +``` + +### Running Unit Tests Choose the FirebaseFunctions_Tests scheme and press Command-u. -## To run integration tests +## Running Integration Tests Before running the integration tests, you'll need to start a backend emulator for them to talk to. diff --git a/Gemfile.lock b/Gemfile.lock index e553857..7a0065e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,13 @@ GIT remote: https://github.com/CocoaPods/CocoaPods.git - revision: df6872de04fd1808fde71c824530e3a784e6a2d2 + revision: 7d6405dc743082f511f0935bc0f66268f202345d specs: - cocoapods (1.4.0) + cocoapods (1.5.2) activesupport (>= 4.0.2, < 5) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.4.0) + cocoapods-core (= 1.5.2) cocoapods-deintegrate (>= 1.0.2, < 2.0) - cocoapods-downloader (>= 1.1.3, < 2.0) + cocoapods-downloader (>= 1.2.0, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-stats (>= 1.0.0, < 2.0) @@ -17,44 +17,44 @@ GIT escape (~> 0.0.4) fourflusher (~> 2.0.1) gh_inspector (~> 1.0) - molinillo (~> 0.6.4) + molinillo (~> 0.6.5) nap (~> 1.0) ruby-macho (~> 1.1) - xcodeproj (>= 1.5.4, < 2.0) + xcodeproj (>= 1.5.8, < 2.0) GIT remote: https://github.com/CocoaPods/Core.git - revision: b4fb2f193897c789c094d126ebca91034edc261d + revision: 0840691058b9fe96f759dacc26642207beb050cd specs: - cocoapods-core (1.4.0) + cocoapods-core (1.5.2) activesupport (>= 4.0.2, < 6) fuzzy_match (~> 2.0.4) nap (~> 1.0) GIT remote: https://github.com/CocoaPods/Xcodeproj.git - revision: 531d8aa8ab4805c84f5e0fe94181ae3995430d05 + revision: 3333a879ecd24c42ebb6fc99ce4897ee099d4ce3 specs: - xcodeproj (1.5.4) - CFPropertyList (~> 2.3.3) - atomos (~> 0.1.0) + xcodeproj (1.5.8) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.2) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.2.3) + nanaimo (~> 0.2.5) GEM remote: https://rubygems.org/ specs: - CFPropertyList (2.3.6) + CFPropertyList (3.0.0) activesupport (4.2.10) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) - atomos (0.1.0) + atomos (0.1.2) claide (1.0.2) cocoapods-deintegrate (1.0.2) - cocoapods-downloader (1.1.3) + cocoapods-downloader (1.2.0) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.0) @@ -68,17 +68,17 @@ GEM escape (0.0.4) fourflusher (2.0.1) fuzzy_match (2.0.4) - gh_inspector (1.0.3) - i18n (0.9.3) + gh_inspector (1.1.3) + i18n (0.9.5) concurrent-ruby (~> 1.0) - minitest (5.11.1) - molinillo (0.6.4) - nanaimo (0.2.3) + minitest (5.11.3) + molinillo (0.6.5) + nanaimo (0.2.5) nap (1.1.0) netrc (0.11.0) ruby-macho (1.1.0) thread_safe (0.3.6) - tzinfo (1.2.4) + tzinfo (1.2.5) thread_safe (~> 0.1) PLATFORMS @@ -90,4 +90,4 @@ DEPENDENCIES xcodeproj! BUNDLED WITH - 1.16.0 + 1.16.1 diff --git a/scripts/build.sh b/scripts/build.sh index ef10059..4d6c230 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -84,7 +84,7 @@ case "$platform" in iOS) xcb_flags=( -sdk 'iphonesimulator' - -destination 'platform=iOS Simulator,name=iPhone 7' + -destination 'platform=iOS Simulator,name=iPhone 8 Plus' ) ;; @@ -111,6 +111,7 @@ esac xcb_flags+=( ONLY_ACTIVE_ARCH=YES CODE_SIGNING_REQUIRED=NO + CODE_SIGNING_ALLOWED=YES ) # TODO(varconst): --warn-unused-vars - right now, it makes the log overflow on @@ -179,10 +180,6 @@ case "$product-$method-$platform" in cd Example sed -i -e 's/use_frameworks/\#use_frameworks/' Podfile pod update --no-repo-update - # Workarounds for https://github.com/CocoaPods/CocoaPods/issues/7592. - # Remove when updating to CocoaPods 1.5.1 - sed -i -e 's/-l"FirebaseMessaging"//' "Pods/Target Support Files/Pods-Messaging_Tests_iOS/Pods-Messaging_Tests_iOS.debug.xcconfig" - sed -i -e 's/-l"FirebaseAuth-iOS" -l"FirebaseCore-iOS"//' "Pods/Target Support Files/Pods-Auth_Tests_iOS/Pods-Auth_Tests_iOS.debug.xcconfig" cd .. RunXcodebuild \ -workspace 'Example/Firebase.xcworkspace' \ @@ -214,6 +211,12 @@ case "$product-$method-$platform" in RunXcodebuild \ -workspace 'Firestore/Example/Firestore.xcworkspace' \ + -scheme "Firestore_IntegrationTests_$platform" \ + "${xcb_flags[@]}" \ + build + + RunXcodebuild \ + -workspace 'Firestore/Example/Firestore.xcworkspace' \ -scheme 'SwiftBuildTest' \ "${xcb_flags[@]}" \ build diff --git a/scripts/check_test_inclusion.py b/scripts/check_test_inclusion.py new file mode 100755 index 0000000..7f5f354 --- /dev/null +++ b/scripts/check_test_inclusion.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python + +# 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. + +"""Verifies that all tests are a part of the project file. +""" + +from __future__ import print_function +import os +import os.path +import re +import sys + + +# Tests that are known not to compile in Xcode and can't be added there. +EXCLUDED = frozenset([ + # b/79496027 + "Firestore/core/test/firebase/firestore/remote/serializer_test.cc", +]) + + +def Main(): + """Runs the style check.""" + + tests = FindTestFiles("Firestore/Example/Tests", "Firestore/core/test") + problems = CheckProject( + "Firestore/Example/Firestore.xcodeproj/project.pbxproj", tests) + + if problems: + Error("Test files exist that are unreferenced in Xcode project files:") + for problem in problems: + Error(problem) + sys.exit(1) + + sys.exit(0) + + +def FindTestFiles(*test_dirs): + """Searches the given source roots for test files. + + Args: + *test_dirs: A list of directories containing test sources. + + Returns: + A list of test source filenames. + """ + + test_file_pattern = re.compile(r"(?:Tests?\.mm?|_test\.(?:cc|mm))$") + + result = [] + for test_dir in test_dirs: + for root, dirs, files in os.walk(test_dir): + del dirs # unused + for basename in files: + filename = os.path.join(root, basename) + if filename not in EXCLUDED and test_file_pattern.search(basename): + result.append(filename) + return result + + +def CheckProject(project_file, test_files): + """Checks the given project file for tests in the given test_dirs. + + Args: + project_file: The path to an Xcode pbxproj file. + test_files: A list of all tests source files in the project. + + Returns: + A sorted list of filenames that aren't referenced in the project_file. + """ + + # An dict of basename to filename + basenames = {os.path.basename(f) : f for f in test_files} + + file_list_pattern = re.compile(r"/\* (\S+) in Sources \*/") + with open(project_file, "r") as fd: + for line in fd: + line = line.rstrip() + m = file_list_pattern.search(line) + if m: + basename = m.group(1) + if basename in basenames: + del basenames[basename] + + return sorted(basenames.values()) + + +def Error(message, *args): + message %= args + print(message, file=sys.stderr) + + +if __name__ == "__main__": + Main() diff --git a/scripts/if_changed.sh b/scripts/if_changed.sh index 697cad2..51217d6 100755 --- a/scripts/if_changed.sh +++ b/scripts/if_changed.sh @@ -49,7 +49,7 @@ else check_changes '^(Firebase|Functions|Example)' ;; - Firestore-xcodebuild) + Firestore-xcodebuild|Firestore-pod-lib-lint) check_changes '^Firestore' ;; @@ -68,6 +68,7 @@ fi # Always rebuild if Travis configuration and/or build scripts changed. check_changes '^.travis.yml' +check_changes '^Gemfile.lock' check_changes '^scripts/(build|if_changed).sh' if [[ "$run" == true ]]; then diff --git a/scripts/if_cron.sh b/scripts/if_cron.sh new file mode 100755 index 0000000..c13a374 --- /dev/null +++ b/scripts/if_cron.sh @@ -0,0 +1,25 @@ +# 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. + +# Within Travis, check if running in a cron job. +# +# Examines the following Travis-supplied environment variables: +# - TRAVIS_EVENT_TYPE - to check if this is a cron job +# + +if [[ "$TRAVIS_EVENT_TYPE" == "cron" ]]; then + "$@" +else + echo "skipped $*" +fi diff --git a/scripts/install_prereqs.sh b/scripts/install_prereqs.sh index c86663f..c943369 100755 --- a/scripts/install_prereqs.sh +++ b/scripts/install_prereqs.sh @@ -19,28 +19,44 @@ # - PROJECT - Firebase or Firestore # - METHOD - xcodebuild or cmake; default is xcodebuild -if [[ -z "$METHOD" ]]; then - METHOD="xcodebuild" -fi +bundle install -case "$PROJECT-$METHOD" in - *-xcodebuild) - bundle install +case "$PROJECT-$PLATFORM-$METHOD" in + Firebase-iOS-xcodebuild) gem install xcpretty + bundle exec pod install --project-directory=Example --repo-update + bundle exec pod install --project-directory=Functions/Example ;; - Firestore-cmake) - bundle install + Firebase-*-xcodebuild) + gem install xcpretty + bundle exec pod install --project-directory=Example --repo-update + ;; + + Firestore-*-xcodebuild) + gem install xcpretty + bundle exec pod install --project-directory=Firestore/Example --repo-update + ;; + + *-pod-lib-lint) + bundle exec pod repo update + ;; + + Firestore-*-cmake) # xcpretty is helpful for the intermediate step which builds FirebaseCore # using xcodebuild. gem install xcpretty brew outdated cmake || brew upgrade cmake brew outdated go || brew upgrade go # Somehow the build for Abseil requires this. + bundle exec pod install --project-directory=Example --repo-update + bundle exec pod install --project-directory=Firestore/Example \ + --no-repo-update ;; *) - echo "Unknown project-method combo" 1>&2 + echo "Unknown project-platform-method combo" 1>&2 echo " PROJECT=$PROJECT" 1>&2 + echo " PLATFORM=$PLATFORM" 1>&2 echo " METHOD=$METHOD" 1>&2 exit 1 ;; diff --git a/scripts/lint.sh b/scripts/lint.sh index 9e33c87..d474129 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -60,7 +60,7 @@ objc_lint_options=( if [[ $# -gt 0 ]]; then # Interpret any command-line argument as a revision range - command=(git diff --name-only) + command=(git diff --name-only --diff-filter=ACMR) git_options+=("$@") else diff --git a/scripts/pod_install.sh b/scripts/pod_install.sh deleted file mode 100755 index ff5ec96..0000000 --- a/scripts/pod_install.sh +++ /dev/null @@ -1,54 +0,0 @@ -# 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. - -# Within Travis, installs prerequisites for a build. - -# Examines the following configured environment variables that should be -# specified in an env: block -# - PROJECT - Firebase or Firestore -# - METHOD - xcodebuild or cmake; default is xcodebuild -# - PLATFORM - iOS, macOS, or tvOS - -if [[ -z "$METHOD" ]]; then - METHOD="xcodebuild" -fi - -case "$PROJECT-$METHOD-$PLATFORM" in - Firebase-xcodebuild-iOS) - bundle exec pod install --project-directory=Example --repo-update - bundle exec pod install --project-directory=Functions/Example - ;; - - Firebase-xcodebuild-*) - bundle exec pod install --project-directory=Example --repo-update - ;; - - Firestore-xcodebuild-*) - bundle exec pod install --project-directory=Firestore/Example --repo-update - ;; - - Firestore-cmake-*) - bundle exec pod install --project-directory=Example --repo-update - bundle exec pod install --project-directory=Firestore/Example \ - --no-repo-update - ;; - - *) - echo "Unknown project-method combo" 1>&2 - echo " PROJECT=$PROJECT" 1>&2 - echo " METHOD=$METHOD" 1>&2 - exit 1 - ;; -esac - |