diff --git a/.gitignore b/.gitignore index 4e55dcc72cb..c897708a9d9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ Example/Database/App/GoogleService-Info.plist Example/Storage/App/GoogleService-Info.plist +# FirebaseInstallations integration tests GoogleService-Info.plist +FirebaseInstallations/Source/Tests/Resources/GoogleService-Info.plist + Secrets.tar # OS X diff --git a/.travis.yml b/.travis.yml index fc60eea26de..f53e50dc052 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ os: osx -osx_image: xcode10.1 +osx_image: xcode11 language: objective-c cache: - bundler @@ -23,9 +23,6 @@ jobs: # The order of builds matters (even though they are run in parallel): # Travis will schedule them in the same order they are listed here. - # Primary platforms - - # Run unit tests - stage: test env: - PROJECT=Firebase PLATFORM=iOS METHOD=xcodebuild @@ -36,20 +33,68 @@ jobs: - stage: test env: - - PROJECT=Core PLATFORM=iOS METHOD=pod-lib-lint - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh + - PROJECT=Core METHOD=pod-lib-lint script: - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --platforms=ios + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --platforms=tvos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --platforms=macos - stage: test + if: type = cron env: - - PROJECT=ABTesting PLATFORM=iOS METHOD=pod-lib-lint + - PROJECT=CoreCron METHOD=pod-lib-lint script: - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=ios - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=tvos - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=macos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --platforms=ios --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --platforms=tvos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --platforms=macos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --platforms=ios --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --platforms=tvos --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCore.podspec --platforms=macos --use-modular-headers + + - stage: test + env: + - PROJECT=CoreDiagnostics METHOD=pod-lib-lint + script: + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnostics.podspec --platforms=ios + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnostics.podspec --platforms=tvos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnostics.podspec --platforms=macos + + - stage: test + if: type = cron + env: + - PROJECT=CoreDiagnosticsCron METHOD=pod-lib-lint + script: + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnostics.podspec --platforms=ios --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnostics.podspec --platforms=tvos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnostics.podspec --platforms=macos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnostics.podspec --platforms=ios --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnostics.podspec --platforms=tvos --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnostics.podspec --platforms=macos --use-modular-headers + + - stage: test + env: + - PROJECT=ABTesting METHOD=pod-lib-lint + script: + # --allow-warnings is needed until Protobuf 3.10.0 releases with fix included with + # https://github.com/protocolbuffers/protobuf/pull/6464 + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=ios --allow-warnings + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=tvos --allow-warnings + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=macos --allow-warnings + + - stage: test + if: type = cron + env: + - PROJECT=ABTestingCron METHOD=pod-lib-lint + script: + # --allow-warnings is needed until Protobuf 3.10.0 releases with fix included with + # https://github.com/protocolbuffers/protobuf/pull/6464 + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=ios --allow-warnings --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=tvos --allow-warnings --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=macos --allow-warnings --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=ios --allow-warnings --use-modular-headers + # One of the next two consistently hang on Travis. Commenting for now. + # - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=tvos --allow-warnings --use-modular-headers + # - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseABTesting.podspec --platforms=macos --allow-warnings --use-modular-headers - stage: test env: @@ -63,13 +108,38 @@ jobs: - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - stage: test + if: type = cron env: - - PROJECT=InstanceID PLATFORM=iOS METHOD=pod-lib-lint + - PROJECT=AuthCron METHOD=pod-lib-lint + script: + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseAuth.podspec --platforms=ios --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseAuth.podspec --platforms=tvos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseAuth.podspec --platforms=macos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseAuth.podspec --platforms=ios --use-modular-headers + # The tvOS and macOS --use-modular-headers tests do not work on travis, perhaps because of interactive + # keystore validation requirements? See https://travis-ci.org/firebase/firebase-ios-sdk/jobs/578656148 + # TODO(paulb777): Retry on next Xcode version update + + - stage: test + env: + - PROJECT=InstanceID METHOD=pod-lib-lint script: - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --platforms=ios - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --platforms=tvos - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --platforms=macos + - stage: test + if: type = cron + env: + - PROJECT=InstanceIDCron METHOD=pod-lib-lint + script: + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --platforms=ios --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --platforms=tvos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --platforms=macos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --platforms=ios --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --platforms=tvos --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --platforms=macos --use-modular-headers + - stage: test env: - PROJECT=Database PLATFORM=all METHOD=xcodebuild @@ -78,13 +148,25 @@ jobs: script: - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM # The pod lib lint tests are fast enough that it's not worth a separate stage. - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --skip-tests - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --use-libraries --skip-tests - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --use-modular-headers --skip-tests + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --skip-tests --platforms=ios + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --skip-tests --platforms=tvos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --skip-tests --platforms=macos + + - stage: test + if: type = cron + env: + - PROJECT=DatabaseCron METHOD=pod-lib-lint + script: + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --use-libraries --skip-tests --platforms=ios + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --use-libraries --skip-tests --platforms=tvos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --use-libraries --skip-tests --platforms=macos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --use-modular-headers --skip-tests --platforms=ios + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --use-modular-headers --skip-tests --platforms=tvos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDatabase.podspec --use-modular-headers --skip-tests --platforms=macos - stage: test env: - - PROJECT=DynamicLinks PLATFORM=all METHOD=pod-lib-lint + - PROJECT=DynamicLinks METHOD=pod-lib-lint script: - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDynamicLinks.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseDynamicLinks.podspec --use-libraries @@ -92,13 +174,52 @@ jobs: - stage: test env: - - PROJECT=Messaging PLATFORM=all METHOD=xcodebuild - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh + - PROJECT=Messaging METHOD=pod-lib-lint script: - # Run both build.sh and pod lib lint tests to get multi iOS version test coverage - - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec + # --allow-warnings is needed until Protobuf 3.10.0 releases with fix included with + # https://github.com/protocolbuffers/protobuf/pull/6464 + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --platforms=ios --allow-warnings + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --platforms=tvos --allow-warnings + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --platforms=macos --allow-warnings + + - stage: test + if: type = cron + env: + - PROJECT=MessagingCron METHOD=pod-lib-lint + script: + # --allow-warnings is needed until Protobuf 3.10.0 releases with fix included with + # https://github.com/protocolbuffers/protobuf/pull/6464 + # FirebaseMessaging includes Swift unit tests so it is not testable with --use-libraries. + # TODO(paulb777): Migrate FirebaseMessaging to pod gen driven tests with a separate test + # target for Swift. + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --platforms=ios --allow-warnings --use-libraries --skip-tests + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --platforms=tvos --allow-warnings --use-libraries --skip-tests + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --platforms=macos --allow-warnings --use-libraries --skip-tests + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --platforms=ios --allow-warnings --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --platforms=tvos --allow-warnings --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --platforms=macos --allow-warnings --use-modular-headers + + - stage: test + env: + - PROJECT=RemoteConfig METHOD=pod-lib-lint + script: + # --allow-warnings is needed until Protobuf 3.10.0 releases with fix included with + # https://github.com/protocolbuffers/protobuf/pull/6464 + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --platforms=ios --allow-warnings + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --platforms=tvos --allow-warnings + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --platforms=macos --allow-warnings + + - stage: test + if: type = cron + env: + - PROJECT=RemoteConfigCron METHOD=pod-lib-lint + script: + - travis_retry ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --use-libraries --allow-warnings --platforms=ios --allow-warnings + - travis_retry ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --use-libraries --allow-warnings --platforms=tvos --allow-warnings + - travis_retry ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --use-libraries --allow-warnings --platforms=macos --allow-warnings + - travis_retry ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --use-modular-headers --platforms=ios --allow-warnings + - travis_retry ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --use-modular-headers --platforms=tvos --allow-warnings + - travis_retry ./scripts/pod_lib_lint.rb FirebaseRemoteConfig.podspec --use-modular-headers --platforms=macos --allow-warnings - stage: test env: @@ -108,13 +229,25 @@ jobs: script: - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM # The pod lib lint tests are fast enough that it's not worth a separate stage. - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --skip-tests - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --use-libraries --skip-tests - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --use-modular-headers --skip-tests + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --skip-tests --platforms=ios + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --skip-tests --platforms=tvos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --skip-tests --platforms=macos + + - stage: test + if: type = cron + env: + - PROJECT=Storage METHOD=pod-lib-lint + script: + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --use-libraries --skip-tests --platforms=ios + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --use-libraries --skip-tests --platforms=tvos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --use-libraries --skip-tests --platforms=macos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --use-modular-headers --skip-tests --platforms=ios + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --use-modular-headers --skip-tests --platforms=tvos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseStorage.podspec --use-modular-headers --skip-tests --platforms=macos - stage: test env: - - PROJECT=Functions PLATFORM=iOS METHOD=pod-lib-lint + - PROJECT=Functions METHOD=pod-lib-lint before_install: - ./scripts/if_changed.sh ./scripts/install_prereqs.sh # Start integration test server script: @@ -123,6 +256,7 @@ jobs: - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseFunctions.podspec --use-modular-headers - stage: test + osx_image: xcode10.3 env: - PROJECT=InAppMessaging PLATFORM=iOS METHOD=xcodebuild before_install: @@ -131,6 +265,9 @@ jobs: - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - stage: test + # TODO(paulb777,wilhuff) Replace with a solution that doesn't include multiple platforms in + # a single Podfile. + osx_image: xcode10.1 env: - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild before_install: @@ -140,23 +277,32 @@ jobs: - stage: test env: - - PROJECT=GoogleUtilities PLATFORM=iOS METHOD=pod-lib-lint + - PROJECT=GoogleUtilities METHOD=pod-lib-lint script: - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleUtilities.podspec - # pod lib lint to check build and warnings for dynamic framework build (use_frameworks!) - stage: test + if: type = cron env: - - PROJECT=Firebase PLATFORM=iOS METHOD=pod-lib-lint + - PROJECT=GoogleUtilitiesCron METHOD=pod-lib-lint + script: + + - travis_retry ./scripts/pod_lib_lint.rb GoogleUtilities.podspec --use-libraries + - travis_retry ./scripts/pod_lib_lint.rb GoogleUtilities.podspec --use-modular-headers + + - stage: test + env: + - PROJECT=Firebase METHOD=pod-lib-lint script: - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseAnalyticsInterop.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseAuthInterop.podspec + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseCoreDiagnosticsInterop.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInAppMessaging.podspec - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInAppMessagingDisplay.podspec - stage: test env: - - PROJECT=Firestore PLATFORM=iOS METHOD=pod-lib-lint + - PROJECT=Firestore METHOD=pod-lib-lint 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. @@ -166,88 +312,62 @@ jobs: - stage: test if: type = cron env: - - PROJECT=FirebasePllCron1 PLATFORM=iOS METHOD=pod-lib-lint + - PROJECT=InAppMessagingCron METHOD=pod-lib-lint script: - # TODO investigate why macos tests hang for FirebaseAuth - keychain related? - - travis_retry ./scripts/pod_lib_lint.rb FirebaseAuth.podspec --use-libraries --platforms=ios,tvos - - travis_retry ./scripts/pod_lib_lint.rb FirebaseAuth.podspec --use-modular-headers --platforms=ios,tvos - - travis_retry ./scripts/pod_lib_lint.rb FirebaseAuth.podspec --use-libraries --platforms=macos --skip-tests - - travis_retry ./scripts/pod_lib_lint.rb FirebaseAuth.podspec --use-modular-headers --platforms=macos --skip-tests - - - travis_retry ./scripts/pod_lib_lint.rb FirebaseAnalyticsInterop.podspec --use-libraries - - travis_retry ./scripts/pod_lib_lint.rb FirebaseAnalyticsInterop.podspec --use-modular-headers - - travis_retry ./scripts/pod_lib_lint.rb FirebaseAuthInterop.podspec --use-libraries - - travis_retry ./scripts/pod_lib_lint.rb FirebaseAuthInterop.podspec --use-modular-headers - travis_retry ./scripts/pod_lib_lint.rb FirebaseInAppMessaging.podspec --use-libraries - travis_retry ./scripts/pod_lib_lint.rb FirebaseInAppMessaging.podspec --use-modular-headers - travis_retry ./scripts/pod_lib_lint.rb FirebaseInAppMessagingDisplay.podspec --use-libraries - travis_retry ./scripts/pod_lib_lint.rb FirebaseInAppMessagingDisplay.podspec --use-modular-headers - # Part 2: Split from previous stage to avoid overflowing 45 minute sub-job limit. - stage: test if: type = cron env: - - PROJECT=FirebasePllCron2 PLATFORM=iOS METHOD=pod-lib-lint + - PROJECT=Firestore METHOD=pod-lib-lint script: - - travis_retry ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --use-libraries --platforms=ios - - travis_retry ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --use-libraries --platforms=tvos - - travis_retry ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --use-libraries --platforms=macos - - travis_retry ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --use-modular-headers --platforms=ios - - travis_retry ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --use-modular-headers --platforms=tvos - - travis_retry ./scripts/pod_lib_lint.rb FirebaseInstanceID.podspec --use-modular-headers --platforms=macos + # TBD - non-portable path warnings + # The travis_wait is necessary because the command takes more than 10 minutes. + - travis_wait 45 ./scripts/pod_lib_lint.rb FirebaseFirestore.podspec --use-libraries --allow-warnings --no-subspecs - # Part 3: Split from previous stage to avoid overflowing 45 minute sub-job limit. - stage: test - if: type = cron + osx_image: xcode10.3 env: - - PROJECT=FirebasePllCron3 PLATFORM=iOS METHOD=pod-lib-lint + - PROJECT=GoogleDataTransport METHOD=pod-lib-lint script: - # The Protobuf dependency of FirebaseMessaging has warnings with --use-libraries. - # FirebaseMessaging includes Swift unit tests so it is not testable with --use-libraries. - # TODO(paulb777): Migrate FirebaseMessaging to pod gen driven tests with a separate test - # target for Swift. - - travis_retry ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --use-libraries --allow-warnings --skip-tests - - travis_retry ./scripts/pod_lib_lint.rb FirebaseMessaging.podspec --use-modular-headers - - travis_retry ./scripts/pod_lib_lint.rb GoogleUtilities.podspec --use-libraries - - travis_retry ./scripts/pod_lib_lint.rb GoogleUtilities.podspec --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=ios + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=macos + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=tvos - stage: test if: type = cron env: - - PROJECT=Firestore PLATFORM=iOS METHOD=pod-lib-lint - script: - # TBD - non-portable path warnings - # The travis_wait is necessary because the command takes more than 10 minutes. - - travis_wait 45 ./scripts/pod_lib_lint.rb FirebaseFirestore.podspec --use-libraries --allow-warnings --no-subspecs - - # GoogleDataTransport unit tests and pod linting using the default Xcode version. - - stage: test - env: - - PROJECT=GoogleDataTransport PLATFORM=iOS METHOD=xcodebuild - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh + - PROJECT=GoogleDataTransportCron METHOD=pod-lib-lint script: - - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=ios --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=macos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=tvos --use-libraries + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=ios --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=macos --use-modular-headers + - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransport.podspec --platforms=tvos --use-modular-headers - # GoogleDataTransport integration tests using the default Xcode version. - stage: test env: - - PROJECT=GoogleDataTransportIntegrationTest PLATFORM=iOS METHOD=xcodebuild - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh + - PROJECT=GoogleDataTransportCCTSupport METHOD=pod-lib-lint script: - - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM + - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec --platforms=ios + - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec --platforms=macos + - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec --platforms=tvos - # GoogleDataTransportCCTSupport unit tests and pod linting using the default Xcode version. - stage: test + if: type = cron env: - - PROJECT=GoogleDataTransportCCTSupport PLATFORM=iOS METHOD=xcodebuild - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh + - PROJECT=GoogleDataTransportCCTSupportCron METHOD=pod-lib-lint script: - - ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec + - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec --platforms=ios --use-libraries + - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec --platforms=macos --use-libraries + - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec --platforms=tvos --use-libraries + - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec --platforms=ios --use-modular-headers + - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec --platforms=macos --use-modular-headers + - ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb GoogleDataTransportCCTSupport.podspec --platforms=tvos --use-modular-headers # Daily test for symbol collisions between Firebase and CocoaPods. - stage: test @@ -278,25 +398,10 @@ jobs: script: - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD - # Community-supported platforms - - - stage: test - env: - - PROJECT=Firebase PLATFORM=macOS METHOD=xcodebuild - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - script: - - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - - - stage: test - env: - - PROJECT=Firebase PLATFORM=tvOS METHOD=xcodebuild - before_install: - - ./scripts/if_changed.sh ./scripts/install_prereqs.sh - script: - - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - - stage: test + # TODO(paulb777,wilhuff) Replace with a solution that doesn't include multiple platforms in + # a single Podfile. + osx_image: xcode10.1 env: - PROJECT=Firestore PLATFORM=macOS METHOD=xcodebuild before_install: @@ -305,6 +410,9 @@ jobs: - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM - stage: test + # TODO(paulb777,wilhuff) Replace with a solution that doesn't include multiple platforms in + # a single Podfile. + osx_image: xcode10.1 env: - PROJECT=Firestore PLATFORM=tvOS METHOD=xcodebuild before_install: @@ -314,7 +422,10 @@ jobs: # Firestore sanitizers - - stage: test + - stage: + # TODO(paulb777,wilhuff) Replace with a solution that doesn't include multiple platforms in + # a single Podfile. + osx_image: xcode10.1 env: - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild SANITIZERS=asan before_install: @@ -323,6 +434,9 @@ jobs: - travis_retry ./scripts/if_changed.sh ./scripts/build.sh $PROJECT $PLATFORM $METHOD - stage: test + # TODO(paulb777,wilhuff) Replace with a solution that doesn't include multiple platforms in + # a single Podfile. + osx_image: xcode10.1 env: - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild SANITIZERS=tsan before_install: @@ -400,6 +514,8 @@ jobs: - stage: test env: - PROJECT=Installations PLATFORM=iOS METHOD=pod-lib-lint + before_install: + - ./scripts/if_changed.sh ./scripts/install_prereqs.sh script: - travis_retry ./scripts/if_changed.sh ./scripts/pod_lib_lint.rb FirebaseInstallations.podspec --platforms=ios,tvos --ignore-local-podspecs=FirebaseInstanceID.podspec # TODO: Fix FBLPromises warnings for macOS. @@ -425,12 +541,8 @@ jobs: # need to make them fatal for the purposes of the test run. # TODO(varconst): disallow sanitizers to fail once we fix all existing issues. - - env: - - PROJECT=Firestore PLATFORM=macOS METHOD=cmake SANITIZERS=asan - env: - PROJECT=Firestore PLATFORM=macOS METHOD=cmake SANITIZERS=tsan - - env: - - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild SANITIZERS=asan - env: - PROJECT=Firestore PLATFORM=iOS METHOD=xcodebuild SANITIZERS=tsan - env: diff --git a/Carthage.md b/Carthage.md index 9aac6c74394..2b204eb35f0 100644 --- a/Carthage.md +++ b/Carthage.md @@ -68,7 +68,7 @@ binary "https://dl.google.com/dl/firebase/ios/carthage/FirebaseStorageBinary.jso into the Xcode project and make sure they're added to the `Copy Bundle Resources` Build Phase : - For Firestore: - - ./Carthage/Build/iOS/FirebaseFirestore.framework/gRPCCertificates.bundle + - ./Carthage/Build/iOS/gRPC-C++.framework/Resources/gRPCCertificates-Cpp.bundle - For FirebaseMLVisionFaceModel: - ./Carthage/Build/iOS/FaceDetector.framework/GoogleMVFaceDetectorResources.bundle - For FirebaseMLVisionTextModel: diff --git a/Example/Core/Tests/FIRAppTest.m b/Example/Core/Tests/FIRAppTest.m index 062211bd201..d3a9c7ead10 100644 --- a/Example/Core/Tests/FIRAppTest.m +++ b/Example/Core/Tests/FIRAppTest.m @@ -651,7 +651,7 @@ - (void)testAnalyticsSetByGlobalDataCollectionSwitch { // Test that the global data collection switch triggers setting Analytics when no explicit flag is // set. id optionsMock = OCMClassMock([FIROptions class]); - OCMStub([optionsMock isAnalyticsCollectionExpicitlySet]).andReturn(NO); + OCMStub([optionsMock isAnalyticsCollectionExplicitlySet]).andReturn(NO); // We need to use the default app name since Analytics only associates with the default app. FIRApp *defaultApp = [[FIRApp alloc] initInstanceWithName:kFIRDefaultAppName options:optionsMock]; @@ -670,7 +670,7 @@ - (void)testAnalyticsSetByGlobalDataCollectionSwitch { - (void)testAnalyticsNotSetByGlobalDataCollectionSwitch { // Test that the global data collection switch doesn't override an explicitly set Analytics flag. id optionsMock = OCMClassMock([FIROptions class]); - OCMStub([optionsMock isAnalyticsCollectionExpicitlySet]).andReturn(YES); + OCMStub([optionsMock isAnalyticsCollectionExplicitlySet]).andReturn(YES); FIRApp *app = [[FIRApp alloc] initInstanceWithName:@"testAnalyticsNotSet" options:optionsMock]; id configurationMock = OCMClassMock([FIRAnalyticsConfiguration class]); diff --git a/Example/Core/Tests/FIRComponentContainerTest.m b/Example/Core/Tests/FIRComponentContainerTest.m index ba0cf1579e0..b6d45d6ff9e 100644 --- a/Example/Core/Tests/FIRComponentContainerTest.m +++ b/Example/Core/Tests/FIRComponentContainerTest.m @@ -17,6 +17,7 @@ #import #import #import +#import #import "FIRTestComponents.h" @@ -49,12 +50,19 @@ - (instancetype)initWithApp:(FIRApp *)app @end -@interface FIRComponentContainerTest : FIRTestCase +@interface FIRComponentContainerTest : FIRTestCase { + FIRApp *_hostApp; +} @end @implementation FIRComponentContainerTest +- (void)tearDown { + _hostApp = nil; + [super tearDown]; +} + #pragma mark - Registration Tests - (void)testRegisteringConformingClass { @@ -101,7 +109,8 @@ - (void)testInstanceNotCached { - (void)testRemoveAllCachedInstances { FIRComponentContainer *container = [self containerWithRegistrants:@ [[FIRTestClass class], [FIRTestClassCached class], - [FIRTestClassEagerCached class]]]; + [FIRTestClassEagerCached class], + [FIRTestClassCachedWithDep class]]]; // Retrieve an instance of FIRTestClassCached to ensure it's cached. id cachedInstance1 = FIR_COMPONENT(FIRTestProtocolCached, container); @@ -148,18 +157,57 @@ - (void)testProtocolAlreadyRegistered { XCTAssert(container.components.count == 1); } +#pragma mark - Dependency Tests + +- (void)testDependencyDoesntBlock { + /// Test a class that has a dependency, and fetching doesn't block the internal queue. + FIRComponentContainer *container = [self + containerWithRegistrants:@ [[FIRTestClassCached class], [FIRTestClassCachedWithDep class]]]; + XCTAssert(container.components.count == 2); + + id instanceWithDep = + FIR_COMPONENT(FIRTestProtocolCachedWithDep, container); + XCTAssertNotNil(instanceWithDep); +} + +- (void)testDependencyRemoveAllCachedInstancesDoesntBlock { + /// Test a class that has a dependency, and fetching doesn't block the internal queue. + FIRComponentContainer *container = [self + containerWithRegistrants:@ [[FIRTestClassCached class], [FIRTestClassCachedWithDep class]]]; + XCTAssert(container.components.count == 2); + + id instanceWithDep = + FIR_COMPONENT(FIRTestProtocolCachedWithDep, container); + XCTAssertNotNil(instanceWithDep); + XCTAssertNotNil(instanceWithDep.testProperty); + + // Both `instanceWithDep` and `testProperty` should be cached now. + XCTAssertTrue(container.cachedInstances.count == 2); + + // Remove the instances and verify cachedInstances is empty, and doesn't block the queue. + [container removeAllCachedInstances]; + XCTAssertTrue(container.cachedInstances.count == 0); +} + #pragma mark - Convenience Methods /// Create a container that has registered the test class. - (FIRComponentContainer *)containerWithRegistrants:(NSArray *)registrants { - id appMock = OCMClassMock([FIRApp class]); + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:kGoogleAppID + GCMSenderID:kGCMSenderID]; + _hostApp = [[FIRApp alloc] initInstanceWithName:@"fake_app" options:options]; NSMutableSet *allRegistrants = [NSMutableSet set]; // Initialize the container with the test classes. for (Class c in registrants) { [FIRComponentContainer registerAsComponentRegistrant:c inSet:allRegistrants]; } - return [[FIRComponentContainer alloc] initWithApp:appMock registrants:allRegistrants]; + + // Override the app's container with the newly instantiated container. + FIRComponentContainer *container = [[FIRComponentContainer alloc] initWithApp:_hostApp + registrants:allRegistrants]; + _hostApp.container = container; + return container; } @end diff --git a/Example/Core/Tests/FIROptionsTest.m b/Example/Core/Tests/FIROptionsTest.m index 85c2ebab121..87ebea0997f 100644 --- a/Example/Core/Tests/FIROptionsTest.m +++ b/Example/Core/Tests/FIROptionsTest.m @@ -510,42 +510,42 @@ - (void)testAnalyticsCollectionExplicitlySet { NSDictionary *optionsDictionary = @{}; FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; NSDictionary *analyticsOptions = [options analyticsOptionsDictionaryWithInfoDictionary:@{}]; - XCTAssertFalse([options isAnalyticsCollectionExpicitlySet]); + XCTAssertFalse([options isAnalyticsCollectionExplicitlySet]); // Test deactivation flag. optionsDictionary = @{kFIRIsAnalyticsCollectionDeactivated : @YES}; options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; analyticsOptions = [options analyticsOptionsDictionaryWithInfoDictionary:@{}]; - XCTAssertTrue([options isAnalyticsCollectionExpicitlySet]); + XCTAssertTrue([options isAnalyticsCollectionExplicitlySet]); // If "deactivated" == NO, that doesn't mean it's explicitly set / enabled so it should be treated // as if it's not set. optionsDictionary = @{kFIRIsAnalyticsCollectionDeactivated : @NO}; options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; analyticsOptions = [options analyticsOptionsDictionaryWithInfoDictionary:@{}]; - XCTAssertFalse([options isAnalyticsCollectionExpicitlySet]); + XCTAssertFalse([options isAnalyticsCollectionExplicitlySet]); // Test the collection enabled flag. optionsDictionary = @{kFIRIsAnalyticsCollectionEnabled : @YES}; options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; analyticsOptions = [options analyticsOptionsDictionaryWithInfoDictionary:@{}]; - XCTAssertTrue([options isAnalyticsCollectionExpicitlySet]); + XCTAssertTrue([options isAnalyticsCollectionExplicitlySet]); optionsDictionary = @{kFIRIsAnalyticsCollectionEnabled : @NO}; options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; analyticsOptions = [options analyticsOptionsDictionaryWithInfoDictionary:@{}]; - XCTAssertTrue([options isAnalyticsCollectionExpicitlySet]); + XCTAssertTrue([options isAnalyticsCollectionExplicitlySet]); // Test the old measurement flag. options = [[FIROptions alloc] initInternalWithOptionsDictionary:@{}]; analyticsOptions = [options analyticsOptionsDictionaryWithInfoDictionary:@{kFIRIsMeasurementEnabled : @YES}]; - XCTAssertTrue([options isAnalyticsCollectionExpicitlySet]); + XCTAssertTrue([options isAnalyticsCollectionExplicitlySet]); options = [[FIROptions alloc] initInternalWithOptionsDictionary:@{}]; analyticsOptions = [options analyticsOptionsDictionaryWithInfoDictionary:@{kFIRIsMeasurementEnabled : @NO}]; - XCTAssertTrue([options isAnalyticsCollectionExpicitlySet]); + XCTAssertTrue([options isAnalyticsCollectionExplicitlySet]); // For good measure, a combination of all 3 (even if they conflict). optionsDictionary = @@ -553,7 +553,7 @@ - (void)testAnalyticsCollectionExplicitlySet { options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; analyticsOptions = [options analyticsOptionsDictionaryWithInfoDictionary:@{kFIRIsMeasurementEnabled : @NO}]; - XCTAssertTrue([options isAnalyticsCollectionExpicitlySet]); + XCTAssertTrue([options isAnalyticsCollectionExplicitlySet]); } - (void)testModifyingOptionsThrows { diff --git a/Example/Core/Tests/FIRTestComponents.h b/Example/Core/Tests/FIRTestComponents.h index c04b42af791..dda97bb3962 100644 --- a/Example/Core/Tests/FIRTestComponents.h +++ b/Example/Core/Tests/FIRTestComponents.h @@ -46,17 +46,33 @@ /// A test class that is a component registrant that provides a component requiring eager /// instantiation, and is cached for easier validation that it was instantiated. @interface FIRTestClassEagerCached - : NSObject + : NSObject @end #pragma mark - Cached Component /// A test protocol to be used for container testing. @protocol FIRTestProtocolCached +- (void)cacheCow; @end /// A test class that is a component registrant that provides a component that requests to be /// cached. @interface FIRTestClassCached - : NSObject + : NSObject +@end + +#pragma mark - Dependency on Standard + +/// A test protocol to be used for container testing. +@protocol FIRTestProtocolCachedWithDep +@property(nonatomic, strong) id testProperty; +@end + +/// A test class that is a component registrant that provides a component with a dependency on +// `FIRTestProtocolCached`. +@interface FIRTestClassCachedWithDep + : NSObject +@property(nonatomic, strong) id testProperty; +- (instancetype)initWithTest:(id)testInstance; @end diff --git a/Example/Core/Tests/FIRTestComponents.m b/Example/Core/Tests/FIRTestComponents.m index f68fa3082e7..fca0910cdd7 100644 --- a/Example/Core/Tests/FIRTestComponents.m +++ b/Example/Core/Tests/FIRTestComponents.m @@ -14,7 +14,9 @@ #import "FIRTestComponents.h" +#import #import +#import #pragma mark - Standard Component @@ -92,9 +94,6 @@ - (void)doSomethingFaster { - (void)appWillBeDeleted:(FIRApp *)app { } -- (void)doSomething { -} - @end #pragma mark - Cached Component @@ -118,7 +117,50 @@ @implementation FIRTestClassCached - (void)appWillBeDeleted:(FIRApp *)app { } -- (void)doSomething { +/// FIRTestProtocolCached conformance. +- (void)cacheCow { +} + +@end + +#pragma mark - Test Component with Dependency + +@implementation FIRTestClassCachedWithDep + +- (instancetype)initWithTest:(id)testInstance { + self = [super init]; + if (self != nil) { + self.testProperty = testInstance; + } + return self; +} + +- (void)appWillBeDeleted:(nonnull FIRApp *)app { + // Do something that depends on the instance from our dependency. + [self.testProperty cacheCow]; + + // Fetch from the container in the deletion function. + id anotherInstance = FIR_COMPONENT(FIRTestProtocolCached, app.container); + [anotherInstance cacheCow]; +} + ++ (nonnull NSArray *)componentsToRegister { + FIRDependency *dep = [FIRDependency dependencyWithProtocol:@protocol(FIRTestProtocolCached)]; + FIRComponent *testComponent = [FIRComponent + componentWithProtocol:@protocol(FIRTestProtocolCachedWithDep) + instantiationTiming:FIRInstantiationTimingLazy + dependencies:@[ dep ] + creationBlock:^id _Nullable(FIRComponentContainer *_Nonnull container, + BOOL *_Nonnull isCacheable) { + // Fetch from the container in the instantiation block. + *isCacheable = YES; + + id test = FIR_COMPONENT(FIRTestProtocolCached, container); + FIRTestClassCachedWithDep *instance = + [[FIRTestClassCachedWithDep alloc] initWithTest:test]; + return instance; + }]; + return @[ testComponent ]; } @end diff --git a/Example/CoreDiagnostics/Tests/FIRCoreDiagnosticsTest.m b/Example/CoreDiagnostics/Tests/FIRCoreDiagnosticsTest.m index 736c05c0383..9c2013fd0d1 100644 --- a/Example/CoreDiagnostics/Tests/FIRCoreDiagnosticsTest.m +++ b/Example/CoreDiagnostics/Tests/FIRCoreDiagnosticsTest.m @@ -21,9 +21,9 @@ #import #import -#import -#import -#import +#import +#import +#import #import #import #import @@ -47,12 +47,12 @@ @interface FIRCoreDiagnostics : NSObject // Initialization. + (instancetype)sharedInstance; -- (instancetype)initWithTransport:(GDTTransport *)transport +- (instancetype)initWithTransport:(GDTCORTransport *)transport heartbeatDateStorage:(FIRCoreDiagnosticsDateFileStorage *)heartbeatDateStorage; // Properties. @property(nonatomic, readonly) dispatch_queue_t diagnosticsQueue; -@property(nonatomic, readonly) GDTTransport *transport; +@property(nonatomic, readonly) GDTCORTransport *transport; @property(nonatomic, readonly) FIRCoreDiagnosticsDateFileStorage *heartbeatDateStorage; // Install string helpers. @@ -111,7 +111,7 @@ - (instancetype)init { @end -@interface FIRCoreDiagnosticsLog : NSObject +@interface FIRCoreDiagnosticsLog : NSObject @property(nonatomic) logs_proto_mobilesdk_ios_ICoreConfiguration config; @@ -136,9 +136,9 @@ @implementation FIRCoreDiagnosticsTest - (void)setUp { [super setUp]; - self.mockTransport = OCMClassMock([GDTTransport class]); + self.mockTransport = OCMClassMock([GDTCORTransport class]); OCMStub([self.mockTransport eventForTransport]) - .andReturn([[GDTEvent alloc] initWithMappingID:@"111" target:2]); + .andReturn([[GDTCOREvent alloc] initWithMappingID:@"111" target:2]); self.mockDateStorage = OCMClassMock([FIRCoreDiagnosticsDateFileStorage class]); self.diagnostics = [[FIRCoreDiagnostics alloc] initWithTransport:self.mockTransport @@ -220,12 +220,10 @@ - (void)populateProto:(logs_proto_mobilesdk_ios_ICoreConfiguration *)config { config->dynamic_framework_count = numFrameworks; config->has_dynamic_framework_count = 1; config->apple_framework_version = FIREncodeString(combinedVersions); -#if !TARGET_OS_IOS NSString *minVersion = [[NSBundle mainBundle] infoDictionary][@"MinimumOSVersion"]; if (minVersion) { config->min_supported_ios_version = FIREncodeString(minVersion); } -#endif // TARGET_OS_IOS config->using_zip_file = 0; config->has_using_zip_file = 1; config->deployment_type = logs_proto_mobilesdk_ios_ICoreConfiguration_DeploymentType_COCOAPODS; @@ -321,8 +319,8 @@ - (void)assertEventSentWithHeartbeat:(BOOL)isHeartbeat { } - (void)expectEventToBeSentToTransportWithHeartbeat:(BOOL)isHeartbeat { - id eventValidation = [OCMArg checkWithBlock:^BOOL(GDTEvent *obj) { - XCTAssert([obj isKindOfClass:[GDTEvent class]]); + id eventValidation = [OCMArg checkWithBlock:^BOOL(GDTCOREvent *obj) { + XCTAssert([obj isKindOfClass:[GDTCOREvent class]]); FIRCoreDiagnosticsLog *dataObject = obj.dataObject; XCTAssert([dataObject isKindOfClass:[FIRCoreDiagnosticsLog class]]); diff --git a/Example/Database/Tests/Integration/FOrderByTests.m b/Example/Database/Tests/Integration/FOrderByTests.m index 4723676d60d..237475bb291 100644 --- a/Example/Database/Tests/Integration/FOrderByTests.m +++ b/Example/Database/Tests/Integration/FOrderByTests.m @@ -235,7 +235,9 @@ - (void)testFiresChildMovedEvents { moved = YES; XCTAssertEqualObjects(snapshot.key, @"greg", @""); XCTAssertEqualObjects(prevName, @"rob", @""); - XCTAssertEqualObjects(snapshot.value, @{@"nuggets" : @57}, @""); + XCTAssertEqualObjects( + snapshot.value, + @{@"nuggets" : @57}, @""); }]; [ref setValue:initial]; diff --git a/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m index 1a79298776f..7be69b2ec6e 100644 --- a/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m +++ b/Example/Database/Tests/Unit/FLevelDBStorageEngineTests.m @@ -473,7 +473,7 @@ - (void)testHugeLeafNodeThenDeeperSet { // It is kind of bad we raise "invalid" data, but at least we don't crash *trollface* - (void)testExtremeDoublesAsServerCache { #ifdef TARGET_OS_IOS - if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion == 11) { + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 11) { // NSJSONSerialization on iOS 11 correctly serializes small and large doubles. return; } @@ -532,8 +532,14 @@ - (void)testIntegersAreReturnedsAsIntegers { XCTAssertEqual(CFNumberGetType((CFNumberRef)actualInt), kCFNumberSInt64Type); XCTAssertEqualObjects([actualLong stringValue], [longValue stringValue]); XCTAssertEqual(CFNumberGetType((CFNumberRef)actualLong), kCFNumberSInt64Type); -#if TARGET_OS_MACCATALYST - // Catalyst uses int128_t but CFNumber still calls it 64 bits +#ifdef TARGET_OS_IOS + // Catalyst and iOS 13 use int128_t but CFNumber still calls it 64 bits. + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion >= 13) { + XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDouble), kCFNumberSInt64Type); + } else { + XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDouble), kCFNumberFloat64Type); + } +#elif TARGET_OS_MACCATALYST XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDouble), kCFNumberSInt64Type); #else XCTAssertEqual(CFNumberGetType((CFNumberRef)actualDouble), kCFNumberFloat64Type); diff --git a/Example/InstanceID/Tests/FIRInstanceIDCheckinServiceTest.m b/Example/InstanceID/Tests/FIRInstanceIDCheckinServiceTest.m index e38c1593e87..519cedd7e2f 100644 --- a/Example/InstanceID/Tests/FIRInstanceIDCheckinServiceTest.m +++ b/Example/InstanceID/Tests/FIRInstanceIDCheckinServiceTest.m @@ -16,12 +16,12 @@ #import +#import #import #import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" #import "Firebase/InstanceID/FIRInstanceIDCheckinService.h" #import "Firebase/InstanceID/FIRInstanceIDUtilities.h" #import "Firebase/InstanceID/NSError+FIRInstanceID.h" -#import "Firebase/InstanceID/Private/FIRInstanceIDCheckinPreferences.h" static NSString *const kDeviceAuthId = @"1234"; static NSString *const kSecretToken = @"567890"; diff --git a/Example/InstanceID/Tests/FIRInstanceIDCheckinStoreTest.m b/Example/InstanceID/Tests/FIRInstanceIDCheckinStoreTest.m index ef95fb3b9cb..4502b275990 100644 --- a/Example/InstanceID/Tests/FIRInstanceIDCheckinStoreTest.m +++ b/Example/InstanceID/Tests/FIRInstanceIDCheckinStoreTest.m @@ -18,6 +18,7 @@ #import +#import #import "FIRInstanceIDFakeKeychain.h" #import "Firebase/InstanceID/FIRInstanceIDAuthKeyChain.h" #import "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h" @@ -27,7 +28,6 @@ #import "Firebase/InstanceID/FIRInstanceIDStore.h" #import "Firebase/InstanceID/FIRInstanceIDUtilities.h" #import "Firebase/InstanceID/FIRInstanceIDVersionUtilities.h" -#import "Firebase/InstanceID/Private/FIRInstanceIDCheckinPreferences.h" static const NSTimeInterval kExpectationTimeout = 12; diff --git a/Example/InstanceID/Tests/FIRInstanceIDStoreTest.m b/Example/InstanceID/Tests/FIRInstanceIDStoreTest.m index 9355673dbdb..fc3b5da167a 100644 --- a/Example/InstanceID/Tests/FIRInstanceIDStoreTest.m +++ b/Example/InstanceID/Tests/FIRInstanceIDStoreTest.m @@ -16,6 +16,7 @@ #import +#import #import #import "FIRInstanceIDFakeKeychain.h" #import "Firebase/InstanceID/FIRInstanceIDBackupExcludedPlist.h" @@ -26,7 +27,6 @@ #import "Firebase/InstanceID/FIRInstanceIDTokenInfo.h" #import "Firebase/InstanceID/FIRInstanceIDTokenStore.h" #import "Firebase/InstanceID/FIRInstanceIDUtilities.h" -#import "Firebase/InstanceID/Private/FIRInstanceIDCheckinPreferences.h" static NSString *const kSubDirectoryName = @"FirebaseInstanceIDStoreTest"; diff --git a/Example/InstanceID/Tests/FIRInstanceIDTokenOperationsTest.m b/Example/InstanceID/Tests/FIRInstanceIDTokenOperationsTest.m index dd7cfd7cb99..05d8d21cbe6 100644 --- a/Example/InstanceID/Tests/FIRInstanceIDTokenOperationsTest.m +++ b/Example/InstanceID/Tests/FIRInstanceIDTokenOperationsTest.m @@ -16,6 +16,7 @@ #import +#import #import #import "Firebase/InstanceID/FIRInstanceIDAuthService.h" #import "Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h" @@ -30,7 +31,6 @@ #import "Firebase/InstanceID/FIRInstanceIDTokenOperation+Private.h" #import "Firebase/InstanceID/FIRInstanceIDTokenOperation.h" #import "Firebase/InstanceID/NSError+FIRInstanceID.h" -#import "Firebase/InstanceID/Public/FIRInstanceID.h" #import diff --git a/Example/Messaging/Tests/FIRMessagingClientTest.m b/Example/Messaging/Tests/FIRMessagingClientTest.m index 9ba97d60eb3..86a24a63de4 100644 --- a/Example/Messaging/Tests/FIRMessagingClientTest.m +++ b/Example/Messaging/Tests/FIRMessagingClientTest.m @@ -15,9 +15,6 @@ */ #import - - - #import #import diff --git a/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m b/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m index a27768293d5..18f36fc7a20 100644 --- a/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m +++ b/Example/Messaging/Tests/FIRMessagingContextManagerServiceTest.m @@ -83,8 +83,10 @@ - (void)testMessageWithFutureStartTime { XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]); XCTAssertEqual(self.scheduledLocalNotifications.count, 1); - +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" UILocalNotification *notification = [self.scheduledLocalNotifications firstObject]; +#pragma clang diagnostic pop NSDate *date = [self.dateFormatter dateFromString:startTimeString]; XCTAssertEqual([notification.fireDate compare:date], NSOrderedSame); #endif @@ -134,7 +136,10 @@ - (void)testMessageWithPastStartAndFutureEndTime { XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]); XCTAssertEqual(self.scheduledLocalNotifications.count, 1); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" UILocalNotification *notification = [self.scheduledLocalNotifications firstObject]; +#pragma clang diagnostic pop // schedule notification after start date XCTAssertEqual([notification.fireDate compare:startDate], NSOrderedDescending); // schedule notification after end date @@ -163,7 +168,10 @@ - (void)testTimedNotificationsUserInfo { XCTAssertTrue([FIRMessagingContextManagerService handleContextManagerMessage:message]); XCTAssertEqual(self.scheduledLocalNotifications.count, 1); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" UILocalNotification *notification = [self.scheduledLocalNotifications firstObject]; +#pragma clang diagnostic pop XCTAssertEqualObjects(notification.userInfo[messageIdentifierKey], messageIdentifier); XCTAssertEqualObjects(notification.userInfo[customDataKey], customData); #endif @@ -175,6 +183,8 @@ - (void)testTimedNotificationsUserInfo { - (void)mockSchedulingLocalNotifications { #if TARGET_OS_IOS id mockApplication = OCMPartialMock([UIApplication sharedApplication]); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" __block UILocalNotification *notificationToSchedule; [[[mockApplication stub] andDo:^(NSInvocation *invocation) { @@ -189,6 +199,7 @@ - (void)mockSchedulingLocalNotifications { } return NO; }]]; +#pragma clang diagnostic pop #endif } diff --git a/Example/Messaging/Tests/FIRMessagingHandlingTest.m b/Example/Messaging/Tests/FIRMessagingHandlingTest.m new file mode 100644 index 00000000000..63233b3a6ba --- /dev/null +++ b/Example/Messaging/Tests/FIRMessagingHandlingTest.m @@ -0,0 +1,244 @@ +/* + * 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 + +#import + +#import +#import +#import +#import + +#import "Example/Messaging/Tests/FIRMessagingTestUtilities.h" +#import "Firebase/Messaging/FIRMessaging_Private.h" +#import "Firebase/Messaging/FIRMessagingAnalytics.h" +#import "Firebase/Messaging/FIRMessagingRmqManager.h" +#import "Firebase/Messaging/FIRMessagingSyncMessageManager.h" + +extern NSString *const kFIRMessagingFCMTokenFetchAPNSOption; + +/// The NSUserDefaults domain for testing. +static NSString *const kFIRMessagingDefaultsTestDomain = @"com.messaging.tests"; + +@interface FIRMessaging () + +@property(nonatomic, readwrite, strong) NSString *defaultFcmToken; +@property(nonatomic, readwrite, strong) FIRInstanceID *instanceID; +@property(nonatomic, readwrite, strong) FIRMessagingRmqManager *rmq2Manager; + +- (BOOL)handleContextManagerMessage:(NSDictionary *)message; +- (void)handleIncomingLinkIfNeededFromMessage:(NSDictionary *)message; + +@end + +/* + * This class checks if we handle the received message properly + * based on each type of messages. Checks include duplicate message handling, + * analytics logging, etc. + */ +@interface FIRMessagingHandlingTest : XCTestCase + +@property(nonatomic, readonly, strong) FIRMessaging *messaging; +@property(nonatomic, strong) FIRMessagingAnalytics *messageAnalytics; +@property(nonatomic, strong) id mockMessaging; +@property(nonatomic, strong) id mockInstanceID; +@property(nonatomic, strong) id mockFirebaseApp; +@property(nonatomic, strong) id mockMessagingAnalytics; + +@end + +@implementation FIRMessagingHandlingTest + +- (void)setUp { + [super setUp]; + + // Create the messaging instance with all the necessary dependencies. + NSUserDefaults *defaults = + [[NSUserDefaults alloc] initWithSuiteName:kFIRMessagingDefaultsTestDomain]; + _messaging = [FIRMessagingTestUtilities messagingForTestsWithUserDefaults:defaults]; + _mockFirebaseApp = OCMClassMock([FIRApp class]); + OCMStub([_mockFirebaseApp defaultApp]).andReturn(_mockFirebaseApp); + _mockInstanceID = OCMPartialMock(self.messaging.instanceID); + [[NSUserDefaults standardUserDefaults] + removePersistentDomainForName:[NSBundle mainBundle].bundleIdentifier]; + _mockMessaging = OCMPartialMock(_messaging); + _mockMessagingAnalytics = OCMClassMock([FIRMessagingAnalytics class]); +} + +- (void)tearDown { + [self.messaging.messagingUserDefaults removePersistentDomainForName:kFIRMessagingDefaultsTestDomain]; + self.messaging.shouldEstablishDirectChannel = NO; + self.messaging.defaultFcmToken = nil; + [_mockMessagingAnalytics stopMocking]; + [_mockMessaging stopMocking]; + [_mockInstanceID stopMocking]; + [_mockFirebaseApp stopMocking]; + _messaging = nil; + [super tearDown]; +} + +-(void)testEmptyNotification { + XCTAssertEqualObjects(@(FIRMessagingMessageStatusUnknown), @([_mockMessaging appDidReceiveMessage:@{}].status)); +} + +-(void)testAPNSDisplayNotification { + NSDictionary *notificationPayload = @{ + @"aps": @{ + @"alert" : @{ + @"body" : @"body of notification", + @"title" : @"title of notification", + } + }, + @"gcm.message_id" : @"1566515013484879", + @"gcm.n.e" : @1, + @"google.c.a.c_id" : @"7379928225816991517", + @"google.c.a.e" : @1, + @"google.c.a.ts" : @1566515009, + @"google.c.a.udt" : @0 + }; + OCMExpect([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMExpect([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMExpect([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); + + OCMReject([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMReject([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMReject([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); + // Clear database + [_messaging.rmq2Manager deleteSyncMessageWithRmqID:@"1566515013484879"]; +} + +-(void)testAPNSContentAvailableNotification { + NSDictionary *notificationPayload = @{ + @"aps": @{ + @"content-available" : @1 + }, + @"gcm.message_id" : @"1566513591299872", + @"image" : @"bunny.png", + @"google.c.a.e" : @1 + }; + OCMExpect([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMExpect([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMExpect([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); + + OCMReject([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMReject([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMReject([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); + [_messaging.rmq2Manager deleteSyncMessageWithRmqID:@"1566513591299872"]; + +} + +-(void)testAPNSContentAvailableContextualNotification { + NSDictionary *notificationPayload = @{ + @"aps" : @{ + @"content-available": @1 + }, + @"gcm.message_id": @"1566515531287827", + @"gcm.n.e" : @1, + @"gcm.notification.body" : @"Local time zone message!", + @"gcm.notification.title" : @"Hello", + @"gcms" : @"gcm.gmsproc.cm", + @"google.c.a.c_id" : @"5941428497527920876", + @"google.c.a.e" : @1, + @"google.c.a.ts" : @1566565920, + @"google.c.a.udt" : @1, + @"google.c.cm.cat" : @"com.google.firebase.messaging.testapp.dev", + @"google.c.cm.lt_end" : @"2019-09-20 13:12:00", + @"google.c.cm.lt_start" : @"2019-08-23 13:12:00", + }; + OCMExpect([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMExpect([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMExpect([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); + + OCMReject([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMReject([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMReject([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); + [_messaging.rmq2Manager deleteSyncMessageWithRmqID:@"1566515531287827"]; + +} + +-(void)testContextualLocalNotification { + NSDictionary *notificationPayload = @{ + @"gcm.message_id": @"1566515531281975", + @"gcm.n.e" : @1, + @"gcm.notification.body" : @"Local time zone message!", + @"gcm.notification.title" : @"Hello", + @"gcms" : @"gcm.gmsproc.cm", + @"google.c.a.c_id" : @"5941428497527920876", + @"google.c.a.e" : @1, + @"google.c.a.ts" : @1566565920, + @"google.c.a.udt" : @1, + }; + OCMExpect([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMExpect([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMExpect([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); + + OCMReject([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMReject([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMReject([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); + [_messaging.rmq2Manager deleteSyncMessageWithRmqID:@"1566515531281975"]; +} + +-(void)testMCSNotification { + NSDictionary *notificationPayload = @{ + @"from" : @"35006771263", + @"image" : @"bunny.png" + }; + OCMExpect([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMExpect([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMExpect([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); + + OCMExpect([_mockMessaging handleContextManagerMessage:notificationPayload]); + OCMExpect([_mockMessaging handleIncomingLinkIfNeededFromMessage:notificationPayload]); + OCMExpect([_mockMessagingAnalytics logMessage:notificationPayload toAnalytics:[OCMArg any]]); + + XCTAssertEqualObjects(@(FIRMessagingMessageStatusNew), + @([_messaging appDidReceiveMessage:notificationPayload].status)); + OCMVerifyAll(_mockMessaging); +} + +@end diff --git a/Example/Messaging/Tests/FIRMessagingTest.m b/Example/Messaging/Tests/FIRMessagingTest.m index 6b59047129c..9d7283429c0 100644 --- a/Example/Messaging/Tests/FIRMessagingTest.m +++ b/Example/Messaging/Tests/FIRMessagingTest.m @@ -29,7 +29,7 @@ extern NSString *const kFIRMessagingFCMTokenFetchAPNSOption; /// The NSUserDefaults domain for testing. -NSString *const kFIRMessagingDefaultsTestDomain = @"com.messaging.tests"; +static NSString *const kFIRMessagingDefaultsTestDomain = @"com.messaging.tests"; @interface FIRMessaging () diff --git a/Example/Podfile b/Example/Podfile index 466f9e8b545..5003dcc1327 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -21,7 +21,7 @@ target 'Core_Example_iOS' do # The next line is the forcing function for the Firebase pod. The Firebase # version's subspecs should depend on the component versions in the # corresponding podspec's in this repo. - pod 'Firebase/CoreOnly', '6.6.0' + pod 'Firebase/CoreOnly', '6.9.0' target 'Core_Tests_iOS' do inherit! :search_paths @@ -148,131 +148,3 @@ target 'Auth_Sample' do pod 'EarlGrey' end end - -target 'Core_Example_macOS' do - platform :osx, '10.11' - - pod 'FirebaseCore', :path => '../' - pod 'FirebaseCoreDiagnostics', :path => '../' - pod 'FirebaseCoreDiagnosticsInterop', :path => '../' - pod 'GoogleDataTransport', :path => '../' - pod 'GoogleDataTransportCCTSupport', :path => '../' - - target 'Core_Tests_macOS' do - inherit! :search_paths - pod 'OCMock' - end -end - -target 'Auth_Example_macOS' do - platform :osx, '10.11' - - pod 'FirebaseAuth', :path => '../' - - target 'Auth_Tests_macOS' do - inherit! :search_paths - pod 'OCMock' - end -end - -target 'Database_Example_macOS' do - platform :osx, '10.11' - - pod 'FirebaseDatabase', :path => '../' - - target 'Database_Tests_macOS' do - inherit! :search_paths - end - - target 'Database_IntegrationTests_macOS' do - inherit! :search_paths - end -end - -target 'Storage_Example_macOS' do - platform :osx, '10.11' - - pod 'FirebaseStorage', :path => '../' - - target 'Storage_Tests_macOS' do - inherit! :search_paths - pod 'OCMock' - end - - target 'Storage_IntegrationTests_macOS' do - inherit! :search_paths - end -end - -target 'Core_Example_tvOS' do - platform :tvos, '10.0' - - target 'Core_Tests_tvOS' do - inherit! :search_paths - pod 'OCMock' - end -end - -target 'Auth_Example_tvOS' do - platform :tvos, '10.0' - - pod 'FirebaseAuth', :path => '../' - - target 'Auth_Tests_tvOS' do - inherit! :search_paths - pod 'OCMock' - end -end - -target 'Database_Example_tvOS' do - platform :tvos, '10.0' - - pod 'FirebaseDatabase', :path => '../' - - target 'Database_Tests_tvOS' do - inherit! :search_paths - end - -# TODO -# target 'Database_IntegrationTests_tvOS' do -# inherit! :search_paths -# end -end - -target 'Storage_Example_tvOS' do - platform :tvos, '10.0' - - pod 'FirebaseStorage', :path => '../' - - target 'Storage_Tests_tvOS' do - inherit! :search_paths - pod 'OCMock' - end - -#TODO Storage_IntegrationTests_tvOS -# target 'Storage_IntegrationTests_tvOS' do -# inherit! :search_paths -# end -end - -target 'Messaging_Example_tvOS' do - platform :tvos, '10.0' - - pod 'FirebaseMessaging', :path => '../' - - target 'Messaging_Tests_tvOS' do - inherit! :search_paths - pod 'OCMock' - end -end - -target 'InstanceID_Example_tvOS' do - platform :tvos, '10.0' - - pod 'FirebaseInstanceID', :path => '../' - - target 'InstanceID_Tests_tvOS' do - inherit! :search_paths - pod 'OCMock' - end -end diff --git a/Firebase/Auth/Source/Public/FIRAuthErrors.h b/Firebase/Auth/Source/Public/FIRAuthErrors.h index 651e20d7e70..e58192707f9 100644 --- a/Firebase/Auth/Source/Public/FIRAuthErrors.h +++ b/Firebase/Auth/Source/Public/FIRAuthErrors.h @@ -321,6 +321,10 @@ typedef NS_ENUM(NSInteger, FIRAuthErrorCode) { */ FIRAuthErrorCodeNullUser = 17067, + /** Indicates that a Firebase Dynamic Link is not activated. + */ + FIRAuthErrorCodeDynamicLinkNotActivated = 17068, + /** * Represents the error code for when the given provider id for a web operation is invalid. */ @@ -331,6 +335,10 @@ typedef NS_ENUM(NSInteger, FIRAuthErrorCode) { */ FIRAuthErrorCodeInvalidDynamicLinkDomain = 17074, + /** Indicates that the credential is rejected because it's misformed or mismatching. + */ + FIRAuthErrorCodeRejectedCredential = 17075, + /** Indicates that the GameKit framework is not linked prior to attempting Game Center signin. */ FIRAuthErrorCodeGameKitNotLinked = 17076, diff --git a/Firebase/Auth/Source/Utilities/FIRAuthErrorUtils.m b/Firebase/Auth/Source/Utilities/FIRAuthErrorUtils.m index 9534d3923db..aeb4f71d5bb 100644 --- a/Firebase/Auth/Source/Utilities/FIRAuthErrorUtils.m +++ b/Firebase/Auth/Source/Utilities/FIRAuthErrorUtils.m @@ -451,6 +451,18 @@ static NSString *const kFIRAuthErrorMessageMalformedJWT = @"Failed to parse JWT. Check the userInfo dictionary for the full token."; +/** @var kFIRAuthErrorMessageDynamicLinkNotActivated + @brief Error message constant describing @c FIRAuthErrorCodeDynamicLinkNotActivated errors. + */ +static NSString *const kFIRAuthErrorMessageDynamicLinkNotActivated = + @"Please activate Dynamic Links in the Firebase Console and agree to the terms and conditions."; + +/** @var kFIRAuthErrorMessageRejectedCredential + @brief Error message constant describing @c FIRAuthErrorCodeRejectedCredential errors. + */ +static NSString *const kFIRAuthErrorMessageRejectedCredential = + @"The request contains malformed or mismatching credentials."; + /** @var FIRAuthErrorDescription @brief The error descrioption, based on the error code. @remarks No default case so that we get a compiler warning if a new value was added to the enum. @@ -583,6 +595,10 @@ return kFIRAuthErrorMessageLocalPlayerNotAuthenticated; case FIRAuthErrorCodeGameKitNotLinked: return kFIRAuthErrorMessageGameKitNotLinked; + case FIRAuthErrorCodeDynamicLinkNotActivated: + return kFIRAuthErrorMessageDynamicLinkNotActivated; + case FIRAuthErrorCodeRejectedCredential: + return kFIRAuthErrorMessageRejectedCredential; } } @@ -718,6 +734,10 @@ return @"ERROR_LOCAL_PLAYER_NOT_AUTHENTICATED"; case FIRAuthErrorCodeGameKitNotLinked: return @"ERROR_GAME_KIT_NOT_LINKED"; + case FIRAuthErrorCodeDynamicLinkNotActivated: + return @"ERROR_DYNAMIC_LINK_NOT_ACTIVATED"; + case FIRAuthErrorCodeRejectedCredential: + return @"ERROR_REJECTED_CREDENTIAL"; } } diff --git a/Firebase/Core/CHANGELOG.md b/Firebase/Core/CHANGELOG.md index 76edafcec98..c38cf48c6a0 100644 --- a/Firebase/Core/CHANGELOG.md +++ b/Firebase/Core/CHANGELOG.md @@ -1,3 +1,7 @@ +# v6.3.0 -- M56 +- [changed] Transitive GoogleDataTransport dependency incremented to v2.0.0. (#3729) +- [fixed] Fixed "expiclitlySet" typo. (#3853) + # v6.2.0 -- M53 - [added] Added AppKit dependency on macOS and UIKit dependency on iOS and tvOS. (#3459) - [added] Added support for Firebase Segmentation. (#3430) diff --git a/Firebase/Core/FIRApp.m b/Firebase/Core/FIRApp.m index 48b76f60e2e..ce71f40b00d 100644 --- a/Firebase/Core/FIRApp.m +++ b/Firebase/Core/FIRApp.m @@ -352,7 +352,7 @@ - (void)setDataCollectionDefaultEnabled:(BOOL)dataCollectionDefaultEnabled { } // Check if the Analytics flag is explicitly set. If so, no further actions are necessary. - if ([self.options isAnalyticsCollectionExpicitlySet]) { + if ([self.options isAnalyticsCollectionExplicitlySet]) { return; } diff --git a/Firebase/Core/FIRComponentContainer.m b/Firebase/Core/FIRComponentContainer.m index 1977d0e3124..0306da55ba0 100644 --- a/Firebase/Core/FIRComponentContainer.m +++ b/Firebase/Core/FIRComponentContainer.m @@ -23,7 +23,9 @@ NS_ASSUME_NONNULL_BEGIN -@interface FIRComponentContainer () +@interface FIRComponentContainer () { + dispatch_queue_t _containerQueue; +} /// The dictionary of components that are registered for a particular app. The key is an NSString /// of the protocol. @@ -67,6 +69,8 @@ - (instancetype)initWithApp:(FIRApp *)app registrants:(NSMutableSet *)all _app = app; _cachedInstances = [NSMutableDictionary dictionary]; _components = [NSMutableDictionary dictionary]; + _containerQueue = + dispatch_queue_create("com.google.FirebaseComponentContainer", DISPATCH_QUEUE_SERIAL); [self populateComponentsFromRegisteredClasses:allRegistrants forApp:app]; } @@ -92,7 +96,7 @@ - (void)populateComponentsFromRegisteredClasses:(NSSet *)classes forApp:( // Store the creation block for later usage. self.components[protocolName] = component.creationBlock; - // Instantiate the + // Instantiate the instance if it has requested to be instantiated. BOOL shouldInstantiateEager = (component.instantiationTiming == FIRInstantiationTimingAlwaysEager); BOOL shouldInstantiateDefaultEager = @@ -136,7 +140,9 @@ - (nullable id)instantiateInstanceForProtocol:(Protocol *)protocol // The instance is ready to be returned, but check if it should be cached first before returning. if (shouldCache) { - self.cachedInstances[protocolName] = instance; + dispatch_sync(_containerQueue, ^{ + self.cachedInstances[protocolName] = instance; + }); } return instance; @@ -147,7 +153,11 @@ - (nullable id)instantiateInstanceForProtocol:(Protocol *)protocol - (nullable id)instanceForProtocol:(Protocol *)protocol { // Check if there is a cached instance, and return it if so. NSString *protocolName = NSStringFromProtocol(protocol); - id cachedInstance = self.cachedInstances[protocolName]; + __block id cachedInstance; + dispatch_sync(_containerQueue, ^{ + cachedInstance = self.cachedInstances[protocolName]; + }); + if (cachedInstance) { return cachedInstance; } @@ -161,14 +171,29 @@ - (nullable id)instanceForProtocol:(Protocol *)protocol { - (void)removeAllCachedInstances { // Loop through the cache and notify each instance that is a maintainer to clean up after itself. - for (id instance in self.cachedInstances.allValues) { + // Design note: we're getting a copy here, unlocking the cached instances, iterating over the + // copy, then locking and removing all cached instances. A race condition *could* exist where a + // new cached instance is created between the copy and the removal, but the chances are slim and + // side-effects are significantly smaller than including the entire loop in the `dispatch_sync` + // block (access to the cache from inside the block would deadlock and crash). + __block NSDictionary *instancesCopy; + dispatch_sync(_containerQueue, ^{ + instancesCopy = [self.cachedInstances copy]; + }); + + for (id instance in instancesCopy.allValues) { if ([instance conformsToProtocol:@protocol(FIRComponentLifecycleMaintainer)] && [instance respondsToSelector:@selector(appWillBeDeleted:)]) { [instance appWillBeDeleted:self.app]; } } - [self.cachedInstances removeAllObjects]; + instancesCopy = nil; + + // Empty the cache. + dispatch_sync(_containerQueue, ^{ + [self.cachedInstances removeAllObjects]; + }); } @end diff --git a/Firebase/Core/FIROptions.m b/Firebase/Core/FIROptions.m index 89a679a74be..e259bf8b336 100644 --- a/Firebase/Core/FIROptions.m +++ b/Firebase/Core/FIROptions.m @@ -399,7 +399,7 @@ - (BOOL)isMeasurementEnabled { return [value boolValue]; } -- (BOOL)isAnalyticsCollectionExpicitlySet { +- (BOOL)isAnalyticsCollectionExplicitlySet { // If it's de-activated, it classifies as explicity set. If not, it's not a good enough indication // that the developer wants FirebaseAnalytics enabled so continue checking. if (self.isAnalyticsCollectionDeactivated) { diff --git a/Firebase/Core/Private/FIROptionsInternal.h b/Firebase/Core/Private/FIROptionsInternal.h index 117efdaa0fa..0660a3cd8c3 100644 --- a/Firebase/Core/Private/FIROptionsInternal.h +++ b/Firebase/Core/Private/FIROptionsInternal.h @@ -65,7 +65,7 @@ extern NSString *const kServiceInfoFileType; * Indicates whether or not Analytics collection was explicitly enabled via a plist flag or at * runtime. */ -@property(nonatomic, readonly) BOOL isAnalyticsCollectionExpicitlySet; +@property(nonatomic, readonly) BOOL isAnalyticsCollectionExplicitlySet; /** * Whether or not Analytics Collection was enabled. Analytics Collection is enabled unless diff --git a/Firebase/CoreDiagnostics/CHANGELOG.md b/Firebase/CoreDiagnostics/CHANGELOG.md index b250fd556f5..1eed8418afe 100644 --- a/Firebase/CoreDiagnostics/CHANGELOG.md +++ b/Firebase/CoreDiagnostics/CHANGELOG.md @@ -1,3 +1,6 @@ +# v1.1.0 +- Updates GDT dependency to GDTCOR prefixed version. + # v1.0.0 Initial Release--for Google use only. This library collects diagnostics and usage data for internal use by Firebase. Data gathered by this library will diff --git a/Firebase/CoreDiagnostics/FIRCDLibrary/FIRCoreDiagnostics.m b/Firebase/CoreDiagnostics/FIRCDLibrary/FIRCoreDiagnostics.m index 8f87e0929c7..dd6a51b75a6 100644 --- a/Firebase/CoreDiagnostics/FIRCDLibrary/FIRCoreDiagnostics.m +++ b/Firebase/CoreDiagnostics/FIRCDLibrary/FIRCoreDiagnostics.m @@ -17,10 +17,10 @@ #import #include -#import -#import -#import -#import +#import +#import +#import +#import #import #import @@ -92,7 +92,8 @@ NSString *const kFIRCoreDiagnosticsHeartbeatDateFileName = @"FIREBASE_DIAGNOSTICS_HEARTBEAT_DATE"; /** - * @note This should implement the GDTEventDataObject protocol, but can't because of weak-linking. + * @note This should implement the GDTCOREventDataObject protocol, but can't because of + * weak-linking. */ @interface FIRCoreDiagnosticsLog : NSObject @@ -111,14 +112,14 @@ - (instancetype)initWithConfig:(logs_proto_mobilesdk_ios_ICoreConfiguration)conf return self; } -// Provided and required by the GDTEventDataObject protocol. +// Provided and required by the GDTCOREventDataObject protocol. - (NSData *)transportBytes { pb_ostream_t sizestream = PB_OSTREAM_SIZING; // Encode 1 time to determine the size. if (!pb_encode(&sizestream, logs_proto_mobilesdk_ios_ICoreConfiguration_fields, &_config)) { - GDTLogError(GDTMCETransportBytesError, @"Error in nanopb encoding for size: %s", - PB_GET_ERROR(&sizestream)); + GDTCORLogError(GDTCORMCETransportBytesError, @"Error in nanopb encoding for size: %s", + PB_GET_ERROR(&sizestream)); } // Encode a 2nd time to actually get the bytes from it. @@ -126,8 +127,8 @@ - (NSData *)transportBytes { CFMutableDataRef dataRef = CFDataCreateMutable(CFAllocatorGetDefault(), bufferSize); pb_ostream_t ostream = pb_ostream_from_buffer((void *)CFDataGetBytePtr(dataRef), bufferSize); if (!pb_encode(&ostream, logs_proto_mobilesdk_ios_ICoreConfiguration_fields, &_config)) { - GDTLogError(GDTMCETransportBytesError, @"Error in nanopb encoding for bytes: %s", - PB_GET_ERROR(&ostream)); + GDTCORLogError(GDTCORMCETransportBytesError, @"Error in nanopb encoding for bytes: %s", + PB_GET_ERROR(&ostream)); } CFDataSetLength(dataRef, ostream.bytes_written); @@ -149,7 +150,7 @@ @interface FIRCoreDiagnostics : NSObject @property(nonatomic, readonly) dispatch_queue_t diagnosticsQueue; /** The transport object used to send data. */ -@property(nonatomic, readonly) GDTTransport *transport; +@property(nonatomic, readonly) GDTCORTransport *transport; /** The storage to store the date of the last sent heartbeat. */ @property(nonatomic, readonly) FIRCoreDiagnosticsDateFileStorage *heartbeatDateStorage; @@ -170,9 +171,9 @@ + (instancetype)sharedInstance { } - (instancetype)init { - GDTTransport *transport = [[GDTTransport alloc] initWithMappingID:@"137" - transformers:nil - target:kGDTTargetCCT]; + GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"137" + transformers:nil + target:kGDTCORTargetCCT]; FIRCoreDiagnosticsDateFileStorage *dateStorage = [[FIRCoreDiagnosticsDateFileStorage alloc] initWithFileURL:[[self class] filePathURLWithName:kFIRCoreDiagnosticsHeartbeatDateFileName]]; @@ -182,11 +183,11 @@ - (instancetype)init { /** Initializer for unit tests. * - * @param transport A `GDTTransport` instance which that be used to send event. + * @param transport A `GDTCORTransport` instance which that be used to send event. * @param heartbeatDateStorage An instanse of date storage to track heartbeat sending. * @return Returns the initialized `FIRCoreDiagnostics` instance. */ -- (instancetype)initWithTransport:(GDTTransport *)transport +- (instancetype)initWithTransport:(GDTCORTransport *)transport heartbeatDateStorage:(FIRCoreDiagnosticsDateFileStorage *)heartbeatDateStorage { self = [super init]; if (self) { @@ -374,7 +375,8 @@ void FIRPopulateProtoWithCommonInfoFromApp(logs_proto_mobilesdk_ios_ICoreConfigu config->has_pod_name = 1; if (!diagnosticObjects[kFIRCDllAppsCountKey]) { - GDTLogError(GDTMCEGeneralError, @"%@", @"App count is a required value in the data dict."); + GDTCORLogError(GDTCORMCEGeneralError, @"%@", + @"App count is a required value in the data dict."); } config->app_count = (int32_t)[diagnosticObjects[kFIRCDllAppsCountKey] integerValue]; config->has_app_count = 1; @@ -635,8 +637,8 @@ - (void)sendDiagnosticsData:(nonnull id)diagnosticsData FIRCoreDiagnosticsLog *log = [[FIRCoreDiagnosticsLog alloc] initWithConfig:icore_config]; // Send the log as a telemetry event. - GDTEvent *event = [self.transport eventForTransport]; - event.dataObject = (id)log; + GDTCOREvent *event = [self.transport eventForTransport]; + event.dataObject = (id)log; [self.transport sendTelemetryEvent:event]; }); } diff --git a/Firebase/DynamicLinks/CHANGELOG.md b/Firebase/DynamicLinks/CHANGELOG.md index 25939e52c8c..b337f9e81f8 100644 --- a/Firebase/DynamicLinks/CHANGELOG.md +++ b/Firebase/DynamicLinks/CHANGELOG.md @@ -1,3 +1,9 @@ +# v4.0.5 +- [fixed] Removed references to UIWebViewDelegate to comply with App Store Submission warning. (#3722) + +# v4.0.4 +- [fixed] Removed references to UIWebView to comply with App Store Submission warning. (#3722) + # v4.0.3 - [added] Added support for custom domains for internal Google apps. (#3540) diff --git a/Firebase/DynamicLinks/FIRDLJavaScriptExecutor.m b/Firebase/DynamicLinks/FIRDLJavaScriptExecutor.m index 9912d256c08..4cf934144ed 100644 --- a/Firebase/DynamicLinks/FIRDLJavaScriptExecutor.m +++ b/Firebase/DynamicLinks/FIRDLJavaScriptExecutor.m @@ -18,14 +18,6 @@ #import "DynamicLinks/FIRDLJavaScriptExecutor.h" -// define below needed because nullability of UIWebViewDelegate method param was changed between -// iOS SDK versions -#if (defined(__IPHONE_10_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0)) -#define FIRDL_NULLABLE_IOS9_NONNULLABLE_IOS10 nonnull -#else -#define FIRDL_NULLABLE_IOS9_NONNULLABLE_IOS10 nullable -#endif - NS_ASSUME_NONNULL_BEGIN static NSString *const kJSMethodName = @"generateFingerprint"; @@ -50,16 +42,15 @@ return methodString; } -@interface FIRDLJavaScriptExecutor () +@interface FIRDLJavaScriptExecutor () @end @implementation FIRDLJavaScriptExecutor { __weak id _delegate; NSString *_script; - // Web views with which to run JavaScript. - UIWebView *_uiWebView; // Used in iOS 7 only. - WKWebView *_wkWebView; // Used in iOS 8+ only. + // Web view with which to run JavaScript. + WKWebView *_wkWebView; } - (instancetype)initWithDelegate:(id)delegate @@ -82,17 +73,9 @@ - (void)start { NSString *htmlContent = [NSString stringWithFormat:@"", _script]; - // Use WKWebView if available as it executes JavaScript more quickly, otherwise, fall back - // on UIWebView. - if ([WKWebView class]) { - _wkWebView = [[WKWebView alloc] init]; - _wkWebView.navigationDelegate = self; - [_wkWebView loadHTMLString:htmlContent baseURL:nil]; - } else { - _uiWebView = [[UIWebView alloc] init]; - _uiWebView.delegate = self; - [_uiWebView loadHTMLString:htmlContent baseURL:nil]; - } + _wkWebView = [[WKWebView alloc] init]; + _wkWebView.navigationDelegate = self; + [_wkWebView loadHTMLString:htmlContent baseURL:nil]; } - (void)handleExecutionResult:(NSString *)result { @@ -109,8 +92,6 @@ - (void)handleExecutionError:(nullable NSError *)error { } - (void)cleanup { - _uiWebView.delegate = nil; - _uiWebView = nil; _wkWebView.navigationDelegate = nil; _wkWebView = nil; } @@ -150,33 +131,6 @@ - (void)webView:(WKWebView *)webView [self handleExecutionError:error]; } -#pragma mark - UIWebViewDelegate - -- (void)webViewDidFinishLoad:(UIWebView *)webView { - // Make sure that the javascript was loaded successfully before calling the method. - NSString *methodType = - [webView stringByEvaluatingJavaScriptFromString:FIRDLTypeofFingerprintJSMethodNameString()]; - if (![methodType isEqualToString:@"function"]) { - // Javascript was not loaded successfully. - [self handleExecutionError:nil]; - return; - } - - // Get the result from javascript. - NSString *result = - [webView stringByEvaluatingJavaScriptFromString:GINFingerprintJSMethodString()]; - if ([result isKindOfClass:[NSString class]]) { - [self handleExecutionResult:result]; - } else { - [self handleExecutionError:nil]; - } -} - -- (void)webView:(UIWebView *)webView - didFailLoadWithError:(FIRDL_NULLABLE_IOS9_NONNULLABLE_IOS10 NSError *)error { - [self handleExecutionError:error]; -} - @end NS_ASSUME_NONNULL_END diff --git a/Firebase/DynamicLinks/Public/FDLURLComponents.h b/Firebase/DynamicLinks/Public/FDLURLComponents.h index 3177859c55e..b28cf32246c 100644 --- a/Firebase/DynamicLinks/Public/FDLURLComponents.h +++ b/Firebase/DynamicLinks/Public/FDLURLComponents.h @@ -397,6 +397,12 @@ FIR_SWIFT_NAME(DynamicLinkOtherPlatformParameters) */ + (instancetype)parameters NS_SWIFT_UNAVAILABLE("Use init()"); +/** + * @method init + * @abstract A method for creating the Other platform parameters object. + * @return Returns an object to be used with FIRDynamicLinkURLComponents to add Other Platform + * parameters to a generated Dynamic Link URL. + */ - (instancetype)init; @end diff --git a/Firebase/InAppMessaging/CHANGELOG.md b/Firebase/InAppMessaging/CHANGELOG.md index 7f381e426e0..9fa1e5c49cc 100644 --- a/Firebase/InAppMessaging/CHANGELOG.md +++ b/Firebase/InAppMessaging/CHANGELOG.md @@ -1,3 +1,6 @@ +# 2019-09-03 -- v0.15.4 +- [fixed] Undeprecated initializer for FIRInAppMessagingAction so it can be used going forward in custom UI display (#3545). + # 2019-07-23 -- v0.15.2 - [fixed] Fixed issue with messages to be triggered on app launch (#3237). diff --git a/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h b/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h index 781f771a039..bd555cd2172 100644 --- a/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h +++ b/Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h @@ -135,9 +135,8 @@ NS_SWIFT_NAME(InAppMessagingAction) /// Unavailable. - (instancetype)init NS_UNAVAILABLE; -/// Deprecated, this class shouldn't be directly instantiated. -- (instancetype)initWithActionText:(nullable NSString *)actionText - actionURL:(NSURL *)actionURL __deprecated; +/// This class should only be initialized from a custom in-app message UI component implementation. +- (instancetype)initWithActionText:(nullable NSString *)actionText actionURL:(NSURL *)actionURL; @end diff --git a/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m index 8fcb2d5c78b..606971e8c9b 100644 --- a/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m +++ b/Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m @@ -347,10 +347,12 @@ - (void)internalStartRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings { activityLogger:self.activityLogger analyticsEventLogger:analyticsEventLogger]; - // Setting the display component. It's needed in case headless SDK is initialized after - // the display component is already set on FIRInAppMessaging. + // Setting the message display component and suppression. It's needed in case + // headless SDK is initialized after the these properties are already set on FIRInAppMessaging. self.displayExecutor.messageDisplayComponent = FIRInAppMessaging.inAppMessaging.messageDisplayComponent; + self.displayExecutor.suppressMessageDisplay = + FIRInAppMessaging.inAppMessaging.messageDisplaySuppressed; // Both display flows are created on startup. But they would only be turned on (started) based on // the sdk mode for the current instance diff --git a/Firebase/InstanceID/CHANGELOG.md b/Firebase/InstanceID/CHANGELOG.md index f77674035e6..6034233c372 100644 --- a/Firebase/InstanceID/CHANGELOG.md +++ b/Firebase/InstanceID/CHANGELOG.md @@ -1,3 +1,9 @@ +# 2019-09 -- 4.2.5 +- [fixed] Fix private header imports (#3796). + +# 2019-09 -- 4.2.4 +- [changed] Moved two headers from internal to private for Remote Config open sourcing (#3621). + # 2019-08 -- 4.2.3 - [fixed] Fixed a crash that could occur if InstanceID was shut down when fetching a new instance ID (#3439). diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h index 8b109a5d362..d2f286d797a 100644 --- a/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h +++ b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences+Internal.h @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "Private/FIRInstanceIDCheckinPreferences.h" +#import @interface FIRInstanceIDCheckinPreferences (Internal) diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinPreferences_Private.h b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences_Private.h index 6e918bc79b2..9c5850ba4ad 100644 --- a/Firebase/InstanceID/FIRInstanceIDCheckinPreferences_Private.h +++ b/Firebase/InstanceID/FIRInstanceIDCheckinPreferences_Private.h @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "Private/FIRInstanceIDCheckinPreferences.h" +#import /** Checkin refresh interval. **/ FOUNDATION_EXPORT const NSTimeInterval kFIRInstanceIDDefaultCheckinInterval; diff --git a/Firebase/InstanceID/FIRInstanceIDCheckinService.h b/Firebase/InstanceID/FIRInstanceIDCheckinService.h index cc97e4700a3..e14b51cb5dd 100644 --- a/Firebase/InstanceID/FIRInstanceIDCheckinService.h +++ b/Firebase/InstanceID/FIRInstanceIDCheckinService.h @@ -16,6 +16,7 @@ #import +#import #import "FIRInstanceIDUtilities.h" NS_ASSUME_NONNULL_BEGIN @@ -31,20 +32,6 @@ FOUNDATION_EXPORT NSString *const kFIRInstanceIDDeviceDataVersionKey; @class FIRInstanceIDCheckinPreferences; -/** - * @related FIRInstanceIDCheckinService - * - * The completion handler invoked once the fetch from Checkin server finishes. - * For successful fetches we returned checkin information by the checkin service - * and `nil` error, else we return the appropriate error object as reported by the - * Checkin Service. - * - * @param checkinPreferences The checkin preferences as fetched from the server. - * @param error The error object which fetching GServices data. - */ -typedef void (^FIRInstanceIDDeviceCheckinCompletion)( - FIRInstanceIDCheckinPreferences *_Nullable checkinPreferences, NSError *_Nullable error); - /** * Register the device with Checkin Service and get back the `authID`, `secret * token` etc. for the client. Checkin results are cached in the diff --git a/Firebase/InstanceID/Private/FIRInstanceID+Private.h b/Firebase/InstanceID/Private/FIRInstanceID+Private.h index d231f26ad81..31d41230530 100644 --- a/Firebase/InstanceID/Private/FIRInstanceID+Private.h +++ b/Firebase/InstanceID/Private/FIRInstanceID+Private.h @@ -14,9 +14,22 @@ * limitations under the License. */ -#import "FIRInstanceID.h" +#import +#import -#import "FIRInstanceIDCheckinService.h" +/** + * @related FIRInstanceIDCheckinService + * + * The completion handler invoked once the fetch from Checkin server finishes. + * For successful fetches we returned checkin information by the checkin service + * and `nil` error, else we return the appropriate error object as reported by the + * Checkin Service. + * + * @param checkinPreferences The checkin preferences as fetched from the server. + * @param error The error object which fetching GServices data. + */ +typedef void (^FIRInstanceIDDeviceCheckinCompletion)( + FIRInstanceIDCheckinPreferences *_Nullable checkinPreferences, NSError *_Nullable error); /** * Private API used by Firebase SDK teams by calling in reflection or internal teams. diff --git a/Firebase/Messaging/CHANGELOG.md b/Firebase/Messaging/CHANGELOG.md index c148c636eef..79614d586a8 100644 --- a/Firebase/Messaging/CHANGELOG.md +++ b/Firebase/Messaging/CHANGELOG.md @@ -1,13 +1,20 @@ +# 2019-09 -- v4.1.5 +- [fixed] Mute FCM deprecated warnings with Xcode 11 and min iOS >= 10. (#3857) + +# 2019-09-03 -- v4.1.4 +- [fixed] Fixed notification open event is not logged when scheduling a local timezone message. (#3670, #3638) +- [fixed] Fixed FirebaseApp.delete() results in unusable Messaging singleton. (#3411) + # 2019-08-20 -- v4.1.3 - [changed] Cleaned up the documents, unused macros, and folders. (#3490, #3537, #3556, #3498) - [changed] Updated the header path to pod repo relative. (#3527) -- [fixed] Fix singleton functionality after a FirebaseApp is deleted and recreated. (#3411) +- [fixed] Fixed singleton functionality after a FirebaseApp is deleted and recreated. (#3411) # 2019-08-08 -- v4.1.2 -- [fixed] Fix hang when token is not available before topic subscription and unsubscription. (#3438) +- [fixed] Fixed hang when token is not available before topic subscription and unsubscription. (#3438) # 2019-07-18 -- v4.1.1 -- [fixed] Fix Xcode 11 tvOS build issue - (#3216) +- [fixed] Fixed Xcode 11 tvOS build issue - (#3216) # 2019-06-18 -- v4.1.0 - [feature] Adding macOS support for Messaging. You can now send push notification to your mac app with Firebase Messaging.(#2880) @@ -16,7 +23,7 @@ - [fixed] Disable data protection when opening the Rmq2PeristentStore. (#2963) # 2019-05-21 -- v4.0.1 -- [fixed] Fix race condition checkin is deleted before writing during app start. This cleans up the corrupted checkin and fixes #2438. (#2860) +- [fixed] Fixed race condition checkin is deleted before writing during app start. This cleans up the corrupted checkin and fixes #2438. (#2860) - [fixed] Separete APNS proxy methods in GULAppDelegateSwizzler so developers don't need to swizzle APNS related method unless explicitly requested, this fixes #2807. (#2835) - [changed] Clean up code. Remove extra layer of class. (#2853) diff --git a/Firebase/Messaging/FIRMessaging.m b/Firebase/Messaging/FIRMessaging.m index 67bfe2f38d4..d560b54599f 100644 --- a/Firebase/Messaging/FIRMessaging.m +++ b/Firebase/Messaging/FIRMessaging.m @@ -86,6 +86,20 @@ NSString *const kFIRMessagingPlistAutoInitEnabled = @"FirebaseMessagingAutoInitEnabled"; // Auto Init Enabled key stored in Info.plist +const BOOL FIRMessagingIsAPNSSyncMessage(NSDictionary *message) { + if ([message[kFIRMessagingMessageViaAPNSRootKey] isKindOfClass:[NSDictionary class]]) { + NSDictionary *aps = message[kFIRMessagingMessageViaAPNSRootKey]; + if (aps && [aps isKindOfClass:[NSDictionary class]]) { + return [aps[kFIRMessagingMessageAPNSContentAvailableKey] boolValue]; + } + } + return NO; +} + + BOOL FIRMessagingIsContextManagerMessage(NSDictionary *message) { + return [FIRMessagingContextManagerService isContextManagerMessage:message]; +} + @interface FIRMessagingMessageInfo () @property(nonatomic, readwrite, assign) FIRMessagingMessageStatus status; @@ -384,21 +398,25 @@ - (FIRMessagingMessageInfo *)appDidReceiveMessage:(NSDictionary *)message { // the message to the device. BOOL isOldMessage = NO; NSString *messageID = message[kFIRMessagingMessageIDKey]; - if ([messageID length]) { + if (messageID.length) { [self.rmq2Manager saveS2dMessageWithRmqId:messageID]; - BOOL isSyncMessage = [[self class] isAPNSSyncMessage:message]; + BOOL isSyncMessage = FIRMessagingIsAPNSSyncMessage(message); if (isSyncMessage) { isOldMessage = [self.syncMessageManager didReceiveAPNSSyncMessage:message]; } - } - // Prevent duplicates by keeping a cache of all the logged messages during each session. - // The duplicates only happen when the 3P app calls `appDidReceiveMessage:` along with - // us swizzling their implementation to call the same method implicitly. - if (!isOldMessage && messageID.length) { - isOldMessage = [self.loggedMessageIDs containsObject:messageID]; - if (!isOldMessage) { - [self.loggedMessageIDs addObject:messageID]; + + // Prevent duplicates by keeping a cache of all the logged messages during each session. + // The duplicates only happen when the 3P app calls `appDidReceiveMessage:` along with + // us swizzling their implementation to call the same method implicitly. + // We need to rule out the contextual message because it shares the same message ID + // as the local notification it will schedule. And because it is also a APNSSync message + // its duplication is already checked previously. + if (!isOldMessage && !FIRMessagingIsContextManagerMessage(message)) { + isOldMessage = [self.loggedMessageIDs containsObject:messageID]; + if (!isOldMessage) { + [self.loggedMessageIDs addObject:messageID]; + } } } @@ -411,20 +429,12 @@ - (FIRMessagingMessageInfo *)appDidReceiveMessage:(NSDictionary *)message { } - (BOOL)handleContextManagerMessage:(NSDictionary *)message { - if ([FIRMessagingContextManagerService isContextManagerMessage:message]) { + if (FIRMessagingIsContextManagerMessage(message)) { return [FIRMessagingContextManagerService handleContextManagerMessage:message]; } return NO; } -+ (BOOL)isAPNSSyncMessage:(NSDictionary *)message { - if ([message[kFIRMessagingMessageViaAPNSRootKey] isKindOfClass:[NSDictionary class]]) { - NSDictionary *aps = message[kFIRMessagingMessageViaAPNSRootKey]; - return [aps[kFIRMessagingMessageAPNSContentAvailableKey] boolValue]; - } - return NO; -} - - (void)handleIncomingLinkIfNeededFromMessage:(NSDictionary *)message { #if TARGET_OS_IOS || TARGET_OS_TV NSURL *url = [self linkURLFromMessage:message]; @@ -864,7 +874,7 @@ - (void)receiver:(FIRMessagingReceiver *)receiver #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" [self.delegate messaging:self didReceiveMessage:remoteMessage]; -#pragma pop +#pragma clang diagnostic pop } else { // Delegate methods weren't implemented, so messages are being dropped, log a warning FIRMessagingLoggerWarn(kFIRMessagingMessageCodeRemoteMessageDelegateMethodNotImplemented, diff --git a/Firebase/Messaging/FIRMessagingContextManagerService.m b/Firebase/Messaging/FIRMessagingContextManagerService.m index 7a313c4283b..6975c2a23a9 100644 --- a/Firebase/Messaging/FIRMessagingContextManagerService.m +++ b/Firebase/Messaging/FIRMessagingContextManagerService.m @@ -44,6 +44,7 @@ NSString *const kFIRMessagingContextManagerSoundKey = kFIRMessagingContextManagerNotificationKeyPrefix @"sound"; NSString *const kFIRMessagingContextManagerContentAvailableKey = kFIRMessagingContextManagerNotificationKeyPrefix @"content-available"; +static NSString *const kFIRMessagingAPNSPayloadKey = @"aps"; typedef NS_ENUM(NSUInteger, FIRMessagingContextManagerMessageType) { FIRMessagingContextManagerMessageTypeNone, @@ -148,7 +149,7 @@ + (void)scheduleLocalNotificationForMessage:(NSDictionary *)message #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" notification.alertTitle = apsDictionary[kFIRMessagingContextManagerTitleKey]; -#pragma pop +#pragma clang diagnostic pop } } @@ -174,7 +175,10 @@ + (void)scheduleLocalNotificationForMessage:(NSDictionary *)message if (!application) { return; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [application scheduleLocalNotification:notification]; +#pragma clang diagnostic pop #endif } @@ -187,6 +191,11 @@ + (NSDictionary *)parseDataFromMessage:(NSDictionary *)message { continue; } else if ([keyString hasPrefix:kContextManagerPrefixKey]) { continue; + } else if ([keyString isEqualToString:kFIRMessagingAPNSPayloadKey]) { + // Local timezone message is scheduled with FCM payload. APNS payload with + // content_available should be ignored and not passed to the scheduled + // messages. + continue; } } data[[key copy]] = message[key]; diff --git a/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m index f95dd0c483e..7ed10fc2db1 100644 --- a/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m +++ b/Firebase/Messaging/FIRMessagingRmq2PersistentStore.m @@ -106,7 +106,7 @@ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" const char *errorStr = sqlite3_errstr(result); -#pragma pop +#pragma clang diagnostic pop NSString *errorString = [NSString stringWithFormat:@"%d - %s", result, errorStr]; return errorString; } diff --git a/Firebase/Storage/CHANGELOG.md b/Firebase/Storage/CHANGELOG.md index cf7086c99a3..0e84fc5edd1 100644 --- a/Firebase/Storage/CHANGELOG.md +++ b/Firebase/Storage/CHANGELOG.md @@ -1,3 +1,6 @@ +# 3.4.1 +- [fixed] Fix crash in FIRStorageUploadTask (#3750). + # 3.4.0 - [fixed] Ensure that users don't accidently invoke `Storage()` instead of `Storage.storage()`. If your code calls the constructor of Storage directly, we will throw an assertion failure, diff --git a/Firebase/Storage/FIRStorageUploadTask.m b/Firebase/Storage/FIRStorageUploadTask.m index 69b837c21aa..0dfdffb8a17 100644 --- a/Firebase/Storage/FIRStorageUploadTask.m +++ b/Firebase/Storage/FIRStorageUploadTask.m @@ -141,8 +141,6 @@ - (void)enqueue { // Process fetches strongSelf.state = FIRStorageTaskStateRunning; -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Warc-retain-cycles" strongSelf->_fetcherCompletion = ^(NSData *_Nullable data, NSError *_Nullable error) { // Fire last progress updates [self fireHandlersForStatus:FIRStorageTaskStatusProgress snapshot:self.snapshot]; @@ -172,11 +170,12 @@ - (void)enqueue { [self finishTaskWithStatus:FIRStorageTaskStatusSuccess snapshot:self.snapshot]; }; -#pragma clang diagnostic pop [strongSelf->_uploadFetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) { - weakSelf.fetcherCompletion(data, error); + if (weakSelf.fetcherCompletion != nil) { + weakSelf.fetcherCompletion(data, error); + } }]; }]; } diff --git a/FirebaseABTesting.podspec b/FirebaseABTesting.podspec index 36a4d567692..f1f1bb0ae55 100644 --- a/FirebaseABTesting.podspec +++ b/FirebaseABTesting.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseABTesting' - s.version = '3.1.0' + s.version = '3.1.1' s.summary = 'Firebase ABTesting for iOS' s.description = <<-DESC @@ -31,7 +31,8 @@ Firebase Cloud Messaging and Firebase Remote Config in your app. base_dir = "FirebaseABTesting/Sources/" s.source_files = base_dir + '**/*.[mh]' s.requires_arc = base_dir + '*.m' - s.public_header_files = base_dir + 'Public/*.h' + s.public_header_files = base_dir + 'Public/*.h', base_dir + 'Protos/developers/mobile/abt/proto/*.h' + s.private_header_files = base_dir + 'Protos/developers/mobile/abt/proto/*.h' s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', 'GCC_PREPROCESSOR_DEFINITIONS' => @@ -41,7 +42,7 @@ Firebase Cloud Messaging and Firebase Remote Config in your app. } s.dependency 'FirebaseAnalyticsInterop', '~> 1.3' s.dependency 'FirebaseCore', '~> 6.1' - s.dependency 'Protobuf', '~> 3.8' + s.dependency 'Protobuf', '~> 3.9', '>= 3.9.2' s.test_spec 'unit' do |unit_tests| unit_tests.source_files = 'FirebaseABTesting/Tests/Unit/*.[mh]' diff --git a/FirebaseABTesting/CHANGELOG.md b/FirebaseABTesting/CHANGELOG.md index 1886ed24e8f..91dd7ada442 100644 --- a/FirebaseABTesting/CHANGELOG.md +++ b/FirebaseABTesting/CHANGELOG.md @@ -1,2 +1,5 @@ +# v3.1.1 +- [fixed] Fixed an Analyzer issue (#3622). + # v3.1.0 - [added] Initial Open Source (#3507). diff --git a/FirebaseABTesting/Sources/FIRExperimentController.m b/FirebaseABTesting/Sources/FIRExperimentController.m index a0bb5d1ca89..21a3fbd2ffd 100644 --- a/FirebaseABTesting/Sources/FIRExperimentController.m +++ b/FirebaseABTesting/Sources/FIRExperimentController.m @@ -246,7 +246,7 @@ - (void)updateExperimentsWithServiceOrigin:(NSString *)origin - (NSTimeInterval)latestExperimentStartTimestampBetweenTimestamp:(NSTimeInterval)timestamp andPayloads:(NSArray *)payloads { - for (NSData *payload in payloads) { + for (NSData *payload in [payloads copy]) { ABTExperimentPayload *experimentPayload = ABTDeserializeExperimentPayload(payload); if (!experimentPayload) { FIRLogInfo(kFIRLoggerABTesting, @"I-ABT000002", diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index f06a11f8f76..39548400003 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseAuth' - s.version = '6.2.2' + s.version = '6.2.3' s.summary = 'The official iOS client for Firebase Authentication (plus community support for macOS and tvOS)' s.description = <<-DESC diff --git a/FirebaseCore.podspec b/FirebaseCore.podspec index afc661b4aba..f31e9291478 100644 --- a/FirebaseCore.podspec +++ b/FirebaseCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCore' - s.version = '6.2.0' + s.version = '6.3.0' s.summary = 'Firebase Core for iOS (plus community support for macOS and tvOS)' s.description = <<-DESC @@ -39,7 +39,7 @@ Firebase Core includes FIRApp and FIROptions which provide central configuration s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', 'GCC_PREPROCESSOR_DEFINITIONS' => - 'FIRCore_VERSION=' + s.version.to_s + ' Firebase_VERSION=6.6.0', + 'FIRCore_VERSION=' + s.version.to_s + ' Firebase_VERSION=6.9.0', 'OTHER_CFLAGS' => '-fno-autolink' } s.test_spec 'unit' do |unit_tests| diff --git a/FirebaseCoreDiagnostics.podspec b/FirebaseCoreDiagnostics.podspec index a8f6b8fbcfe..b74f15ddd58 100644 --- a/FirebaseCoreDiagnostics.podspec +++ b/FirebaseCoreDiagnostics.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseCoreDiagnostics' - s.version = '1.0.1' + s.version = '1.1.0' s.summary = 'Firebase Core Diagnostics' s.description = <<-DESC @@ -48,6 +48,7 @@ non-Cocoapod integration. This library also respects the Firebase global data co s.dependency 'GoogleDataTransportCCTSupport', '~> 1.0' s.dependency 'GoogleUtilities/Environment', '~> 6.2' s.dependency 'GoogleUtilities/Logger', '~> 6.2' + s.dependency 'nanopb', '~> 0.3.901' s.test_spec 'unit' do |unit_tests| unit_tests.dependency 'GoogleUtilities/UserDefaults', '~> 6.2' diff --git a/FirebaseDatabase.podspec b/FirebaseDatabase.podspec index 048322653ba..2ab1b9a3058 100644 --- a/FirebaseDatabase.podspec +++ b/FirebaseDatabase.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDatabase' - s.version = '6.0.0' + s.version = '6.1.0' s.summary = 'Firebase Open Source Libraries for iOS (plus community support for macOS and tvOS)' s.description = <<-DESC @@ -31,7 +31,7 @@ Simplify your iOS development, grow your user base, and monetize more effectivel s.public_header_files = base_dir + 'Public/*.h' s.libraries = ['c++', 'icucore'] s.frameworks = 'CFNetwork', 'Security', 'SystemConfiguration' - s.dependency 'leveldb-library', '~> 1.18' + s.dependency 'leveldb-library', '~> 1.22' s.dependency 'FirebaseAuthInterop', '~> 1.0' s.dependency 'FirebaseCore', '~> 6.0' s.pod_target_xcconfig = { diff --git a/FirebaseDynamicLinks.podspec b/FirebaseDynamicLinks.podspec index 9940683eb16..9deb12818ee 100644 --- a/FirebaseDynamicLinks.podspec +++ b/FirebaseDynamicLinks.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseDynamicLinks' - s.version = '4.0.2' + s.version = '4.0.5' s.summary = 'Firebase DynamicLinks for iOS' s.description = <<-DESC diff --git a/FirebaseFirestore.podspec b/FirebaseFirestore.podspec index 6f2e09321ce..10a7e585fd4 100644 --- a/FirebaseFirestore.podspec +++ b/FirebaseFirestore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestore' - s.version = '1.4.3' + s.version = '1.5.1' s.summary = 'Google Cloud Firestore for iOS' s.description = <<-DESC @@ -50,8 +50,8 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, s.dependency 'FirebaseAuthInterop', '~> 1.0' s.dependency 'FirebaseCore', '~> 6.2' s.dependency 'gRPC-C++', '0.0.9' - s.dependency 'leveldb-library', '~> 1.20' - s.dependency 'Protobuf', '~> 3.1' + s.dependency 'leveldb-library', '~> 1.22' + s.dependency 'Protobuf', '~> 3.9', '>= 3.9.2' s.dependency 'nanopb', '~> 0.3.901' s.ios.frameworks = 'MobileCoreServices', 'SystemConfiguration' diff --git a/FirebaseFirestoreSwift.podspec b/FirebaseFirestoreSwift.podspec index 048d8c7e3ab..0fca97e0e8c 100644 --- a/FirebaseFirestoreSwift.podspec +++ b/FirebaseFirestoreSwift.podspec @@ -33,6 +33,7 @@ Google Cloud Firestore is a NoSQL document database built for automatic scaling, s.requires_arc = true s.source_files = [ 'Firestore/Swift/Source/**/*.swift', + 'Firestore/third_party/FirestoreEncoder/*.swift', ] s.dependency 'FirebaseFirestore', ">= 0.10.0" diff --git a/FirebaseInAppMessaging.podspec b/FirebaseInAppMessaging.podspec index 390a79c1aef..398fcad7c88 100644 --- a/FirebaseInAppMessaging.podspec +++ b/FirebaseInAppMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInAppMessaging' - s.version = '0.15.3' + s.version = '0.15.4' s.summary = 'Firebase In-App Messaging for iOS' s.description = <<-DESC diff --git a/FirebaseInAppMessagingDisplay.podspec b/FirebaseInAppMessagingDisplay.podspec index ef1a934b2c3..b8ff63cee13 100644 --- a/FirebaseInAppMessagingDisplay.podspec +++ b/FirebaseInAppMessagingDisplay.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInAppMessagingDisplay' - s.version = '0.15.3' + s.version = '0.15.4' s.summary = 'Firebase In-App Messaging UI for iOS' s.description = <<-DESC diff --git a/FirebaseInstallations.podspec b/FirebaseInstallations.podspec index e7fa5a2a356..ded95253345 100644 --- a/FirebaseInstallations.podspec +++ b/FirebaseInstallations.podspec @@ -49,6 +49,10 @@ Pod::Spec.new do |s| s.test_spec 'integration' do |int_tests| int_tests.source_files = base_dir + 'Tests/Integration/**/*.[mh]' int_tests.resources = base_dir + 'Tests/Resources/**/*' + s.pod_target_xcconfig = { + 'GCC_PREPROCESSOR_DEFINITIONS' => + 'FIR_INSTALLATIONS_INTEGRATION_TESTS_REQUIRED=1' + } int_tests.requires_app_host = true int_tests.dependency 'OCMock' end diff --git a/FirebaseInstallations/Source/Tests/Integration/FIRInstallationsIntegrationTests.m b/FirebaseInstallations/Source/Tests/Integration/FIRInstallationsIntegrationTests.m index e3449b8739a..21ed7e746aa 100644 --- a/FirebaseInstallations/Source/Tests/Integration/FIRInstallationsIntegrationTests.m +++ b/FirebaseInstallations/Source/Tests/Integration/FIRInstallationsIntegrationTests.m @@ -14,10 +14,8 @@ * limitations under the License. */ -// Uncomment to enable integration tests. -//#define FIR_INSTALLATIONS_INTEGRATION_TESTS_ENABLED 1 - -#ifdef FIR_INSTALLATIONS_INTEGRATION_TESTS_ENABLED +// Uncomment or set the flag in GCC_PREPROCESSOR_DEFINITIONS to enable integration tests. +//#define FIR_INSTALLATIONS_INTEGRATION_TESTS_REQUIRED 1 #import @@ -31,6 +29,8 @@ #import #import +static BOOL sFIRInstallationsFirebaseDefaultAppConfigured = NO; + @interface FIRInstallationsIntegrationTests : XCTestCase @property(nonatomic) FIRInstallations *installations; @end @@ -38,10 +38,11 @@ @interface FIRInstallationsIntegrationTests : XCTestCase @implementation FIRInstallationsIntegrationTests - (void)setUp { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [FIRApp configure]; - }); + [self configureFirebaseDefaultAppIfCan]; + + if (![self isDefaultAppConfigured]) { + return; + } self.installations = [FIRInstallations installationsWithApp:[FIRApp defaultApp]]; } @@ -54,19 +55,29 @@ - (void)tearDown { // Wait for any pending background job to be completed. FBLWaitForPromisesWithTimeout(10); + + [FIRApp resetApps]; } // TODO: Enable the test once Travis configured. // Need to configure the GoogleService-Info.plist copying from the encrypted archive. // So far, let's run the tests locally. -- (void)disabled_testGetFID { +- (void)testGetFID { + if (![self isDefaultAppConfigured]) { + return; + } + NSString *FID1 = [self getFID]; NSString *FID2 = [self getFID]; XCTAssertEqualObjects(FID1, FID2); } -- (void)disabled_testAuthToken { +- (void)testAuthToken { + if (![self isDefaultAppConfigured]) { + return; + } + XCTestExpectation *authTokenExpectation = [self expectationWithDescription:@"authTokenExpectation"]; @@ -84,7 +95,11 @@ - (void)disabled_testAuthToken { [self waitForExpectations:@[ authTokenExpectation ] timeout:2]; } -- (void)disabled_testDeleteInstallation { +- (void)testDeleteInstallation { + if (![self isDefaultAppConfigured]) { + return; + } + NSString *FIDBefore = [self getFID]; FIRInstallationsAuthTokenResult *authTokenBefore = [self getAuthToken]; @@ -111,11 +126,13 @@ - (void)testInstallationsWithApp { // Wait for finishing all background operations. FBLWaitForPromisesWithTimeout(10); - - [FIRApp resetApps]; } - (void)testDefaultAppInstallation { + if (![self isDefaultAppConfigured]) { + return; + } + XCTAssertNotNil(self.installations); XCTAssertEqualObjects(self.installations.appOptions.googleAppID, [FIRApp defaultApp].options.googleAppID); @@ -123,8 +140,6 @@ - (void)testDefaultAppInstallation { // Wait for finishing all background operations. FBLWaitForPromisesWithTimeout(10); - - [FIRApp resetApps]; } #endif // !TARGET_OS_OSX @@ -197,6 +212,30 @@ - (FIRApp *)createAndConfigureAppWithName:(NSString *)name { return [FIRApp appNamed:name]; } -@end +- (void)configureFirebaseDefaultAppIfCan { + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSString *plistPath = [bundle pathForResource:@"GoogleService-Info" ofType:@"plist"]; + if (plistPath == nil) { + return; + } -#endif // FIR_INSTALLATIONS_INTEGRATION_TESTS_ENABLED + FIROptions *options = [[FIROptions alloc] initWithContentsOfFile:plistPath]; + [FIRApp configureWithOptions:options]; + sFIRInstallationsFirebaseDefaultAppConfigured = YES; +} + +- (BOOL)isDefaultAppConfigured { + if (!sFIRInstallationsFirebaseDefaultAppConfigured) { +#if FIR_INSTALLATIONS_INTEGRATION_TESTS_REQUIRED + XCTFail(@"GoogleService-Info.plist for integration tests was not found. Please add the file to " + @"your project."); +#else + NSLog(@"GoogleService-Info.plist for integration tests was not found. Skipping the test %@", + self.name); +#endif // FIR_INSTALLATIONS_INTEGRATION_TESTS_REQUIRED + } + + return sFIRInstallationsFirebaseDefaultAppConfigured; +} + +@end diff --git a/FirebaseInstallations/Source/Tests/Resources/GoogleService-Info.plist b/FirebaseInstallations/Source/Tests/Resources/GoogleService-Info.plist deleted file mode 100644 index 3f7547fb48d..00000000000 --- a/FirebaseInstallations/Source/Tests/Resources/GoogleService-Info.plist +++ /dev/null @@ -1,28 +0,0 @@ - - - - - API_KEY - correct_api_key - TRACKING_ID - correct_tracking_id - CLIENT_ID - correct_client_id - REVERSED_CLIENT_ID - correct_reversed_client_id - GOOGLE_APP_ID - 1:123:ios:123abc - GCM_SENDER_ID - correct_gcm_sender_id - PLIST_VERSION - 1 - BUNDLE_ID - com.google.FirebaseSDKTests - PROJECT_ID - abc-xyz-123 - DATABASE_URL - https://abc-xyz-123.firebaseio.com - STORAGE_BUCKET - project-id-123.storage.firebase.com - - diff --git a/FirebaseInstallations/Source/Tests/Unit/FIRInstallationsAPIServiceTests.m b/FirebaseInstallations/Source/Tests/Unit/FIRInstallationsAPIServiceTests.m index c2bfe2f554a..61a23591b95 100644 --- a/FirebaseInstallations/Source/Tests/Unit/FIRInstallationsAPIServiceTests.m +++ b/FirebaseInstallations/Source/Tests/Unit/FIRInstallationsAPIServiceTests.m @@ -436,9 +436,9 @@ - (id)refreshTokenRequestValidationArgWithInstallation:(FIRInstallationsItem *)i error:&error]; XCTAssertNotNil(body, @"Error: %@, test: %@", error, self.name); - XCTAssertEqualObjects(body, - @{@"installation" : @{@"sdkVersion" : [self SDKVersion]}}, @"%@", - self.name); + XCTAssertEqualObjects( + body, + @{@"installation" : @{@"sdkVersion" : [self SDKVersion]}}, @"%@", self.name); return YES; }]; diff --git a/FirebaseInstanceID.podspec b/FirebaseInstanceID.podspec index d2641969558..66ac732d274 100644 --- a/FirebaseInstanceID.podspec +++ b/FirebaseInstanceID.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseInstanceID' - s.version = '4.2.3' + s.version = '4.2.5' s.summary = 'Firebase InstanceID for iOS' s.description = <<-DESC diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index 8c56654bb34..1f31cfaf5c0 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseMessaging' - s.version = '4.1.2' + s.version = '4.1.5' s.summary = 'Firebase Messaging for iOS' s.description = <<-DESC @@ -49,7 +49,7 @@ device, and it is completely free. s.dependency 'GoogleUtilities/Reachability', '~> 6.2' s.dependency 'GoogleUtilities/Environment', '~> 6.2' s.dependency 'GoogleUtilities/UserDefaults', '~> 6.2' - s.dependency 'Protobuf', '~> 3.1' + s.dependency 'Protobuf', '~> 3.9', '>= 3.9.2' s.test_spec 'unit' do |unit_tests| unit_tests.source_files = 'Example/Messaging/Tests/*.{m,h,swift}' diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec new file mode 100644 index 00000000000..c3f760efda6 --- /dev/null +++ b/FirebaseRemoteConfig.podspec @@ -0,0 +1,72 @@ +Pod::Spec.new do |s| + s.name = 'FirebaseRemoteConfig' + s.version = '4.4.1' + s.summary = 'Firebase RemoteConfig for iOS' + + s.description = <<-DESC +Firebase Remote Config is a cloud service that lets you change the +appearance and behavior of your app without requiring users to download an +app update. + DESC + + s.homepage = 'https://firebase.google.com' + s.license = { :type => 'Apache', :file => 'LICENSE' } + s.authors = 'Google, Inc.' + + s.source = { + :git => 'https://github.com/firebase/firebase-ios-sdk.git', + :tag => 'RemoteConfig-' + s.version.to_s + } + s.social_media_url = 'https://twitter.com/Firebase' + s.ios.deployment_target = '8.0' + s.osx.deployment_target = '10.11' + s.tvos.deployment_target = '10.0' + + s.cocoapods_version = '>= 1.4.0' + s.static_framework = true + s.prefix_header_file = false + + base_dir = "FirebaseRemoteConfig/Sources/" + s.source_files = base_dir + '**/*.[mh]' + s.requires_arc = base_dir + '*.m' + s.public_header_files = base_dir + 'Public/*.h' + s.private_header_files = base_dir + 'Private/*.h' + s.pod_target_xcconfig = { + 'GCC_C_LANGUAGE_STANDARD' => 'c99', + 'GCC_PREPROCESSOR_DEFINITIONS' => + 'GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS=1 ' + + 'FIRRemoteConfig_VERSION=' + String(s.version), + 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"' + } + s.dependency 'FirebaseAnalyticsInterop', '~> 1.4' + s.dependency 'FirebaseABTesting', '~> 3.1' + s.dependency 'FirebaseCore', '~> 6.2' + s.dependency 'FirebaseInstanceID', '~> 4.2' + s.dependency 'GoogleUtilities/Environment', '~> 6.2' + s.dependency 'GoogleUtilities/NSData+zlib', '~> 6.2' + s.dependency 'Protobuf', '~> 3.9', '>= 3.9.2' + + s.test_spec 'unit' do |unit_tests| + # TODO(dmandar) - Update or delete the commented files. + unit_tests.source_files = + 'FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m', + 'FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m', + 'FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m', +# 'FirebaseRemoteConfig/Tests/Unit/RCNConfigSettingsTest.m', +# 'FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m', + 'FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m', + 'FirebaseRemoteConfig/Tests/Unit/RCNConfigValueTest.m', +# 'FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfig+FIRAppTest.m', + 'FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m', +# 'FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m', + 'FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.m', + 'FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m', + 'FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h' + # Supply plist custom plist testing. + unit_tests.resources = + 'FirebaseRemoteConfig/Tests/Unit/Defaults-testInfo.plist', + 'FirebaseRemoteConfig/Tests/Unit/SecondApp-GoogleService-Info.plist' + unit_tests.requires_app_host = true + unit_tests.dependency 'OCMock' + end +end diff --git a/FirebaseRemoteConfig/CHANGELOG.md b/FirebaseRemoteConfig/CHANGELOG.md new file mode 100644 index 00000000000..48c7bd3cf0f --- /dev/null +++ b/FirebaseRemoteConfig/CHANGELOG.md @@ -0,0 +1,131 @@ +Version 4.4.1 +================================== +- Fix docs issue. (#3846) + +Version 4.3.0 +================================== +- Open source. (TBD) +- Community macOS (#1674) and tvOS support. +- Catalyst build support. + +Version 4.2.2 +================================== +- Bug fix for a crash seen by some users (#3508) +- Internal changes and stability improvements. + +Version 4.2.1 +================================== +- Bug fix for a crash seen by some users. (#3344) + +Version 4.2.0 +================================== +- Improved shared instance initialization sequence during 'FirebaseApp.configure()'. + +Version 4.1.0 +================================== +- Async initialization with new API for ensuring initialization completed with completion handler. +- Support for multiple active instances of Remote Config in the same app (Analytics only supported with default Firebase app instance). + - All Remote Config API with explicit namespace are deprecated. +- New fetchAndActivate API to perform both fetch and activation upon a successful fetch in a single API call with async completion. +- New property in the FIRRemoteConfigValue class for reading value of a param as a jsonValue. +- developerModeEnabled is now deprecated. Use minimumFetchInterval or call fetchWithExpirationDuration: to force a fetch to the Remote Config backend. +- New config settings for minimumFetchInterval and fetch timeout. +- Async activate API with completion handler. + +Version 4.0.0 +================================== +- FirebaseAnalytics is no longer a hard dependency in the RemoteConfig pod. If you were installing Remote Config via pod ''Firebase/RemoteConfig'', you should add 'pod 'Firebase/Analytics'' to the Podfile to maintain full RemoteConfig functionality. If you previously have 'pod 'Firebase/Core'' in the Podfile, no change is necessary. No major changes to functionality. + +Version 3.1.0 +================================== +- Internal changes to support the new version of Firebase Performance SDK. + +Version 3.0.2 +================================== +- Bug fixes. + +Version 3.0.1 +================================== +- Bug fix for a memory leak bug. (#488) + + +Version 3.0.0 +================================== +- Change the designated initializer for FIRRemoteConfigSettings to return a nonnull FIRRemoteConfigSettings object. + +Version 2.1.3 +================================== +- Improve documentation on GDPR usage. + +Version 2.1.2 +================================== +- Improve language targeting. Simplied Chinese (zh_hans) and Traditional Chinese (Taiwan) (zh_TW) language targeting should also be more accurate. + +Version 2.1.1 +================================== +- Fix an issue that throttle rate drops during developer mode. +- Replaced FIR_SWIFT_NAME with NS_SWIFT_NAME. + +Version 2.1.0 +================================== +- Add ABTesting feature to allow developers to run experiments using Remote Config. + +Version 2.0.3 +================================== +- Resolved an issue that config values are not updating correctly when targeted by a user property condition. + +Version 2.0.2 +================================== +- Fix an issue that prevent app from crashing when main bundle ID is missing. Also notify developers remote config might not work if main bundle ID is missing. + +Version 2.0.1 +================================== +- Add a warning message if a plist file can't be found when setting default values from it. +- Internal clean up removing code for testing that is no longer used. + +Version 2.0.0 +================================== +- Change Swift API names to better align with Swift convention. +- Change Error message to debug message when getting InstanceID operation is in progress as this is an expected behavior. + +Version 1.3.4 +================================== +- Fix the issue with Remote Config getting an incorrect configuration when user configured multiple projects. +- Fix the issue with existing users getting empty config results. + +Version 1.3.3 +================================== +- Switches to the new Protobuf from ProtocolBuffers2. + +Version 1.3.2 +================================== +Resolved Issues: +- Fix an issue that activateFetched called when app starts will remove cached results. +- Fix an issue that multiple fetches without activateFetched will not get recent changes. + +Version 1.3.1 +================================== +Resolved Issues: +- Better documentation on the public headers. + +Version 1.3.0 +================================== +Features: +- Support user property targeting for analytics abilities. + +Resolved Issues: +- Fix critical crashes due to concurrent fetches, make it more thread safe. + +Version 1.2.0 +================================== +Features: +- Add two new API methods to allow developers to get all the keys based on a key prefix. + +Resolved Issues: +- Fix a crash issue during fetching config. +- Clarify the confusion on the documents of activateFetched method. +- Correct the cast error in the comment of remoteConfig method. + +Version 1.1.1 +================================== +Initial release in Google I/O 2016. diff --git a/FirebaseRemoteConfig/README.md b/FirebaseRemoteConfig/README.md new file mode 100644 index 00000000000..9f001f42017 --- /dev/null +++ b/FirebaseRemoteConfig/README.md @@ -0,0 +1,11 @@ +# Firebase Remote Config SDK for iOS + +This pod contains the Firebase Remote Config SDK for iOS, supporting both +Objective-C and Swift. + +Firebase Remote Config is a cloud service that lets you change the appearance +and behavior of your app without requiring users to download an app update. + +Please visit [our developer site] +(https://firebase.google.com/docs/remote-config/) for integration instructions, +documentation, support information, and terms of service. diff --git a/FirebaseRemoteConfig/Sources/FIRConfigValue.m b/FirebaseRemoteConfig/Sources/FIRConfigValue.m new file mode 100644 index 00000000000..719c3608c9e --- /dev/null +++ b/FirebaseRemoteConfig/Sources/FIRConfigValue.m @@ -0,0 +1,91 @@ +/* + * Copyright 2019 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 + +#import +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" + +@implementation FIRRemoteConfigValue { + /// Data backing the config value. + NSData *_data; + FIRRemoteConfigSource _source; +} + +/// Designated initializer +- (instancetype)initWithData:(NSData *)data source:(FIRRemoteConfigSource)source { + self = [super init]; + if (self) { + _data = [data copy]; + _source = source; + } + return self; +} + +/// Superclass's designated initializer +- (instancetype)init { + return [self initWithData:nil source:FIRRemoteConfigSourceStatic]; +} + +/// The string is a UTF-8 representation of NSData. +- (NSString *)stringValue { + return [[NSString alloc] initWithData:_data encoding:NSUTF8StringEncoding]; +} + +/// Number representation of a UTF-8 string. +- (NSNumber *)numberValue { + return [NSNumber numberWithDouble:self.stringValue.doubleValue]; +} + +/// Internal representation of the FIRRemoteConfigValue as a NSData object. +- (NSData *)dataValue { + return _data; +} + +/// Boolean representation of a UTF-8 string. +- (BOOL)boolValue { + return self.stringValue.boolValue; +} + +/// Returns a foundation object (NSDictionary / NSArray) representation for JSON data. +- (id)JSONValue { + NSError *error; + if (!_data) { + return nil; + } + id JSONObject = [NSJSONSerialization JSONObjectWithData:_data options:kNilOptions error:&error]; + if (error) { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000065", @"Error parsing data as JSON."); + return nil; + } + return JSONObject; +} + +/// Debug description showing the representations of all types. +- (NSString *)debugDescription { + NSString *content = [NSString + stringWithFormat:@"Boolean: %d, String: %@, Number: %@, JSON:%@, Data: %@, Source: %zd", + self.boolValue, self.stringValue, self.numberValue, self.JSONValue, _data, + (long)self.source]; + return [NSString stringWithFormat:@"<%@: %p, %@>", [self class], self, content]; +} + +/// Copy method. +- (id)copyWithZone:(NSZone *)zone { + FIRRemoteConfigValue *value = [[[self class] allocWithZone:zone] initWithData:_data]; + return value; +} +@end diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m new file mode 100644 index 00000000000..afc067de4aa --- /dev/null +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfig.m @@ -0,0 +1,632 @@ +/* + * Copyright 2019 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 + +#import +#import +#import +#import +#import +#import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h" +#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" +#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigFetch.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" +#import "FirebaseRemoteConfig/Sources/RCNDevice.h" + +/// Remote Config Error Domain. +/// TODO: Rename according to obj-c style for constants. +NSString *const FIRRemoteConfigErrorDomain = @"com.google.remoteconfig.ErrorDomain"; +/// Remote Config Error Info End Time Seconds; +NSString *const FIRRemoteConfigThrottledEndTimeInSecondsKey = @"error_throttled_end_time_seconds"; +/// Remote Config Developer Mode Key +static NSString *const kRemoteConfigDeveloperKey = @"_rcn_developer"; +/// Minimum required time interval between fetch requests made to the backend. +static NSString *const kRemoteConfigMinimumFetchIntervalKey = @"_rcn_minimum_fetch_interval"; +/// Timeout value for waiting on a fetch response. +static NSString *const kRemoteConfigFetchTimeoutKey = @"_rcn_fetch_timeout"; + +@interface FIRRemoteConfigSettings () { + BOOL _developerModeEnabled; +} +@end + +// Implementations depend upon multiple deprecated APIs +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@implementation FIRRemoteConfigSettings +- (instancetype)initWithDeveloperModeEnabled:(BOOL)developerModeEnabled { + self = [self init]; + if (self) { + _developerModeEnabled = developerModeEnabled; + } + return self; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _developerModeEnabled = NO; + _minimumFetchInterval = RCNDefaultMinimumFetchInterval; + _fetchTimeout = RCNHTTPDefaultConnectionTimeout; + } + return self; +} + +- (BOOL)isDeveloperModeEnabled { + return _developerModeEnabled; +} + +@end + +@implementation FIRRemoteConfig { + /// All the config content. + RCNConfigContent *_configContent; + RCNConfigDBManager *_DBManager; + RCNConfigSettings *_settings; + RCNConfigFetch *_configFetch; + RCNConfigExperiment *_configExperiment; + dispatch_queue_t _queue; + NSString *_appName; +} + +static NSMutableDictionary *> + *RCInstances; + ++ (nonnull FIRRemoteConfig *)remoteConfigWithApp:(FIRApp *_Nonnull)firebaseApp { + return [FIRRemoteConfig remoteConfigWithFIRNamespace:FIRNamespaceGoogleMobilePlatform + app:firebaseApp]; +} + ++ (nonnull FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *_Nonnull)firebaseNamespace { + if (![FIRApp isDefaultAppConfigured]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000047", + @"FIRApp not configured. Please make sure you have called [FIRApp configure]"); + // TODO: Maybe throw an exception here? That'd be a breaking change though, but at this point + // RC can't work as expected. + } + + return [FIRRemoteConfig remoteConfigWithFIRNamespace:firebaseNamespace app:[FIRApp defaultApp]]; +} + ++ (nonnull FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *_Nonnull)firebaseNamespace + app:(FIRApp *_Nonnull)firebaseApp { + // Use the provider to generate and return instances of FIRRemoteConfig for this specific app and + // namespace. This will ensure the app is configured before Remote Config can return an instance. + id provider = + FIR_COMPONENT(FIRRemoteConfigProvider, firebaseApp.container); + return [provider remoteConfigForNamespace:firebaseNamespace]; +} + ++ (FIRRemoteConfig *)remoteConfig { + // If the default app is not configured at this point, warn the developer. + if (![FIRApp isDefaultAppConfigured]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000047", + @"FIRApp not configured. Please make sure you have called [FIRApp configure]"); + // TODO: Maybe throw an exception here? That'd be a breaking change though, but at this point + // RC can't work as expected. + } + + return [FIRRemoteConfig remoteConfigWithFIRNamespace:FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; +} + +/// Singleton instance of serial queue for queuing all incoming RC calls. ++ (dispatch_queue_t)sharedRemoteConfigSerialQueue { + static dispatch_once_t onceToken; + static dispatch_queue_t sharedRemoteConfigQueue; + dispatch_once(&onceToken, ^{ + sharedRemoteConfigQueue = + dispatch_queue_create(RCNRemoteConfigQueueLabel, DISPATCH_QUEUE_SERIAL); + }); + return sharedRemoteConfigQueue; +} + +/// Designated initializer +- (instancetype)initWithAppName:(NSString *)appName + FIROptions:(FIROptions *)options + namespace:(NSString *)FIRNamespace + DBManager:(RCNConfigDBManager *)DBManager + configContent:(RCNConfigContent *)configContent + analytics:(nullable id)analytics { + self = [super init]; + if (self) { + _appName = appName; + _DBManager = DBManager; + // The fully qualified Firebase namespace is namespace:firappname. + _FIRNamespace = [NSString stringWithFormat:@"%@:%@", FIRNamespace, appName]; + + // Initialize RCConfigContent if not already. + _configContent = configContent; + _settings = [[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager + namespace:_FIRNamespace + firebaseAppName:appName + googleAppID:options.googleAppID]; + + FIRExperimentController *experimentController = [FIRExperimentController sharedInstance]; + _configExperiment = [[RCNConfigExperiment alloc] initWithDBManager:_DBManager + experimentController:experimentController]; + /// Serial queue for read and write lock. + _queue = [FIRRemoteConfig sharedRemoteConfigSerialQueue]; + + // Initialize with default config settings. + [self setDefaultConfigSettings]; + _configFetch = [[RCNConfigFetch alloc] initWithContent:_configContent + DBManager:_DBManager + settings:_settings + analytics:analytics + experiment:_configExperiment + queue:_queue + namespace:_FIRNamespace + options:options]; + + [_settings loadConfigFromMetadataTable]; + } + return self; +} + +// Initialize with default config settings. +- (void)setDefaultConfigSettings { + // Set the default config settings. + self->_settings.fetchTimeout = RCNHTTPDefaultConnectionTimeout; + self->_settings.minimumFetchInterval = RCNDefaultMinimumFetchInterval; +} + +- (void)ensureInitializedWithCompletionHandler: + (nonnull FIRRemoteConfigInitializationCompletion)completionHandler { + __weak FIRRemoteConfig *weakSelf = self; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ + FIRRemoteConfig *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + BOOL initializationSuccess = [self->_configContent initializationSuccessful]; + NSError *error = nil; + if (!initializationSuccess) { + error = [[NSError alloc] + initWithDomain:FIRRemoteConfigErrorDomain + code:FIRRemoteConfigErrorInternalError + userInfo:@{NSLocalizedDescriptionKey : @"Timed out waiting for database load."}]; + } + completionHandler(error); + }); +} + +#pragma mark - fetch + +- (void)fetchWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler { + [self fetchWithExpirationDuration:_settings.minimumFetchInterval + completionHandler:completionHandler]; +} + +- (void)fetchWithExpirationDuration:(NSTimeInterval)expirationDuration + completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler { + FIRRemoteConfigFetchCompletion completionHandlerCopy = nil; + if (completionHandler) { + completionHandlerCopy = [completionHandler copy]; + } + [_configFetch fetchAllConfigsWithExpirationDuration:expirationDuration + completionHandler:completionHandlerCopy]; +} + +#pragma mark - fetchAndActivate + +- (void)fetchAndActivateWithCompletionHandler: + (FIRRemoteConfigFetchAndActivateCompletion)completionHandler { + __weak FIRRemoteConfig *weakSelf = self; + FIRRemoteConfigFetchCompletion fetchCompletion = + ^(FIRRemoteConfigFetchStatus fetchStatus, NSError *error) { + FIRRemoteConfig *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + // Fetch completed. We are being called on the main queue. + // If fetch is successful, try to activate the fetched config + bool didActivate = false; + if (fetchStatus == FIRRemoteConfigFetchStatusSuccess && !error) { + didActivate = [strongSelf activateFetched]; + } + if (completionHandler) { + FIRRemoteConfigFetchAndActivateStatus status = FIRRemoteConfigFetchAndActivateStatusError; + if (fetchStatus == FIRRemoteConfigFetchStatusSuccess) { + status = didActivate ? FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote + : FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData; + } else { + status = FIRRemoteConfigFetchAndActivateStatusError; + } + // Pass along the fetch error e.g. throttled. + completionHandler(status, error); + } + }; + [self fetchWithCompletionHandler:fetchCompletion]; +} + +#pragma mark - apply + +- (BOOL)activateFetched { + // TODO: We block on the async activate to complete. This method is deprecated and needs + // to be removed at the next possible breaking change. + __block dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block BOOL didActivate = NO; + [self activateWithCompletionHandler:^(NSError *_Nullable error) { + didActivate = error ? false : true; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + return didActivate; +} + +- (void)activateWithCompletionHandler:(FIRRemoteConfigActivateCompletion)completionHandler { + __weak FIRRemoteConfig *weakSelf = self; + void (^applyBlock)(void) = ^(void) { + FIRRemoteConfig *strongSelf = weakSelf; + if (!strongSelf) { + NSError *error = [NSError errorWithDomain:FIRRemoteConfigErrorDomain + code:FIRRemoteConfigErrorInternalError + userInfo:@{@"ActivationFailureReason" : @"Internal Error."}]; + if (completionHandler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionHandler(error); + }); + } + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000068", @"Internal error activating config."); + return; + } + // If Fetched Config is no fresher than Active Config. + if (strongSelf->_settings.lastFetchTimeInterval == 0 || + strongSelf->_settings.lastFetchTimeInterval <= + strongSelf->_settings.lastApplyTimeInterval) { + FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000069", + @"Most recently fetched config is already activated."); + NSError *error = [NSError + errorWithDomain:FIRRemoteConfigErrorDomain + code:FIRRemoteConfigErrorInternalError + userInfo:@{ + @"ActivationFailureReason" : @"Most recently fetched config is already activated" + }]; + if (completionHandler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionHandler(error); + }); + } + return; + } + [strongSelf->_configContent copyFromDictionary:self->_configContent.fetchedConfig + toSource:RCNDBSourceActive + forNamespace:self->_FIRNamespace]; + [strongSelf updateExperiments]; + strongSelf->_settings.lastApplyTimeInterval = [[NSDate date] timeIntervalSince1970]; + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000069", @"Config activated."); + if (completionHandler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + completionHandler(nil); + }); + } + }; + dispatch_async(_queue, applyBlock); +} + +- (void)updateExperiments { + [self->_configExperiment updateExperiments]; +} + +#pragma mark - helpers +- (NSString *)fullyQualifiedNamespace:(NSString *)namespace { + // If this is already a fully qualified namespace, return. + if ([namespace rangeOfString:@":"].location != NSNotFound) { + return namespace; + } + NSString *fullyQualifiedNamespace = [NSString stringWithFormat:@"%@:%@", namespace, _appName]; + return fullyQualifiedNamespace; +} + +#pragma mark - Get Config Result + +- (FIRRemoteConfigValue *)objectForKeyedSubscript:(NSString *)key { + return [self configValueForKey:key]; +} + +- (FIRRemoteConfigValue *)configValueForKey:(NSString *)key { + return [self configValueForKey:key namespace:_FIRNamespace]; +} + +- (FIRRemoteConfigValue *)configValueForKey:(NSString *)key namespace:(NSString *)aNamespace { + if (!key || !aNamespace) { + return [[FIRRemoteConfigValue alloc] initWithData:[NSData data] + source:FIRRemoteConfigSourceStatic]; + } + NSString *FQNamespace = [self fullyQualifiedNamespace:aNamespace]; + + __block FIRRemoteConfigValue *value; + dispatch_sync(_queue, ^{ + value = self->_configContent.activeConfig[FQNamespace][key]; + if (value) { + if (value.source != FIRRemoteConfigSourceRemote) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000001", + @"Key %@ should come from source:%zd instead coming from source: %zd.", key, + (long)FIRRemoteConfigSourceRemote, (long)value.source); + } + return; + } + value = self->_configContent.defaultConfig[FQNamespace][key]; + if (value) { + return; + } + + value = [[FIRRemoteConfigValue alloc] initWithData:[NSData data] + source:FIRRemoteConfigSourceStatic]; + }); + return value; +} + +- (FIRRemoteConfigValue *)configValueForKey:(NSString *)key source:(FIRRemoteConfigSource)source { + return [self configValueForKey:key namespace:_FIRNamespace source:source]; +} + +- (FIRRemoteConfigValue *)configValueForKey:(NSString *)key + namespace:(NSString *)aNamespace + source:(FIRRemoteConfigSource)source { + if (!key || !aNamespace) { + return [[FIRRemoteConfigValue alloc] initWithData:[NSData data] + source:FIRRemoteConfigSourceStatic]; + } + NSString *FQNamespace = [self fullyQualifiedNamespace:aNamespace]; + + __block FIRRemoteConfigValue *value; + dispatch_sync(_queue, ^{ + if (source == FIRRemoteConfigSourceRemote) { + value = self->_configContent.activeConfig[FQNamespace][key]; + } else if (source == FIRRemoteConfigSourceDefault) { + value = self->_configContent.defaultConfig[FQNamespace][key]; + } else { + value = [[FIRRemoteConfigValue alloc] initWithData:[NSData data] + source:FIRRemoteConfigSourceStatic]; + } + }); + return value; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state + objects:(id __unsafe_unretained[])stackbuf + count:(NSUInteger)len { + __block NSUInteger localValue; + dispatch_sync(_queue, ^{ + localValue = + [self->_configContent.activeConfig[self->_FIRNamespace] countByEnumeratingWithState:state + objects:stackbuf + count:len]; + }); + return localValue; +} + +#pragma mark - Properties + +/// Last fetch completion time. +- (NSDate *)lastFetchTime { + __block NSDate *fetchTime; + dispatch_sync(_queue, ^{ + NSTimeInterval lastFetchTime = self->_settings.lastFetchTimeInterval; + fetchTime = [NSDate dateWithTimeIntervalSince1970:lastFetchTime]; + }); + return fetchTime; +} + +- (FIRRemoteConfigFetchStatus)lastFetchStatus { + __block FIRRemoteConfigFetchStatus currentStatus; + dispatch_sync(_queue, ^{ + currentStatus = self->_settings.lastFetchStatus; + }); + return currentStatus; +} + +- (NSArray *)allKeysFromSource:(FIRRemoteConfigSource)source { + return [self allKeysFromSource:source namespace:_FIRNamespace]; +} + +- (NSArray *)allKeysFromSource:(FIRRemoteConfigSource)source namespace:(NSString *)aNamespace { + __block NSArray *keys = [[NSArray alloc] init]; + dispatch_sync(_queue, ^{ + if (!aNamespace) { + return; + } + NSString *FQNamespace = [self fullyQualifiedNamespace:aNamespace]; + switch (source) { + case FIRRemoteConfigSourceDefault: + if (self->_configContent.defaultConfig[FQNamespace]) { + keys = [[self->_configContent.defaultConfig[FQNamespace] allKeys] copy]; + } + break; + case FIRRemoteConfigSourceRemote: + if (self->_configContent.activeConfig[FQNamespace]) { + keys = [[self->_configContent.activeConfig[FQNamespace] allKeys] copy]; + } + break; + default: + break; + } + }); + return keys; +} + +- (nonnull NSSet *)keysWithPrefix:(nullable NSString *)prefix { + return [self keysWithPrefix:prefix namespace:_FIRNamespace]; +} + +- (nonnull NSSet *)keysWithPrefix:(nullable NSString *)prefix + namespace:(nullable NSString *)aNamespace { + __block NSMutableSet *keys = [[NSMutableSet alloc] init]; + __block NSString *namespaceToCheck = aNamespace; + dispatch_sync(_queue, ^{ + if (!namespaceToCheck.length) { + return; + } + NSString *FQNamespace = [self fullyQualifiedNamespace:namespaceToCheck]; + if (self->_configContent.activeConfig[FQNamespace]) { + NSArray *allKeys = [self->_configContent.activeConfig[FQNamespace] allKeys]; + if (!prefix.length) { + keys = [NSMutableSet setWithArray:allKeys]; + } else { + for (NSString *key in allKeys) { + if ([key hasPrefix:prefix]) { + [keys addObject:key]; + } + } + } + } + }); + return [keys copy]; +} + +#pragma mark - Defaults + +- (void)setDefaults:(NSDictionary *)defaults { + [self setDefaults:defaults namespace:_FIRNamespace]; +} + +- (void)setDefaults:(NSDictionary *)defaultConfig + namespace:(NSString *)aNamespace { + if (!aNamespace) { + FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000036", @"The namespace cannot be empty or nil."); + return; + } + NSString *FQNamespace = [self fullyQualifiedNamespace:aNamespace]; + NSDictionary *defaultConfigCopy = [[NSDictionary alloc] init]; + if (defaultConfig) { + defaultConfigCopy = [defaultConfig copy]; + } + void (^setDefaultsBlock)(void) = ^(void) { + NSDictionary *namespaceToDefaults = @{FQNamespace : defaultConfigCopy}; + [self->_configContent copyFromDictionary:namespaceToDefaults + toSource:RCNDBSourceDefault + forNamespace:FQNamespace]; + self->_settings.lastSetDefaultsTimeInterval = [[NSDate date] timeIntervalSince1970]; + }; + dispatch_async(_queue, setDefaultsBlock); +} + +- (FIRRemoteConfigValue *)defaultValueForKey:(NSString *)key { + return [self defaultValueForKey:key namespace:_FIRNamespace]; +} + +- (FIRRemoteConfigValue *)defaultValueForKey:(NSString *)key namespace:(NSString *)aNamespace { + if (!key || !aNamespace) { + return nil; + } + NSString *FQNamespace = [self fullyQualifiedNamespace:aNamespace]; + __block FIRRemoteConfigValue *value; + dispatch_sync(_queue, ^{ + NSDictionary *defaultConfig = self->_configContent.defaultConfig; + value = defaultConfig[FQNamespace][key]; + if (value) { + if (value.source != FIRRemoteConfigSourceDefault) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000002", + @"Key %@ should come from source:%zd instead coming from source: %zd", key, + (long)FIRRemoteConfigSourceDefault, (long)value.source); + } + } + }); + return value; +} + +- (void)setDefaultsFromPlistFileName:(nullable NSString *)fileName { + return [self setDefaultsFromPlistFileName:fileName namespace:_FIRNamespace]; +} + +- (void)setDefaultsFromPlistFileName:(nullable NSString *)fileName + namespace:(nullable NSString *)namespace { + if (!namespace || namespace.length == 0) { + FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000036", @"The namespace cannot be empty or nil."); + return; + } + NSString *FQNamespace = [self fullyQualifiedNamespace:namespace]; + if (!fileName || fileName.length == 0) { + FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000037", + @"The plist file '%@' could not be found by Remote Config.", fileName); + return; + } + NSArray *bundles = @[ [NSBundle mainBundle], [NSBundle bundleForClass:[self class]] ]; + + for (NSBundle *bundle in bundles) { + NSString *plistFile = [bundle pathForResource:fileName ofType:@"plist"]; + // Use the first one we find. + if (plistFile) { + NSDictionary *defaultConfig = [[NSDictionary alloc] initWithContentsOfFile:plistFile]; + if (defaultConfig) { + [self setDefaults:defaultConfig namespace:FQNamespace]; + } + return; + } + } + FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000037", + @"The plist file '%@' could not be found by Remote Config.", fileName); +} + +#pragma mark - custom variables + +- (FIRRemoteConfigSettings *)configSettings { + __block BOOL developerModeEnabled = NO; + __block NSTimeInterval minimumFetchInterval = RCNDefaultMinimumFetchInterval; + __block NSTimeInterval fetchTimeout = RCNHTTPDefaultConnectionTimeout; + dispatch_sync(_queue, ^{ + developerModeEnabled = [self->_settings.customVariables[kRemoteConfigDeveloperKey] boolValue]; + minimumFetchInterval = self->_settings.minimumFetchInterval; + fetchTimeout = self->_settings.fetchTimeout; + }); + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000066", + @"Successfully read configSettings. Developer Mode: %@, Minimum Fetch Interval:%f, " + @"Fetch timeout: %f", + developerModeEnabled ? @"true" : @"false", minimumFetchInterval, fetchTimeout); + FIRRemoteConfigSettings *settings = + [[FIRRemoteConfigSettings alloc] initWithDeveloperModeEnabled:developerModeEnabled]; + settings.minimumFetchInterval = minimumFetchInterval; + settings.fetchTimeout = fetchTimeout; + /// The NSURLSession needs to be recreated whenever the fetch timeout may be updated. + [_configFetch recreateNetworkSession]; + return settings; +} + +- (void)setConfigSettings:(FIRRemoteConfigSettings *)configSettings { + void (^setConfigSettingsBlock)(void) = ^(void) { + if (!configSettings) { + return; + } + + NSDictionary *settingsToSave = @{ + kRemoteConfigDeveloperKey : @(configSettings.isDeveloperModeEnabled), + }; + self->_settings.customVariables = settingsToSave; + self->_settings.minimumFetchInterval = configSettings.minimumFetchInterval; + self->_settings.fetchTimeout = configSettings.fetchTimeout; + /// The NSURLSession needs to be recreated whenever the fetch timeout may be updated. + [self->_configFetch recreateNetworkSession]; + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000067", + @"Successfully set configSettings. Developer Mode: %@, Minimum Fetch Interval:%f, " + @"Fetch timeout:%f", + configSettings.isDeveloperModeEnabled ? @"true" : @"false", + configSettings.minimumFetchInterval, configSettings.fetchTimeout); + }; + dispatch_async(_queue, setConfigSettingsBlock); +} + +#pragma clang diagnostic push // "-Wdeprecated-declarations" + +@end diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h new file mode 100644 index 00000000000..217a00c1bdd --- /dev/null +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h @@ -0,0 +1,58 @@ +/* + * Copyright 2019 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 + +#import + +@class FIRApp; +@class FIRRemoteConfig; + +NS_ASSUME_NONNULL_BEGIN + +/// Provides and creates instances of Remote Config based on the namespace provided. Used in the +/// interop registration process to keep track of RC instances for each `FIRApp` instance. +@protocol FIRRemoteConfigProvider + +/// Cached instances of Remote Config objects. +@property(nonatomic, strong) NSMutableDictionary *instances; + +/// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist. +- (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace; + +@end + +/// A concrete implementation for FIRRemoteConfigInterop to create Remote Config instances and +/// register with Core's component system. +@interface FIRRemoteConfigComponent : NSObject + +/// The FIRApp that instances will be set up with. +@property(nonatomic, weak, readonly) FIRApp *app; + +/// Cached instances of Remote Config objects. +@property(nonatomic, strong) NSMutableDictionary *instances; + +/// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist. +- (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace; + +/// Default initializer. +- (instancetype)initWithApp:(FIRApp *)app NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __attribute__((unavailable("Use `initWithApp:`."))); + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m new file mode 100644 index 00000000000..eb052dac467 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.m @@ -0,0 +1,122 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h" + +#import +#import +#import +#import +#import +#import +#import +#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" + +#ifndef FIRRemoteConfig_VERSION +#error "FIRRemoteConfig_VERSION is not defined: \ +add -DFIRRemoteConfig_VERSION=... to the build invocation" +#endif + +#define STR(x) STR_EXPAND(x) +#define STR_EXPAND(x) #x + +@implementation FIRRemoteConfigComponent + +/// Default method for retrieving a Remote Config instance, or creating one if it doesn't exist. +- (FIRRemoteConfig *)remoteConfigForNamespace:(NSString *)remoteConfigNamespace { + if (!remoteConfigNamespace) { + // TODO: Throw an error? Return nil? What do we want to do? + return nil; + } + + // Validate the required information is available. + FIROptions *options = self.app.options; + NSString *errorPropertyName; + if (options.googleAppID.length == 0) { + errorPropertyName = @"googleAppID"; + } else if (options.GCMSenderID.length == 0) { + errorPropertyName = @"GCMSenderID"; + } + + if (errorPropertyName) { + [NSException + raise:kFirebaseConfigErrorDomain + format:@"%@", + [NSString + stringWithFormat: + @"Firebase Remote Config is missing the required %@ property from the " + @"configured FirebaseApp and will not be able to function properly. Please " + @"fix this issue to ensure that Firebase is correctly configured.", + errorPropertyName]]; + } + + FIRRemoteConfig *instance = self.instances[remoteConfigNamespace]; + if (!instance) { + FIRApp *app = self.app; + id analytics = + app.isDefaultApp ? FIR_COMPONENT(FIRAnalyticsInterop, app.container) : nil; + instance = [[FIRRemoteConfig alloc] initWithAppName:app.name + FIROptions:app.options + namespace:remoteConfigNamespace + DBManager:[RCNConfigDBManager sharedInstance] + configContent:[RCNConfigContent sharedInstance] + analytics:analytics]; + self.instances[remoteConfigNamespace] = instance; + } + + return instance; +} + +/// Default initializer. +- (instancetype)initWithApp:(FIRApp *)app { + self = [super init]; + if (self) { + _app = app; + _instances = [[NSMutableDictionary alloc] initWithCapacity:1]; + } + return self; +} + +#pragma mark - Lifecycle + ++ (void)load { + // Register as an internal library to be part of the initialization process. The name comes from + // go/firebase-sdk-platform-info. + [FIRApp registerInternalLibrary:self + withName:@"fire-rc" + withVersion:[NSString stringWithUTF8String:STR(FIRRemoteConfig_VERSION)]]; +} + +#pragma mark - Interoperability + ++ (NSArray *)componentsToRegister { + FIRDependency *analyticsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRAnalyticsInterop) + isRequired:NO]; + FIRComponent *rcProvider = [FIRComponent + componentWithProtocol:@protocol(FIRRemoteConfigProvider) + instantiationTiming:FIRInstantiationTimingAlwaysEager + dependencies:@[ analyticsDep ] + creationBlock:^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) { + // Cache the component so instances of Remote Config are cached. + *isCacheable = YES; + return [[FIRRemoteConfigComponent alloc] initWithApp:container.app]; + }]; + return @[ rcProvider ]; +} + +@end diff --git a/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h new file mode 100644 index 00000000000..4155621da1d --- /dev/null +++ b/FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h @@ -0,0 +1,71 @@ +/* + * Copyright 2019 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 + +#import +#import + +@class FIROptions; +@class RCNConfigContent; +@class RCNConfigDBManager; + +NS_ASSUME_NONNULL_BEGIN + +@class RCNConfigSettings; + +@interface FIRRemoteConfig () { + NSString *_FIRNamespace; +} + +/// Internal settings +@property(nonatomic, readonly, strong) RCNConfigSettings *settings; + +/// Returns the FIRRemoteConfig instance for your namespace and for the default Firebase App. +/// This singleton object contains the complete set of Remote Config parameter values available to +/// the app, including the Active Config and Default Config.. This object also caches values fetched +/// from the Remote Config Server until they are copied to the Active Config by calling +/// activateFetched. When you fetch values from the Remote Config Server using the default Firebase +/// namespace service, you should use this class method to create a shared instance of the +/// FIRRemoteConfig object to ensure that your app will function properly with the Remote Config +/// Server and the Firebase service. This API is used internally by 2P teams. ++ (FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *)remoteConfigNamespace + NS_SWIFT_NAME(remoteConfig(FIRNamespace:)); + +/// Returns the FIRRemoteConfig instance for your namespace and for the default 3P developer's app. +/// This singleton object contains the complete set of Remote Config parameter values available to +/// the app, including the Active Config and Default Config. This object also caches values fetched +/// from the Remote Config Server until they are copied to the Active Config by calling +/// activateFetched. When you fetch values from the Remote Config Server using the default Firebase +/// namespace service, you should use this class method to create a shared instance of the +/// FIRRemoteConfig object to ensure that your app will function properly with the Remote Config +/// Server and the Firebase service. ++ (FIRRemoteConfig *)remoteConfigWithFIRNamespace:(NSString *)remoteConfigNamespace + app:(FIRApp *)app + NS_SWIFT_NAME(remoteConfig(FIRNamespace:app:)); + +/// Initialize a FIRRemoteConfig instance with all the required parameters directly. This exists so +/// tests can create FIRRemoteConfig objects without needing FIRApp. +- (instancetype)initWithAppName:(NSString *)appName + FIROptions:(FIROptions *)options + namespace:(NSString *)FIRNamespace + DBManager:(RCNConfigDBManager *)DBManager + configContent:(RCNConfigContent *)configContent + analytics:(nullable id)analytics; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h new file mode 100644 index 00000000000..2a553b30a55 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h @@ -0,0 +1,121 @@ +/* + * Copyright 2019 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 + +#import + +@class RCNConfigDBManager; + +/// This internal class contains a set of variables that are unique among all the config instances. +/// It also handles all metadata and internal metadata. This class is not thread safe and does not +/// inherently allow for synchronized accesss. Callers are responsible for synchronization +/// (currently using serial dispatch queues). +@interface RCNConfigSettings : NSObject + +/// The time interval that config data stays fresh. +@property(nonatomic, readwrite, assign) NSTimeInterval minimumFetchInterval; + +/// The timeout to set for outgoing fetch requests. +@property(nonatomic, readwrite, assign) NSTimeInterval fetchTimeout; + +#pragma mark - Data required by config request. +/// Device authentication ID required by config request. +@property(nonatomic, copy) NSString *deviceAuthID; +/// Secret Token required by config request. +@property(nonatomic, copy) NSString *secretToken; +/// Device data version of checkin information. +@property(nonatomic, copy) NSString *deviceDataVersion; +/// InstanceID. +@property(nonatomic, copy) NSString *configInstanceID; +/// InstanceID token. +@property(nonatomic, copy) NSString *configInstanceIDToken; + +/// A list of successful fetch timestamps in milliseconds. +/// TODO Not used anymore. Safe to remove. +@property(nonatomic, readonly, copy) NSArray *successFetchTimes; +/// A list of failed fetch timestamps in milliseconds. +@property(nonatomic, readonly, copy) NSArray *failureFetchTimes; +/// Custom variable (aka App context digest). This is the pending custom variables request before +/// fetching. +@property(nonatomic, copy) NSDictionary *customVariables; +/// Cached internal metadata from internal metadata table. It contains customized information such +/// as HTTP connection timeout, HTTP read timeout, success/failure throttling rate and time +/// interval. Client has the default value of each parameters, they are only saved in +/// internalMetadata if they have been customize by developers. +@property(nonatomic, readonly, copy) NSDictionary *internalMetadata; +/// Device conditions since last successful fetch from the backend. Device conditions including +/// app +/// version, iOS version, device localte, language, GMP project ID and Game project ID. Used for +/// determing whether to throttle. +@property(nonatomic, readonly, copy) NSDictionary *deviceContext; +/// Bundle Identifier +@property(nonatomic, readonly, copy) NSString *bundleIdentifier; +/// The time of last successful config fetch. +@property(nonatomic, readonly, assign) NSTimeInterval lastFetchTimeInterval; +/// Last fetch status. +@property(nonatomic, readwrite, assign) FIRRemoteConfigFetchStatus lastFetchStatus; +/// The reason that last fetch failed. +@property(nonatomic, readwrite, assign) FIRRemoteConfigError lastFetchError; +/// The time of last apply timestamp. +@property(nonatomic, readwrite, assign) NSTimeInterval lastApplyTimeInterval; +/// The time of last setDefaults timestamp. +@property(nonatomic, readwrite, assign) NSTimeInterval lastSetDefaultsTimeInterval; +/// The latest eTag value stored from the last successful response. +@property(nonatomic, readwrite, assign) NSString *lastETag; + +#pragma mark Throttling properties + +/// Throttling intervals are based on https://cloud.google.com/storage/docs/exponential-backoff +/// Returns true if client has fetched config and has not got back from server. This is used to +/// determine whether there is another config task infight when fetching. +@property(nonatomic, readwrite, assign) BOOL isFetchInProgress; +/// Returns the current retry interval in seconds set for exponential backoff. +@property(nonatomic, readwrite, assign) double exponentialBackoffRetryInterval; +/// Returns the time in seconds until the next request is allowed while in exponential backoff mode. +@property(nonatomic, readonly, assign) NSTimeInterval exponentialBackoffThrottleEndTime; + +#pragma mark Throttling Methods + +/// Designated initializer. +- (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager + namespace:(NSString *)FIRNamespace + firebaseAppName:(NSString *)appName + googleAppID:(NSString *)googleAppID; + +/// Returns a fetch request with the latest device and config change. +/// Whenever user issues a fetch api call, collect the latest request. +/// @param userProperties User properties to set to config request. +/// @return Config fetch request string +- (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties; + +/// Returns metadata from metadata table. +- (NSDictionary *)loadConfigFromMetadataTable; + +/// Updates internal content with the latest successful config response. +- (void)updateInternalContentWithResponse:(NSDictionary *)response; + +/// Updates the metadata table with the current fetch status. +/// @param fetchSuccess True if fetch was successful. +- (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess; + +/// Returns true if we are in exponential backoff mode and it is not yet the next request time. +- (BOOL)shouldThrottle; + +/// Returns true if the last fetch is outside the minimum fetch interval supplied. +- (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval; + +@end diff --git a/FirebaseRemoteConfig/Sources/Protos/PortableProtoFilterTemplate.asciipb b/FirebaseRemoteConfig/Sources/Protos/PortableProtoFilterTemplate.asciipb new file mode 100644 index 00000000000..655c88b647a --- /dev/null +++ b/FirebaseRemoteConfig/Sources/Protos/PortableProtoFilterTemplate.asciipb @@ -0,0 +1,14 @@ +allowed_enum: "android.config.ConfigDeviceType" +allowed_message: "android.config.PackageData" + allowed_message: "android.config.NamedValue" +allowed_message: "android.config.KeyValue" +allowed_message: "android.config.ConfigFetchRequest" + allowed_message: "android.AndroidConfigFetchProto" + allowed_message: "android.ConfigFetchReason" + allowed_enum: "android.ConfigFetchReason.AndroidConfigFetchType" +allowed_message: "android.config.PackageTable" +allowed_message: "android.config.AppNamespaceConfigTable" + allowed_enum: "android.config.AppNamespaceConfigTable.NamespaceStatus" +allowed_message: "android.config.AppConfigTable" +allowed_message: "android.config.ConfigFetchResponse" + allowed_enum: "android.config.ConfigFetchResponse.ResponseStatus" diff --git a/FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.h b/FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.h new file mode 100755 index 00000000000..eed2724b979 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.h @@ -0,0 +1,504 @@ +/* + * Copyright 2019 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: wireless/android/config/proto/config.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers.h" +#endif + +#if GOOGLE_PROTOBUF_OBJC_VERSION < 30002 +#error This file was generated by a newer version of protoc which is incompatible with your Protocol Buffer library sources. +#endif +#if 30002 < GOOGLE_PROTOBUF_OBJC_MIN_SUPPORTED_VERSION +#error This file was generated by an older version of protoc which is incompatible with your Protocol Buffer library sources. +#endif + +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +CF_EXTERN_C_BEGIN + +@class AndroidConfigFetchProto; +@class RCNAppConfigTable; +@class RCNAppNamespaceConfigTable; +@class RCNKeyValue; +@class RCNNamedValue; +@class RCNPackageData; +@class RCNPackageTable; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - Enum RCNConfigDeviceType + +typedef GPB_ENUM(RCNConfigDeviceType) { + RCNConfigDeviceType_Unknown = 0, + RCNConfigDeviceType_Android = 1, + RCNConfigDeviceType_Ios = 2, + RCNConfigDeviceType_ChromeBrowser = 3, + RCNConfigDeviceType_ChromeOs = 4, + RCNConfigDeviceType_Desktop = 5, +}; + +GPBEnumDescriptor *RCNConfigDeviceType_EnumDescriptor(void); + +/** + * Checks to see if the given value is defined by the enum or was not known at + * the time this source was generated. + **/ +BOOL RCNConfigDeviceType_IsValidValue(int32_t value); + +#pragma mark - Enum RCNAppNamespaceConfigTable_NamespaceStatus + +typedef GPB_ENUM(RCNAppNamespaceConfigTable_NamespaceStatus) { + RCNAppNamespaceConfigTable_NamespaceStatus_Update = 0, + RCNAppNamespaceConfigTable_NamespaceStatus_NoTemplate = 1, + RCNAppNamespaceConfigTable_NamespaceStatus_NoChange = 2, + RCNAppNamespaceConfigTable_NamespaceStatus_EmptyConfig = 3, + RCNAppNamespaceConfigTable_NamespaceStatus_NotAuthorized = 4, +}; + +GPBEnumDescriptor *RCNAppNamespaceConfigTable_NamespaceStatus_EnumDescriptor(void); + +/** + * Checks to see if the given value is defined by the enum or was not known at + * the time this source was generated. + **/ +BOOL RCNAppNamespaceConfigTable_NamespaceStatus_IsValidValue(int32_t value); + +#pragma mark - Enum RCNConfigFetchResponse_ResponseStatus + +typedef GPB_ENUM(RCNConfigFetchResponse_ResponseStatus) { + RCNConfigFetchResponse_ResponseStatus_Success = 0, + RCNConfigFetchResponse_ResponseStatus_NoPackagesInRequest = 1, +}; + +GPBEnumDescriptor *RCNConfigFetchResponse_ResponseStatus_EnumDescriptor(void); + +/** + * Checks to see if the given value is defined by the enum or was not known at + * the time this source was generated. + **/ +BOOL RCNConfigFetchResponse_ResponseStatus_IsValidValue(int32_t value); + +#pragma mark - RCNConfigRoot + +/** + * Exposes the extension registry for this file. + * + * The base class provides: + * @code + * + (GPBExtensionRegistry *)extensionRegistry; + * @endcode + * which is a @c GPBExtensionRegistry that includes all the extensions defined by + * this file and all files that it depends on. + **/ +@interface RCNConfigRoot : GPBRootObject +@end + +#pragma mark - RCNPackageData + +typedef GPB_ENUM(RCNPackageData_FieldNumber) { + RCNPackageData_FieldNumber_PackageName = 1, + RCNPackageData_FieldNumber_VersionCode = 2, + RCNPackageData_FieldNumber_Digest = 3, + RCNPackageData_FieldNumber_CertHash = 4, + RCNPackageData_FieldNumber_ProjectId = 5, + RCNPackageData_FieldNumber_GmpProjectId = 6, + RCNPackageData_FieldNumber_GamesProjectId = 7, + RCNPackageData_FieldNumber_NamespaceDigestArray = 8, + RCNPackageData_FieldNumber_CustomVariableArray = 9, + RCNPackageData_FieldNumber_AppCertHash = 10, + RCNPackageData_FieldNumber_AppVersionCode = 11, + RCNPackageData_FieldNumber_AppInstanceId = 12, + RCNPackageData_FieldNumber_AppVersion = 13, + RCNPackageData_FieldNumber_AppInstanceIdToken = 14, + RCNPackageData_FieldNumber_RequestedHiddenNamespaceArray = 15, + RCNPackageData_FieldNumber_SdkVersion = 16, + RCNPackageData_FieldNumber_AnalyticsUserPropertyArray = 17, + RCNPackageData_FieldNumber_RequestedCacheExpirationSeconds = 18, + RCNPackageData_FieldNumber_FetchedConfigAgeSeconds = 19, + RCNPackageData_FieldNumber_ActiveConfigAgeSeconds = 20, +}; + +@interface RCNPackageData : GPBMessage + + +@property(nonatomic, readwrite) int32_t versionCode; + +@property(nonatomic, readwrite) BOOL hasVersionCode; + +@property(nonatomic, readwrite, copy, null_resettable) NSData *digest; +/** Test to see if @c digest has been set. */ +@property(nonatomic, readwrite) BOOL hasDigest; + + +@property(nonatomic, readwrite, copy, null_resettable) NSData *certHash; +/** Test to see if @c certHash has been set. */ +@property(nonatomic, readwrite) BOOL hasCertHash; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *projectId; +/** Test to see if @c projectId has been set. */ +@property(nonatomic, readwrite) BOOL hasProjectId; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *packageName; +/** Test to see if @c packageName has been set. */ +@property(nonatomic, readwrite) BOOL hasPackageName; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *gmpProjectId; +/** Test to see if @c gmpProjectId has been set. */ +@property(nonatomic, readwrite) BOOL hasGmpProjectId; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *gamesProjectId; +/** Test to see if @c gamesProjectId has been set. */ +@property(nonatomic, readwrite) BOOL hasGamesProjectId; + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *namespaceDigestArray; +/** The number of items in @c namespaceDigestArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger namespaceDigestArray_Count; + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *customVariableArray; +/** The number of items in @c customVariableArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger customVariableArray_Count; + + +@property(nonatomic, readwrite, copy, null_resettable) NSData *appCertHash; +/** Test to see if @c appCertHash has been set. */ +@property(nonatomic, readwrite) BOOL hasAppCertHash; + + +@property(nonatomic, readwrite) int32_t appVersionCode; + +@property(nonatomic, readwrite) BOOL hasAppVersionCode; + +@property(nonatomic, readwrite, copy, null_resettable) NSString *appVersion; +/** Test to see if @c appVersion has been set. */ +@property(nonatomic, readwrite) BOOL hasAppVersion; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *appInstanceId; +/** Test to see if @c appInstanceId has been set. */ +@property(nonatomic, readwrite) BOOL hasAppInstanceId; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *appInstanceIdToken; +/** Test to see if @c appInstanceIdToken has been set. */ +@property(nonatomic, readwrite) BOOL hasAppInstanceIdToken; + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *requestedHiddenNamespaceArray; +/** The number of items in @c requestedHiddenNamespaceArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger requestedHiddenNamespaceArray_Count; + + +@property(nonatomic, readwrite) int32_t sdkVersion; + +@property(nonatomic, readwrite) BOOL hasSdkVersion; + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *analyticsUserPropertyArray; +/** The number of items in @c analyticsUserPropertyArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger analyticsUserPropertyArray_Count; + + +@property(nonatomic, readwrite) int32_t requestedCacheExpirationSeconds; + +@property(nonatomic, readwrite) BOOL hasRequestedCacheExpirationSeconds; + +@property(nonatomic, readwrite) int32_t fetchedConfigAgeSeconds; + +@property(nonatomic, readwrite) BOOL hasFetchedConfigAgeSeconds; + +@property(nonatomic, readwrite) int32_t activeConfigAgeSeconds; + +@property(nonatomic, readwrite) BOOL hasActiveConfigAgeSeconds; +@end + +#pragma mark - RCNKeyValue + +typedef GPB_ENUM(RCNKeyValue_FieldNumber) { + RCNKeyValue_FieldNumber_Key = 1, + RCNKeyValue_FieldNumber_Value = 2, +}; + +@interface RCNKeyValue : GPBMessage + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *key; +/** Test to see if @c key has been set. */ +@property(nonatomic, readwrite) BOOL hasKey; + + +@property(nonatomic, readwrite, copy, null_resettable) NSData *value; +/** Test to see if @c value has been set. */ +@property(nonatomic, readwrite) BOOL hasValue; + +@end + +#pragma mark - RCNNamedValue + +typedef GPB_ENUM(RCNNamedValue_FieldNumber) { + RCNNamedValue_FieldNumber_Name = 1, + RCNNamedValue_FieldNumber_Value = 2, +}; + +@interface RCNNamedValue : GPBMessage + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *name; +/** Test to see if @c name has been set. */ +@property(nonatomic, readwrite) BOOL hasName; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *value; +/** Test to see if @c value has been set. */ +@property(nonatomic, readwrite) BOOL hasValue; + +@end + +#pragma mark - RCNConfigFetchRequest + +typedef GPB_ENUM(RCNConfigFetchRequest_FieldNumber) { + RCNConfigFetchRequest_FieldNumber_AndroidId = 1, + RCNConfigFetchRequest_FieldNumber_PackageDataArray = 2, + RCNConfigFetchRequest_FieldNumber_DeviceDataVersionInfo = 3, + RCNConfigFetchRequest_FieldNumber_SecurityToken = 4, + RCNConfigFetchRequest_FieldNumber_Config = 5, + RCNConfigFetchRequest_FieldNumber_ClientVersion = 6, + RCNConfigFetchRequest_FieldNumber_GmsCoreVersion = 7, + RCNConfigFetchRequest_FieldNumber_ApiLevel = 8, + RCNConfigFetchRequest_FieldNumber_DeviceCountry = 9, + RCNConfigFetchRequest_FieldNumber_DeviceLocale = 10, + RCNConfigFetchRequest_FieldNumber_DeviceType = 11, + RCNConfigFetchRequest_FieldNumber_DeviceSubtype = 12, + RCNConfigFetchRequest_FieldNumber_OsVersion = 13, + RCNConfigFetchRequest_FieldNumber_DeviceTimezoneId = 14, +}; + +@interface RCNConfigFetchRequest : GPBMessage + + +@property(nonatomic, readwrite, strong, null_resettable) AndroidConfigFetchProto *config; +/** Test to see if @c config has been set. */ +@property(nonatomic, readwrite) BOOL hasConfig; + + +@property(nonatomic, readwrite) uint64_t androidId; + +@property(nonatomic, readwrite) BOOL hasAndroidId; + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *packageDataArray; +/** The number of items in @c packageDataArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger packageDataArray_Count; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *deviceDataVersionInfo; +/** Test to see if @c deviceDataVersionInfo has been set. */ +@property(nonatomic, readwrite) BOOL hasDeviceDataVersionInfo; + + +@property(nonatomic, readwrite) uint64_t securityToken; + +@property(nonatomic, readwrite) BOOL hasSecurityToken; + +@property(nonatomic, readwrite) int32_t clientVersion; + +@property(nonatomic, readwrite) BOOL hasClientVersion; + +@property(nonatomic, readwrite) int32_t gmsCoreVersion; + +@property(nonatomic, readwrite) BOOL hasGmsCoreVersion; + +@property(nonatomic, readwrite) int32_t apiLevel; + +@property(nonatomic, readwrite) BOOL hasApiLevel; + +@property(nonatomic, readwrite, copy, null_resettable) NSString *deviceCountry; +/** Test to see if @c deviceCountry has been set. */ +@property(nonatomic, readwrite) BOOL hasDeviceCountry; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *deviceLocale; +/** Test to see if @c deviceLocale has been set. */ +@property(nonatomic, readwrite) BOOL hasDeviceLocale; + + +@property(nonatomic, readwrite) int32_t deviceType; + +@property(nonatomic, readwrite) BOOL hasDeviceType; + +@property(nonatomic, readwrite) int32_t deviceSubtype; + +@property(nonatomic, readwrite) BOOL hasDeviceSubtype; + +@property(nonatomic, readwrite, copy, null_resettable) NSString *osVersion; +/** Test to see if @c osVersion has been set. */ +@property(nonatomic, readwrite) BOOL hasOsVersion; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *deviceTimezoneId; +/** Test to see if @c deviceTimezoneId has been set. */ +@property(nonatomic, readwrite) BOOL hasDeviceTimezoneId; + +@end + +#pragma mark - RCNPackageTable + +typedef GPB_ENUM(RCNPackageTable_FieldNumber) { + RCNPackageTable_FieldNumber_PackageName = 1, + RCNPackageTable_FieldNumber_EntryArray = 2, + RCNPackageTable_FieldNumber_ProjectId = 3, +}; + +@interface RCNPackageTable : GPBMessage + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *packageName; +/** Test to see if @c packageName has been set. */ +@property(nonatomic, readwrite) BOOL hasPackageName; + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *entryArray; +/** The number of items in @c entryArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger entryArray_Count; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *projectId; +/** Test to see if @c projectId has been set. */ +@property(nonatomic, readwrite) BOOL hasProjectId; + +@end + +#pragma mark - RCNAppNamespaceConfigTable + +typedef GPB_ENUM(RCNAppNamespaceConfigTable_FieldNumber) { + RCNAppNamespaceConfigTable_FieldNumber_Namespace_p = 1, + RCNAppNamespaceConfigTable_FieldNumber_Digest = 2, + RCNAppNamespaceConfigTable_FieldNumber_EntryArray = 3, + RCNAppNamespaceConfigTable_FieldNumber_Status = 4, +}; + +@interface RCNAppNamespaceConfigTable : GPBMessage + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *namespace_p; +/** Test to see if @c namespace_p has been set. */ +@property(nonatomic, readwrite) BOOL hasNamespace_p; + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *digest; +/** Test to see if @c digest has been set. */ +@property(nonatomic, readwrite) BOOL hasDigest; + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *entryArray; +/** The number of items in @c entryArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger entryArray_Count; + + +@property(nonatomic, readwrite) RCNAppNamespaceConfigTable_NamespaceStatus status; + +@property(nonatomic, readwrite) BOOL hasStatus; +@end + +#pragma mark - RCNAppConfigTable + +typedef GPB_ENUM(RCNAppConfigTable_FieldNumber) { + RCNAppConfigTable_FieldNumber_AppName = 1, + RCNAppConfigTable_FieldNumber_NamespaceConfigArray = 2, + RCNAppConfigTable_FieldNumber_ExperimentPayloadArray = 3, + RCNAppConfigTable_FieldNumber_EnabledFeatureKeysArray = 5, +}; + +@interface RCNAppConfigTable : GPBMessage + + +@property(nonatomic, readwrite, copy, null_resettable) NSString *appName; +/** Test to see if @c appName has been set. */ +@property(nonatomic, readwrite) BOOL hasAppName; + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *namespaceConfigArray; +/** The number of items in @c namespaceConfigArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger namespaceConfigArray_Count; + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *experimentPayloadArray; +/** The number of items in @c experimentPayloadArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger experimentPayloadArray_Count; + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *enabledFeatureKeysArray; +/** The number of items in @c enabledFeatureKeysArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger enabledFeatureKeysArray_Count; + +@end + +#pragma mark - RCNConfigFetchResponse + +typedef GPB_ENUM(RCNConfigFetchResponse_FieldNumber) { + RCNConfigFetchResponse_FieldNumber_PackageTableArray = 1, + RCNConfigFetchResponse_FieldNumber_Status = 2, + RCNConfigFetchResponse_FieldNumber_InternalMetadataArray = 3, + RCNConfigFetchResponse_FieldNumber_AppConfigArray = 4, +}; + +@interface RCNConfigFetchResponse : GPBMessage + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *packageTableArray; +/** The number of items in @c packageTableArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger packageTableArray_Count; + + +@property(nonatomic, readwrite) RCNConfigFetchResponse_ResponseStatus status; + +@property(nonatomic, readwrite) BOOL hasStatus; + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *internalMetadataArray; +/** The number of items in @c internalMetadataArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger internalMetadataArray_Count; + + +@property(nonatomic, readwrite, strong, null_resettable) NSMutableArray *appConfigArray; +/** The number of items in @c appConfigArray without causing the array to be created. */ +@property(nonatomic, readonly) NSUInteger appConfigArray_Count; + +@end + +NS_ASSUME_NONNULL_END + +CF_EXTERN_C_END + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.m b/FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.m new file mode 100755 index 00000000000..271a6b4a684 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.m @@ -0,0 +1,1044 @@ +/* + * Copyright 2019 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. + */ + +// Generated by the protocol buffer compiler. DO NOT EDIT! +// source: wireless/android/config/proto/config.proto + +// This CPP symbol can be defined to use imports that match up to the framework +// imports needed when using CocoaPods. +#if !defined(GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS) + #define GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS 0 +#endif + +#if GPB_USE_PROTOBUF_FRAMEWORK_IMPORTS + #import +#else + #import "GPBProtocolBuffers_RuntimeSupport.h" +#endif + +#import + +#import "FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.h" +//#import "FirebaseRemoteConfig/Sources/Protos/logs/wireless/android/AndroidConfig.pbobjc.h" +// @@protoc_insertion_point(imports) + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +#pragma mark - RCNConfigRoot + +@implementation RCNConfigRoot + +// No extensions in the file and none of the imports (direct or indirect) +// defined extensions, so no need to generate +extensionRegistry. + +@end + +#pragma mark - RCNConfigRoot_FileDescriptor + +static GPBFileDescriptor *RCNConfigRoot_FileDescriptor(void) { + // This is called by +initialize so there is no need to worry + // about thread safety of the singleton. + static GPBFileDescriptor *descriptor = NULL; + if (!descriptor) { + GPB_DEBUG_CHECK_RUNTIME_VERSIONS(); + descriptor = [[GPBFileDescriptor alloc] initWithPackage:@"android.config" + objcPrefix:@"RCN" + syntax:GPBFileSyntaxProto2]; + } + return descriptor; +} + +#pragma mark - Enum RCNConfigDeviceType + +GPBEnumDescriptor *RCNConfigDeviceType_EnumDescriptor(void) { + static _Atomic(GPBEnumDescriptor*) descriptor = nil; + if (!descriptor) { + static const char *valueNames = + "Unknown\000Android\000Ios\000ChromeBrowser\000Chrome" + "Os\000Desktop\000"; + static const int32_t values[] = { + RCNConfigDeviceType_Unknown, + RCNConfigDeviceType_Android, + RCNConfigDeviceType_Ios, + RCNConfigDeviceType_ChromeBrowser, + RCNConfigDeviceType_ChromeOs, + RCNConfigDeviceType_Desktop, + }; + GPBEnumDescriptor *worker = + [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(RCNConfigDeviceType) + valueNames:valueNames + values:values + count:(uint32_t)(sizeof(values) / sizeof(int32_t)) + enumVerifier:RCNConfigDeviceType_IsValidValue]; + GPBEnumDescriptor *expected = nil; + if (!atomic_compare_exchange_strong(&descriptor, &expected, worker)) { + [worker release]; + } + } + return descriptor; +} + +BOOL RCNConfigDeviceType_IsValidValue(int32_t value__) { + switch (value__) { + case RCNConfigDeviceType_Unknown: + case RCNConfigDeviceType_Android: + case RCNConfigDeviceType_Ios: + case RCNConfigDeviceType_ChromeBrowser: + case RCNConfigDeviceType_ChromeOs: + case RCNConfigDeviceType_Desktop: + return YES; + default: + return NO; + } +} + +#pragma mark - RCNPackageData + +@implementation RCNPackageData + +@dynamic hasVersionCode, versionCode; +@dynamic hasDigest, digest; +@dynamic hasCertHash, certHash; +@dynamic hasProjectId, projectId; +@dynamic hasPackageName, packageName; +@dynamic hasGmpProjectId, gmpProjectId; +@dynamic hasGamesProjectId, gamesProjectId; +@dynamic namespaceDigestArray, namespaceDigestArray_Count; +@dynamic customVariableArray, customVariableArray_Count; +@dynamic hasAppCertHash, appCertHash; +@dynamic hasAppVersionCode, appVersionCode; +@dynamic hasAppVersion, appVersion; +@dynamic hasAppInstanceId, appInstanceId; +@dynamic hasAppInstanceIdToken, appInstanceIdToken; +@dynamic requestedHiddenNamespaceArray, requestedHiddenNamespaceArray_Count; +@dynamic hasSdkVersion, sdkVersion; +@dynamic analyticsUserPropertyArray, analyticsUserPropertyArray_Count; +@dynamic hasRequestedCacheExpirationSeconds, requestedCacheExpirationSeconds; +@dynamic hasFetchedConfigAgeSeconds, fetchedConfigAgeSeconds; +@dynamic hasActiveConfigAgeSeconds, activeConfigAgeSeconds; + +typedef struct RCNPackageData__storage_ { + uint32_t _has_storage_[1]; + int32_t versionCode; + int32_t appVersionCode; + int32_t sdkVersion; + int32_t requestedCacheExpirationSeconds; + int32_t fetchedConfigAgeSeconds; + int32_t activeConfigAgeSeconds; + NSString *packageName; + NSData *digest; + NSData *certHash; + NSString *projectId; + NSString *gmpProjectId; + NSString *gamesProjectId; + NSMutableArray *namespaceDigestArray; + NSMutableArray *customVariableArray; + NSData *appCertHash; + NSString *appInstanceId; + NSString *appVersion; + NSString *appInstanceIdToken; + NSMutableArray *requestedHiddenNamespaceArray; + NSMutableArray *analyticsUserPropertyArray; +} RCNPackageData__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "packageName", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_PackageName, + .hasIndex = 4, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, packageName), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "versionCode", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_VersionCode, + .hasIndex = 0, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, versionCode), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "digest", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_Digest, + .hasIndex = 1, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, digest), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "certHash", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_CertHash, + .hasIndex = 2, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, certHash), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "projectId", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_ProjectId, + .hasIndex = 3, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, projectId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "gmpProjectId", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_GmpProjectId, + .hasIndex = 5, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, gmpProjectId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "gamesProjectId", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_GamesProjectId, + .hasIndex = 6, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, gamesProjectId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "namespaceDigestArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNNamedValue), + .number = RCNPackageData_FieldNumber_NamespaceDigestArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, namespaceDigestArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "customVariableArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNNamedValue), + .number = RCNPackageData_FieldNumber_CustomVariableArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, customVariableArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "appCertHash", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_AppCertHash, + .hasIndex = 7, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, appCertHash), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + { + .name = "appVersionCode", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_AppVersionCode, + .hasIndex = 8, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, appVersionCode), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "appInstanceId", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_AppInstanceId, + .hasIndex = 10, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, appInstanceId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "appVersion", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_AppVersion, + .hasIndex = 9, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, appVersion), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "appInstanceIdToken", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_AppInstanceIdToken, + .hasIndex = 11, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, appInstanceIdToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "requestedHiddenNamespaceArray", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_RequestedHiddenNamespaceArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, requestedHiddenNamespaceArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeString, + }, + { + .name = "sdkVersion", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_SdkVersion, + .hasIndex = 12, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, sdkVersion), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "analyticsUserPropertyArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNNamedValue), + .number = RCNPackageData_FieldNumber_AnalyticsUserPropertyArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, analyticsUserPropertyArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "requestedCacheExpirationSeconds", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_RequestedCacheExpirationSeconds, + .hasIndex = 13, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, requestedCacheExpirationSeconds), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "fetchedConfigAgeSeconds", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_FetchedConfigAgeSeconds, + .hasIndex = 14, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, fetchedConfigAgeSeconds), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "activeConfigAgeSeconds", + .dataTypeSpecific.className = NULL, + .number = RCNPackageData_FieldNumber_ActiveConfigAgeSeconds, + .hasIndex = 15, + .offset = (uint32_t)offsetof(RCNPackageData__storage_, activeConfigAgeSeconds), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[RCNPackageData class] + rootClass:[RCNConfigRoot class] + file:RCNConfigRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(RCNPackageData__storage_) + flags:GPBDescriptorInitializationFlag_None]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - RCNKeyValue + +@implementation RCNKeyValue + +@dynamic hasKey, key; +@dynamic hasValue, value; + +typedef struct RCNKeyValue__storage_ { + uint32_t _has_storage_[1]; + NSString *key; + NSData *value; +} RCNKeyValue__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "key", + .dataTypeSpecific.className = NULL, + .number = RCNKeyValue_FieldNumber_Key, + .hasIndex = 0, + .offset = (uint32_t)offsetof(RCNKeyValue__storage_, key), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "value", + .dataTypeSpecific.className = NULL, + .number = RCNKeyValue_FieldNumber_Value, + .hasIndex = 1, + .offset = (uint32_t)offsetof(RCNKeyValue__storage_, value), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[RCNKeyValue class] + rootClass:[RCNConfigRoot class] + file:RCNConfigRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(RCNKeyValue__storage_) + flags:GPBDescriptorInitializationFlag_None]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - RCNNamedValue + +@implementation RCNNamedValue + +@dynamic hasName, name; +@dynamic hasValue, value; + +typedef struct RCNNamedValue__storage_ { + uint32_t _has_storage_[1]; + NSString *name; + NSString *value; +} RCNNamedValue__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "name", + .dataTypeSpecific.className = NULL, + .number = RCNNamedValue_FieldNumber_Name, + .hasIndex = 0, + .offset = (uint32_t)offsetof(RCNNamedValue__storage_, name), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "value", + .dataTypeSpecific.className = NULL, + .number = RCNNamedValue_FieldNumber_Value, + .hasIndex = 1, + .offset = (uint32_t)offsetof(RCNNamedValue__storage_, value), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[RCNNamedValue class] + rootClass:[RCNConfigRoot class] + file:RCNConfigRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(RCNNamedValue__storage_) + flags:GPBDescriptorInitializationFlag_None]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - RCNConfigFetchRequest + +@implementation RCNConfigFetchRequest + +@dynamic hasConfig, config; +@dynamic hasAndroidId, androidId; +@dynamic packageDataArray, packageDataArray_Count; +@dynamic hasDeviceDataVersionInfo, deviceDataVersionInfo; +@dynamic hasSecurityToken, securityToken; +@dynamic hasClientVersion, clientVersion; +@dynamic hasGmsCoreVersion, gmsCoreVersion; +@dynamic hasApiLevel, apiLevel; +@dynamic hasDeviceCountry, deviceCountry; +@dynamic hasDeviceLocale, deviceLocale; +@dynamic hasDeviceType, deviceType; +@dynamic hasDeviceSubtype, deviceSubtype; +@dynamic hasOsVersion, osVersion; +@dynamic hasDeviceTimezoneId, deviceTimezoneId; + +typedef struct RCNConfigFetchRequest__storage_ { + uint32_t _has_storage_[1]; + int32_t clientVersion; + int32_t gmsCoreVersion; + int32_t apiLevel; + int32_t deviceType; + int32_t deviceSubtype; + NSMutableArray *packageDataArray; + NSString *deviceDataVersionInfo; + AndroidConfigFetchProto *config; + NSString *deviceCountry; + NSString *deviceLocale; + NSString *osVersion; + NSString *deviceTimezoneId; + uint64_t androidId; + uint64_t securityToken; +} RCNConfigFetchRequest__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "androidId", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_AndroidId, + .hasIndex = 1, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, androidId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeFixed64, + }, + { + .name = "packageDataArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNPackageData), + .number = RCNConfigFetchRequest_FieldNumber_PackageDataArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, packageDataArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "deviceDataVersionInfo", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_DeviceDataVersionInfo, + .hasIndex = 2, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, deviceDataVersionInfo), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "securityToken", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_SecurityToken, + .hasIndex = 3, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, securityToken), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeFixed64, + }, + { + .name = "config", + .dataTypeSpecific.className = GPBStringifySymbol(AndroidConfigFetchProto), + .number = RCNConfigFetchRequest_FieldNumber_Config, + .hasIndex = 0, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, config), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "clientVersion", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_ClientVersion, + .hasIndex = 4, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, clientVersion), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "gmsCoreVersion", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_GmsCoreVersion, + .hasIndex = 5, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, gmsCoreVersion), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "apiLevel", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_ApiLevel, + .hasIndex = 6, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, apiLevel), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "deviceCountry", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_DeviceCountry, + .hasIndex = 7, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, deviceCountry), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "deviceLocale", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_DeviceLocale, + .hasIndex = 8, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, deviceLocale), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "deviceType", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_DeviceType, + .hasIndex = 9, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, deviceType), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "deviceSubtype", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_DeviceSubtype, + .hasIndex = 10, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, deviceSubtype), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + { + .name = "osVersion", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_OsVersion, + .hasIndex = 11, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, osVersion), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "deviceTimezoneId", + .dataTypeSpecific.className = NULL, + .number = RCNConfigFetchRequest_FieldNumber_DeviceTimezoneId, + .hasIndex = 12, + .offset = (uint32_t)offsetof(RCNConfigFetchRequest__storage_, deviceTimezoneId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[RCNConfigFetchRequest class] + rootClass:[RCNConfigRoot class] + file:RCNConfigRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(RCNConfigFetchRequest__storage_) + flags:GPBDescriptorInitializationFlag_None]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - RCNPackageTable + +@implementation RCNPackageTable + +@dynamic hasPackageName, packageName; +@dynamic entryArray, entryArray_Count; +@dynamic hasProjectId, projectId; + +typedef struct RCNPackageTable__storage_ { + uint32_t _has_storage_[1]; + NSString *packageName; + NSMutableArray *entryArray; + NSString *projectId; +} RCNPackageTable__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "packageName", + .dataTypeSpecific.className = NULL, + .number = RCNPackageTable_FieldNumber_PackageName, + .hasIndex = 0, + .offset = (uint32_t)offsetof(RCNPackageTable__storage_, packageName), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "entryArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNKeyValue), + .number = RCNPackageTable_FieldNumber_EntryArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNPackageTable__storage_, entryArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "projectId", + .dataTypeSpecific.className = NULL, + .number = RCNPackageTable_FieldNumber_ProjectId, + .hasIndex = 1, + .offset = (uint32_t)offsetof(RCNPackageTable__storage_, projectId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[RCNPackageTable class] + rootClass:[RCNConfigRoot class] + file:RCNConfigRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(RCNPackageTable__storage_) + flags:GPBDescriptorInitializationFlag_None]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - RCNAppNamespaceConfigTable + +@implementation RCNAppNamespaceConfigTable + +@dynamic hasNamespace_p, namespace_p; +@dynamic hasDigest, digest; +@dynamic entryArray, entryArray_Count; +@dynamic hasStatus, status; + +typedef struct RCNAppNamespaceConfigTable__storage_ { + uint32_t _has_storage_[1]; + RCNAppNamespaceConfigTable_NamespaceStatus status; + NSString *namespace_p; + NSString *digest; + NSMutableArray *entryArray; +} RCNAppNamespaceConfigTable__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "namespace_p", + .dataTypeSpecific.className = NULL, + .number = RCNAppNamespaceConfigTable_FieldNumber_Namespace_p, + .hasIndex = 0, + .offset = (uint32_t)offsetof(RCNAppNamespaceConfigTable__storage_, namespace_p), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "digest", + .dataTypeSpecific.className = NULL, + .number = RCNAppNamespaceConfigTable_FieldNumber_Digest, + .hasIndex = 1, + .offset = (uint32_t)offsetof(RCNAppNamespaceConfigTable__storage_, digest), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "entryArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNKeyValue), + .number = RCNAppNamespaceConfigTable_FieldNumber_EntryArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNAppNamespaceConfigTable__storage_, entryArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "status", + .dataTypeSpecific.enumDescFunc = RCNAppNamespaceConfigTable_NamespaceStatus_EnumDescriptor, + .number = RCNAppNamespaceConfigTable_FieldNumber_Status, + .hasIndex = 2, + .offset = (uint32_t)offsetof(RCNAppNamespaceConfigTable__storage_, status), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor), + .dataType = GPBDataTypeEnum, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[RCNAppNamespaceConfigTable class] + rootClass:[RCNConfigRoot class] + file:RCNConfigRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(RCNAppNamespaceConfigTable__storage_) + flags:GPBDescriptorInitializationFlag_None]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - Enum RCNAppNamespaceConfigTable_NamespaceStatus + +GPBEnumDescriptor *RCNAppNamespaceConfigTable_NamespaceStatus_EnumDescriptor(void) { + static _Atomic(GPBEnumDescriptor*) descriptor = nil; + if (!descriptor) { + static const char *valueNames = + "Update\000NoTemplate\000NoChange\000EmptyConfig\000N" + "otAuthorized\000"; + static const int32_t values[] = { + RCNAppNamespaceConfigTable_NamespaceStatus_Update, + RCNAppNamespaceConfigTable_NamespaceStatus_NoTemplate, + RCNAppNamespaceConfigTable_NamespaceStatus_NoChange, + RCNAppNamespaceConfigTable_NamespaceStatus_EmptyConfig, + RCNAppNamespaceConfigTable_NamespaceStatus_NotAuthorized, + }; + GPBEnumDescriptor *worker = + [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(RCNAppNamespaceConfigTable_NamespaceStatus) + valueNames:valueNames + values:values + count:(uint32_t)(sizeof(values) / sizeof(int32_t)) + enumVerifier:RCNAppNamespaceConfigTable_NamespaceStatus_IsValidValue]; + GPBEnumDescriptor *expected = nil; + if (!atomic_compare_exchange_strong(&descriptor, &expected, worker)) { + [worker release]; + } + } + return descriptor; +} + +BOOL RCNAppNamespaceConfigTable_NamespaceStatus_IsValidValue(int32_t value__) { + switch (value__) { + case RCNAppNamespaceConfigTable_NamespaceStatus_Update: + case RCNAppNamespaceConfigTable_NamespaceStatus_NoTemplate: + case RCNAppNamespaceConfigTable_NamespaceStatus_NoChange: + case RCNAppNamespaceConfigTable_NamespaceStatus_EmptyConfig: + case RCNAppNamespaceConfigTable_NamespaceStatus_NotAuthorized: + return YES; + default: + return NO; + } +} + +#pragma mark - RCNAppConfigTable + +@implementation RCNAppConfigTable + +@dynamic hasAppName, appName; +@dynamic namespaceConfigArray, namespaceConfigArray_Count; +@dynamic experimentPayloadArray, experimentPayloadArray_Count; +@dynamic enabledFeatureKeysArray, enabledFeatureKeysArray_Count; + +typedef struct RCNAppConfigTable__storage_ { + uint32_t _has_storage_[1]; + NSString *appName; + NSMutableArray *namespaceConfigArray; + NSMutableArray *experimentPayloadArray; + NSMutableArray *enabledFeatureKeysArray; +} RCNAppConfigTable__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "appName", + .dataTypeSpecific.className = NULL, + .number = RCNAppConfigTable_FieldNumber_AppName, + .hasIndex = 0, + .offset = (uint32_t)offsetof(RCNAppConfigTable__storage_, appName), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "namespaceConfigArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNAppNamespaceConfigTable), + .number = RCNAppConfigTable_FieldNumber_NamespaceConfigArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNAppConfigTable__storage_, namespaceConfigArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "experimentPayloadArray", + .dataTypeSpecific.className = NULL, + .number = RCNAppConfigTable_FieldNumber_ExperimentPayloadArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNAppConfigTable__storage_, experimentPayloadArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeBytes, + }, + { + .name = "enabledFeatureKeysArray", + .dataTypeSpecific.className = NULL, + .number = RCNAppConfigTable_FieldNumber_EnabledFeatureKeysArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNAppConfigTable__storage_, enabledFeatureKeysArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[RCNAppConfigTable class] + rootClass:[RCNConfigRoot class] + file:RCNConfigRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(RCNAppConfigTable__storage_) + flags:GPBDescriptorInitializationFlag_None]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - RCNConfigFetchResponse + +@implementation RCNConfigFetchResponse + +@dynamic packageTableArray, packageTableArray_Count; +@dynamic hasStatus, status; +@dynamic internalMetadataArray, internalMetadataArray_Count; +@dynamic appConfigArray, appConfigArray_Count; + +typedef struct RCNConfigFetchResponse__storage_ { + uint32_t _has_storage_[1]; + RCNConfigFetchResponse_ResponseStatus status; + NSMutableArray *packageTableArray; + NSMutableArray *internalMetadataArray; + NSMutableArray *appConfigArray; +} RCNConfigFetchResponse__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "packageTableArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNPackageTable), + .number = RCNConfigFetchResponse_FieldNumber_PackageTableArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNConfigFetchResponse__storage_, packageTableArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "status", + .dataTypeSpecific.enumDescFunc = RCNConfigFetchResponse_ResponseStatus_EnumDescriptor, + .number = RCNConfigFetchResponse_FieldNumber_Status, + .hasIndex = 0, + .offset = (uint32_t)offsetof(RCNConfigFetchResponse__storage_, status), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldHasEnumDescriptor), + .dataType = GPBDataTypeEnum, + }, + { + .name = "internalMetadataArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNKeyValue), + .number = RCNConfigFetchResponse_FieldNumber_InternalMetadataArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNConfigFetchResponse__storage_, internalMetadataArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + { + .name = "appConfigArray", + .dataTypeSpecific.className = GPBStringifySymbol(RCNAppConfigTable), + .number = RCNConfigFetchResponse_FieldNumber_AppConfigArray, + .hasIndex = GPBNoHasBit, + .offset = (uint32_t)offsetof(RCNConfigFetchResponse__storage_, appConfigArray), + .flags = GPBFieldRepeated, + .dataType = GPBDataTypeMessage, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[RCNConfigFetchResponse class] + rootClass:[RCNConfigRoot class] + file:RCNConfigRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(RCNConfigFetchResponse__storage_) + flags:GPBDescriptorInitializationFlag_None]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - Enum RCNConfigFetchResponse_ResponseStatus + +GPBEnumDescriptor *RCNConfigFetchResponse_ResponseStatus_EnumDescriptor(void) { + static _Atomic(GPBEnumDescriptor*) descriptor = nil; + if (!descriptor) { + static const char *valueNames = + "Success\000NoPackagesInRequest\000"; + static const int32_t values[] = { + RCNConfigFetchResponse_ResponseStatus_Success, + RCNConfigFetchResponse_ResponseStatus_NoPackagesInRequest, + }; + GPBEnumDescriptor *worker = + [GPBEnumDescriptor allocDescriptorForName:GPBNSStringifySymbol(RCNConfigFetchResponse_ResponseStatus) + valueNames:valueNames + values:values + count:(uint32_t)(sizeof(values) / sizeof(int32_t)) + enumVerifier:RCNConfigFetchResponse_ResponseStatus_IsValidValue]; + GPBEnumDescriptor *expected = nil; + if (!atomic_compare_exchange_strong(&descriptor, &expected, worker)) { + [worker release]; + } + } + return descriptor; +} + +BOOL RCNConfigFetchResponse_ResponseStatus_IsValidValue(int32_t value__) { + switch (value__) { + case RCNConfigFetchResponse_ResponseStatus_Success: + case RCNConfigFetchResponse_ResponseStatus_NoPackagesInRequest: + return YES; + default: + return NO; + } +} + + +#pragma clang diagnostic pop + +// @@protoc_insertion_point(global_scope) diff --git a/FirebaseRemoteConfig/Sources/Public/FIRRemoteConfig.h b/FirebaseRemoteConfig/Sources/Public/FIRRemoteConfig.h new file mode 100644 index 00000000000..04b3386f5f6 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/Public/FIRRemoteConfig.h @@ -0,0 +1,369 @@ +/* + * Copyright 2019 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 + +@class FIRApp; + +/// The Firebase Remote Config service default namespace, to be used if the API method does not +/// specify a different namespace. Use the default namespace if configuring from the Google Firebase +/// service. +extern NSString *const _Nonnull FIRNamespaceGoogleMobilePlatform NS_SWIFT_NAME( + NamespaceGoogleMobilePlatform); + +/// Key used to manage throttling in NSError user info when the refreshing of Remote Config +/// parameter values (data) is throttled. The value of this key is the elapsed time since 1970, +/// measured in seconds. +extern NSString *const _Nonnull FIRRemoteConfigThrottledEndTimeInSecondsKey NS_SWIFT_NAME( + RemoteConfigThrottledEndTimeInSecondsKey); + +/// Indicates whether updated data was successfully fetched. +typedef NS_ENUM(NSInteger, FIRRemoteConfigFetchStatus) { + /// Config has never been fetched. + FIRRemoteConfigFetchStatusNoFetchYet, + /// Config fetch succeeded. + FIRRemoteConfigFetchStatusSuccess, + /// Config fetch failed. + FIRRemoteConfigFetchStatusFailure, + /// Config fetch was throttled. + FIRRemoteConfigFetchStatusThrottled, +} NS_SWIFT_NAME(RemoteConfigFetchStatus); + +/// Indicates whether updated data was successfully fetched and activated. +typedef NS_ENUM(NSInteger, FIRRemoteConfigFetchAndActivateStatus) { + /// The remote fetch succeeded and fetched data was activated. + FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote, + /// The fetch and activate succeeded from already fetched but yet unexpired config data. You can + /// control this using minimumFetchInterval property in FIRRemoteConfigSettings. + FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData, + /// The fetch and activate failed. + FIRRemoteConfigFetchAndActivateStatusError +} NS_SWIFT_NAME(RemoteConfigFetchAndActivateStatus); + +/// Remote Config error domain that handles errors when fetching data from the service. +extern NSString *const _Nonnull FIRRemoteConfigErrorDomain NS_SWIFT_NAME(RemoteConfigErrorDomain); +/// Firebase Remote Config service fetch error. +typedef NS_ENUM(NSInteger, FIRRemoteConfigError) { + /// Unknown or no error. + FIRRemoteConfigErrorUnknown = 8001, + /// Frequency of fetch requests exceeds throttled limit. + FIRRemoteConfigErrorThrottled = 8002, + /// Internal error that covers all internal HTTP errors. + FIRRemoteConfigErrorInternalError = 8003, +} NS_SWIFT_NAME(RemoteConfigError); + +/// Enumerated value that indicates the source of Remote Config data. Data can come from +/// the Remote Config service, the DefaultConfig that is available when the app is first installed, +/// or a static initialized value if data is not available from the service or DefaultConfig. +typedef NS_ENUM(NSInteger, FIRRemoteConfigSource) { + FIRRemoteConfigSourceRemote, ///< The data source is the Remote Config service. + FIRRemoteConfigSourceDefault, ///< The data source is the DefaultConfig defined for this app. + FIRRemoteConfigSourceStatic, ///< The data doesn't exist, return a static initialized value. +} NS_SWIFT_NAME(RemoteConfigSource); + +/// Completion handler invoked by fetch methods when they get a response from the server. +/// +/// @param status Config fetching status. +/// @param error Error message on failure. +typedef void (^FIRRemoteConfigFetchCompletion)(FIRRemoteConfigFetchStatus status, + NSError *_Nullable error) + NS_SWIFT_NAME(RemoteConfigFetchCompletion); + +/// Completion handler invoked by activate method upon completion. +/// @param error Error message on failure. Nil if activation was successful. +typedef void (^FIRRemoteConfigActivateCompletion)(NSError *_Nullable error) + NS_SWIFT_NAME(RemoteConfigActivateCompletion); + +/// Completion handler invoked upon completion of Remote Config initialization. +/// +/// @param initializationError nil if initialization succeeded. +typedef void (^FIRRemoteConfigInitializationCompletion)(NSError *_Nullable initializationError) + NS_SWIFT_NAME(RemoteConfigInitializationCompletion); + +/// Completion handler invoked by the fetchAndActivate method. Used to convey status of fetch and, +/// if successful, resultant activate call +/// @param status Config fetching status. +/// @param error Error message on failure of config fetch +typedef void (^FIRRemoteConfigFetchAndActivateCompletion)( + FIRRemoteConfigFetchAndActivateStatus status, NSError *_Nullable error) + NS_SWIFT_NAME(RemoteConfigFetchAndActivateCompletion); + +#pragma mark - FIRRemoteConfigValue +/// This class provides a wrapper for Remote Config parameter values, with methods to get parameter +/// values as different data types. +NS_SWIFT_NAME(RemoteConfigValue) +@interface FIRRemoteConfigValue : NSObject +/// Gets the value as a string. +@property(nonatomic, readonly, nullable) NSString *stringValue; +/// Gets the value as a number value. +@property(nonatomic, readonly, nullable) NSNumber *numberValue; +/// Gets the value as a NSData object. +@property(nonatomic, readonly, nonnull) NSData *dataValue; +/// Gets the value as a boolean. +@property(nonatomic, readonly) BOOL boolValue; +/// Gets a foundation object (NSDictionary / NSArray) by parsing the value as JSON. This method uses +/// NSJSONSerialization's JSONObjectWithData method with an options value of 0. +@property(nonatomic, readonly, nullable) id JSONValue NS_SWIFT_NAME(jsonValue); +/// Identifies the source of the fetched value. +@property(nonatomic, readonly) FIRRemoteConfigSource source; +@end + +#pragma mark - FIRRemoteConfigSettings +/// Firebase Remote Config settings. +NS_SWIFT_NAME(RemoteConfigSettings) +@interface FIRRemoteConfigSettings : NSObject +/// Indicates the default value in seconds to set for the minimum interval that needs to elapse +/// before a fetch request can again be made to the Remote Config backend. After a fetch request to +/// the backend has succeeded, no additional fetch requests to the backend will be allowed until the +/// minimum fetch interval expires. Note that you can override this default on a per-fetch request +/// basis using -[FIRRemoteConfig fetchWithExpirationDuration:completionHandler]. For E.g. setting +/// the expiration duration to 0 in the fetch request will override the minimumFetchInterval and +/// allow the request to the backend. +@property(nonatomic, assign) NSTimeInterval minimumFetchInterval; +/// Indicates the default value in seconds to abandon a pending fetch request made to the backend. +/// This value is set for outgoing requests as the timeoutIntervalForRequest as well as the +/// timeoutIntervalForResource on the NSURLSession's configuration. +@property(nonatomic, assign) NSTimeInterval fetchTimeout; +/// Indicates whether Developer Mode is enabled. +@property(nonatomic, readonly) BOOL isDeveloperModeEnabled DEPRECATED_MSG_ATTRIBUTE( + "This no longer needs to be set during development. Refer to documentation for additional " + "details."); +/// Initializes FIRRemoteConfigSettings, which is used to set properties for custom settings. To +/// make custom settings take effect, pass the FIRRemoteConfigSettings instance to the +/// configSettings property of FIRRemoteConfig. +- (nonnull FIRRemoteConfigSettings *)initWithDeveloperModeEnabled:(BOOL)developerModeEnabled + DEPRECATED_MSG_ATTRIBUTE("This no longer needs to be set during development. Refer to " + "documentation for additional details."); +@end + +#pragma mark - FIRRemoteConfig +/// Firebase Remote Config class. The shared instance method +remoteConfig can be created and used +/// to fetch, activate and read config results and set default config results. +NS_SWIFT_NAME(RemoteConfig) +@interface FIRRemoteConfig : NSObject +/// Last successful fetch completion time. +@property(nonatomic, readwrite, strong, nullable) NSDate *lastFetchTime; +/// Last fetch status. The status can be any enumerated value from FIRRemoteConfigFetchStatus. +@property(nonatomic, readonly, assign) FIRRemoteConfigFetchStatus lastFetchStatus; +/// Config settings are custom settings. +@property(nonatomic, readwrite, strong, nonnull) FIRRemoteConfigSettings *configSettings; + +/// Returns the FIRRemoteConfig instance configured for the default Firebase app. This singleton +/// object contains the complete set of Remote Config parameter values available to the app, +/// including the Active Config and Default Config. This object also caches values fetched from the +/// Remote Config Server until they are copied to the Active Config by calling activateFetched. When +/// you fetch values from the Remote Config Server using the default Firebase namespace service, you +/// should use this class method to create a shared instance of the FIRRemoteConfig object to ensure +/// that your app will function properly with the Remote Config Server and the Firebase service. ++ (nonnull FIRRemoteConfig *)remoteConfig NS_SWIFT_NAME(remoteConfig()); + +/// Returns the FIRRemoteConfig instance for your (non-default) Firebase appID. Note that Firebase +/// analytics does not work for non-default app instances. This singleton object contains the +/// complete set of Remote Config parameter values available to the app, including the Active Config +/// and Default Config. This object also caches values fetched from the Remote Config Server until +/// they are copied to the Active Config by calling activateFetched. When you fetch values from the +/// Remote Config Server using the default Firebase namespace service, you should use this class +/// method to create a shared instance of the FIRRemoteConfig object to ensure that your app will +/// function properly with the Remote Config Server and the Firebase service. ++ (nonnull FIRRemoteConfig *)remoteConfigWithApp:(nonnull FIRApp *)app + NS_SWIFT_NAME(remoteConfig(app:)); + +/// Unavailable. Use +remoteConfig instead. +- (nonnull instancetype)init __attribute__((unavailable("Use +remoteConfig instead."))); + +/// Ensures initialization is complete and clients can begin querying for Remote Config values. +/// @param completionHandler Initialization complete callback. +- (void)ensureInitializedWithCompletionHandler: + (nonnull FIRRemoteConfigInitializationCompletion)completionHandler; +#pragma mark - Fetch +/// Fetches Remote Config data with a callback. Call activateFetched to make fetched data available +/// to your app. +/// +/// Note: This method uses a Firebase Instance ID token to identify the app instance, and once it's +/// called, it periodically sends data to the Firebase backend. (see +/// `[FIRInstanceID getIDWithHandler:]`). +/// To stop the periodic sync, developers need to call `[FIRInstanceID deleteIDWithHandler:]` and +/// avoid calling this method again. +/// +/// @param completionHandler Fetch operation callback. +- (void)fetchWithCompletionHandler:(nullable FIRRemoteConfigFetchCompletion)completionHandler; + +/// Fetches Remote Config data and sets a duration that specifies how long config data lasts. +/// Call activateFetched to make fetched data available to your app. +/// +/// Note: This method uses a Firebase Instance ID token to identify the app instance, and once it's +/// called, it periodically sends data to the Firebase backend. (see +/// `[FIRInstanceID getIDWithHandler:]`). +/// To stop the periodic sync, developers need to call `[FIRInstanceID deleteIDWithHandler:]` and +/// avoid calling this method again. +/// +/// @param expirationDuration Override the (default or optionally set minimumFetchInterval property +/// in FIRRemoteConfigSettings) minimumFetchInterval for only the current request, in seconds. +/// Setting a value of 0 seconds will force a fetch to the backend. +/// @param completionHandler Fetch operation callback. +- (void)fetchWithExpirationDuration:(NSTimeInterval)expirationDuration + completionHandler:(nullable FIRRemoteConfigFetchCompletion)completionHandler; + +/// Fetches Remote Config data and if successful, activates fetched data. Optional completion +/// handler callback is invoked after the attempted activation of data, if the fetch call succeeded. +/// +/// Note: This method uses a Firebase Instance ID token to identify the app instance, and once it's +/// called, it periodically sends data to the Firebase backend. (see +/// `[FIRInstanceID getIDWithHandler:]`). +/// To stop the periodic sync, developers need to call `[FIRInstanceID deleteIDWithHandler:]` and +/// avoid calling this method again. +/// +/// @param completionHandler Fetch operation callback. +- (void)fetchAndActivateWithCompletionHandler: + (nullable FIRRemoteConfigFetchAndActivateCompletion)completionHandler; + +#pragma mark - Apply + +/// Applies Fetched Config data to the Active Config, causing updates to the behavior and appearance +/// of the app to take effect (depending on how config data is used in the app). +/// @param completionHandler Activate operation callback. +- (void)activateWithCompletionHandler:(nullable FIRRemoteConfigActivateCompletion)completionHandler; + +/// This method is deprecated. Please use -[FIRRemoteConfig activateWithCompletionHandler:] instead. +/// Applies Fetched Config data to the Active Config, causing updates to the behavior and appearance +/// of the app to take effect (depending on how config data is used in the app). +/// Returns true if there was a Fetched Config, and it was activated. +/// Returns false if no Fetched Config was found, or the Fetched Config was already activated. +- (BOOL)activateFetched DEPRECATED_MSG_ATTRIBUTE("Use -[FIRRemoteConfig activate] " + "instead."); + +#pragma mark - Get Config +/// Enables access to configuration values by using object subscripting syntax. +///
+/// // Example:
+/// FIRRemoteConfig *config = [FIRRemoteConfig remoteConfig];
+/// FIRRemoteConfigValue *value = config[@"yourKey"];
+/// BOOL b = value.boolValue;
+/// NSNumber *number = config[@"yourKey"].numberValue;
+/// 
+- (nonnull FIRRemoteConfigValue *)objectForKeyedSubscript:(nonnull NSString *)key; + +/// Gets the config value. +/// @param key Config key. +- (nonnull FIRRemoteConfigValue *)configValueForKey:(nullable NSString *)key; + +/// Gets the config value of a given namespace. +/// @param key Config key. +/// @param aNamespace Config results under a given namespace. +- (nonnull FIRRemoteConfigValue *)configValueForKey:(nullable NSString *)key + namespace:(nullable NSString *)aNamespace + DEPRECATED_MSG_ATTRIBUTE("Use -[FIRRemoteConfig configValueForKey:] " + "instead."); + +/// Gets the config value of a given namespace and a given source. +/// @param key Config key. +/// @param source Config value source. +- (nonnull FIRRemoteConfigValue *)configValueForKey:(nullable NSString *)key + source:(FIRRemoteConfigSource)source; + +/// Gets the config value of a given namespace and a given source. +/// @param key Config key. +/// @param aNamespace Config results under a given namespace. +/// @param source Config value source. +- (nonnull FIRRemoteConfigValue *)configValueForKey:(nullable NSString *)key + namespace:(nullable NSString *)aNamespace + source:(FIRRemoteConfigSource)source + DEPRECATED_MSG_ATTRIBUTE("Use -[FIRRemoteConfig configValueForKey:source:] " + "instead."); + +/// Gets all the parameter keys from a given source and a given namespace. +/// +/// @param source The config data source. +/// @return An array of keys under the given source and namespace. +- (nonnull NSArray *)allKeysFromSource:(FIRRemoteConfigSource)source; + +/// Gets all the parameter keys from a given source and a given namespace. +/// +/// @param source The config data source. +/// @param aNamespace The config data namespace. +/// @return An array of keys under the given source and namespace. +- (nonnull NSArray *)allKeysFromSource:(FIRRemoteConfigSource)source + namespace:(nullable NSString *)aNamespace + DEPRECATED_MSG_ATTRIBUTE("Use -[FIRRemoteConfig allKeysFromSource:] instead."); + +/// Returns the set of parameter keys that start with the given prefix, from the default namespace +/// in the active config. +/// +/// @param prefix The key prefix to look for. If prefix is nil or empty, returns all the +/// keys. +/// @return The set of parameter keys that start with the specified prefix. +- (nonnull NSSet *)keysWithPrefix:(nullable NSString *)prefix; + +/// Returns the set of parameter keys that start with the given prefix, from the given namespace in +/// the active config. +/// +/// @param prefix The key prefix to look for. If prefix is nil or empty, returns all the +/// keys in the given namespace. +/// @param aNamespace The namespace in which to look up the keys. If the namespace is invalid, +/// returns an empty set. +/// @return The set of parameter keys that start with the specified prefix. +- (nonnull NSSet *)keysWithPrefix:(nullable NSString *)prefix + namespace:(nullable NSString *)aNamespace + DEPRECATED_MSG_ATTRIBUTE("Use -[FIRRemoteConfig keysWithPrefix:] instead."); + +#pragma mark - Defaults +/// Sets config defaults for parameter keys and values in the default namespace config. +/// @param defaults A dictionary mapping a NSString * key to a NSObject * value. +- (void)setDefaults:(nullable NSDictionary *)defaults; + +/// Sets config defaults for parameter keys and values in the default namespace config. +/// +/// @param defaults A dictionary mapping a NSString * key to a NSObject * value. +/// @param aNamespace Config under a given namespace. +- (void)setDefaults:(nullable NSDictionary *)defaults + namespace:(nullable NSString *)aNamespace + DEPRECATED_MSG_ATTRIBUTE("Use -[FIRRemoteConfig setDefaults:] instead."); + +/// Sets default configs from plist for default namespace; +/// @param fileName The plist file name, with no file name extension. For example, if the plist file +/// is defaultSamples.plist, call: +/// [[FIRRemoteConfig remoteConfig] setDefaultsFromPlistFileName:@"defaultSamples"]; +- (void)setDefaultsFromPlistFileName:(nullable NSString *)fileName + NS_SWIFT_NAME(setDefaults(fromPlist:)); + +/// Sets default configs from plist for a given namespace; +/// @param fileName The plist file name, with no file name extension. For example, if the plist file +/// is defaultSamples.plist, call: +/// [[FIRRemoteConfig remoteConfig] setDefaultsFromPlistFileName:@"defaultSamples"]; +/// @param aNamespace The namespace where the default config is set. +- (void)setDefaultsFromPlistFileName:(nullable NSString *)fileName + namespace:(nullable NSString *)aNamespace + NS_SWIFT_NAME(setDefaults(fromPlist:namespace:)) + DEPRECATED_MSG_ATTRIBUTE("Use -[FIRRemoteConfig setDefaultsFromPlistFileName:] instead."); + +/// Returns the default value of a given key and a given namespace from the default config. +/// +/// @param key The parameter key of default config. +/// @return Returns the default value of the specified key and namespace. Returns +/// nil if the key or namespace doesn't exist in the default config. +- (nullable FIRRemoteConfigValue *)defaultValueForKey:(nullable NSString *)key; + +/// Returns the default value of a given key and a given namespace from the default config. +/// +/// @param key The parameter key of default config. +/// @param aNamespace The namespace of default config. +/// @return Returns the default value of the specified key and namespace. Returns +/// nil if the key or namespace doesn't exist in the default config. +- (nullable FIRRemoteConfigValue *)defaultValueForKey:(nullable NSString *)key + namespace:(nullable NSString *)aNamespace + DEPRECATED_MSG_ATTRIBUTE("Use -[FIRRemoteConfig defaultValueForKey:] instead."); + +@end diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploadPackage.m b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig.h similarity index 85% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploadPackage.m rename to FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig.h index 77cd92c7006..9ae8cea4a3c 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploadPackage.m +++ b/FirebaseRemoteConfig/Sources/Public/FirebaseRemoteConfig.h @@ -14,8 +14,4 @@ * limitations under the License. */ -#import "GDTTests/Unit/Helpers/GDTTestUploadPackage.h" - -@implementation GDTTestUploadPackage - -@end +#import "FIRRemoteConfig.h" diff --git a/FirebaseRemoteConfig/Sources/RCNConfigConstants.h b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h new file mode 100644 index 00000000000..d1097c8451b --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigConstants.h @@ -0,0 +1,65 @@ +/* + * Copyright 2019 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 + +#define RCN_SEC_PER_MIN 60 +#define RCN_MSEC_PER_SEC 1000 + +/// Remote Config SDK internal version that is different than +/// FIRRemoteConfigPodVersion. This is for config server to track down iOS +/// client app version. Each version can only go up to 99. +static const int kRCNMajorVersion = 1; +static const int kRCNMinorVersion = 2; +static const int kRCNPatchVersion = 10; + +/// Key prefix applied to all the packages (bundle IDs) in internal metadata. +static NSString *const RCNInternalMetadataAllPackagesPrefix = @"all_packages"; + +/// HTTP connection default timeout in seconds. +static const NSTimeInterval RCNHTTPDefaultConnectionTimeout = 60; +/// Default duration of how long config data lasts to stay fresh. +static const NSTimeInterval RCNDefaultMinimumFetchInterval = 43200; + +/// Label for serial queue for read/write lock on ivars. +static const char *RCNRemoteConfigQueueLabel = "com.google.GoogleConfigService.FIRRemoteConfig"; + +/// Constants for key names in the fetch response. +/// Key that includes an array of template entries. +static NSString *const RCNFetchResponseKeyEntries = @"entries"; +/// Key that includes data for experiment descriptions in ABT. +static NSString *const RCNFetchResponseKeyExperimentDescriptions = @"experimentDescriptions"; +/// Error key. +static NSString *const RCNFetchResponseKeyError = @"error"; +/// Error code. +static NSString *const RCNFetchResponseKeyErrorCode = @"code"; +/// Error status. +static NSString *const RCNFetchResponseKeyErrorStatus = @"status"; +/// Error message. +static NSString *const RCNFetchResponseKeyErrorMessage = @"message"; +/// The current state of the backend template. +static NSString *const RCNFetchResponseKeyState = @"state"; +/// Default state (when not set). +static NSString *const RCNFetchResponseKeyStateUnspecified = @"INSTANCE_STATE_UNSPECIFIED"; +/// Config key/value map and/or ABT experiment list differs from last fetch. +/// TODO: Migrate to the new HTTP error codes once available in the backend. b/117182055 +static NSString *const RCNFetchResponseKeyStateUpdate = @"UPDATE"; +/// No template fetched. +static NSString *const RCNFetchResponseKeyStateNoTemplate = @"NO_TEMPLATE"; +/// Config key/value map and ABT experiment list both match last fetch. +static NSString *const RCNFetchResponseKeyStateNoChange = @"NO_CHANGE"; +/// Template found, but evaluates to empty (e.g. all keys omitted). +static NSString *const RCNFetchResponseKeyStateEmptyConfig = @"EMPTY_CONFIG"; diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.h b/FirebaseRemoteConfig/Sources/RCNConfigContent.h new file mode 100644 index 00000000000..cc83c2ebb19 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.h @@ -0,0 +1,60 @@ +/* + * Copyright 2019 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 + +typedef NS_ENUM(NSInteger, RCNDBSource) { + RCNDBSourceActive, + RCNDBSourceDefault, + RCNDBSourceFetched, +}; + +@class RCNConfigDBManager; + +/// This class handles all the config content that is fetched from the server, cached in local +/// config or persisted in database. +@interface RCNConfigContent : NSObject +/// Shared Singleton Instance ++ (instancetype)sharedInstance; + +/// Fetched config (aka pending config) data that is latest data from server that might or might +/// not be applied. +@property(nonatomic, readonly, copy) NSDictionary *fetchedConfig; +/// Active config that is available to external users; +@property(nonatomic, readonly, copy) NSDictionary *activeConfig; +/// Local default config that is provided by external users; +@property(nonatomic, readonly, copy) NSDictionary *defaultConfig; + +- (instancetype)init NS_UNAVAILABLE; + +/// Designated initializer; +- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager NS_DESIGNATED_INITIALIZER; + +/// Returns true if initalization succeeded. +- (BOOL)initializationSuccessful; + +/// Update config content from fetch response in JSON format. +- (void)updateConfigContentWithResponse:(NSDictionary *)response + forNamespace:(NSString *)FIRNamespace; + +/// Copy from a given dictionary to one of the data source. +/// @param fromDictionary The data to copy from. +/// @param source The data source to copy to(pending/active/default). +- (void)copyFromDictionary:(NSDictionary *)fromDictionary + toSource:(RCNDBSource)source + forNamespace:(NSString *)FIRNamespace; + +@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigContent.m b/FirebaseRemoteConfig/Sources/RCNConfigContent.m new file mode 100644 index 00000000000..3932a4e8482 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigContent.m @@ -0,0 +1,337 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Sources/RCNConfigContent.h" + +#import +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" + +#import +#import + +@implementation RCNConfigContent { + /// Active config data that is currently used. + NSMutableDictionary *_activeConfig; + /// Pending config (aka Fetched config) data that is latest data from server that might or might + /// not be applied. + NSMutableDictionary *_fetchedConfig; + /// Default config provided by user. + NSMutableDictionary *_defaultConfig; + /// DBManager + RCNConfigDBManager *_DBManager; + /// Current bundle identifier; + NSString *_bundleIdentifier; + /// Dispatch semaphore to block all config reads until we have read from the database. This only + /// potentially blocks on the first read. Should be a no-wait for all subsequent reads once we + /// have data read into memory from the database. + dispatch_semaphore_t _configLoadFromDBSemaphore; + /// Boolean indicating if initial DB load of fetched,active and default config has succeeded. + BOOL _isConfigLoadFromDBCompleted; + /// Boolean indicating that the load from database has initiated at least once. + BOOL _isDatabaseLoadAlreadyInitiated; +} + +/// Default timeout when waiting to read data from database. +static const NSTimeInterval kDatabaseLoadTimeoutSecs = 30.0; + +/// Singleton instance of RCNConfigContent. ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + static RCNConfigContent *sharedInstance; + dispatch_once(&onceToken, ^{ + sharedInstance = + [[RCNConfigContent alloc] initWithDBManager:[RCNConfigDBManager sharedInstance]]; + }); + return sharedInstance; +} + +- (instancetype)init { + NSAssert(NO, @"Invalid initializer."); + return nil; +} + +/// Designated initializer +- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager { + self = [super init]; + if (self) { + _activeConfig = [[NSMutableDictionary alloc] init]; + _fetchedConfig = [[NSMutableDictionary alloc] init]; + _defaultConfig = [[NSMutableDictionary alloc] init]; + _bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; + if (!_bundleIdentifier) { + FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038", + @"Main bundle identifier is missing. Remote Config might not work properly."); + _bundleIdentifier = @""; + } + _DBManager = DBManager; + _configLoadFromDBSemaphore = dispatch_semaphore_create(0); + [self loadConfigFromMainTable]; + } + return self; +} + +// Blocking call that returns true/false once database load completes / times out. +// @return Initialization status. +- (BOOL)initializationSuccessful { + RCN_MUST_NOT_BE_MAIN_THREAD(); + BOOL isDatabaseLoadSuccessful = [self checkAndWaitForInitialDatabaseLoad]; + return isDatabaseLoadSuccessful; +} + +#pragma mark - update +/// This function is for copying dictionary when user set up a default config or when user clicks +/// activate. For now the DBSource can only be Active or Default. +- (void)copyFromDictionary:(NSDictionary *)fromDict + toSource:(RCNDBSource)DBSource + forNamespace:(NSString *)FIRNamespace { + // Make sure database load has completed. + [self checkAndWaitForInitialDatabaseLoad]; + NSMutableDictionary *toDict; + if (!fromDict) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000007", + @"The source dictionary to copy from does not exist."); + return; + } + FIRRemoteConfigSource source = FIRRemoteConfigSourceRemote; + switch (DBSource) { + case RCNDBSourceDefault: + toDict = _defaultConfig; + source = FIRRemoteConfigSourceDefault; + break; + case RCNDBSourceFetched: + FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000008", + @"This shouldn't happen. Destination dictionary should never be pending type."); + return; + case RCNDBSourceActive: + toDict = _activeConfig; + source = FIRRemoteConfigSourceRemote; + [toDict removeObjectForKey:FIRNamespace]; + break; + default: + toDict = _activeConfig; + source = FIRRemoteConfigSourceRemote; + [toDict removeObjectForKey:FIRNamespace]; + break; + } + + // Completely wipe out DB first. + [_DBManager deleteRecordFromMainTableWithNamespace:FIRNamespace + bundleIdentifier:_bundleIdentifier + fromSource:DBSource]; + + toDict[FIRNamespace] = [[NSMutableDictionary alloc] init]; + NSDictionary *config = fromDict[FIRNamespace]; + for (NSString *key in config) { + if (DBSource == FIRRemoteConfigSourceDefault) { + NSObject *value = config[key]; + NSData *valueData; + if ([value isKindOfClass:[NSData class]]) { + valueData = (NSData *)value; + } else if ([value isKindOfClass:[NSString class]]) { + valueData = [(NSString *)value dataUsingEncoding:NSUTF8StringEncoding]; + } else if ([value isKindOfClass:[NSNumber class]]) { + NSString *strValue = [(NSNumber *)value stringValue]; + valueData = [(NSString *)strValue dataUsingEncoding:NSUTF8StringEncoding]; + } else if ([value isKindOfClass:[NSDate class]]) { + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + NSString *strValue = [dateFormatter stringFromDate:(NSDate *)value]; + valueData = [(NSString *)strValue dataUsingEncoding:NSUTF8StringEncoding]; + } else { + continue; + } + toDict[FIRNamespace][key] = [[FIRRemoteConfigValue alloc] initWithData:valueData + source:source]; + NSArray *values = @[ _bundleIdentifier, FIRNamespace, key, valueData ]; + [self updateMainTableWithValues:values fromSource:DBSource]; + } else { + FIRRemoteConfigValue *value = config[key]; + toDict[FIRNamespace][key] = [[FIRRemoteConfigValue alloc] initWithData:value.dataValue + source:source]; + NSArray *values = @[ _bundleIdentifier, FIRNamespace, key, value.dataValue ]; + [self updateMainTableWithValues:values fromSource:DBSource]; + } + } +} + +- (void)updateConfigContentWithResponse:(NSDictionary *)response + forNamespace:(NSString *)currentNamespace { + // Make sure database load has completed. + [self checkAndWaitForInitialDatabaseLoad]; + NSString *state = response[RCNFetchResponseKeyState]; + + if (!state) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000049", @"State field in fetch response is nil."); + return; + } + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000059", + @"Updating config content from Response for namespace:%@ with state: %@", + currentNamespace, response[RCNFetchResponseKeyState]); + + if ([state isEqualToString:RCNFetchResponseKeyStateNoChange]) { + [self handleNoChangeStateForConfigNamespace:currentNamespace]; + return; + } + + /// Handle empty config state + if ([state isEqualToString:RCNFetchResponseKeyStateEmptyConfig]) { + [self handleEmptyConfigStateForConfigNamespace:currentNamespace]; + return; + } + + /// Handle no template state. + if ([state isEqualToString:RCNFetchResponseKeyStateNoTemplate]) { + [self handleNoTemplateStateForConfigNamespace:currentNamespace]; + return; + } + + /// Handle update state + if ([state isEqualToString:RCNFetchResponseKeyStateUpdate]) { + [self handleUpdateStateForConfigNamespace:currentNamespace + withEntries:response[RCNFetchResponseKeyEntries]]; + return; + } +} + +#pragma mark State handling +- (void)handleNoChangeStateForConfigNamespace:(NSString *)currentNamespace { + if (!_fetchedConfig[currentNamespace]) { + _fetchedConfig[currentNamespace] = [[NSMutableDictionary alloc] init]; + } +} + +- (void)handleEmptyConfigStateForConfigNamespace:(NSString *)currentNamespace { + if (_fetchedConfig[currentNamespace]) { + [_fetchedConfig[currentNamespace] removeAllObjects]; + } else { + // If namespace has empty status and it doesn't exist in _fetchedConfig, we will + // still add an entry for that namespace. Even if it will not be persisted in database. + // TODO: Add generics for all collection types. + _fetchedConfig[currentNamespace] = [[NSMutableDictionary alloc] init]; + } + [_DBManager deleteRecordFromMainTableWithNamespace:currentNamespace + bundleIdentifier:_bundleIdentifier + fromSource:RCNDBSourceFetched]; +} + +- (void)handleNoTemplateStateForConfigNamespace:(NSString *)currentNamespace { + // Remove the namespace. + [_fetchedConfig removeObjectForKey:currentNamespace]; + [_DBManager deleteRecordFromMainTableWithNamespace:currentNamespace + bundleIdentifier:_bundleIdentifier + fromSource:RCNDBSourceFetched]; +} +- (void)handleUpdateStateForConfigNamespace:(NSString *)currentNamespace + withEntries:(NSDictionary *)entries { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000058", @"Update config in DB for namespace:%@", + currentNamespace); + // Clear before updating + [_DBManager deleteRecordFromMainTableWithNamespace:currentNamespace + bundleIdentifier:_bundleIdentifier + fromSource:RCNDBSourceFetched]; + if ([_fetchedConfig objectForKey:currentNamespace]) { + [_fetchedConfig[currentNamespace] removeAllObjects]; + } else { + _fetchedConfig[currentNamespace] = [[NSMutableDictionary alloc] init]; + } + + // Store the fetched config values. + for (NSString *key in entries) { + NSData *valueData = [entries[key] dataUsingEncoding:NSUTF8StringEncoding]; + if (!valueData) { + continue; + } + _fetchedConfig[currentNamespace][key] = + [[FIRRemoteConfigValue alloc] initWithData:valueData source:FIRRemoteConfigSourceRemote]; + NSArray *values = @[ _bundleIdentifier, currentNamespace, key, valueData ]; + [self updateMainTableWithValues:values fromSource:RCNDBSourceFetched]; + } +} + +#pragma mark - database + +/// This method is only meant to be called at init time. The underlying logic will need to be +/// revaluated if the assumption changes at a later time. +- (void)loadConfigFromMainTable { + if (!_DBManager) { + return; + } + + NSAssert(!_isDatabaseLoadAlreadyInitiated, @"Database load has already been initiated"); + _isDatabaseLoadAlreadyInitiated = true; + + [_DBManager + loadMainWithBundleIdentifier:_bundleIdentifier + completionHandler:^(BOOL success, NSDictionary *fetchedConfig, + NSDictionary *activeConfig, NSDictionary *defaultConfig) { + self->_fetchedConfig = [fetchedConfig mutableCopy]; + self->_activeConfig = [activeConfig mutableCopy]; + self->_defaultConfig = [defaultConfig mutableCopy]; + dispatch_semaphore_signal(self->_configLoadFromDBSemaphore); + }]; +} + +/// Update the current config result to main table. +/// @param values Values in a row to write to the table. +/// @param source The source the config data is coming from. It determines which table to write to. +- (void)updateMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source { + [_DBManager insertMainTableWithValues:values fromSource:source completionHandler:nil]; +} +#pragma mark - getter/setter +- (NSDictionary *)fetchedConfig { + /// If this is the first time reading the fetchedConfig, we might still be reading it from the + /// database. + [self checkAndWaitForInitialDatabaseLoad]; + return _fetchedConfig; +} + +- (NSDictionary *)activeConfig { + /// If this is the first time reading the activeConfig, we might still be reading it from the + /// database. + [self checkAndWaitForInitialDatabaseLoad]; + return _activeConfig; +} + +- (NSDictionary *)defaultConfig { + /// If this is the first time reading the fetchedConfig, we might still be reading it from the + /// database. + [self checkAndWaitForInitialDatabaseLoad]; + return _defaultConfig; +} + +/// We load the database async at init time. Block all further calls to active/fetched/default +/// configs until load is done. +/// @return Database load completion status. +- (BOOL)checkAndWaitForInitialDatabaseLoad { + /// Wait on semaphore until done. This should be a no-op for subsequent calls. + if (!_isConfigLoadFromDBCompleted) { + long result = dispatch_semaphore_wait( + _configLoadFromDBSemaphore, + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDatabaseLoadTimeoutSecs * NSEC_PER_SEC))); + if (result != 0) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000048", + @"Timed out waiting for fetched config to be loaded from DB"); + return false; + } + _isConfigLoadFromDBCompleted = true; + } + return true; +} + +@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h new file mode 100644 index 00000000000..94a7dca685a --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.h @@ -0,0 +1,118 @@ +/* + * Copyright 2019 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 + +#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" + +typedef NS_ENUM(NSInteger, RCNUpdateOption) { + RCNUpdateOptionApplyTime, + RCNUpdateOptionDefaultTime, + RCNUpdateOptionFetchStatus, +}; + +/// Column names in metadata table +static NSString *const RCNKeyBundleIdentifier = @"bundle_identifier"; +static NSString *const RCNKeyFetchTime = @"fetch_time"; +static NSString *const RCNKeyDigestPerNamespace = @"digest_per_ns"; +static NSString *const RCNKeyDeviceContext = @"device_context"; +static NSString *const RCNKeyAppContext = @"app_context"; +static NSString *const RCNKeySuccessFetchTime = @"success_fetch_time"; +static NSString *const RCNKeyFailureFetchTime = @"failure_fetch_time"; +static NSString *const RCNKeyLastFetchStatus = @"last_fetch_status"; +static NSString *const RCNKeyLastFetchError = @"last_fetch_error"; +static NSString *const RCNKeyLastApplyTime = @"last_apply_time"; +static NSString *const RCNKeyLastSetDefaultsTime = @"last_set_defaults_time"; + +/// Persist config data in sqlite database on device. Managing data read/write from/to database. +@interface RCNConfigDBManager : NSObject +/// Shared Singleton Instance ++ (instancetype)sharedInstance; + +/// Database Operation Completion callback. +/// @param success Decide whether the DB operation succeeds. +/// @param result Return operation result data. +typedef void (^RCNDBCompletion)(BOOL success, NSDictionary *result); + +/// Database Load Operation Completion callback. +/// @param success Decide whether the DB operation succeeds. +/// @param fetchedConfig Return fetchedConfig loaded from DB +/// @param activeConfig Return activeConfig loaded from DB +/// @param defaultConfig Return defaultConfig loaded from DB +typedef void (^RCNDBLoadCompletion)(BOOL success, + NSDictionary *fetchedConfig, + NSDictionary *activeConfig, + NSDictionary *defaultConfig); + +/// Returns the current version of the Remote Config database. ++ (NSString *)remoteConfigPathForDatabase; + +/// Load config content from main table to cached memory during app start. +- (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier + completionHandler:(RCNDBLoadCompletion)handler; +/// Load config settings from metadata table to cached memory during app start. Config settings +/// include success/failure fetch times, device contenxt, app context, etc. +- (NSDictionary *)loadMetadataWithBundleIdentifier:(NSString *)bundleIdentifier; +/// Load internal metadata from internal metadata table, such as customized HTTP connection/read +/// timeout, throttling time interval and number limit of throttling, etc. +/// This call needs to be blocking to ensure throttling works during apps starts. +- (NSDictionary *)loadInternalMetadataTable; +/// Load experiment from experiment table. +/// @param handler The callback when reading from DB is complete. +- (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler; + +/// Insert a record in metadata table. +/// @param columnNameToValue The column name and its value to be inserted in metadata table. +/// @param handler The callback. +- (void)insertMetadataTableWithValues:(NSDictionary *)columnNameToValue + completionHandler:(RCNDBCompletion)handler; +/// Insert a record in main table. +/// @param values Values to be inserted. +- (void)insertMainTableWithValues:(NSArray *)values + fromSource:(RCNDBSource)source + completionHandler:(RCNDBCompletion)handler; +/// Insert a record in internal metadata table. +/// @param values Values to be inserted. +- (void)insertInternalMetadataTableWithValues:(NSArray *)values + completionHandler:(RCNDBCompletion)handler; +/// Insert exepriment data in experiment table. +/// @param key The key of experiment data belongs to, which are defined in +/// RCNConfigDefines.h. +/// @param value The value that experiment. +/// @param handler The callback. +- (void)insertExperimentTableWithKey:(NSString *)key + value:(NSData *)value + completionHandler:(RCNDBCompletion)handler; + +- (void)updateMetadataWithOption:(RCNUpdateOption)option + values:(NSArray *)values + completionHandler:(RCNDBCompletion)handler; +/// Clear the record of given namespace and package name +/// before updating the table. +- (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_p + bundleIdentifier:(NSString *)bundleIdentifier + fromSource:(RCNDBSource)source; +/// Remove all the records of given package name from metadata/internal metadata DB before updating +/// new values from response. +- (void)deleteRecordWithBundleIdentifier:(NSString *)bundlerIdentifier + isInternalDB:(BOOL)isInternalDB; +/// Remove all the records from a config content table. +- (void)deleteAllRecordsFromTableWithSource:(RCNDBSource)source; + +/// Remove all the records from experiment table with given key. +/// @param key The key of experiment data belongs to, which are defined in RCNConfigDefines.h. +- (void)deleteExperimentTableForKey:(NSString *)key; +@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m new file mode 100644 index 00000000000..cbe9e1f3fe4 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigDBManager.m @@ -0,0 +1,1039 @@ +/* + * Copyright 2019 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 + +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" + +#import +#import + +/// Using macro for securely preprocessing string concatenation in query before runtime. +#define RCNTableNameMain "main" +#define RCNTableNameMainActive "main_active" +#define RCNTableNameMainDefault "main_default" +#define RCNTableNameMetadata "fetch_metadata" +#define RCNTableNameInternalMetadata "internal_metadata" +#define RCNTableNameExperiment "experiment" + +/// SQLite file name in versions 0, 1 and 2. +static NSString *const RCNDatabaseName = @"RemoteConfig.sqlite3"; +/// The application support sub-directory that the Remote Config database resides in. +static NSString *const RCNRemoteConfigApplicationSupportSubDirectory = @"Google/RemoteConfig"; + +/// Remote Config database path for deprecated V0 version. +static NSString *RemoteConfigPathForOldDatabaseV0() { + NSArray *dirPaths = + NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + NSString *docPath = dirPaths.firstObject; + return [docPath stringByAppendingPathComponent:RCNDatabaseName]; +} + +/// Remote Config database path for current database. +static NSString *RemoteConfigPathForDatabase(void) { + NSArray *dirPaths = + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString *appSupportPath = dirPaths.firstObject; + NSArray *components = + @[ appSupportPath, RCNRemoteConfigApplicationSupportSubDirectory, RCNDatabaseName ]; + return [NSString pathWithComponents:components]; +} + +static BOOL RemoteConfigAddSkipBackupAttributeToItemAtPath(NSString *filePathString) { + NSURL *URL = [NSURL fileURLWithPath:filePathString]; + assert([[NSFileManager defaultManager] fileExistsAtPath:[URL path]]); + + NSError *error = nil; + BOOL success = [URL setResourceValue:[NSNumber numberWithBool:YES] + forKey:NSURLIsExcludedFromBackupKey + error:&error]; + if (!success) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000017", @"Error excluding %@ from backup %@.", + [URL lastPathComponent], error); + } + return success; +} + +static BOOL RemoteConfigCreateFilePathIfNotExist(NSString *filePath) { + if (!filePath || !filePath.length) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000018", + @"Failed to create subdirectory for an empty file path."); + return NO; + } + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:filePath]) { + NSError *error; + [fileManager createDirectoryAtPath:[filePath stringByDeletingLastPathComponent] + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (error) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000019", + @"Failed to create subdirectory for database file: %@.", error); + return NO; + } + } + return YES; +} + +static NSArray *RemoteConfigMetadataTableColumnsInOrder() { + return @[ + RCNKeyBundleIdentifier, RCNKeyFetchTime, RCNKeyDigestPerNamespace, RCNKeyDeviceContext, + RCNKeyAppContext, RCNKeySuccessFetchTime, RCNKeyFailureFetchTime, RCNKeyLastFetchStatus, + RCNKeyLastFetchError, RCNKeyLastApplyTime, RCNKeyLastSetDefaultsTime + ]; +} + +@interface RCNConfigDBManager () { + /// Database storing all the config information. + sqlite3 *_database; + /// Serial queue for database read/write operations. + dispatch_queue_t _databaseOperationQueue; +} +@end + +@implementation RCNConfigDBManager + ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + static RCNConfigDBManager *sharedInstance; + dispatch_once(&onceToken, ^{ + sharedInstance = [[RCNConfigDBManager alloc] init]; + }); + return sharedInstance; +} + +/// Returns the current version of the Remote Config database. ++ (NSString *)remoteConfigPathForDatabase { + return RemoteConfigPathForDatabase(); +} + +- (instancetype)init { + self = [super init]; + if (self) { + _databaseOperationQueue = + dispatch_queue_create("com.google.GoogleConfigService.database", DISPATCH_QUEUE_SERIAL); + [self createOrOpenDatabase]; + } + return self; +} + +#pragma mark - database +- (void)migrateV1NamespaceToV2Namespace { + for (int table = 0; table < 3; table++) { + NSString *tableName = @"" RCNTableNameMain; + switch (table) { + case 1: + tableName = @"" RCNTableNameMainActive; + break; + case 2: + tableName = @"" RCNTableNameMainDefault; + break; + default: + break; + } + NSString *SQLString = [NSString + stringWithFormat:@"SELECT namespace FROM %@ WHERE namespace NOT LIKE '%%:%%'", tableName]; + const char *SQL = [SQLString UTF8String]; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return; + } + NSMutableArray *namespaceArray = [[NSMutableArray alloc] init]; + while (sqlite3_step(statement) == SQLITE_ROW) { + NSString *configNamespace = + [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)]; + [namespaceArray addObject:configNamespace]; + } + sqlite3_finalize(statement); + + // Update. + for (NSString *namespaceToUpdate in namespaceArray) { + NSString *newNamespace = + [NSString stringWithFormat:@"%@:%@", namespaceToUpdate, kFIRDefaultAppName]; + NSString *updateSQLString = + [NSString stringWithFormat:@"UPDATE %@ SET namespace = ? WHERE namespace = ?", tableName]; + const char *updateSQL = [updateSQLString UTF8String]; + sqlite3_stmt *updateStatement = [self prepareSQL:updateSQL]; + if (!updateStatement) { + return; + } + NSArray *updateParams = @[ newNamespace, namespaceToUpdate ]; + [self bindStringsToStatement:updateStatement stringArray:updateParams]; + + int result = sqlite3_step(updateStatement); + if (result != SQLITE_DONE) { + [self logErrorWithSQL:SQL finalizeStatement:updateStatement returnValue:NO]; + return; + } + sqlite3_finalize(updateStatement); + } + } +} + +- (void)createOrOpenDatabase { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + RCNConfigDBManager *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + NSString *oldV0DBPath = RemoteConfigPathForOldDatabaseV0(); + // Backward Compatibility + if ([[NSFileManager defaultManager] fileExistsAtPath:oldV0DBPath]) { + FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000009", + @"Old database V0 exists, removed it and replace with the new one."); + [strongSelf removeDatabase:oldV0DBPath]; + } + NSString *dbPath = [RCNConfigDBManager remoteConfigPathForDatabase]; + FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000062", @"Loading database at path %@", dbPath); + const char *databasePath = dbPath.UTF8String; + // Create or open database path. + if (!RemoteConfigCreateFilePathIfNotExist(dbPath)) { + return; + } + int flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FILEPROTECTION_COMPLETE | + SQLITE_OPEN_FULLMUTEX; + if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) { + // Always try to create table if not exists for backward compatibility. + if (![strongSelf createTableSchema]) { + // Remove database before fail. + [strongSelf removeDatabase:dbPath]; + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010", @"Failed to create table."); + // Create a new database if existing database file is corrupted. + if (!RemoteConfigCreateFilePathIfNotExist(dbPath)) { + return; + } + if (sqlite3_open_v2(databasePath, &strongSelf->_database, flags, NULL) == SQLITE_OK) { + if (![strongSelf createTableSchema]) { + // Remove database before fail. + [strongSelf removeDatabase:dbPath]; + // If it failed again, there's nothing we can do here. + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000010", @"Failed to create table."); + } else { + // Exclude the app data used from iCloud backup. + RemoteConfigAddSkipBackupAttributeToItemAtPath(dbPath); + } + } else { + [strongSelf logDatabaseError]; + } + } else { + // DB file already exists. Migrate any V1 namespace column entries to V2 fully qualified + // 'namespace:FIRApp' entries. + [self migrateV1NamespaceToV2Namespace]; + // Exclude the app data used from iCloud backup. + RemoteConfigAddSkipBackupAttributeToItemAtPath(dbPath); + } + } else { + [strongSelf logDatabaseError]; + } + }); +} + +- (BOOL)createTableSchema { + RCN_MUST_NOT_BE_MAIN_THREAD(); + static const char *createTableMain = + "create TABLE IF NOT EXISTS " RCNTableNameMain + " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)"; + + static const char *createTableMainActive = + "create TABLE IF NOT EXISTS " RCNTableNameMainActive + " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)"; + + static const char *createTableMainDefault = + "create TABLE IF NOT EXISTS " RCNTableNameMainDefault + " (_id INTEGER PRIMARY KEY, bundle_identifier TEXT, namespace TEXT, key TEXT, value BLOB)"; + + static const char *createTableMetadata = + "create TABLE IF NOT EXISTS " RCNTableNameMetadata + " (_id INTEGER PRIMARY KEY, bundle_identifier" + " TEXT, fetch_time INTEGER, digest_per_ns BLOB, device_context BLOB, app_context BLOB, " + "success_fetch_time BLOB, failure_fetch_time BLOB, last_fetch_status INTEGER, " + "last_fetch_error INTEGER, last_apply_time INTEGER, last_set_defaults_time INTEGER)"; + + static const char *createTableInternalMetadata = + "create TABLE IF NOT EXISTS " RCNTableNameInternalMetadata + " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)"; + + static const char *createTableExperiment = "create TABLE IF NOT EXISTS " RCNTableNameExperiment + " (_id INTEGER PRIMARY KEY, key TEXT, value BLOB)"; + + return [self executeQuery:createTableMain] && [self executeQuery:createTableMainActive] && + [self executeQuery:createTableMainDefault] && [self executeQuery:createTableMetadata] && + [self executeQuery:createTableInternalMetadata] && + [self executeQuery:createTableExperiment]; +} + +- (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_sync(_databaseOperationQueue, ^{ + RCNConfigDBManager *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + if (sqlite3_close(strongSelf->_database) != SQLITE_OK) { + [self logDatabaseError]; + } + strongSelf->_database = nil; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + if (![fileManager removeItemAtPath:path error:&error]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011", + @"Failed to remove database at path %@ for error %@.", path, error); + } + }); +} + +- (void)removeDatabase:(NSString *)path { + if (sqlite3_close(_database) != SQLITE_OK) { + [self logDatabaseError]; + } + _database = nil; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error; + if (![fileManager removeItemAtPath:path error:&error]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000011", + @"Failed to remove database at path %@ for error %@.", path, error); + } +} + +#pragma mark - execute +- (BOOL)executeQuery:(const char *)SQL { + RCN_MUST_NOT_BE_MAIN_THREAD(); + char *error; + if (sqlite3_exec(_database, SQL, nil, nil, &error) != SQLITE_OK) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000012", @"Failed to execute query with error %s.", + error); + return NO; + } + return YES; +} + +#pragma mark - insert +- (void)insertMetadataTableWithValues:(NSDictionary *)columnNameToValue + completionHandler:(RCNDBCompletion)handler { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + BOOL success = [weakSelf insertMetadataTableWithValues:columnNameToValue]; + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(success, nil); + }); + } + }); +} + +- (BOOL)insertMetadataTableWithValues:(NSDictionary *)columnNameToValue { + RCN_MUST_NOT_BE_MAIN_THREAD(); + static const char *SQL = + "INSERT INTO " RCNTableNameMetadata + " (bundle_identifier, fetch_time, digest_per_ns, device_context, " + "app_context, success_fetch_time, failure_fetch_time, last_fetch_status, " + "last_fetch_error, last_apply_time, last_set_defaults_time) values (?, ?, ?, ?, ?, " + "?, ?, ?, ?, ?, ?)"; + + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + [self logErrorWithSQL:SQL finalizeStatement:nil returnValue:NO]; + return NO; + } + + NSArray *columns = RemoteConfigMetadataTableColumnsInOrder(); + int index = 0; + for (NSString *columnName in columns) { + if ([columnName isEqualToString:RCNKeyBundleIdentifier]) { + NSString *value = columnNameToValue[columnName]; + if (![self bindStringToStatement:statement index:++index string:value]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + } else if ([columnName isEqualToString:RCNKeyFetchTime] || + [columnName isEqualToString:RCNKeyLastApplyTime] || + [columnName isEqualToString:RCNKeyLastSetDefaultsTime]) { + double value = [columnNameToValue[columnName] doubleValue]; + if (sqlite3_bind_double(statement, ++index, value) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + } else if ([columnName isEqualToString:RCNKeyLastFetchStatus] || + [columnName isEqualToString:RCNKeyLastFetchError]) { + int value = [columnNameToValue[columnName] intValue]; + if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + } else { + NSData *data = columnNameToValue[columnName]; + if (sqlite3_bind_blob(statement, ++index, data.bytes, (int)data.length, NULL) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + } + } + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + +- (void)insertMainTableWithValues:(NSArray *)values + fromSource:(RCNDBSource)source + completionHandler:(RCNDBCompletion)handler { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + BOOL success = [weakSelf insertMainTableWithValues:values fromSource:source]; + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(success, nil); + }); + } + }); +} + +- (BOOL)insertMainTableWithValues:(NSArray *)values fromSource:(RCNDBSource)source { + RCN_MUST_NOT_BE_MAIN_THREAD(); + if (values.count != 4) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000013", + @"Failed to insert config record. Wrong number of give parameters, current " + @"number is %ld, correct number is 4.", + (long)values.count); + return NO; + } + const char *SQL = "INSERT INTO " RCNTableNameMain + " (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)"; + if (source == RCNDBSourceDefault) { + SQL = "INSERT INTO " RCNTableNameMainDefault + " (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)"; + } else if (source == RCNDBSourceActive) { + SQL = "INSERT INTO " RCNTableNameMainActive + " (bundle_identifier, namespace, key, value) values (?, ?, ?, ?)"; + } + + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + + NSString *aString = values[0]; + if (![self bindStringToStatement:statement index:1 string:aString]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + aString = values[1]; + if (![self bindStringToStatement:statement index:2 string:aString]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + aString = values[2]; + if (![self bindStringToStatement:statement index:3 string:aString]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + NSData *blobData = values[3]; + if (sqlite3_bind_blob(statement, 4, blobData.bytes, (int)blobData.length, NULL) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + +- (void)insertInternalMetadataTableWithValues:(NSArray *)values + completionHandler:(RCNDBCompletion)handler { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + BOOL success = [weakSelf insertInternalMetadataWithValues:values]; + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(success, nil); + }); + } + }); +} + +- (BOOL)insertInternalMetadataWithValues:(NSArray *)values { + RCN_MUST_NOT_BE_MAIN_THREAD(); + if (values.count != 2) { + return NO; + } + const char *SQL = + "INSERT OR REPLACE INTO " RCNTableNameInternalMetadata " (key, value) values (?, ?)"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + NSString *aString = values[0]; + if (![self bindStringToStatement:statement index:1 string:aString]) { + [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + return NO; + } + NSData *blobData = values[1]; + if (sqlite3_bind_blob(statement, 2, blobData.bytes, (int)blobData.length, NULL) != SQLITE_OK) { + [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + return NO; + } + if (sqlite3_step(statement) != SQLITE_DONE) { + [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + return NO; + } + sqlite3_finalize(statement); + return YES; +} + +- (void)insertExperimentTableWithKey:(NSString *)key + value:(NSData *)serializedValue + completionHandler:(RCNDBCompletion)handler { + dispatch_async(_databaseOperationQueue, ^{ + BOOL success = [self insertExperimentTableWithKey:key value:serializedValue]; + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(success, nil); + }); + } + }); +} + +- (BOOL)insertExperimentTableWithKey:(NSString *)key value:(NSData *)dataValue { + if ([key isEqualToString:@RCNExperimentTableKeyMetadata]) { + return [self updateExperimentMetadata:dataValue]; + } + + RCN_MUST_NOT_BE_MAIN_THREAD(); + const char *SQL = "INSERT INTO " RCNTableNameExperiment " (key, value) values (?, ?)"; + + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + + if (![self bindStringToStatement:statement index:1 string:key]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_bind_blob(statement, 2, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + +- (BOOL)updateExperimentMetadata:(NSData *)dataValue { + RCN_MUST_NOT_BE_MAIN_THREAD(); + const char *SQL = "INSERT OR REPLACE INTO " RCNTableNameExperiment + " (_id, key, value) values ((SELECT _id from " RCNTableNameExperiment + " WHERE key = ?), ?, ?)"; + + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + + if (![self bindStringToStatement:statement index:1 string:@RCNExperimentTableKeyMetadata]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (![self bindStringToStatement:statement index:2 string:@RCNExperimentTableKeyMetadata]) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + if (sqlite3_bind_blob(statement, 3, dataValue.bytes, (int)dataValue.length, NULL) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + +#pragma mark - update + +- (void)updateMetadataWithOption:(RCNUpdateOption)option + values:(NSArray *)values + completionHandler:(RCNDBCompletion)handler { + dispatch_async(_databaseOperationQueue, ^{ + BOOL success = [self updateMetadataTableWithOption:option andValues:values]; + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler(success, nil); + }); + } + }); +} + +- (BOOL)updateMetadataTableWithOption:(RCNUpdateOption)option andValues:(NSArray *)values { + RCN_MUST_NOT_BE_MAIN_THREAD(); + static const char *SQL = + "UPDATE " RCNTableNameMetadata " (last_fetch_status, last_fetch_error, last_apply_time, " + "last_set_defaults_time) values (?, ?, ?, ?)"; + if (option == RCNUpdateOptionFetchStatus) { + SQL = "UPDATE " RCNTableNameMetadata " SET last_fetch_status = ?, last_fetch_error = ?"; + } else if (option == RCNUpdateOptionApplyTime) { + SQL = "UPDATE " RCNTableNameMetadata " SET last_apply_time = ?"; + } else if (option == RCNUpdateOptionDefaultTime) { + SQL = "UPDATE " RCNTableNameMetadata " SET last_set_defaults_time = ?"; + } else { + return NO; + } + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + + int index = 0; + if ((option == RCNUpdateOptionApplyTime || option == RCNUpdateOptionDefaultTime) && + values.count == 1) { + double value = [values[0] doubleValue]; + if (sqlite3_bind_double(statement, ++index, value) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + } else if (option == RCNUpdateOptionFetchStatus && values.count == 2) { + int value = [values[0] intValue]; + if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + value = [values[1] intValue]; + if (sqlite3_bind_int(statement, ++index, value) != SQLITE_OK) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + } + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} +#pragma mark - read from DB + +- (NSDictionary *)loadMetadataWithBundleIdentifier:(NSString *)bundleIdentifier { + __block NSDictionary *metadataTableResult; + __weak RCNConfigDBManager *weakSelf = self; + dispatch_sync(_databaseOperationQueue, ^{ + metadataTableResult = [weakSelf loadMetadataTableWithBundleIdentifier:bundleIdentifier]; + }); + if (metadataTableResult) { + return metadataTableResult; + } + return [[NSDictionary alloc] init]; +} + +- (NSMutableDictionary *)loadMetadataTableWithBundleIdentifier:(NSString *)bundleIdentifier { + NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; + const char *SQL = + "SELECT bundle_identifier, fetch_time, digest_per_ns, device_context, app_context, " + "success_fetch_time, failure_fetch_time , last_fetch_status, " + "last_fetch_error, last_apply_time, last_set_defaults_time FROM " RCNTableNameMetadata + " WHERE bundle_identifier = ?"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return nil; + } + + NSArray *params = @[ bundleIdentifier ]; + [self bindStringsToStatement:statement stringArray:params]; + + while (sqlite3_step(statement) == SQLITE_ROW) { + NSString *dbBundleIdentifier = + [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)]; + + if (dbBundleIdentifier && ![dbBundleIdentifier isEqualToString:bundleIdentifier]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000014", + @"Load Metadata from table error: Wrong package name %@, should be %@.", + dbBundleIdentifier, bundleIdentifier); + return nil; + } + + double fetchTime = sqlite3_column_double(statement, 1); + NSData *digestPerNamespace = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 2) + length:sqlite3_column_bytes(statement, 2)]; + NSData *deviceContext = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 3) + length:sqlite3_column_bytes(statement, 3)]; + NSData *appContext = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 4) + length:sqlite3_column_bytes(statement, 4)]; + NSData *successTimeDigest = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 5) + length:sqlite3_column_bytes(statement, 5)]; + NSData *failureTimeDigest = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 6) + length:sqlite3_column_bytes(statement, 6)]; + + int lastFetchStatus = sqlite3_column_int(statement, 7); + int lastFetchFailReason = sqlite3_column_int(statement, 8); + double lastApplyTimestamp = sqlite3_column_double(statement, 9); + double lastSetDefaultsTimestamp = sqlite3_column_double(statement, 10); + + NSError *error; + NSMutableDictionary *deviceContextDict = nil; + if (deviceContext) { + deviceContextDict = [NSJSONSerialization JSONObjectWithData:deviceContext + options:NSJSONReadingMutableContainers + error:&error]; + } + + NSMutableDictionary *appContextDict = nil; + if (appContext) { + appContextDict = [NSJSONSerialization JSONObjectWithData:appContext + options:NSJSONReadingMutableContainers + error:&error]; + } + + NSMutableDictionary *digestPerNamespaceDictionary = nil; + if (digestPerNamespace) { + digestPerNamespaceDictionary = + [NSJSONSerialization JSONObjectWithData:digestPerNamespace + options:NSJSONReadingMutableContainers + error:&error]; + } + + NSMutableArray *successTimes = nil; + if (successTimeDigest) { + successTimes = [NSJSONSerialization JSONObjectWithData:successTimeDigest + options:NSJSONReadingMutableContainers + error:&error]; + } + + NSMutableArray *failureTimes = nil; + if (failureTimeDigest) { + failureTimes = [NSJSONSerialization JSONObjectWithData:failureTimeDigest + options:NSJSONReadingMutableContainers + error:&error]; + } + + dict[RCNKeyBundleIdentifier] = bundleIdentifier; + dict[RCNKeyFetchTime] = @(fetchTime); + dict[RCNKeyDigestPerNamespace] = digestPerNamespaceDictionary; + dict[RCNKeyDeviceContext] = deviceContextDict; + dict[RCNKeyAppContext] = appContextDict; + dict[RCNKeySuccessFetchTime] = successTimes; + dict[RCNKeyFailureFetchTime] = failureTimes; + dict[RCNKeyLastFetchStatus] = @(lastFetchStatus); + dict[RCNKeyLastFetchError] = @(lastFetchFailReason); + dict[RCNKeyLastApplyTime] = @(lastApplyTimestamp); + dict[RCNKeyLastSetDefaultsTime] = @(lastSetDefaultsTimestamp); + + break; + } + sqlite3_finalize(statement); + return dict; +} + +- (void)loadExperimentWithCompletionHandler:(RCNDBCompletion)handler { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + RCNConfigDBManager *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + NSMutableArray *experimentPayloads = + [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyPayload]; + if (!experimentPayloads) { + experimentPayloads = [[NSMutableArray alloc] init]; + } + + NSMutableDictionary *experimentMetadata; + NSMutableArray *experiments = + [strongSelf loadExperimentTableFromKey:@RCNExperimentTableKeyMetadata]; + // There should be only one entry for experiment metadata. + if (experiments.count > 0) { + NSError *error; + experimentMetadata = [NSJSONSerialization JSONObjectWithData:experiments[0] + options:NSJSONReadingMutableContainers + error:&error]; + } + if (!experimentMetadata) { + experimentMetadata = [[NSMutableDictionary alloc] init]; + } + + if (handler) { + dispatch_async(dispatch_get_main_queue(), ^{ + handler( + YES, @{ + @RCNExperimentTableKeyPayload : [experimentPayloads copy], + @RCNExperimentTableKeyMetadata : [experimentMetadata copy] + }); + }); + } + }); +} + +- (NSMutableArray *)loadExperimentTableFromKey:(NSString *)key { + RCN_MUST_NOT_BE_MAIN_THREAD(); + + NSMutableArray *results = [[NSMutableArray alloc] init]; + const char *SQL = "SELECT value FROM " RCNTableNameExperiment " WHERE key = ?"; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return nil; + } + + NSArray *params = @[ key ]; + [self bindStringsToStatement:statement stringArray:params]; + NSData *experimentData; + while (sqlite3_step(statement) == SQLITE_ROW) { + experimentData = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 0) + length:sqlite3_column_bytes(statement, 0)]; + if (experimentData) { + [results addObject:experimentData]; + } + } + + sqlite3_finalize(statement); + return results; +} + +- (NSDictionary *)loadInternalMetadataTable { + __block NSMutableDictionary *internalMetadataTableResult; + __weak RCNConfigDBManager *weakSelf = self; + dispatch_sync(_databaseOperationQueue, ^{ + internalMetadataTableResult = [weakSelf loadInternalMetadataTableInternal]; + }); + return internalMetadataTableResult; +} + +- (NSMutableDictionary *)loadInternalMetadataTableInternal { + NSMutableDictionary *internalMetadata = [[NSMutableDictionary alloc] init]; + const char *SQL = "SELECT key, value FROM " RCNTableNameInternalMetadata; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return nil; + } + + while (sqlite3_step(statement) == SQLITE_ROW) { + NSString *key = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 0)]; + + NSData *dataValue = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 1) + length:sqlite3_column_bytes(statement, 1)]; + internalMetadata[key] = dataValue; + } + sqlite3_finalize(statement); + return internalMetadata; +} + +/// This method is only meant to be called at init time. The underlying logic will need to be +/// revaluated if the assumption changes at a later time. +- (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier + completionHandler:(RCNDBLoadCompletion)handler { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + RCNConfigDBManager *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + __block NSDictionary *fetchedConfig = + [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier + fromSource:RCNDBSourceFetched]; + __block NSDictionary *activeConfig = + [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier + fromSource:RCNDBSourceActive]; + __block NSDictionary *defaultConfig = + [strongSelf loadMainTableWithBundleIdentifier:bundleIdentifier + fromSource:RCNDBSourceDefault]; + if (handler) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + fetchedConfig = fetchedConfig ? fetchedConfig : [[NSDictionary alloc] init]; + activeConfig = activeConfig ? activeConfig : [[NSDictionary alloc] init]; + defaultConfig = defaultConfig ? defaultConfig : [[NSDictionary alloc] init]; + handler(YES, fetchedConfig, activeConfig, defaultConfig); + }); + } + }); +} + +- (NSMutableDictionary *)loadMainTableWithBundleIdentifier:(NSString *)bundleIdentifier + fromSource:(RCNDBSource)source { + NSMutableDictionary *namespaceToConfig = [[NSMutableDictionary alloc] init]; + const char *SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMain + " WHERE bundle_identifier = ?"; + if (source == RCNDBSourceDefault) { + SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMainDefault + " WHERE bundle_identifier = ?"; + } else if (source == RCNDBSourceActive) { + SQL = "SELECT bundle_identifier, namespace, key, value FROM " RCNTableNameMainActive + " WHERE bundle_identifier = ?"; + } + NSArray *params = @[ bundleIdentifier ]; + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return nil; + } + [self bindStringsToStatement:statement stringArray:params]; + + while (sqlite3_step(statement) == SQLITE_ROW) { + NSString *configNamespace = + [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 1)]; + NSString *key = [[NSString alloc] initWithUTF8String:(char *)sqlite3_column_text(statement, 2)]; + NSData *value = [NSData dataWithBytes:(char *)sqlite3_column_blob(statement, 3) + length:sqlite3_column_bytes(statement, 3)]; + if (!namespaceToConfig[configNamespace]) { + namespaceToConfig[configNamespace] = [[NSMutableDictionary alloc] init]; + } + + if (source == RCNDBSourceDefault) { + namespaceToConfig[configNamespace][key] = + [[FIRRemoteConfigValue alloc] initWithData:value source:FIRRemoteConfigSourceDefault]; + } else { + namespaceToConfig[configNamespace][key] = + [[FIRRemoteConfigValue alloc] initWithData:value source:FIRRemoteConfigSourceRemote]; + } + } + sqlite3_finalize(statement); + return namespaceToConfig; +} + +#pragma mark - delete +- (void)deleteRecordFromMainTableWithNamespace:(NSString *)namespace_p + bundleIdentifier:(NSString *)bundleIdentifier + fromSource:(RCNDBSource)source { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + RCNConfigDBManager *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + NSArray *params = @[ bundleIdentifier, namespace_p ]; + const char *SQL = + "DELETE FROM " RCNTableNameMain " WHERE bundle_identifier = ? and namespace = ?"; + if (source == RCNDBSourceDefault) { + SQL = "DELETE FROM " RCNTableNameMainDefault " WHERE bundle_identifier = ? and namespace = ?"; + } else if (source == RCNDBSourceActive) { + SQL = "DELETE FROM " RCNTableNameMainActive " WHERE bundle_identifier = ? and namespace = ?"; + } + [strongSelf executeQuery:SQL withParams:params]; + }); +} + +- (void)deleteRecordWithBundleIdentifier:(NSString *)bundleIdentifier + isInternalDB:(BOOL)isInternalDB { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + RCNConfigDBManager *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + const char *SQL = "DELETE FROM " RCNTableNameInternalMetadata " WHERE key LIKE ?"; + if (!isInternalDB) { + SQL = "DELETE FROM " RCNTableNameMetadata " WHERE bundle_identifier = ?"; + } + NSArray *params = @[ bundleIdentifier ]; + [strongSelf executeQuery:SQL withParams:params]; + }); +} + +- (void)deleteAllRecordsFromTableWithSource:(RCNDBSource)source { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + RCNConfigDBManager *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + const char *SQL = "DELETE FROM " RCNTableNameMain; + if (source == RCNDBSourceDefault) { + SQL = "DELETE FROM " RCNTableNameMainDefault; + } else if (source == RCNDBSourceActive) { + SQL = "DELETE FROM " RCNTableNameMainActive; + } + [strongSelf executeQuery:SQL]; + }); +} + +- (void)deleteExperimentTableForKey:(NSString *)key { + __weak RCNConfigDBManager *weakSelf = self; + dispatch_async(_databaseOperationQueue, ^{ + RCNConfigDBManager *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + NSArray *params = @[ key ]; + const char *SQL = "DELETE FROM " RCNTableNameExperiment " WHERE key = ?"; + [strongSelf executeQuery:SQL withParams:params]; + }); +} + +#pragma mark - helper +- (BOOL)executeQuery:(const char *)SQL withParams:(NSArray *)params { + RCN_MUST_NOT_BE_MAIN_THREAD(); + sqlite3_stmt *statement = [self prepareSQL:SQL]; + if (!statement) { + return NO; + } + + [self bindStringsToStatement:statement stringArray:params]; + if (sqlite3_step(statement) != SQLITE_DONE) { + return [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + } + sqlite3_finalize(statement); + return YES; +} + +/// Params only accept TEXT format string. +- (BOOL)bindStringsToStatement:(sqlite3_stmt *)statement stringArray:(NSArray *)array { + int index = 1; + for (NSString *param in array) { + if (![self bindStringToStatement:statement index:index string:param]) { + return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO]; + } + index++; + } + return YES; +} + +- (BOOL)bindStringToStatement:(sqlite3_stmt *)statement index:(int)index string:(NSString *)value { + if (sqlite3_bind_text(statement, index, [value UTF8String], -1, SQLITE_TRANSIENT) != SQLITE_OK) { + return [self logErrorWithSQL:nil finalizeStatement:statement returnValue:NO]; + } + return YES; +} + +- (sqlite3_stmt *)prepareSQL:(const char *)SQL { + sqlite3_stmt *statement = nil; + if (sqlite3_prepare_v2(_database, SQL, -1, &statement, NULL) != SQLITE_OK) { + [self logErrorWithSQL:SQL finalizeStatement:statement returnValue:NO]; + return nil; + } + return statement; +} + +- (NSString *)errorMessage { + return [NSString stringWithFormat:@"%s", sqlite3_errmsg(_database)]; +} + +- (int)errorCode { + return sqlite3_errcode(_database); +} + +- (void)logDatabaseError { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000015", @"Error message: %@. Error code: %d.", + [self errorMessage], [self errorCode]); +} + +- (BOOL)logErrorWithSQL:(const char *)SQL + finalizeStatement:(sqlite3_stmt *)statement + returnValue:(BOOL)returnValue { + if (SQL) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000016", @"Failed with SQL: %s.", SQL); + } + [self logDatabaseError]; + + if (statement) { + sqlite3_finalize(statement); + } + + return returnValue; +} + +@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigDefines.h b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h new file mode 100644 index 00000000000..9181355bc9d --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigDefines.h @@ -0,0 +1,34 @@ +/* + * Copyright 2019 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 RCNConfigDefines_h +#define RCNConfigDefines_h + +#if defined(DEBUG) +#define RCN_MUST_NOT_BE_MAIN_THREAD() \ + do { \ + NSAssert(![NSThread isMainThread], @"Must not be executing on the main thread."); \ + } while (0); +#else +#define RCN_MUST_NOT_BE_MAIN_THREAD() \ + do { \ + } while (0); +#endif + +#define RCNExperimentTableKeyPayload "experiment_payload" +#define RCNExperimentTableKeyMetadata "experiment_metadata" + +#endif diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h new file mode 100644 index 00000000000..34779d42839 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.h @@ -0,0 +1,37 @@ +/* + * Copyright 2019 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 + +@class FIRExperimentController; +@class RCNConfigDBManager; + +/// Handles experiment information update and persistence. +@interface RCNConfigExperiment : NSObject + +/// Designated initializer; +- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager + experimentController:(FIRExperimentController *)controller NS_DESIGNATED_INITIALIZER; + +/// Use `initWithDBManager:` instead. +- (instancetype)init NS_UNAVAILABLE; + +/// Update/Persist experiment information from config fetch response. +- (void)updateExperimentsWithResponse:(NSArray *> *)response; + +/// Update experiments to Firebase Analytics when activateFetched happens. +- (void)updateExperiments; +@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m new file mode 100644 index 00000000000..6eb7f8fa0f1 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigExperiment.m @@ -0,0 +1,224 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" + +#import "Protos/wireless/android/config/proto/Config.pbobjc.h" + +#import +#import +#import +#import +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" + +static NSString *const kExperimentMetadataKeyLastStartTime = @"last_experiment_start_time"; +/// Based on proto: +/// http://google3/googlemac/iPhone/Firebase/ABTesting/Source/Protos/developers/mobile/abt/proto/ExperimentPayload.pbobjc.m +static NSString *const kExperimentPayloadKeyExperimentID = @"experimentId"; +static NSString *const kExperimentPayloadKeyVariantID = @"variantId"; +static NSString *const kExperimentPayloadKeyExperimentStartTime = @"experimentStartTime"; +static NSString *const kExperimentPayloadKeyTriggerEvent = @"triggerEvent"; +static NSString *const kExperimentPayloadKeyTriggerTimeoutMillis = @"triggerTimeoutMillis"; +static NSString *const kExperimentPayloadKeyTimeToLiveMillis = @"timeToLiveMillis"; +static NSString *const kExperimentPayloadKeySetEventToLog = @"setEventToLog"; +static NSString *const kExperimentPayloadKeyActivateEventToLog = @"activateEventToLog"; +static NSString *const kExperimentPayloadKeyClearEventToLog = @"clearEventToLog"; +static NSString *const kExperimentPayloadKeyTimeoutEventToLog = @"timeoutEventToLog"; +static NSString *const kExperimentPayloadKeyTTLExpiryEventToLog = @"ttlExpiryEventToLog"; +static NSString *const kExperimentPayloadKeyOverflowPolicy = @"overflowPolicy"; + +static NSString *const kServiceOrigin = @"frc"; +static NSString *const kMethodNameLatestStartTime = + @"latestExperimentStartTimestampBetweenTimestamp:andPayloads:"; + +@interface RCNConfigExperiment () +@property(nonatomic, strong) + NSMutableArray *experimentPayloads; ///< Experiment payloads. +@property(nonatomic, strong) + NSMutableDictionary *experimentMetadata; ///< Experiment metadata +@property(nonatomic, strong) RCNConfigDBManager *DBManager; ///< Database Manager. +@property(nonatomic, strong) FIRExperimentController *experimentController; +@property(nonatomic, strong) NSDateFormatter *experimentStartTimeDateFormatter; +@end + +@implementation RCNConfigExperiment +/// Designated initializer +- (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager + experimentController:(FIRExperimentController *)controller { + self = [super init]; + if (self) { + _experimentPayloads = [[NSMutableArray alloc] init]; + _experimentMetadata = [[NSMutableDictionary alloc] init]; + _experimentStartTimeDateFormatter = [[NSDateFormatter alloc] init]; + [_experimentStartTimeDateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; + [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + // Locale needs to be hardcoded. See + // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details. + [_experimentStartTimeDateFormatter + setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [_experimentStartTimeDateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; + + _DBManager = DBManager; + _experimentController = controller; + [self loadExperimentFromTable]; + } + return self; +} + +- (void)loadExperimentFromTable { + if (!_DBManager) { + return; + } + __weak RCNConfigExperiment *weakSelf = self; + RCNDBCompletion completionHandler = ^(BOOL success, NSDictionary *result) { + RCNConfigExperiment *strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + if (result[@RCNExperimentTableKeyPayload]) { + [strongSelf->_experimentPayloads removeAllObjects]; + for (NSData *experiment in result[@RCNExperimentTableKeyPayload]) { + // Try to parse the experimentpayload as JSON. + NSError *error; + id experimentPayloadJSON = [NSJSONSerialization JSONObjectWithData:experiment + options:kNilOptions + error:&error]; + if (!experimentPayloadJSON || error) { + FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000031", + @"Experiment payload could not be parsed as JSON."); + // Add this as serialized proto. + [strongSelf->_experimentPayloads addObject:experiment]; + } else { + // Convert to protobuf. + NSData *protoPayload = [self convertABTExperimentPayloadToProto:experimentPayloadJSON]; + [strongSelf->_experimentPayloads addObject:protoPayload]; + } + } + } + if (result[@RCNExperimentTableKeyMetadata]) { + strongSelf->_experimentMetadata = [result[@RCNExperimentTableKeyMetadata] mutableCopy]; + } + }; + [_DBManager loadExperimentWithCompletionHandler:completionHandler]; +} + +/// This method converts the ABT experiment payload to a serialized protobuf which is consumable by +/// the ABT SDK. +- (NSData *)convertABTExperimentPayloadToProto:(NSDictionary *)experimentPayload { + ABTExperimentPayload *ABTExperiment = [[ABTExperimentPayload alloc] init]; + ABTExperiment.experimentId = experimentPayload[kExperimentPayloadKeyExperimentID]; + ABTExperiment.variantId = experimentPayload[kExperimentPayloadKeyVariantID]; + NSDate *experimentStartTime = [self.experimentStartTimeDateFormatter + dateFromString:experimentPayload[kExperimentPayloadKeyExperimentStartTime]]; + ABTExperiment.experimentStartTimeMillis = + [@([experimentStartTime timeIntervalSince1970] * 1000) longLongValue]; + ABTExperiment.triggerEvent = experimentPayload[kExperimentPayloadKeyTriggerEvent]; + ABTExperiment.triggerTimeoutMillis = + experimentPayload[kExperimentPayloadKeyTriggerTimeoutMillis] + ? atoll([experimentPayload[kExperimentPayloadKeyTriggerTimeoutMillis] UTF8String]) + : 0; + ABTExperiment.timeToLiveMillis = + experimentPayload[kExperimentPayloadKeyTimeToLiveMillis] + ? atoll([experimentPayload[kExperimentPayloadKeyTimeToLiveMillis] UTF8String]) + : 0; + ABTExperiment.setEventToLog = experimentPayload[kExperimentPayloadKeySetEventToLog]; + ABTExperiment.activateEventToLog = experimentPayload[kExperimentPayloadKeyActivateEventToLog]; + ABTExperiment.clearEventToLog = experimentPayload[kExperimentPayloadKeyClearEventToLog]; + ABTExperiment.timeoutEventToLog = experimentPayload[kExperimentPayloadKeyTimeoutEventToLog]; + ABTExperiment.ttlExpiryEventToLog = experimentPayload[kExperimentPayloadKeyTTLExpiryEventToLog]; + ABTExperiment.overflowPolicy = [experimentPayload[kExperimentPayloadKeyOverflowPolicy] intValue]; + + // Serialize the experiment payload. + NSData *serializedABTExperiment = ABTExperiment.data; + return serializedABTExperiment; +} + +- (void)updateExperimentsWithResponse:(NSArray *> *)response { + // cache fetched experiment payloads. + [_experimentPayloads removeAllObjects]; + [_DBManager deleteExperimentTableForKey:@RCNExperimentTableKeyPayload]; + + for (NSDictionary *experiment in response) { + NSData *protoPayload = [self convertABTExperimentPayloadToProto:experiment]; + [_experimentPayloads addObject:protoPayload]; + // We will add the new serialized JSON data to the database. + // TODO: (b/129272809). Eventually, RC and ABT need to be migrated to move off protos once + // (most) customers have migrated to using the new SDK (and hence saving the new JSON based + // payload in the database). + NSError *error; + NSData *JSONPayload = [NSJSONSerialization dataWithJSONObject:experiment + options:kNilOptions + error:&error]; + if (!JSONPayload || error) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000030", + @"Invalid experiment payload to be serialized."); + } + + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload + value:JSONPayload + completionHandler:nil]; + } +} + +- (void)updateExperiments { + FIRLifecycleEvents *lifecycleEvent = [[FIRLifecycleEvents alloc] init]; + + // Get the last experiment start time prior to the latest payload. + NSTimeInterval lastStartTime = + [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue]; + + // Update the last experiment start time with the latest payload. + [self updateExperimentStartTime]; + + [self.experimentController + updateExperimentsWithServiceOrigin:kServiceOrigin + events:lifecycleEvent + policy:ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest + lastStartTime:lastStartTime + payloads:_experimentPayloads]; +} + +- (void)updateExperimentStartTime { + NSTimeInterval existingLastStartTime = + [_experimentMetadata[kExperimentMetadataKeyLastStartTime] doubleValue]; + + NSTimeInterval latestStartTime = + [self latestStartTimeWithExistingLastStartTime:existingLastStartTime]; + + _experimentMetadata[kExperimentMetadataKeyLastStartTime] = @(latestStartTime); + + if (![NSJSONSerialization isValidJSONObject:_experimentMetadata]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028", + @"Invalid fetched experiment metadata to be serialized."); + return; + } + NSError *error; + NSData *serializedExperimentMetadata = + [NSJSONSerialization dataWithJSONObject:_experimentMetadata + options:NSJSONWritingPrettyPrinted + error:&error]; + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata + value:serializedExperimentMetadata + completionHandler:nil]; +} + +- (NSTimeInterval)latestStartTimeWithExistingLastStartTime:(NSTimeInterval)existingLastStartTime { + return [self.experimentController + latestExperimentStartTimestampBetweenTimestamp:existingLastStartTime + andPayloads:_experimentPayloads]; +} +@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigFetch.h b/FirebaseRemoteConfig/Sources/RCNConfigFetch.h new file mode 100644 index 00000000000..bf09ce6abe1 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigFetch.h @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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 + +#import +#import + +@class FIROptions; +@class RCNConfigContent; +@class RCNConfigSettings; +@class RCNConfigExperiment; +@class RCNConfigDBManager; + +NS_ASSUME_NONNULL_BEGIN + +/// Completion handler invoked by NSSessionFetcher. +typedef void (^RCNConfigFetcherCompletion)(NSData *data, NSURLResponse *response, NSError *error); + +/// Test block used for global NSSessionFetcher. +typedef void (^RCNConfigFetcherTestBlock)(RCNConfigFetcherCompletion completion); + +@interface RCNConfigFetch : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/// Designated initializer +- (instancetype)initWithContent:(RCNConfigContent *)content + DBManager:(RCNConfigDBManager *)DBManager + settings:(RCNConfigSettings *)settings + analytics:(nullable id)analytics + experiment:(nullable RCNConfigExperiment *)experiment + queue:(dispatch_queue_t)queue + namespace:(NSString *)firebaseNamespace + options:(FIROptions *)firebaseOptions NS_DESIGNATED_INITIALIZER; + +/// Fetches all config data keyed by namespace. Completion block will be called on the main queue. +/// @param expirationDuration Expiration duration, in seconds. +/// @param completionHandler Callback handler. +- (void)fetchAllConfigsWithExpirationDuration:(NSTimeInterval)expirationDuration + completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler; + +/// Add the ability to update NSURLSession's timeout after a session has already been created. +- (void)recreateNetworkSession; + +/// Sets the test block to mock the fetch response instead of performing the fetch task from server. ++ (void)setGlobalTestBlock:(RCNConfigFetcherTestBlock)block; + +NS_ASSUME_NONNULL_END + +@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigSettings.m b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m new file mode 100644 index 00000000000..1e79dd732af --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigSettings.m @@ -0,0 +1,440 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" + +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" +#import "FirebaseRemoteConfig/Sources/RCNDevice.h" +#import "FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h" + +#import +#import +#import +#import + +static NSString *const kRCNGroupPrefix = @"frc.group."; +static NSString *const kRCNUserDefaultsKeyNamelastETag = @"lastETag"; +static NSString *const kRCNUserDefaultsKeyNameLastSuccessfulFetchTime = @"lastSuccessfulFetchTime"; +static const int kRCNExponentialBackoffMinimumInterval = 60 * 2; // 2 mins. +static const int kRCNExponentialBackoffMaximumInterval = 60 * 60 * 4; // 4 hours. + +@interface RCNConfigSettings () { + /// A list of successful fetch timestamps in seconds. + NSMutableArray *_successFetchTimes; + /// A list of failed fetch timestamps in seconds. + NSMutableArray *_failureFetchTimes; + /// Device conditions since last successful fetch from the backend. Device conditions including + /// app + /// version, iOS version, device localte, language, GMP project ID and Game project ID. Used for + /// determing whether to throttle. + NSMutableDictionary *_deviceContext; + /// Custom variables (aka App context digest). This is the pending custom variables request before + /// fetching. + NSMutableDictionary *_customVariables; + /// Cached internal metadata from internal metadata table. It contains customized information such + /// as HTTP connection timeout, HTTP read timeout, success/failure throttling rate and time + /// interval. Client has the default value of each parameters, they are only saved in + /// internalMetadata if they have been customize by developers. + NSMutableDictionary *_internalMetadata; + /// Last fetch status. + FIRRemoteConfigFetchStatus _lastFetchStatus; + /// Last fetch Error. + FIRRemoteConfigError _lastFetchError; + /// The time of last apply timestamp. + NSTimeInterval _lastApplyTimeInterval; + /// The time of last setDefaults timestamp. + NSTimeInterval _lastSetDefaultsTimeInterval; + /// The database manager. + RCNConfigDBManager *_DBManager; + // The namespace for this instance. + NSString *_FIRNamespace; + // The Google App ID of the configured FIRApp. + NSString *_googleAppID; + /// The user defaults manager scoped to this RC instance of FIRApp and namespace. + RCNUserDefaultsManager *_userDefaultsManager; +} +@end + +@implementation RCNConfigSettings + +- (instancetype)initWithDatabaseManager:(RCNConfigDBManager *)manager + namespace:(NSString *)FIRNamespace + firebaseAppName:(NSString *)appName + googleAppID:(NSString *)googleAppID { + self = [super init]; + if (self) { + _FIRNamespace = FIRNamespace; + _googleAppID = googleAppID; + _bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; + if (!_bundleIdentifier) { + FIRLogNotice(kFIRLoggerRemoteConfig, @"I-RCN000038", + @"Main bundle identifier is missing. Remote Config might not work properly."); + _bundleIdentifier = @""; + } + _minimumFetchInterval = RCNDefaultMinimumFetchInterval; + _deviceContext = [[NSMutableDictionary alloc] init]; + _customVariables = [[NSMutableDictionary alloc] init]; + _successFetchTimes = [[NSMutableArray alloc] init]; + _failureFetchTimes = [[NSMutableArray alloc] init]; + _DBManager = manager; + + _internalMetadata = [[_DBManager loadInternalMetadataTable] mutableCopy]; + if (!_internalMetadata) { + _internalMetadata = [[NSMutableDictionary alloc] init]; + } + _userDefaultsManager = [[RCNUserDefaultsManager alloc] initWithAppName:appName + bundleID:_bundleIdentifier + namespace:_FIRNamespace]; + _isFetchInProgress = NO; + } + return self; +} + +#pragma mark - read from / update userDefaults +- (NSString *)lastETag { + return [_userDefaultsManager lastETag]; +} + +- (void)setLastETag:(NSString *)lastETag { + [_userDefaultsManager setLastETag:lastETag]; +} + +- (NSTimeInterval)lastFetchTimeInterval { + return _userDefaultsManager.lastFetchTime; +} + +// TODO: Update logic for app extensions as required. +- (void)updateLastFetchTimeInterval:(NSTimeInterval)lastFetchTimeInterval { + _userDefaultsManager.lastFetchTime = lastFetchTimeInterval; +} + +#pragma mark - load from DB +- (NSDictionary *)loadConfigFromMetadataTable { + NSDictionary *metadata = [[_DBManager loadMetadataWithBundleIdentifier:_bundleIdentifier] copy]; + if (metadata) { + // TODO: Remove (all metadata in general) once ready to + // migrate to user defaults completely. + if (metadata[RCNKeyDeviceContext]) { + self->_deviceContext = [metadata[RCNKeyDeviceContext] mutableCopy]; + } + if (metadata[RCNKeyAppContext]) { + self->_customVariables = [metadata[RCNKeyAppContext] mutableCopy]; + } + if (metadata[RCNKeySuccessFetchTime]) { + self->_successFetchTimes = [metadata[RCNKeySuccessFetchTime] mutableCopy]; + } + if (metadata[RCNKeyFailureFetchTime]) { + self->_failureFetchTimes = [metadata[RCNKeyFailureFetchTime] mutableCopy]; + } + if (metadata[RCNKeyLastFetchStatus]) { + self->_lastFetchStatus = + (FIRRemoteConfigFetchStatus)[metadata[RCNKeyLastFetchStatus] intValue]; + } + if (metadata[RCNKeyLastFetchError]) { + self->_lastFetchError = (FIRRemoteConfigError)[metadata[RCNKeyLastFetchError] intValue]; + } + if (metadata[RCNKeyLastApplyTime]) { + self->_lastApplyTimeInterval = [metadata[RCNKeyLastApplyTime] doubleValue]; + } + if (metadata[RCNKeyLastFetchStatus]) { + self->_lastSetDefaultsTimeInterval = [metadata[RCNKeyLastSetDefaultsTime] doubleValue]; + } + } + return metadata; +} + +#pragma mark - update DB/cached + +// Update internal metadata content to cache and DB. +- (void)updateInternalContentWithResponse:(NSDictionary *)response { + // Remove all the keys with current pakcage name. + [_DBManager deleteRecordWithBundleIdentifier:_bundleIdentifier isInternalDB:YES]; + + for (NSString *key in _internalMetadata.allKeys) { + if ([key hasPrefix:_bundleIdentifier]) { + [_internalMetadata removeObjectForKey:key]; + } + } + + for (NSString *entry in response) { + NSData *val = [response[entry] dataUsingEncoding:NSUTF8StringEncoding]; + NSArray *values = @[ entry, val ]; + _internalMetadata[entry] = response[entry]; + [self updateInternalMetadataTableWithValues:values]; + } +} + +- (void)updateInternalMetadataTableWithValues:(NSArray *)values { + [_DBManager insertInternalMetadataTableWithValues:values completionHandler:nil]; +} + +/// If the last fetch was not successful, update the (exponential backoff) period that we wait until +/// fetching again. Any subsequent fetch requests will be checked and allowed only if past this +/// throttle end time. +- (void)updateExponentialBackoffTime { + // If not in exponential backoff mode, reset the retry interval. + if (_lastFetchStatus == FIRRemoteConfigFetchStatusSuccess) { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000057", + @"Throttling: Entering exponential backoff mode."); + _exponentialBackoffRetryInterval = kRCNExponentialBackoffMinimumInterval; + } else { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000057", + @"Throttling: Updating throttling interval."); + // Double the retry interval until we hit the truncated exponential backoff. More info here: + // https://cloud.google.com/storage/docs/exponential-backoff + _exponentialBackoffRetryInterval = + ((_exponentialBackoffRetryInterval * 2) < kRCNExponentialBackoffMaximumInterval) + ? _exponentialBackoffRetryInterval * 2 + : _exponentialBackoffRetryInterval; + } + + // Randomize the next retry interval. + int randomPlusMinusInterval = ((arc4random() % 2) == 0) ? -1 : 1; + NSTimeInterval randomizedRetryInterval = + _exponentialBackoffRetryInterval + + (0.5 * _exponentialBackoffRetryInterval * randomPlusMinusInterval); + _exponentialBackoffThrottleEndTime = + [[NSDate date] timeIntervalSince1970] + randomizedRetryInterval; +} + +- (void)updateMetadataWithFetchSuccessStatus:(BOOL)fetchSuccess { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000056", @"Updating metadata with fetch result."); + if (!fetchSuccess) { + [self updateExponentialBackoffTime]; + } + + [self updateFetchTimeWithSuccessFetch:fetchSuccess]; + _lastFetchStatus = + fetchSuccess ? FIRRemoteConfigFetchStatusSuccess : FIRRemoteConfigFetchStatusFailure; + _lastFetchError = fetchSuccess ? FIRRemoteConfigErrorUnknown : FIRRemoteConfigErrorInternalError; + if (fetchSuccess) { + [self updateLastFetchTimeInterval:[[NSDate date] timeIntervalSince1970]]; + // Note: We expect the googleAppID to always be available. + _deviceContext = FIRRemoteConfigDeviceContextWithProjectIdentifier(_googleAppID); + } + + [self updateMetadataTable]; +} + +- (void)updateFetchTimeWithSuccessFetch:(BOOL)isSuccessfulFetch { + NSTimeInterval epochTimeInterval = [[NSDate date] timeIntervalSince1970]; + if (isSuccessfulFetch) { + [_successFetchTimes addObject:@(epochTimeInterval)]; + } else { + [_failureFetchTimes addObject:@(epochTimeInterval)]; + } +} + +- (void)updateMetadataTable { + [_DBManager deleteRecordWithBundleIdentifier:_bundleIdentifier isInternalDB:NO]; + NSError *error; + // Objects to be serialized cannot be invalid. + if (!_bundleIdentifier) { + return; + } + if (![NSJSONSerialization isValidJSONObject:_customVariables]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000028", + @"Invalid custom variables to be serialized."); + return; + } + if (![NSJSONSerialization isValidJSONObject:_deviceContext]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000029", + @"Invalid device context to be serialized."); + return; + } + + if (![NSJSONSerialization isValidJSONObject:_successFetchTimes]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000031", + @"Invalid success fetch times to be serialized."); + return; + } + if (![NSJSONSerialization isValidJSONObject:_failureFetchTimes]) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000032", + @"Invalid failure fetch times to be serialized."); + return; + } + NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:_customVariables + options:NSJSONWritingPrettyPrinted + error:&error]; + NSData *serializedDeviceContext = + [NSJSONSerialization dataWithJSONObject:_deviceContext + options:NSJSONWritingPrettyPrinted + error:&error]; + // The digestPerNamespace is not used and only meant for backwards DB compatibility. + NSData *serializedDigestPerNamespace = + [NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error]; + NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:_successFetchTimes + options:NSJSONWritingPrettyPrinted + error:&error]; + NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:_failureFetchTimes + options:NSJSONWritingPrettyPrinted + error:&error]; + + if (!serializedDigestPerNamespace || !serializedDeviceContext || !serializedAppContext || + !serializedSuccessTime || !serializedFailureTime) { + return; + } + + NSDictionary *columnNameToValue = @{ + RCNKeyBundleIdentifier : _bundleIdentifier, + RCNKeyFetchTime : @(self.lastFetchTimeInterval), + RCNKeyDigestPerNamespace : serializedDigestPerNamespace, + RCNKeyDeviceContext : serializedDeviceContext, + RCNKeyAppContext : serializedAppContext, + RCNKeySuccessFetchTime : serializedSuccessTime, + RCNKeyFailureFetchTime : serializedFailureTime, + RCNKeyLastFetchStatus : [NSString stringWithFormat:@"%ld", (long)_lastFetchStatus], + RCNKeyLastFetchError : [NSString stringWithFormat:@"%ld", (long)_lastFetchError], + RCNKeyLastApplyTime : @(_lastApplyTimeInterval), + RCNKeyLastSetDefaultsTime : @(_lastSetDefaultsTimeInterval) + }; + + [_DBManager insertMetadataTableWithValues:columnNameToValue completionHandler:nil]; +} + +#pragma mark - fetch request + +/// Returns a fetch request with the latest device and config change. +/// Whenever user issues a fetch api call, collect the latest request. +- (NSString *)nextRequestWithUserProperties:(NSDictionary *)userProperties { + // Note: We only set user properties as mentioned in the new REST API Design doc + NSString *ret = [NSString stringWithFormat:@"{"]; + ret = [ret stringByAppendingString:[NSString stringWithFormat:@"app_instance_id:'%@'", + _configInstanceID]]; + ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_instance_id_token:'%@'", + _configInstanceIDToken]]; + ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_id:'%@'", _googleAppID]]; + + ret = [ret stringByAppendingString:[NSString stringWithFormat:@", country_code:'%@'", + FIRRemoteConfigDeviceCountry()]]; + ret = [ret stringByAppendingString:[NSString stringWithFormat:@", language_code:'%@'", + FIRRemoteConfigDeviceLocale()]]; + ret = [ret + stringByAppendingString:[NSString stringWithFormat:@", platform_version:'%@'", + [GULAppEnvironmentUtil systemVersion]]]; + ret = [ret stringByAppendingString:[NSString stringWithFormat:@", time_zone:'%@'", + FIRRemoteConfigTimezone()]]; + ret = [ret stringByAppendingString:[NSString stringWithFormat:@", package_name:'%@'", + _bundleIdentifier]]; + ret = [ret stringByAppendingString:[NSString stringWithFormat:@", app_version:'%@'", + FIRRemoteConfigAppVersion()]]; + ret = [ret stringByAppendingString:[NSString stringWithFormat:@", sdk_version:'%d'", + FIRRemoteConfigSDKVersion()]]; + + if (userProperties && userProperties.count > 0) { + NSError *error; + NSData *jsonData = [NSJSONSerialization dataWithJSONObject:userProperties + options:0 + error:&error]; + if (!error) { + ret = [ret + stringByAppendingString:[NSString + stringWithFormat:@", analytics_user_properties:%@", + [[NSString alloc] + initWithData:jsonData + encoding:NSUTF8StringEncoding]]]; + } + } + ret = [ret stringByAppendingString:@"}"]; + return ret; +} + +#pragma mark - getter/setter + +- (void)setLastFetchError:(FIRRemoteConfigError)lastFetchError { + if (_lastFetchError != lastFetchError) { + _lastFetchError = lastFetchError; + [_DBManager updateMetadataWithOption:RCNUpdateOptionFetchStatus + values:@[ @(_lastFetchStatus), @(_lastFetchError) ] + completionHandler:nil]; + } +} + +- (NSArray *)successFetchTimes { + return [_successFetchTimes copy]; +} + +- (NSArray *)failureFetchTimes { + return [_failureFetchTimes copy]; +} + +- (NSDictionary *)customVariables { + return [_customVariables copy]; +} + +- (NSDictionary *)internalMetadata { + return [_internalMetadata copy]; +} + +- (NSDictionary *)deviceContext { + return [_deviceContext copy]; +} + +- (void)setCustomVariables:(NSDictionary *)customVariables { + _customVariables = [[NSMutableDictionary alloc] initWithDictionary:customVariables]; + [self updateMetadataTable]; +} + +- (void)setMinimumFetchInterval:(NSTimeInterval)minimumFetchInterval { + if (minimumFetchInterval < 0) { + _minimumFetchInterval = 0; + } else { + _minimumFetchInterval = minimumFetchInterval; + } +} + +- (void)setFetchTimeout:(NSTimeInterval)fetchTimeout { + if (fetchTimeout <= 0) { + _fetchTimeout = RCNHTTPDefaultConnectionTimeout; + } else { + _fetchTimeout = fetchTimeout; + } +} + +- (void)setLastApplyTimeInterval:(NSTimeInterval)lastApplyTimestamp { + _lastApplyTimeInterval = lastApplyTimestamp; + [_DBManager updateMetadataWithOption:RCNUpdateOptionApplyTime + values:@[ @(lastApplyTimestamp) ] + completionHandler:nil]; +} + +- (void)setLastSetDefaultsTimeInterval:(NSTimeInterval)lastSetDefaultsTimestamp { + _lastSetDefaultsTimeInterval = lastSetDefaultsTimestamp; + [_DBManager updateMetadataWithOption:RCNUpdateOptionDefaultTime + values:@[ @(lastSetDefaultsTimestamp) ] + completionHandler:nil]; +} + +#pragma mark Throttling + +- (BOOL)hasMinimumFetchIntervalElapsed:(NSTimeInterval)minimumFetchInterval { + if (self.lastFetchTimeInterval == 0) return YES; + + // Check if last config fetch is within minimum fetch interval in seconds. + NSTimeInterval diffInSeconds = [[NSDate date] timeIntervalSince1970] - self.lastFetchTimeInterval; + return diffInSeconds > minimumFetchInterval; +} + +- (BOOL)shouldThrottle { + NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + return ((self.lastFetchTimeInterval > 0) && + (_lastFetchStatus != FIRRemoteConfigFetchStatusSuccess) && + (_exponentialBackoffThrottleEndTime - now > 0)); +} + +@end diff --git a/FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h b/FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h new file mode 100644 index 00000000000..28d2d693f7a --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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 + +@interface FIRRemoteConfigValue () +@property(nonatomic, readwrite, assign) FIRRemoteConfigSource source; + +/// Designated initializer. +- (instancetype)initWithData:(NSData *)data + source:(FIRRemoteConfigSource)source NS_DESIGNATED_INITIALIZER; +@end diff --git a/FirebaseRemoteConfig/Sources/RCNConstants3P.m b/FirebaseRemoteConfig/Sources/RCNConstants3P.m new file mode 100644 index 00000000000..687d58784d7 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNConstants3P.m @@ -0,0 +1,20 @@ +/* + * Copyright 2019 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 + +/// Firebase Remote Config service default namespace. +NSString *const FIRNamespaceGoogleMobilePlatform = @"firebase"; diff --git a/FirebaseRemoteConfig/Sources/RCNDevice.h b/FirebaseRemoteConfig/Sources/RCNDevice.h new file mode 100644 index 00000000000..05395e32d34 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNDevice.h @@ -0,0 +1,59 @@ +/* + * Copyright 2019 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 + +typedef NS_ENUM(NSInteger, RCNDeviceModel) { + RCNDeviceModelOther, + RCNDeviceModelPhone, + RCNDeviceModelTablet, + RCNDeviceModelTV, + RCNDeviceModelGlass, + RCNDeviceModelCar, + RCNDeviceModelWearable, +}; + +/// CocoaPods SDK version +NSString *FIRRemoteConfigPodVersion(void); + +/// App version. +NSString *FIRRemoteConfigAppVersion(void); + +/// Device country, in lowercase. +NSString *FIRRemoteConfigDeviceCountry(void); + +/// Device locale, in language_country format, e.g. en_US. +NSString *FIRRemoteConfigDeviceLocale(void); + +/// Device subtype. +RCNDeviceModel FIRRemoteConfigDeviceSubtype(void); + +/// Device timezone. +NSString *FIRRemoteConfigTimezone(void); + +/// SDK version. This is different than CocoaPods SDK version. +/// It is used for config server to keep track iOS client version. +/// major * 10000 + minor + 100 + patch. +int FIRRemoteConfigSDKVersion(void); + +/// Update device context to the given dictionary. +NSMutableDictionary *FIRRemoteConfigDeviceContextWithProjectIdentifier( + NSString *GMPProjectIdentifier); + +/// Check whether client has changed device context, including app version, +/// iOS version, device country etc. This is used to determine whether to throttle. +BOOL FIRRemoteConfigHasDeviceContextChanged(NSDictionary *deviceContext, + NSString *GMPProjectIdentifier); diff --git a/FirebaseRemoteConfig/Sources/RCNDevice.m b/FirebaseRemoteConfig/Sources/RCNDevice.m new file mode 100644 index 00000000000..7733da45aa9 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNDevice.m @@ -0,0 +1,235 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Sources/RCNDevice.h" + +#import + +#import +#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" + +#define STR(x) STR_EXPAND(x) +#define STR_EXPAND(x) #x + +static NSString *const RCNDeviceContextKeyVersion = @"app_version"; +static NSString *const RCNDeviceContextKeyOSVersion = @"os_version"; +static NSString *const RCNDeviceContextKeyDeviceLocale = @"device_locale"; +static NSString *const RCNDeviceContextKeyLocaleLanguage = @"locale_language"; +static NSString *const RCNDeviceContextKeyGMPProjectIdentifier = @"GMP_project_Identifier"; + +NSString *FIRRemoteConfigAppVersion() { + return [[NSBundle mainBundle] infoDictionary][@"CFBundleVersion"]; +} + +NSString *FIRRemoteConfigPodVersion() { + return [NSString stringWithUTF8String:STR(FIRRemoteConfig_VERSION)]; +} + +RCNDeviceModel FIRRemoteConfigDeviceSubtype() { + NSString *model = [GULAppEnvironmentUtil deviceModel]; + if ([model hasPrefix:@"iPhone"]) { + return RCNDeviceModelPhone; + } + if ([model isEqualToString:@"iPad"]) { + return RCNDeviceModelTablet; + } + return RCNDeviceModelOther; +} + +NSString *FIRRemoteConfigDeviceCountry() { + return [[[NSLocale currentLocale] objectForKey:NSLocaleCountryCode] lowercaseString]; +} + +NSDictionary *FIRRemoteConfigFirebaseLocaleMap() { + return @{ + // Albanian + @"sq" : @[ @"sq_AL" ], + // Belarusian + @"be" : @[ @"be_BY" ], + // Bulgarian + @"bg" : @[ @"bg_BG" ], + // Catalan + @"ca" : @[ @"ca", @"ca_ES" ], + // Croatian + @"hr" : @[ @"hr", @"hr_HR" ], + // Czech + @"cs" : @[ @"cs", @"cs_CZ" ], + // Danish + @"da" : @[ @"da", @"da_DK" ], + // Estonian + @"et" : @[ @"et_EE" ], + // Finnish + @"fi" : @[ @"fi", @"fi_FI" ], + // Hebrew + @"he" : @[ @"he", @"iw_IL" ], + // Hindi + @"hi" : @[ @"hi_IN" ], + // Hungarian + @"hu" : @[ @"hu", @"hu_HU" ], + // Icelandic + @"is" : @[ @"is_IS" ], + // Indonesian + @"id" : @[ @"id", @"in_ID", @"id_ID" ], + // Irish + @"ga" : @[ @"ga_IE" ], + // Korean + @"ko" : @[ @"ko", @"ko_KR", @"ko-KR" ], + // Latvian + @"lv" : @[ @"lv_LV" ], + // Lithuanian + @"lt" : @[ @"lt_LT" ], + // Macedonian + @"mk" : @[ @"mk_MK" ], + // Malay + @"ms" : @[ @"ms_MY" ], + // Maltese + @"ms" : @[ @"mt_MT" ], + // Polish + @"pl" : @[ @"pl", @"pl_PL", @"pl-PL" ], + // Romanian + @"ro" : @[ @"ro", @"ro_RO" ], + // Russian + @"ru" : @[ @"ru_RU", @"ru", @"ru_BY", @"ru_KZ", @"ru-RU" ], + // Slovak + @"sk" : @[ @"sk", @"sk_SK" ], + // Slovenian + @"sl" : @[ @"sl_SI" ], + // Swedish + @"sv" : @[ @"sv", @"sv_SE", @"sv-SE" ], + // Turkish + @"tr" : @[ @"tr", @"tr-TR", @"tr_TR" ], + // Ukrainian + @"uk" : @[ @"uk", @"uk_UA" ], + // Vietnamese + @"vi" : @[ @"vi", @"vi_VN" ], + // The following are groups of locales or locales that sub-divide a + // language). + // Arabic + @"ar" : @[ + @"ar", @"ar_DZ", @"ar_BH", @"ar_EG", @"ar_IQ", @"ar_JO", @"ar_KW", + @"ar_LB", @"ar_LY", @"ar_MA", @"ar_OM", @"ar_QA", @"ar_SA", @"ar_SD", + @"ar_SY", @"ar_TN", @"ar_AE", @"ar_YE", @"ar_GB", @"ar-IQ", @"ar_US" + ], + // Simplified Chinese + @"zh_Hans" : @[ @"zh_CN", @"zh_SG", @"zh-Hans" ], + // Traditional Chinese + // Remove zh_HK until console added to the list. Otherwise client sends + // zh_HK and server/console falls back to zh. + // @"zh_Hant" : @[ @"zh_HK", @"zh_TW", @"zh-Hant", @"zh-HK", @"zh-TW" ], + @"zh_Hant" : @[ @"zh_TW", @"zh-Hant", @"zh-TW" ], + // Dutch + @"nl" : @[ @"nl", @"nl_BE", @"nl_NL", @"nl-NL" ], + // English + @"en" : @[ + @"en", @"en_AU", @"en_CA", @"en_IN", @"en_IE", @"en_MT", @"en_NZ", @"en_PH", + @"en_SG", @"en_ZA", @"en_GB", @"en_US", @"en_AE", @"en-AE", @"en_AS", @"en-AU", + @"en_BD", @"en-CA", @"en_EG", @"en_ES", @"en_GB", @"en-GB", @"en_HK", @"en_ID", + @"en-IN", @"en_NG", @"en-PH", @"en_PK", @"en-SG", @"en-US" + ], + // French + @"fr" : + @[ @"fr", @"fr_BE", @"fr_CA", @"fr_FR", @"fr_LU", @"fr_CH", @"fr-CA", @"fr-FR", @"fr_MA" ], + // German + @"de" : @[ @"de", @"de_AT", @"de_DE", @"de_LU", @"de_CH", @"de-DE" ], + // Greek + @"el" : @[ @"el", @"el_CY", @"el_GR" ], + // Italian + @"it" : @[ @"it", @"it_IT", @"it_CH", @"it-IT" ], + // Japanese + @"ja" : @[ @"ja", @"ja_JP", @"ja_JP_JP", @"ja-JP" ], + // Norwegian + @"no" : @[ @"nb", @"no_NO", @"no_NO_NY", @"nb_NO" ], + // Brazilian Portuguese + @"pt_BR" : @[ @"pt_BR", @"pt-BR" ], + // European Portuguese + @"pt_PT" : @[ @"pt", @"pt_PT", @"pt-PT" ], + // Serbian + @"sr" : @[ @"sr_BA", @"sr_ME", @"sr_RS", @"sr_Latn_BA", @"sr_Latn_ME", @"sr_Latn_RS" ], + // European Spanish + @"es_ES" : @[ @"es", @"es_ES", @"es-ES" ], + // Mexican Spanish + @"es_MX" : @[ @"es-MX", @"es_MX", @"es_US", @"es-US" ], + // Latin American Spanish + @"es_419" : @[ + @"es_AR", @"es_BO", @"es_CL", @"es_CO", @"es_CR", @"es_DO", @"es_EC", + @"es_SV", @"es_GT", @"es_HN", @"es_NI", @"es_PA", @"es_PY", @"es_PE", + @"es_PR", @"es_UY", @"es_VE", @"es-AR", @"es-CL", @"es-CO" + ], + // Thai + @"th" : @[ @"th", @"th_TH", @"th_TH_TH" ], + }; +} + +NSArray *FIRRemoteConfigAppManagerLocales() { + NSMutableArray *locales = [NSMutableArray array]; + NSDictionary *localesMap = FIRRemoteConfigFirebaseLocaleMap(); + for (NSString *key in localesMap) { + [locales addObjectsFromArray:localesMap[key]]; + } + return locales; +} +NSString *FIRRemoteConfigDeviceLocale() { + NSArray *locales = FIRRemoteConfigAppManagerLocales(); + NSArray *preferredLocalizations = + [NSBundle preferredLocalizationsFromArray:locales + forPreferences:[NSLocale preferredLanguages]]; + NSString *legalDocsLanguage = [preferredLocalizations firstObject]; + // Use en as the default language + return legalDocsLanguage ? legalDocsLanguage : @"en"; +} + +NSString *FIRRemoteConfigTimezone() { + NSTimeZone *timezone = [NSTimeZone systemTimeZone]; + return timezone.name; +} + +int FIRRemoteConfigSDKVersion() { + return kRCNMajorVersion * 10000 + kRCNMinorVersion * 100 + kRCNPatchVersion; +} + +NSMutableDictionary *FIRRemoteConfigDeviceContextWithProjectIdentifier( + NSString *GMPProjectIdentifier) { + NSMutableDictionary *deviceContext = [[NSMutableDictionary alloc] init]; + deviceContext[RCNDeviceContextKeyVersion] = FIRRemoteConfigAppVersion(); + deviceContext[RCNDeviceContextKeyOSVersion] = [GULAppEnvironmentUtil systemVersion]; + deviceContext[RCNDeviceContextKeyDeviceLocale] = FIRRemoteConfigDeviceLocale(); + // NSDictionary setObjectForKey will fail if there's no GMP project ID, must check ahead. + if (GMPProjectIdentifier) { + deviceContext[RCNDeviceContextKeyGMPProjectIdentifier] = GMPProjectIdentifier; + } + return deviceContext; +} + +BOOL FIRRemoteConfigHasDeviceContextChanged(NSDictionary *deviceContext, + NSString *GMPProjectIdentifier) { + if (![deviceContext[RCNDeviceContextKeyVersion] isEqual:FIRRemoteConfigAppVersion()]) { + return YES; + } + if (! + [deviceContext[RCNDeviceContextKeyOSVersion] isEqual:[GULAppEnvironmentUtil systemVersion]]) { + return YES; + } + if (![deviceContext[RCNDeviceContextKeyDeviceLocale] isEqual:FIRRemoteConfigDeviceLocale()]) { + return YES; + } + // GMP project id is optional. + if (deviceContext[RCNDeviceContextKeyGMPProjectIdentifier] && + ![deviceContext[RCNDeviceContextKeyGMPProjectIdentifier] isEqual:GMPProjectIdentifier]) { + return YES; + } + return NO; +} diff --git a/FirebaseRemoteConfig/Sources/RCNFetch.m b/FirebaseRemoteConfig/Sources/RCNFetch.m new file mode 100644 index 00000000000..7969529eedf --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNFetch.m @@ -0,0 +1,554 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Sources/RCNConfigFetch.h" + +#import +#import +#import +#import +#import +#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" +#import "FirebaseRemoteConfig/Sources/RCNDevice.h" + +#ifdef RCN_STAGING_SERVER +static NSString *const kServerURLDomain = + @"https://staging-firebaseremoteconfig.sandbox.googleapis.com"; +#else +static NSString *const kServerURLDomain = @"https://firebaseremoteconfig.googleapis.com"; +#endif + +static NSString *const kServerURLVersion = @"/v1"; +static NSString *const kServerURLProjects = @"/projects/"; +static NSString *const kServerURLNamespaces = @"/namespaces/"; +static NSString *const kServerURLQuery = @":fetch?"; +static NSString *const kServerURLKey = @"key="; +static NSString *const kRequestJSONKeyAppID = @"app_id"; +static NSString *const kRequestJSONKeyAppInstanceID = @"app_instance_id"; + +static NSString *const kHTTPMethodPost = @"POST"; ///< HTTP request method config fetch using +static NSString *const kContentTypeHeaderName = @"Content-Type"; ///< HTTP Header Field Name +static NSString *const kContentEncodingHeaderName = + @"Content-Encoding"; ///< HTTP Header Field Name +static NSString *const kAcceptEncodingHeaderName = @"Accept-Encoding"; ///< HTTP Header Field Name +static NSString *const kETagHeaderName = @"etag"; ///< HTTP Header Field Name +static NSString *const kIfNoneMatchETagHeaderName = @"if-none-match"; ///< HTTP Header Field Name +// Sends the bundle ID. Refer to b/130301479 for details. +static NSString *const kiOSBundleIdentifierHeaderName = + @"X-Ios-Bundle-Identifier"; ///< HTTP Header Field Name + +/// Config HTTP request content type proto buffer +static NSString *const kContentTypeValueJSON = @"application/json"; +static NSString *const kInstanceIDScopeConfig = @"*"; /// InstanceID scope + +/// HTTP status codes. Ref: https://cloud.google.com/apis/design/errors#error_retries +static NSInteger const kRCNFetchResponseHTTPStatusCodeOK = 200; +static NSInteger const kRCNFetchResponseHTTPStatusTooManyRequests = 429; +static NSInteger const kRCNFetchResponseHTTPStatusCodeInternalError = 500; +static NSInteger const kRCNFetchResponseHTTPStatusCodeServiceUnavailable = 503; +static NSInteger const kRCNFetchResponseHTTPStatusCodeGatewayTimeout = 504; + +// Deprecated error code previously from FirebaseCore +static const NSInteger FIRErrorCodeConfigFailed = -114; + +static RCNConfigFetcherTestBlock gGlobalTestBlock; + +#pragma mark - RCNConfig + +@implementation RCNConfigFetch { + RCNConfigContent *_content; + RCNConfigSettings *_settings; + id _analytics; + RCNConfigExperiment *_experiment; + dispatch_queue_t _lockQueue; /// Guard the read/write operation. + NSURLSession *_fetchSession; /// Managed internally by the fetch instance. + NSString *_FIRNamespace; + FIROptions *_options; +} + +- (instancetype)init { + NSAssert(NO, @"Invalid initializer."); + return nil; +} + +/// Designated initializer +- (instancetype)initWithContent:(RCNConfigContent *)content + DBManager:(RCNConfigDBManager *)DBManager + settings:(RCNConfigSettings *)settings + analytics:(nullable id)analytics + experiment:(RCNConfigExperiment *)experiment + queue:(dispatch_queue_t)queue + namespace:(NSString *)FIRNamespace + options:(FIROptions *)options { + self = [super init]; + if (self) { + _FIRNamespace = FIRNamespace; + _settings = settings; + _analytics = analytics; + _experiment = experiment; + _lockQueue = queue; + _content = content; + _fetchSession = [self newFetchSession]; + _options = options; + } + return self; +} + +/// Force a new NSURLSession creation for updated config. +- (void)recreateNetworkSession { + if (_fetchSession) { + [_fetchSession invalidateAndCancel]; + } + _fetchSession = [self newFetchSession]; +} + +/// Return the current session. (Tests). +- (NSURLSession *)currentNetworkSession { + return _fetchSession; +} + +- (void)dealloc { + [_fetchSession invalidateAndCancel]; +} + +#pragma mark - Fetch Config API + +- (void)fetchAllConfigsWithExpirationDuration:(NSTimeInterval)expirationDuration + completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler { + // Note: We expect the googleAppID to always be available. + BOOL hasDeviceContextChanged = + FIRRemoteConfigHasDeviceContextChanged(_settings.deviceContext, _options.googleAppID); + + __weak RCNConfigFetch *weakSelf = self; + RCNConfigFetch *fetchWithExpirationSelf = weakSelf; + dispatch_async(fetchWithExpirationSelf->_lockQueue, ^{ + RCNConfigFetch *strongSelf = fetchWithExpirationSelf; + // Check whether we are outside of the minimum fetch interval. + if (![strongSelf->_settings hasMinimumFetchIntervalElapsed:expirationDuration] && + !hasDeviceContextChanged) { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000051", @"Returning cached data."); + return [strongSelf reportCompletionOnHandler:completionHandler + withStatus:FIRRemoteConfigFetchStatusSuccess + withError:nil]; + } + if (strongSelf->_settings.isFetchInProgress) { + if (strongSelf->_settings.lastFetchTimeInterval > 0) { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000052", + @"A fetch is already in progress. Using previous fetch results."); + return [strongSelf reportCompletionOnHandler:completionHandler + withStatus:strongSelf->_settings.lastFetchStatus + withError:nil]; + } else { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000053", + @"A fetch is already in progress. Ignoring duplicate request."); + NSError *error = + [NSError errorWithDomain:FIRRemoteConfigErrorDomain + code:FIRErrorCodeConfigFailed + userInfo:@{ + @"FetchError" : @"Duplicate request while the previous one is pending" + }]; + return [strongSelf reportCompletionOnHandler:completionHandler + withStatus:FIRRemoteConfigFetchStatusFailure + withError:error]; + } + } + + // Check whether cache data is within throttle limit. + if ([strongSelf->_settings shouldThrottle] && !hasDeviceContextChanged) { + // Must set lastFetchStatus before FailReason. + strongSelf->_settings.lastFetchStatus = FIRRemoteConfigFetchStatusThrottled; + strongSelf->_settings.lastFetchError = FIRRemoteConfigErrorThrottled; + NSTimeInterval throttledEndTime = strongSelf->_settings.exponentialBackoffThrottleEndTime; + + NSError *error = + [NSError errorWithDomain:FIRRemoteConfigErrorDomain + code:FIRRemoteConfigErrorThrottled + userInfo:@{ + FIRRemoteConfigThrottledEndTimeInSecondsKey : @(throttledEndTime) + }]; + return [strongSelf reportCompletionOnHandler:completionHandler + withStatus:strongSelf->_settings.lastFetchStatus + withError:error]; + } + strongSelf->_settings.isFetchInProgress = YES; + [strongSelf refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler:completionHandler]; + }); +} + +#pragma mark - Fetch helpers + +/// Refresh instance ID token before fetching config. Instance ID is an optional field in config +/// request. +- (void)refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler: + (FIRRemoteConfigFetchCompletion)completionHandler { + FIRInstanceID *instanceID = [FIRInstanceID instanceID]; + // Only refresh instance ID when a valid sender ID is provided. If not, continue without + // fetching instance ID. Instance ID is for data analytics purpose, which is only optional for + // config fetching. + if (!_options.GCMSenderID) { + [self fetchCheckinInfoWithCompletionHandler:completionHandler]; + return; + } + __weak RCNConfigFetch *weakSelf = self; + FIRInstanceIDTokenHandler instanceIDHandler = ^(NSString *token, NSError *error) { + RCNConfigFetch *instanceIDHandlerSelf = weakSelf; + dispatch_async(instanceIDHandlerSelf->_lockQueue, ^{ + RCNConfigFetch *strongSelf = instanceIDHandlerSelf; + if (error) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000020", + @"Failed to register InstanceID with error : %@.", error); + } + if (token) { + NSError *IIDError; + strongSelf->_settings.configInstanceIDToken = [token copy]; + strongSelf->_settings.configInstanceID = [instanceID appInstanceID:&IIDError]; + FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000022", @"Success to get iid : %@.", + strongSelf->_settings.configInstanceID); + } + // Continue the fetch regardless of whether fetch of instance ID succeeded. + [strongSelf fetchCheckinInfoWithCompletionHandler:completionHandler]; + }); + }; + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000039", @"Starting requesting token."); + // Note: We expect the GCMSenderID to always be available by the time this request is made. + [instanceID tokenWithAuthorizedEntity:_options.GCMSenderID + scope:kInstanceIDScopeConfig + options:nil + handler:instanceIDHandler]; +} + +/// Fetch checkin info before fetching config. Checkin info including device authentication ID, +/// secret token and device data version are optional fields in config request. +- (void)fetchCheckinInfoWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler { + FIRInstanceID *instanceID = [FIRInstanceID instanceID]; + __weak RCNConfigFetch *weakSelf = self; + [instanceID fetchCheckinInfoWithHandler:^(FIRInstanceIDCheckinPreferences *preferences, + NSError *error) { + RCNConfigFetch *fetchCheckinInfoWithHandlerSelf = weakSelf; + dispatch_async(fetchCheckinInfoWithHandlerSelf->_lockQueue, ^{ + RCNConfigFetch *strongSelf = fetchCheckinInfoWithHandlerSelf; + if (error) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000023", @"Failed to fetch checkin info: %@.", + error); + } else { + strongSelf->_settings.deviceAuthID = preferences.deviceID; + strongSelf->_settings.secretToken = preferences.secretToken; + strongSelf->_settings.deviceDataVersion = preferences.deviceDataVersion; + if (strongSelf->_settings.deviceAuthID.length && strongSelf->_settings.secretToken.length) { + FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000024", + @"Success to get device authentication ID: %@, security token: %@.", + self->_settings.deviceAuthID, self->_settings.secretToken); + } + } + // Checkin info is optional, continue fetch config regardless fetch of checkin info + // succeeded. + [strongSelf fetchWithUserPropertiesCompletionHandler:^(NSDictionary *userProperties) { + dispatch_async(strongSelf->_lockQueue, ^{ + [strongSelf fetchWithUserProperties:userProperties completionHandler:completionHandler]; + }); + }]; + }); + }]; +} + +- (void)fetchWithUserPropertiesCompletionHandler: + (FIRAInteropUserPropertiesCallback)completionHandler { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000060", @"Fetch with user properties completed."); + id analytics = self->_analytics; + if (analytics == nil) { + completionHandler(@{}); + } else { + [analytics getUserPropertiesWithCallback:completionHandler]; + } +} + +- (void)reportCompletionOnHandler:(FIRRemoteConfigFetchCompletion)completionHandler + withStatus:(FIRRemoteConfigFetchStatus)status + withError:(NSError *)error { + if (completionHandler) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(status, error); + }); + } +} + +- (void)fetchWithUserProperties:(NSDictionary *)userProperties + completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000061", @"Fetch with user properties initiated."); + + NSString *postRequestString = [_settings nextRequestWithUserProperties:userProperties]; + + // Get POST request content. + NSData *content = [postRequestString dataUsingEncoding:NSUTF8StringEncoding]; + NSError *compressionError; + NSData *compressedContent = [NSData gul_dataByGzippingData:content error:&compressionError]; + if (compressionError) { + NSString *errString = [NSString stringWithFormat:@"Failed to compress the config request."]; + FIRLogWarning(kFIRLoggerRemoteConfig, @"I-RCN000033", @"%@", errString); + + return [self + reportCompletionOnHandler:completionHandler + withStatus:FIRRemoteConfigFetchStatusFailure + withError:[NSError + errorWithDomain:FIRRemoteConfigErrorDomain + code:FIRRemoteConfigErrorInternalError + userInfo:@{NSLocalizedDescriptionKey : errString}]]; + } + + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000040", @"Start config fetch."); + __weak RCNConfigFetch *weakSelf = self; + RCNConfigFetcherCompletion fetcherCompletion = ^(NSData *data, NSURLResponse *response, + NSError *error) { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000050", + @"config fetch completed. Error: %@ StatusCode: %ld", (error ? error : @"nil"), + (long)[((NSHTTPURLResponse *)response) statusCode]); + RCNConfigFetch *fetcherCompletionSelf = weakSelf; + if (!fetcherCompletionSelf) { + return; + }; + + dispatch_async(fetcherCompletionSelf->_lockQueue, ^{ + RCNConfigFetch *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + strongSelf->_settings.isFetchInProgress = NO; + NSInteger statusCode = [((NSHTTPURLResponse *)response) statusCode]; + + if (error || (statusCode != kRCNFetchResponseHTTPStatusCodeOK)) { + // Update failure fetch time and database. + [strongSelf->_settings updateMetadataWithFetchSuccessStatus:NO]; + if (error) { + if (strongSelf->_settings.lastFetchStatus == FIRRemoteConfigFetchStatusSuccess) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000025", + @"RCN Fetch failure: %@. Using cached config result.", error); + } else { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000026", + @"RCN Fetch failure: %@. No cached config result.", error); + } + } + if (statusCode != kRCNFetchResponseHTTPStatusCodeOK) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000026", + @"RCN Fetch failure. Response http error code: %ld", (long)statusCode); + // Response error code 429, 500, 503 will trigger exponential backoff mode. + if (statusCode == kRCNFetchResponseHTTPStatusTooManyRequests || + statusCode == kRCNFetchResponseHTTPStatusCodeInternalError || + statusCode == kRCNFetchResponseHTTPStatusCodeServiceUnavailable || + statusCode == kRCNFetchResponseHTTPStatusCodeGatewayTimeout) { + if ([strongSelf->_settings shouldThrottle]) { + // Must set lastFetchStatus before FailReason. + strongSelf->_settings.lastFetchStatus = FIRRemoteConfigFetchStatusThrottled; + strongSelf->_settings.lastFetchError = FIRRemoteConfigErrorThrottled; + NSTimeInterval throttledEndTime = + strongSelf->_settings.exponentialBackoffThrottleEndTime; + + NSError *error = [NSError + errorWithDomain:FIRRemoteConfigErrorDomain + code:FIRRemoteConfigErrorThrottled + userInfo:@{ + FIRRemoteConfigThrottledEndTimeInSecondsKey : @(throttledEndTime) + }]; + return [strongSelf reportCompletionOnHandler:completionHandler + withStatus:strongSelf->_settings.lastFetchStatus + withError:error]; + } else { + // Return back the received error. + // Must set lastFetchStatus before setting Fetch Error. + strongSelf->_settings.lastFetchStatus = FIRRemoteConfigFetchStatusFailure; + strongSelf->_settings.lastFetchError = FIRRemoteConfigErrorInternalError; + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey : + (error ? [error localizedDescription] + : [NSString stringWithFormat:@"Internal Error. Status code: %ld", + (long)statusCode]) + }; + return [strongSelf + reportCompletionOnHandler:completionHandler + withStatus:FIRRemoteConfigFetchStatusFailure + withError:[NSError + errorWithDomain:FIRRemoteConfigErrorDomain + code:FIRRemoteConfigErrorInternalError + userInfo:userInfo]]; + } + } + } + } + + // Fetch was successful. Check if we have data. + NSError *retError; + if (!data) { + FIRLogInfo(kFIRLoggerRemoteConfig, @"I-RCN000043", @"RCN Fetch: No data in fetch response"); + return [strongSelf reportCompletionOnHandler:completionHandler + withStatus:FIRRemoteConfigFetchStatusSuccess + withError:nil]; + } + + // Config fetch succeeded. + // JSONObjectWithData is always expected to return an NSDictionary in our case + NSMutableDictionary *fetchedConfig = + [NSJSONSerialization JSONObjectWithData:data + options:NSJSONReadingMutableContainers + error:&retError]; + if (retError) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000042", + @"RCN Fetch failure: %@. Could not parse response data as JSON", error); + } + + // Check and log if we received an error from the server + if (fetchedConfig && fetchedConfig.count == 1 && fetchedConfig[RCNFetchResponseKeyError]) { + NSString *errStr = [NSString stringWithFormat:@"RCN Fetch Failure: Server returned error:"]; + NSDictionary *errDict = fetchedConfig[RCNFetchResponseKeyError]; + if (errDict[RCNFetchResponseKeyErrorCode]) { + errStr = [errStr + stringByAppendingString:[NSString + stringWithFormat:@"code: %@", + errDict[RCNFetchResponseKeyErrorCode]]]; + } + if (errDict[RCNFetchResponseKeyErrorStatus]) { + errStr = [errStr stringByAppendingString: + [NSString stringWithFormat:@". Status: %@", + errDict[RCNFetchResponseKeyErrorStatus]]]; + } + if (errDict[RCNFetchResponseKeyErrorMessage]) { + errStr = + [errStr stringByAppendingString: + [NSString stringWithFormat:@". Message: %@", + errDict[RCNFetchResponseKeyErrorMessage]]]; + } + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000044", @"%@.", errStr); + return [strongSelf + reportCompletionOnHandler:completionHandler + withStatus:FIRRemoteConfigFetchStatusFailure + withError:[NSError + errorWithDomain:FIRRemoteConfigErrorDomain + code:FIRRemoteConfigErrorInternalError + userInfo:@{NSLocalizedDescriptionKey : errStr}]]; + } + + // Add the fetched config to the database. + if (fetchedConfig) { + // Update config content to cache and DB. + [self->_content updateConfigContentWithResponse:fetchedConfig + forNamespace:self->_FIRNamespace]; + // Update experiments. + [strongSelf->_experiment + updateExperimentsWithResponse:fetchedConfig[RCNFetchResponseKeyExperimentDescriptions]]; + } else { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000063", + @"Empty response with no fetched config."); + } + + // We had a successful fetch. Update the current eTag in settings. + self->_settings.lastETag = ((NSHTTPURLResponse *)response).allHeaderFields[kETagHeaderName]; + + [self->_settings updateMetadataWithFetchSuccessStatus:YES]; + return [strongSelf reportCompletionOnHandler:completionHandler + withStatus:FIRRemoteConfigFetchStatusSuccess + withError:nil]; + }); + }; + + if (gGlobalTestBlock) { + gGlobalTestBlock(fetcherCompletion); + return; + } + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000061", @"Making remote config fetch."); + + NSURLSessionDataTask *dataTask = [self URLSessionDataTaskWithContent:compressedContent + completionHandler:fetcherCompletion]; + [dataTask resume]; +} + ++ (void)setGlobalTestBlock:(RCNConfigFetcherTestBlock)block { + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000027", + @"Set global test block for NSSessionFetcher, it will not fetch from server."); + gGlobalTestBlock = [block copy]; +} + +- (NSString *)constructServerURL { + NSString *serverURLStr = [[NSString alloc] initWithString:kServerURLDomain]; + serverURLStr = [serverURLStr stringByAppendingString:kServerURLVersion]; + + if (_options.projectID) { + serverURLStr = [serverURLStr stringByAppendingString:kServerURLProjects]; + serverURLStr = [serverURLStr stringByAppendingString:_options.projectID]; + } else { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000070", + @"Missing `projectID` from `FirebaseOptions`, please ensure the configured " + @"`FirebaseApp` is configured with `FirebaseOptions` that contains a `projectID`."); + } + + serverURLStr = [serverURLStr stringByAppendingString:kServerURLNamespaces]; + + // Get the namespace from the fully qualified namespace string of "namespace:FIRAppName". + NSString *namespace = + [_FIRNamespace substringToIndex:[_FIRNamespace rangeOfString:@":"].location]; + serverURLStr = [serverURLStr stringByAppendingString:namespace]; + serverURLStr = [serverURLStr stringByAppendingString:kServerURLQuery]; + + if (_options.APIKey) { + serverURLStr = [serverURLStr stringByAppendingString:kServerURLKey]; + serverURLStr = [serverURLStr stringByAppendingString:_options.APIKey]; + } else { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000071", + @"Missing `APIKey` from `FirebaseOptions`, please ensure the configured " + @"`FirebaseApp` is configured with `FirebaseOptions` that contains an `APIKey`."); + } + + return serverURLStr; +} + +- (NSURLSession *)newFetchSession { + NSURLSessionConfiguration *config = + [[NSURLSessionConfiguration defaultSessionConfiguration] copy]; + config.timeoutIntervalForRequest = _settings.fetchTimeout; + config.timeoutIntervalForResource = _settings.fetchTimeout; + NSURLSession *session = [NSURLSession sessionWithConfiguration:config]; + return session; +} + +- (NSURLSessionDataTask *)URLSessionDataTaskWithContent:(NSData *)content + completionHandler: + (RCNConfigFetcherCompletion)fetcherCompletion { + NSURL *URL = [NSURL URLWithString:[self constructServerURL]]; + FIRLogDebug(kFIRLoggerRemoteConfig, @"I-RCN000046", @"%@", + [NSString stringWithFormat:@"Making config request: %@", [URL absoluteString]]); + + NSTimeInterval timeoutInterval = _fetchSession.configuration.timeoutIntervalForResource; + NSMutableURLRequest *URLRequest = + [[NSMutableURLRequest alloc] initWithURL:URL + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:timeoutInterval]; + URLRequest.HTTPMethod = kHTTPMethodPost; + [URLRequest setValue:kContentTypeValueJSON forHTTPHeaderField:kContentTypeHeaderName]; + [URLRequest setValue:[[NSBundle mainBundle] bundleIdentifier] + forHTTPHeaderField:kiOSBundleIdentifierHeaderName]; + [URLRequest setValue:@"gzip" forHTTPHeaderField:kContentEncodingHeaderName]; + [URLRequest setValue:@"gzip" forHTTPHeaderField:kAcceptEncodingHeaderName]; + // Set the eTag from the last successful fetch, if available. + if (_settings.lastETag) { + [URLRequest setValue:_settings.lastETag forHTTPHeaderField:kIfNoneMatchETagHeaderName]; + } + [URLRequest setHTTPBody:content]; + + return [_fetchSession dataTaskWithRequest:URLRequest completionHandler:fetcherCompletion]; +} + +@end diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h new file mode 100644 index 00000000000..4246096bf3a --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h @@ -0,0 +1,51 @@ +/* + * Copyright 2019 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface RCNUserDefaultsManager : NSObject + +/// The last eTag received from the backend. +@property(nonatomic, assign) NSString *lastETag; +/// The time of the last successful fetch. +@property(nonatomic, assign) NSTimeInterval lastFetchTime; +/// The time of the last successful fetch. +@property(nonatomic, assign) NSString *lastFetchStatus; +/// Boolean indicating if the last (one or more) fetch(es) was/were unsuccessful, in which case we +/// are in an exponential backoff mode. +@property(nonatomic, assign) BOOL isClientThrottledWithExponentialBackoff; +/// Time when the next request can be made while being throttled. +@property(nonatomic, assign) NSTimeInterval throttleEndTime; +/// The retry interval increases exponentially for cumulative fetch failures. Refer to +/// go/rc-client-throttling for details. +@property(nonatomic, assign) NSTimeInterval currentThrottlingRetryIntervalSeconds; + +/// Designated initializer. +- (instancetype)initWithAppName:(NSString *)appName + bundleID:(NSString *)bundleIdentifier + namespace:(NSString *)firebaseNamespace NS_DESIGNATED_INITIALIZER; + +// NOLINTBEGIN +/// Use `initWithAppName:bundleID:namespace:` instead. +- (instancetype)init + __attribute__((unavailable("Use `initWithAppName:bundleID:namespace:` instead."))); +// NOLINTEND + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m new file mode 100644 index 00000000000..6bd28a06931 --- /dev/null +++ b/FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.m @@ -0,0 +1,201 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h" +#import +#import + +static NSString *const kRCNGroupPrefix = @"group"; +static NSString *const kRCNGroupSuffix = @"firebase"; +static NSString *const kRCNUserDefaultsKeyNamelastETag = @"lastETag"; +static NSString *const kRCNUserDefaultsKeyNameLastSuccessfulFetchTime = @"lastSuccessfulFetchTime"; +static NSString *const kRCNUserDefaultsKeyNamelastFetchStatus = @"lastFetchStatus"; +static NSString *const kRCNUserDefaultsKeyNameIsClientThrottled = + @"isClientThrottledWithExponentialBackoff"; +static NSString *const kRCNUserDefaultsKeyNameThrottleEndTime = @"throttleEndTime"; +static NSString *const kRCNUserDefaultsKeyNamecurrentThrottlingRetryInterval = + @"currentThrottlingRetryInterval"; + +@interface RCNUserDefaultsManager () { + /// User Defaults instance for this bundleID. NSUserDefaults is guaranteed to be thread-safe. + NSUserDefaults *_userDefaults; + /// The suite name for this user defaults instance. It is a combination of a prefix and the + /// bundleID. This is because you cannot use just the bundleID of the current app as the suite + /// name when initializing user defaults. + NSString *_userDefaultsSuiteName; + /// The FIRApp that this instance is scoped within. + NSString *_firebaseAppName; + /// The Firebase Namespace that this instance is scoped within. + NSString *_firebaseNamespace; + /// The bundleID of the app. In case of an extension, this will be the bundleID of the parent app. + NSString *_bundleIdentifier; +} + +@end + +@implementation RCNUserDefaultsManager + +#pragma mark Initializers. + +/// Designated initializer. +- (instancetype)initWithAppName:(NSString *)appName + bundleID:(NSString *)bundleIdentifier + namespace:(NSString *)firebaseNamespace { + self = [super init]; + if (self) { + _firebaseAppName = appName; + _bundleIdentifier = bundleIdentifier; + NSInteger location = [firebaseNamespace rangeOfString:@":"].location; + if (location == NSNotFound) { + FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000064", + @"Error: Namespace %@ is not fully qualified app:namespace.", firebaseNamespace); + _firebaseNamespace = firebaseNamespace; + } else { + _firebaseNamespace = [firebaseNamespace substringToIndex:location]; + } + + // Initialize the user defaults with a prefix and the bundleID. For app extensions, this will be + // the bundleID of the app extension. + _userDefaults = + [RCNUserDefaultsManager sharedUserDefaultsForBundleIdentifier:_bundleIdentifier]; + } + + return self; +} + ++ (NSUserDefaults *)sharedUserDefaultsForBundleIdentifier:(NSString *)bundleIdentifier { + static dispatch_once_t onceToken; + static NSUserDefaults *sharedInstance; + dispatch_once(&onceToken, ^{ + NSString *userDefaultsSuiteName = + [RCNUserDefaultsManager userDefaultsSuiteNameForBundleIdentifier:bundleIdentifier]; + sharedInstance = [[NSUserDefaults alloc] initWithSuiteName:userDefaultsSuiteName]; + }); + return sharedInstance; +} + ++ (NSString *)userDefaultsSuiteNameForBundleIdentifier:(NSString *)bundleIdentifier { + NSString *suiteName = + [NSString stringWithFormat:@"%@.%@.%@", kRCNGroupPrefix, bundleIdentifier, kRCNGroupSuffix]; + return suiteName; +} + +#pragma mark Public properties. + +- (NSString *)lastETag { + return [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastETag]; +} + +- (void)setLastETag:(NSString *)lastETag { + if (lastETag) { + [self setInstanceUserDefaultsValue:lastETag forKey:kRCNUserDefaultsKeyNamelastETag]; + } +} + +- (NSTimeInterval)lastFetchTime { + NSNumber *lastFetchTime = + [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNameLastSuccessfulFetchTime]; + return lastFetchTime.doubleValue; +} + +- (void)setLastFetchTime:(NSTimeInterval)lastFetchTime { + [self setInstanceUserDefaultsValue:@(lastFetchTime) + forKey:kRCNUserDefaultsKeyNameLastSuccessfulFetchTime]; +} + +- (NSString *)lastFetchStatus { + return [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNamelastFetchStatus]; +} + +- (void)setLastFetchStatus:(NSString *)lastFetchStatus { + if (lastFetchStatus) { + [self setInstanceUserDefaultsValue:lastFetchStatus + forKey:kRCNUserDefaultsKeyNamelastFetchStatus]; + } +} + +- (BOOL)isClientThrottledWithExponentialBackoff { + NSNumber *isClientThrottled = + [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNameIsClientThrottled]; + return isClientThrottled.boolValue; +} + +- (void)setIsClientThrottledWithExponentialBackoff:(BOOL)isClientThrottled { + [self setInstanceUserDefaultsValue:@(isClientThrottled) + forKey:kRCNUserDefaultsKeyNameIsClientThrottled]; +} + +- (NSTimeInterval)throttleEndTime { + NSNumber *throttleEndTime = + [[self instanceUserDefaults] objectForKey:kRCNUserDefaultsKeyNameThrottleEndTime]; + return throttleEndTime.doubleValue; +} + +- (void)setThrottleEndTime:(NSTimeInterval)throttleEndTime { + [self setInstanceUserDefaultsValue:@(throttleEndTime) + forKey:kRCNUserDefaultsKeyNameThrottleEndTime]; +} + +- (NSTimeInterval)currentThrottlingRetryIntervalSeconds { + NSNumber *throttleEndTime = [[self instanceUserDefaults] + objectForKey:kRCNUserDefaultsKeyNamecurrentThrottlingRetryInterval]; + return throttleEndTime.doubleValue; +} + +- (void)setCurrentThrottlingRetryIntervalSeconds:(NSTimeInterval)throttlingRetryIntervalSeconds { + [self setInstanceUserDefaultsValue:@(throttlingRetryIntervalSeconds) + forKey:kRCNUserDefaultsKeyNamecurrentThrottlingRetryInterval]; +} + +#pragma mark Private methods. + +// There is a nested hierarchy for the userdefaults as follows: +// [FIRAppName][FIRNamespaceName][Key] +- (nonnull NSDictionary *)appUserDefaults { + NSString *appPath = _firebaseAppName; + NSDictionary *appDict = [_userDefaults valueForKeyPath:appPath]; + if (!appDict) { + appDict = [[NSDictionary alloc] init]; + } + return appDict; +} + +// Search for the user defaults for this (app, namespace) instance using the valueForKeyPath method. +- (nonnull NSDictionary *)instanceUserDefaults { + NSString *appNamespacePath = + [NSString stringWithFormat:@"%@.%@", _firebaseAppName, _firebaseNamespace]; + NSDictionary *appNamespaceDict = [_userDefaults valueForKeyPath:appNamespacePath]; + + if (!appNamespaceDict) { + appNamespaceDict = [[NSMutableDictionary alloc] init]; + } + return appNamespaceDict; +} + +// Update users defaults for just this (app, namespace) instance. +- (void)setInstanceUserDefaultsValue:(NSObject *)value forKey:(NSString *)key { + @synchronized(_userDefaults) { + NSMutableDictionary *appUserDefaults = [[self appUserDefaults] mutableCopy]; + NSMutableDictionary *appNamespaceUserDefaults = [[self instanceUserDefaults] mutableCopy]; + [appNamespaceUserDefaults setObject:value forKey:key]; + [appUserDefaults setObject:appNamespaceUserDefaults forKey:_firebaseNamespace]; + [_userDefaults setObject:appUserDefaults forKey:_firebaseAppName]; + // We need to synchronize to have this value updated for the extension. + [_userDefaults synchronize]; + } +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Sample/Podfile b/FirebaseRemoteConfig/Tests/Sample/Podfile new file mode 100644 index 00000000000..4fe4feb4a2e --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/Podfile @@ -0,0 +1,18 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'RemoteConfigSampleApp' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + pod 'FirebaseAnalytics' + pod 'FirebaseCore', :path => '../../../' + pod 'FirebaseRemoteConfig', :path => '../../../' + + # Pods for RemoteConfigSampleApp + + target 'RemoteConfigSampleAppUITests' do + inherit! :search_paths + # Pods for testing + end + +end diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp.xcodeproj/project.pbxproj b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..de2704ba46d --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp.xcodeproj/project.pbxproj @@ -0,0 +1,582 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 5B1A9B14232846F3001809E9 /* FRCLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B1A9B13232846F3001809E9 /* FRCLog.m */; }; + 5B1A9B1723284735001809E9 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B1A9B1523284734001809E9 /* LaunchScreen.xib */; }; + 5B1A9B1A232849FC001809E9 /* SecondApp-GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5B1A9B18232849FC001809E9 /* SecondApp-GoogleService-Info.plist */; }; + 5B1A9B1B232849FC001809E9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5B1A9B19232849FC001809E9 /* GoogleService-Info.plist */; }; + 5BE818E323271577004DE6BA /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5BE818E223271577004DE6BA /* AppDelegate.m */; }; + 5BE818E623271577004DE6BA /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5BE818E523271577004DE6BA /* ViewController.m */; }; + 5BE818E923271577004DE6BA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5BE818E723271577004DE6BA /* Main.storyboard */; }; + 5BE818EB23271579004DE6BA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5BE818EA23271579004DE6BA /* Assets.xcassets */; }; + 5BE818F123271579004DE6BA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5BE818F023271579004DE6BA /* main.m */; }; + 5BE818FB23271579004DE6BA /* RemoteConfigSampleAppUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 5BE818FA23271579004DE6BA /* RemoteConfigSampleAppUITests.m */; }; + D5C72761A16C64F0DB522FE9 /* Pods_RemoteConfigSampleAppUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1244CD4C99B6BF70752BE1B6 /* Pods_RemoteConfigSampleAppUITests.framework */; }; + F149334D7027F03ADF9FF9FC /* Pods_RemoteConfigSampleApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1D431F3C235B420372A7847C /* Pods_RemoteConfigSampleApp.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 5BE818F723271579004DE6BA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5BE818D623271577004DE6BA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5BE818DD23271577004DE6BA; + remoteInfo = RemoteConfigSampleApp; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1244CD4C99B6BF70752BE1B6 /* Pods_RemoteConfigSampleAppUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RemoteConfigSampleAppUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1D431F3C235B420372A7847C /* Pods_RemoteConfigSampleApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RemoteConfigSampleApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 320B1622DD22D8F84814CDA9 /* Pods-RemoteConfigSampleAppUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleAppUITests.release.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleAppUITests/Pods-RemoteConfigSampleAppUITests.release.xcconfig"; sourceTree = ""; }; + 5B1A9B12232846F3001809E9 /* FRCLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FRCLog.h; sourceTree = ""; }; + 5B1A9B13232846F3001809E9 /* FRCLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FRCLog.m; sourceTree = ""; }; + 5B1A9B1623284734001809E9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 5B1A9B18232849FC001809E9 /* SecondApp-GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "SecondApp-GoogleService-Info.plist"; sourceTree = ""; }; + 5B1A9B19232849FC001809E9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + 5BE818DE23271577004DE6BA /* RemoteConfigSampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RemoteConfigSampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 5BE818E123271577004DE6BA /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 5BE818E223271577004DE6BA /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 5BE818E423271577004DE6BA /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; + 5BE818E523271577004DE6BA /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; + 5BE818E823271577004DE6BA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 5BE818EA23271579004DE6BA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 5BE818EF23271579004DE6BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5BE818F023271579004DE6BA /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 5BE818F623271579004DE6BA /* RemoteConfigSampleAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RemoteConfigSampleAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 5BE818FA23271579004DE6BA /* RemoteConfigSampleAppUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RemoteConfigSampleAppUITests.m; sourceTree = ""; }; + 5BE818FC23271579004DE6BA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 5CC24231A56D124F27682F1C /* Pods-RemoteConfigSampleApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleApp.debug.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleApp/Pods-RemoteConfigSampleApp.debug.xcconfig"; sourceTree = ""; }; + DA0A0BDC31A76C0D70249412 /* Pods-RemoteConfigSampleApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleApp.release.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleApp/Pods-RemoteConfigSampleApp.release.xcconfig"; sourceTree = ""; }; + DEB267EAC843EF4BD455C9EC /* Pods-RemoteConfigSampleAppUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RemoteConfigSampleAppUITests.debug.xcconfig"; path = "Target Support Files/Pods-RemoteConfigSampleAppUITests/Pods-RemoteConfigSampleAppUITests.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5BE818DB23271577004DE6BA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F149334D7027F03ADF9FF9FC /* Pods_RemoteConfigSampleApp.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BE818F323271579004DE6BA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D5C72761A16C64F0DB522FE9 /* Pods_RemoteConfigSampleAppUITests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0C290DF47A3C2D196A167E1F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1D431F3C235B420372A7847C /* Pods_RemoteConfigSampleApp.framework */, + 1244CD4C99B6BF70752BE1B6 /* Pods_RemoteConfigSampleAppUITests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 3758ED599824787428F6BB71 /* Pods */ = { + isa = PBXGroup; + children = ( + 5CC24231A56D124F27682F1C /* Pods-RemoteConfigSampleApp.debug.xcconfig */, + DA0A0BDC31A76C0D70249412 /* Pods-RemoteConfigSampleApp.release.xcconfig */, + DEB267EAC843EF4BD455C9EC /* Pods-RemoteConfigSampleAppUITests.debug.xcconfig */, + 320B1622DD22D8F84814CDA9 /* Pods-RemoteConfigSampleAppUITests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 5BE818D523271577004DE6BA = { + isa = PBXGroup; + children = ( + 5BE818E023271577004DE6BA /* RemoteConfigSampleApp */, + 5BE818F923271579004DE6BA /* RemoteConfigSampleAppUITests */, + 5BE818DF23271577004DE6BA /* Products */, + 3758ED599824787428F6BB71 /* Pods */, + 0C290DF47A3C2D196A167E1F /* Frameworks */, + ); + sourceTree = ""; + }; + 5BE818DF23271577004DE6BA /* Products */ = { + isa = PBXGroup; + children = ( + 5BE818DE23271577004DE6BA /* RemoteConfigSampleApp.app */, + 5BE818F623271579004DE6BA /* RemoteConfigSampleAppUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 5BE818E023271577004DE6BA /* RemoteConfigSampleApp */ = { + isa = PBXGroup; + children = ( + 5B1A9B19232849FC001809E9 /* GoogleService-Info.plist */, + 5B1A9B18232849FC001809E9 /* SecondApp-GoogleService-Info.plist */, + 5B1A9B12232846F3001809E9 /* FRCLog.h */, + 5B1A9B13232846F3001809E9 /* FRCLog.m */, + 5BE818E123271577004DE6BA /* AppDelegate.h */, + 5BE818E223271577004DE6BA /* AppDelegate.m */, + 5BE818E423271577004DE6BA /* ViewController.h */, + 5BE818E523271577004DE6BA /* ViewController.m */, + 5BE818E723271577004DE6BA /* Main.storyboard */, + 5BE818EA23271579004DE6BA /* Assets.xcassets */, + 5BE818EF23271579004DE6BA /* Info.plist */, + 5BE818F023271579004DE6BA /* main.m */, + 5B1A9B1523284734001809E9 /* LaunchScreen.xib */, + ); + path = RemoteConfigSampleApp; + sourceTree = ""; + }; + 5BE818F923271579004DE6BA /* RemoteConfigSampleAppUITests */ = { + isa = PBXGroup; + children = ( + 5BE818FA23271579004DE6BA /* RemoteConfigSampleAppUITests.m */, + 5BE818FC23271579004DE6BA /* Info.plist */, + ); + path = RemoteConfigSampleAppUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5BE818DD23271577004DE6BA /* RemoteConfigSampleApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5BE818FF23271579004DE6BA /* Build configuration list for PBXNativeTarget "RemoteConfigSampleApp" */; + buildPhases = ( + 906F74C3F776C198B98BCEDD /* [CP] Check Pods Manifest.lock */, + 5BE818DA23271577004DE6BA /* Sources */, + 5BE818DB23271577004DE6BA /* Frameworks */, + 5BE818DC23271577004DE6BA /* Resources */, + 2AE302E89210FD0E9ADD4DA7 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RemoteConfigSampleApp; + productName = RemoteConfigSampleApp; + productReference = 5BE818DE23271577004DE6BA /* RemoteConfigSampleApp.app */; + productType = "com.apple.product-type.application"; + }; + 5BE818F523271579004DE6BA /* RemoteConfigSampleAppUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5BE8190223271579004DE6BA /* Build configuration list for PBXNativeTarget "RemoteConfigSampleAppUITests" */; + buildPhases = ( + 64DE2AB2473E2BB72172C6F0 /* [CP] Check Pods Manifest.lock */, + 5BE818F223271579004DE6BA /* Sources */, + 5BE818F323271579004DE6BA /* Frameworks */, + 5BE818F423271579004DE6BA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 5BE818F823271579004DE6BA /* PBXTargetDependency */, + ); + name = RemoteConfigSampleAppUITests; + productName = RemoteConfigSampleAppUITests; + productReference = 5BE818F623271579004DE6BA /* RemoteConfigSampleAppUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5BE818D623271577004DE6BA /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "Google Inc"; + TargetAttributes = { + 5BE818DD23271577004DE6BA = { + CreatedOnToolsVersion = 10.3; + }; + 5BE818F523271579004DE6BA = { + CreatedOnToolsVersion = 10.3; + TestTargetID = 5BE818DD23271577004DE6BA; + }; + }; + }; + buildConfigurationList = 5BE818D923271577004DE6BA /* Build configuration list for PBXProject "RemoteConfigSampleApp" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5BE818D523271577004DE6BA; + productRefGroup = 5BE818DF23271577004DE6BA /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5BE818DD23271577004DE6BA /* RemoteConfigSampleApp */, + 5BE818F523271579004DE6BA /* RemoteConfigSampleAppUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 5BE818DC23271577004DE6BA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BE818EB23271579004DE6BA /* Assets.xcassets in Resources */, + 5B1A9B1A232849FC001809E9 /* SecondApp-GoogleService-Info.plist in Resources */, + 5B1A9B1B232849FC001809E9 /* GoogleService-Info.plist in Resources */, + 5B1A9B1723284735001809E9 /* LaunchScreen.xib in Resources */, + 5BE818E923271577004DE6BA /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BE818F423271579004DE6BA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2AE302E89210FD0E9ADD4DA7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RemoteConfigSampleApp/Pods-RemoteConfigSampleApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-RemoteConfigSampleApp/Pods-RemoteConfigSampleApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-RemoteConfigSampleApp/Pods-RemoteConfigSampleApp-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 64DE2AB2473E2BB72172C6F0 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RemoteConfigSampleAppUITests-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; + }; + 906F74C3F776C198B98BCEDD /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RemoteConfigSampleApp-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 5BE818DA23271577004DE6BA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5B1A9B14232846F3001809E9 /* FRCLog.m in Sources */, + 5BE818E623271577004DE6BA /* ViewController.m in Sources */, + 5BE818F123271579004DE6BA /* main.m in Sources */, + 5BE818E323271577004DE6BA /* AppDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5BE818F223271579004DE6BA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BE818FB23271579004DE6BA /* RemoteConfigSampleAppUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 5BE818F823271579004DE6BA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5BE818DD23271577004DE6BA /* RemoteConfigSampleApp */; + targetProxy = 5BE818F723271579004DE6BA /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 5B1A9B1523284734001809E9 /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 5B1A9B1623284734001809E9 /* Base */, + ); + name = LaunchScreen.xib; + sourceTree = ""; + }; + 5BE818E723271577004DE6BA /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 5BE818E823271577004DE6BA /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 5BE818FD23271579004DE6BA /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 5BE818FE23271579004DE6BA /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.4; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5BE8190023271579004DE6BA /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5CC24231A56D124F27682F1C /* Pods-RemoteConfigSampleApp.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 246BUJU4JM; + INFOPLIST_FILE = RemoteConfigSampleApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.config.testapp.dev.RemoteConfigSampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5BE8190123271579004DE6BA /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DA0A0BDC31A76C0D70249412 /* Pods-RemoteConfigSampleApp.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 246BUJU4JM; + INFOPLIST_FILE = RemoteConfigSampleApp/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.config.testapp.dev.RemoteConfigSampleApp; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 5BE8190323271579004DE6BA /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DEB267EAC843EF4BD455C9EC /* Pods-RemoteConfigSampleAppUITests.debug.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 246BUJU4JM; + INFOPLIST_FILE = RemoteConfigSampleAppUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.config.testapp.dev.RemoteConfigSampleAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = RemoteConfigSampleApp; + }; + name = Debug; + }; + 5BE8190423271579004DE6BA /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 320B1622DD22D8F84814CDA9 /* Pods-RemoteConfigSampleAppUITests.release.xcconfig */; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = 246BUJU4JM; + INFOPLIST_FILE = RemoteConfigSampleAppUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.firebase.config.testapp.dev.RemoteConfigSampleAppUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = RemoteConfigSampleApp; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5BE818D923271577004DE6BA /* Build configuration list for PBXProject "RemoteConfigSampleApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BE818FD23271579004DE6BA /* Debug */, + 5BE818FE23271579004DE6BA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5BE818FF23271579004DE6BA /* Build configuration list for PBXNativeTarget "RemoteConfigSampleApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BE8190023271579004DE6BA /* Debug */, + 5BE8190123271579004DE6BA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5BE8190223271579004DE6BA /* Build configuration list for PBXNativeTarget "RemoteConfigSampleAppUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5BE8190323271579004DE6BA /* Debug */, + 5BE8190423271579004DE6BA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5BE818D623271577004DE6BA /* Project object */; +} diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/AppDelegate.h b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/AppDelegate.h new file mode 100644 index 00000000000..3e55b05f9a2 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/AppDelegate.h @@ -0,0 +1,21 @@ +// Copyright 2019 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 + +@interface AppDelegate : UIResponder + +@property(strong, nonatomic) UIWindow *window; + +@end diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/AppDelegate.m b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/AppDelegate.m new file mode 100644 index 00000000000..6bc281700fd --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/AppDelegate.m @@ -0,0 +1,40 @@ +// Copyright 2019 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 "AppDelegate.h" + +#import +#import +#import + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + FIROptions *options = [FIROptions defaultOptions]; + [FIRApp configureWithOptions:options]; + + FIROptions *secondAppOptions = [[FIROptions alloc] + initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"SecondApp-GoogleService-Info" + ofType:@"plist"]]; + [FIRApp configureWithName:@"secondFIRApp" options:secondAppOptions]; + [[FIRConfiguration sharedInstance] setLoggerLevel:FIRLoggerLevelMax]; + return YES; +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..d8db8d65fd7 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Assets.xcassets/Contents.json b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Base.lproj/LaunchScreen.xib b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Base.lproj/LaunchScreen.xib new file mode 100644 index 00000000000..012f054aa98 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Base.lproj/LaunchScreen.xib @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Base.lproj/Main.storyboard b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..9d603d08592 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Base.lproj/Main.storyboard @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/FRCLog.h b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/FRCLog.h new file mode 100644 index 00000000000..3659a835a94 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/FRCLog.h @@ -0,0 +1,24 @@ +// Copyright 2019 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 +#import + +@interface FRCLog : NSObject + ++ (instancetype)sharedInstance; + +- (void)setLogView:(UITextView*)view; +- (void)logToConsole:(NSString*)text; +@end diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/FRCLog.m b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/FRCLog.m new file mode 100644 index 00000000000..d39689abb75 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/FRCLog.m @@ -0,0 +1,76 @@ +// Copyright 2019 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 "FRCLog.h" +#import + +@implementation FRCLog + +__weak UITextView* _logView; +NSString* _textUntilTextViewSet; + ++ (instancetype)sharedInstance { + static dispatch_once_t onceToken; + static FRCLog* sharedInstance; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FRCLog alloc] init]; + }); + return sharedInstance; +} + +- (void)setLogView:(UITextView*)view { + _logView = view; + _logView.text = @""; +} + +- (void)logToConsoleInternal:(NSString*)text { + NSDate* now = [NSDate date]; + NSDateFormatter* dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"HH:mm:ss"]; + + NSString* append = + [[NSString stringWithFormat:@"FRCLog(%@): ", [dateFormatter stringFromDate:now]] + stringByAppendingString:text]; + append = [append stringByAppendingString:@"\n"]; + NSLog(@"%@", append); + if (!_logView) { + NSLog(@"FRCLog: Logview not set"); + if (!_textUntilTextViewSet) _textUntilTextViewSet = [[NSString alloc] initWithString:append]; + [_textUntilTextViewSet stringByAppendingString:append]; + } else { + if (_textUntilTextViewSet) { + _logView.text = _textUntilTextViewSet; + _textUntilTextViewSet = nil; + } + _logView.text = [_logView.text stringByAppendingString:append]; + } + [self scrollToBottom]; +} + +- (void)logToConsole:(NSString*)text { + if ([NSThread isMainThread]) { + [self logToConsoleInternal:text]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + [self logToConsoleInternal:text]; + }); + } +} + +- (void)scrollToBottom { + if (_logView && _logView.text.length > 0) { + [_logView scrollRangeToVisible:NSMakeRange(_logView.text.length - 1, 1)]; + } +} +@end diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Info.plist b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Info.plist new file mode 100644 index 00000000000..16be3b68112 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.h b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.h new file mode 100644 index 00000000000..43179174ad9 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.h @@ -0,0 +1,20 @@ +// Copyright 2019 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 +/// Main view controller with a button that triggered the fetch config api call +@interface ViewController + : UIViewController + +@end diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m new file mode 100644 index 00000000000..f63da9f9442 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/ViewController.m @@ -0,0 +1,501 @@ +// Copyright 2019 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 "ViewController.h" + +#import +#import +#import +#import +#import +#import +#import "FRCLog.h" + +static NSString *const FIRPerfNamespace = @"fireperf"; +static NSString *const FIRDefaultFIRAppName = @"__FIRAPP_DEFAULT"; +static NSString *const FIRSecondFIRAppName = @"secondFIRApp"; + +@interface ViewController () +@property(nonatomic, strong) IBOutlet UIButton *fetchButton; +@property(nonatomic, strong) IBOutlet UIButton *applyButton; +@property(nonatomic, strong) IBOutlet UIButton *refreshButton; +@property(nonatomic, strong) IBOutlet UIButton *clearLogsButton; +@property(nonatomic, strong) IBOutlet UITextView *mainTextView; +/// Key of custom variable to be added by user. +@property(nonatomic, weak) IBOutlet UITextField *keyLabel; +/// Value of custom variable to be added by user. +@property(nonatomic, weak) IBOutlet UITextField *valueLabel; +/// Expiration in seconds to be set by user. +@property(nonatomic, weak) IBOutlet UITextField *expirationLabel; +/// Config Defaults. +@property(nonatomic, strong) NSMutableDictionary *configDefaults; +/// developer mode switch. +@property(strong, nonatomic) IBOutlet UISwitch *developerModeEnabled; +/// Current selected namespace. +@property(nonatomic, copy) NSString *currentNamespace; +/// Curret selected FIRApp instance name. +@property(nonatomic, copy) NSString *FIRAppName; +/// Selected namespace picker control view. +@property(nonatomic, strong) IBOutlet UIPickerView *namespacePicker; +/// Selected app picker control view. +@property(nonatomic, strong) IBOutlet UIPickerView *appPicker; +/// Array of prepopulated namespaces supported by this app. +@property(nonatomic, strong) NSArray *namespacePickerData; +/// Array of prepopulated FIRApp names supported by this app. +@property(nonatomic, strong) NSArray *appPickerData; +/// Array of Remote Config instances. +@property(nonatomic, strong) NSMutableDictionary *RCInstances; +@end + +@implementation ViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[FRCLog sharedInstance] setLogView:self.mainTextView]; + [[FRCLog sharedInstance] logToConsole:@"Viewcontroller loaded"]; + [[FRCLog sharedInstance] logToConsole:[[NSBundle mainBundle] bundleIdentifier]]; + + // Setup UI + self.expirationLabel.text = [NSString stringWithFormat:@"0"]; + self.configDefaults = [[NSMutableDictionary alloc] init]; + self.keyLabel.delegate = self; + self.valueLabel.delegate = self; + self.expirationLabel.delegate = self; + self.mainTextView.editable = NO; + + // TODO(mandard): Add support for deleting and adding namespaces in the app. + self.namespacePickerData = + [[NSArray alloc] initWithObjects:FIRNamespaceGoogleMobilePlatform, FIRPerfNamespace, nil]; + self.appPickerData = + [[NSArray alloc] initWithObjects:FIRDefaultFIRAppName, FIRSecondFIRAppName, nil]; + self.RCInstances = [[NSMutableDictionary alloc] init]; + for (NSString *namespaceString in self.namespacePickerData) { + for (NSString *appString in self.appPickerData) { + // Check for the default instance. + if (!self.RCInstances[namespaceString]) { + self.RCInstances[namespaceString] = [[NSMutableDictionary alloc] init]; + } + if ([namespaceString isEqualToString:FIRNamespaceGoogleMobilePlatform] && + [appString isEqualToString:FIRDefaultFIRAppName]) { + self.RCInstances[namespaceString][appString] = [FIRRemoteConfig remoteConfig]; + } else { + FIRApp *firebaseApp = ([appString isEqualToString:FIRDefaultFIRAppName]) + ? [FIRApp defaultApp] + : [FIRApp appNamed:appString]; + self.RCInstances[namespaceString][appString] = + [FIRRemoteConfig remoteConfigWithFIRNamespace:namespaceString app:firebaseApp]; + } + FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init]; + settings.fetchTimeout = 300; + settings.minimumFetchInterval = 0; + ((FIRRemoteConfig *)(self.RCInstances[namespaceString][appString])).configSettings = settings; + } + } + [[FRCLog sharedInstance] logToConsole:@"RC instances inited"]; + + self.namespacePicker.dataSource = self; + self.namespacePicker.delegate = self; + self.appPicker.dataSource = self; + self.appPicker.delegate = self; + [self.developerModeEnabled setOn:true animated:false]; +} + +- (IBAction)fetchButtonPressed:(id)sender { + [[FRCLog sharedInstance] logToConsole:@"Fetch button pressed"]; + // fetchConfig api callback, this is triggered when client receives response from server + ViewController *__weak weakSelf = self; + FIRRemoteConfigFetchCompletion completion = ^(FIRRemoteConfigFetchStatus status, NSError *error) { + ViewController *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + [[FRCLog sharedInstance] + logToConsole:[NSString stringWithFormat:@"Fetch completed. Status=%@", + [strongSelf statusString:status]]]; + if (error) { + [[FRCLog sharedInstance] logToConsole:[NSString stringWithFormat:@"Fetch Error=%@", error]]; + } + + NSMutableString *output = [NSMutableString + stringWithFormat:@"Fetch status : %@.\n\n", [strongSelf statusString:status]]; + if (error) { + [output appendFormat:@"%@\n", error]; + } + if (status == FIRRemoteConfigFetchStatusFailure) { + [output appendString:[NSString stringWithFormat:@"Fetch Error :%@.\n", + [strongSelf errorString:error.code]]]; + if (error.code == FIRRemoteConfigErrorThrottled) { + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + NSNumber *throttledTime = + (NSNumber *)error.userInfo[FIRRemoteConfigThrottledEndTimeInSecondsKey]; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:[throttledTime doubleValue]]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + NSString *timeString = [dateFormatter stringFromDate:date]; + + [output appendString:[NSString stringWithFormat:@"Throttled end at: %@ \n", timeString]]; + } + } + [[FRCLog sharedInstance] logToConsole:output]; + }; + + // fetchConfig api call + [[FRCLog sharedInstance] logToConsole:@"Calling fetchWithExpirationDuration.."]; + [self.RCInstances[self.currentNamespace][self.FIRAppName] + fetchWithExpirationDuration:self.expirationLabel.text.integerValue + completionHandler:completion]; +} + +- (IBAction)fetchAndActivateButtonPressed:(id)sender { + // fetchConfig api callback, this is triggered when client receives response from server + ViewController *__weak weakSelf = self; + FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion = ^( + FIRRemoteConfigFetchAndActivateStatus status, NSError *error) { + ViewController *strongSelf = weakSelf; + if (!strongSelf) { + return; + } + NSMutableString *output = [NSMutableString + stringWithFormat:@"Fetch and activate status : %@.\n\n", + (status == FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote || + status == FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData) + ? @"Success" + : [NSString + stringWithFormat:@"Failure: %@", [error localizedDescription]]]; + if (error) { + [output appendFormat:@"%@\n", error]; + } + if (status == FIRRemoteConfigFetchAndActivateStatusError) { + [output appendString:[NSString stringWithFormat:@"Fetch And Activate Error :%@.\n", + [strongSelf errorString:error.code]]]; + if (error.code == FIRRemoteConfigErrorThrottled) { + [output appendString:[NSString stringWithFormat:@"Throttled.\n"]]; + } + } + // activate status + [[FRCLog sharedInstance] logToConsole:output]; + if (status == FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote || + status == FIRRemoteConfigFetchAndActivateStatusSuccessUsingPreFetchedData) { + [strongSelf printResult:[[NSMutableString alloc] init]]; + } + }; + + // fetchConfig api call + [self.RCInstances[self.currentNamespace][self.FIRAppName] + fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion]; +} + +- (IBAction)activateButtonPressed:(id)sender { + [self apply]; +} + +- (IBAction)refreshButtonPressed:(id)sender { + NSMutableString *output = [[NSMutableString alloc] init]; + [self printResult:output]; +} + +- (IBAction)setDefaultFromPlistButtonPressed:(id)sender { + [self.RCInstances[self.currentNamespace][self.FIRAppName] + setDefaultsFromPlistFileName:@"Defaults"]; + [self printDefaultConfigs]; +} + +- (IBAction)setDefaultButtonPressed:(id)sender { + if (self.configDefaults.count) { + [self.RCInstances[self.currentNamespace][self.FIRAppName] setDefaults:self.configDefaults]; + [self.configDefaults removeAllObjects]; + [self printDefaultConfigs]; + } else { + [[FRCLog sharedInstance] logToConsole:@"Nothing to set for defaults."]; + } +} + +- (IBAction)onClearLogsButtonPressed:(id)sender { + self.mainTextView.text = @""; +} + +- (void)printDefaultConfigs { + NSMutableString *output = [[NSMutableString alloc] init]; + [output appendString:@"\n-------Default config------\n"]; + NSArray *result = [self.RCInstances[self.currentNamespace][self.FIRAppName] + allKeysFromSource:FIRRemoteConfigSourceDefault]; + if (result) { + NSString *stringPerNs = @""; + for (NSString *key in result) { + FIRRemoteConfigValue *value = + [self.RCInstances[self.currentNamespace][self.FIRAppName] defaultValueForKey:key]; + stringPerNs = [NSString stringWithFormat:@"%@%@ : %@ : %@\n", stringPerNs, + self.currentNamespace, key, value.stringValue]; + } + [output appendString:stringPerNs]; + } + [[FRCLog sharedInstance] logToConsole:output]; +} + +- (IBAction)getValueButtonPressed:(id)sender { + [[FRCLog sharedInstance] logToConsole:[self.RCInstances[self.currentNamespace][self.FIRAppName] + configValueForKey:self.keyLabel.text] + .debugDescription]; +} + +- (IBAction)logValueButtonPressed:(id)sender { + [[FRCLog sharedInstance] + logToConsole:[NSString stringWithFormat:@"key: %@ logged", self.keyLabel.text]]; +} + +// add default variable button pressed +- (IBAction)addButtonPressed:(id)sender { + [self addNewEntryToVariables:self.configDefaults isDefaults:YES]; +} + +- (IBAction)developerModeSwitched:(id)sender { + FIRRemoteConfigSettings *configSettings = + [[FIRRemoteConfigSettings alloc] initWithDeveloperModeEnabled:self.developerModeEnabled.isOn]; + ((FIRRemoteConfig *)(self.RCInstances[self.currentNamespace][self.FIRAppName])).configSettings = + configSettings; + [[FRCLog sharedInstance] + logToConsole:[NSString + stringWithFormat:@"Developer Mode Enabled: %d\n", + ((FIRRemoteConfig *)(self.RCInstances[self.currentNamespace] + [self.FIRAppName])) + .configSettings.isDeveloperModeEnabled]]; +} + +- (void)addNewEntryToVariables:(NSMutableDictionary *)variables isDefaults:(BOOL)isDefaults { + if ([self.keyLabel.text length]) { + variables[self.keyLabel.text] = self.valueLabel.text; + + NSString *showText = @"custom variables "; + if (isDefaults) { + showText = @"config defaults"; + } + [[FRCLog sharedInstance] + logToConsole:[NSString stringWithFormat:@"New %@ added %@ : %@\n", showText, + self.keyLabel.text, self.valueLabel.text]]; + + self.keyLabel.text = @""; + self.valueLabel.text = @""; + } +} + +- (void)apply { + [self.RCInstances[self.currentNamespace][self.FIRAppName] + activateWithCompletionHandler:^(NSError *_Nullable error) { + NSMutableString *output = [[NSMutableString alloc] init]; + [output appendString:[NSString stringWithFormat:@"ActivateFetched = %@\n", + error ? @"NO" : @"YES"]]; + [[FRCLog sharedInstance] logToConsole:output]; + if (!error) { + [self printResult:output]; + } else { + [self printResult:[[NSString stringWithFormat:@"Activate failed. Error: %@", + error.localizedDescription] mutableCopy]]; + } + }]; +} + +// print out fetch result +- (void)printResult:(NSMutableString *)output { + FIRRemoteConfig *currentRCInstance = self.RCInstances[self.currentNamespace][self.FIRAppName]; + NSString *namespace_p = self.currentNamespace; + [output appendString:@"-------Active config------\n"]; + + NSArray *result = [self.RCInstances[self.currentNamespace][self.FIRAppName] + allKeysFromSource:FIRRemoteConfigSourceRemote]; + if (result) { + NSString *stringPerNs = @""; + for (NSString *key in result) { + FIRRemoteConfigValue *value = [currentRCInstance configValueForKey:key]; + stringPerNs = [NSString + stringWithFormat:@"%@%@ : %@ : %@\n", stringPerNs, namespace_p, key, value.stringValue]; + } + [output appendString:stringPerNs]; + } + [output appendString:@"\n-------Default config------\n"]; + result = [currentRCInstance allKeysFromSource:FIRRemoteConfigSourceDefault]; + if (result) { + NSString *stringPerNs = @""; + for (NSString *key in result) { + FIRRemoteConfigValue *value = [currentRCInstance defaultValueForKey:key]; + stringPerNs = [NSString + stringWithFormat:@"%@%@ : %@ : %@\n", stringPerNs, namespace_p, key, value.stringValue]; + } + [output appendString:stringPerNs]; + } + + [output appendString:@"\n--------Custom Variables--------\n"]; + + [output + appendString:[NSString + stringWithFormat:@"Developer Mode Enabled: %d\n", + currentRCInstance.configSettings.isDeveloperModeEnabled]]; + + [output appendString:@"\n----------Last fetch time----------------\n"]; + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + [output + appendString:[NSString stringWithFormat:@"%@\n", + [dateFormatter + stringFromDate:currentRCInstance.lastFetchTime]]]; + [output appendString:@"\n-----------Last fetch status------------\n"]; + [output appendString:[NSString + stringWithFormat:@"%@\n", + [self statusString:currentRCInstance.lastFetchStatus]]]; + + FIRInstanceID *instanceID = [FIRInstanceID instanceID]; + NSError *error; + NSString *instanceIDString = [instanceID appInstanceID:&error]; + [output appendString:@"\n-----------Instance ID------------------\n"]; + [output appendString:[NSString stringWithFormat:@"%@\n", instanceIDString]]; + + [output appendString:@"\n-----------Instance ID token------------\n"]; + [output + appendString:[NSString + stringWithFormat:@"%@\n", currentRCInstance.settings.configInstanceIDToken]]; + + [output appendString:@"\n-----------Android ID------------\n"]; + [output + appendString:[NSString stringWithFormat:@"%@\n", currentRCInstance.settings.deviceAuthID]]; + + [[FRCLog sharedInstance] logToConsole:output]; +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + [textField resignFirstResponder]; + return YES; +} + +- (NSString *)statusString:(FIRRemoteConfigFetchStatus)status { + switch (status) { + case FIRRemoteConfigFetchStatusSuccess: + return @"Success"; + case FIRRemoteConfigFetchStatusNoFetchYet: + return @"NotFetchYet"; + case FIRRemoteConfigFetchStatusFailure: + return @"Failure"; + case FIRRemoteConfigFetchStatusThrottled: + return @"Throttled"; + default: + return @"Unknown"; + } + return @""; +} + +- (NSString *)errorString:(FIRRemoteConfigError)error { + switch (error) { + case FIRRemoteConfigErrorInternalError: + return @"Internal Error"; + case FIRRemoteConfigErrorUnknown: + return @"Unknown Error"; + case FIRRemoteConfigErrorThrottled: + return @"Throttled"; + default: + return @"unknown"; + } + return @""; +} + +- (IBAction)fetchIIDButtonClicked:(id)sender { + FIRInstanceID *instanceID = [FIRInstanceID instanceID]; + FIRInstanceIDTokenHandler instanceIDHandler = ^(NSString *token, NSError *error) { + if (error) { + [[FRCLog sharedInstance] logToConsole:[NSString stringWithFormat:@"%@", error]]; + } + if (token) { + ((FIRRemoteConfig *)self.RCInstances[self.currentNamespace][self.FIRAppName]) + .settings.configInstanceIDToken = token; + NSString *iid = [instanceID appInstanceID:&error]; + [[FRCLog sharedInstance] + logToConsole: + [NSString + stringWithFormat:@"Successfully getting InstanceID : \n\n%@\n\nToken : \n\n%@\n", + iid, token]]; + } + }; + [instanceID tokenWithAuthorizedEntity:[FIRApp appNamed:self.FIRAppName].options.GCMSenderID + scope:@"*" + options:nil + handler:instanceIDHandler]; +} + +- (IBAction)searchButtonClicked:(id)sender { + NSString *output = @"-------Active Config------\n"; + + for (NSString *key in [self.RCInstances[self.currentNamespace][self.FIRAppName] + keysWithPrefix:self.keyLabel.text]) { + FIRRemoteConfigValue *value = + ((FIRRemoteConfig *)(self.RCInstances[self.currentNamespace][self.FIRAppName]))[key]; + output = [NSString stringWithFormat:@"%@%@ : %@ : %@\n", output, self.currentNamespace, key, + value.stringValue]; + } + + [[FRCLog sharedInstance] logToConsole:output]; +} + +- (IBAction)userPropertyButtonClicked:(id)sender { + [FIRAnalytics setUserPropertyString:self.valueLabel.text forName:self.keyLabel.text]; + + NSString *output = [NSString + stringWithFormat:@"Set User Property => %@ : %@\n", self.keyLabel.text, self.valueLabel.text]; + [[FRCLog sharedInstance] logToConsole:output]; +} + +#pragma mark - picker + +// The number of columns of data +- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { + // App and currentNamespace pickers. + return 2; +} + +// The number of rows of data +- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component { + NSInteger rowCount = (component == 0) ? self.namespacePickerData.count : self.appPickerData.count; + return rowCount; +} + +// The data to return for the row and component (column) that's being passed in +- (NSString *)pickerView:(UIPickerView *)pickerView + titleForRow:(NSInteger)row + forComponent:(NSInteger)component { + if (component == 0) { + self.currentNamespace = self.namespacePickerData[row]; + return self.namespacePickerData[row]; + } else { + self.FIRAppName = self.appPickerData[row]; + return self.appPickerData[row]; + } +} + +- (UIView *)pickerView:(UIPickerView *)pickerView + viewForRow:(NSInteger)row + forComponent:(NSInteger)component + reusingView:(UIView *)view { + UILabel *tView = (UILabel *)view; + if (!tView) { + tView = [[UILabel alloc] init]; + [tView setFont:[UIFont fontWithName:@"Helvetica" size:15]]; + tView.numberOfLines = 3; + } + if (component == 0) { + self.currentNamespace = self.namespacePickerData[row]; + tView.text = self.namespacePickerData[row]; + } else { + self.FIRAppName = self.appPickerData[row]; + tView.text = self.appPickerData[row]; + } + + return tView; +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/main.m b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/main.m new file mode 100644 index 00000000000..078f4b680d5 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleApp/main.m @@ -0,0 +1,23 @@ +// Copyright 2019 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 + +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleAppUITests/Info.plist b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleAppUITests/Info.plist new file mode 100644 index 00000000000..6c40a6cd0c4 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleAppUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleAppUITests/RemoteConfigSampleAppUITests.m b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleAppUITests/RemoteConfigSampleAppUITests.m new file mode 100644 index 00000000000..0242fa33fdc --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Sample/RemoteConfigSampleAppUITests/RemoteConfigSampleAppUITests.m @@ -0,0 +1,48 @@ +// Copyright 2019 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 + +@interface RemoteConfigSampleAppUITests : XCTestCase + +@end + +@implementation RemoteConfigSampleAppUITests + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the + // class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + self.continueAfterFailure = NO; + + // UI tests must launch the application that they test. Doing this in setup will make sure it + // happens for each test method. + [[[XCUIApplication alloc] init] launch]; + + // In UI tests it’s important to set the initial state - such as interface orientation - required + // for your tests before they run. The setUp method is a good place to do this. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. +} + +- (void)testExample { + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/Defaults-testInfo.plist b/FirebaseRemoteConfig/Tests/Unit/Defaults-testInfo.plist new file mode 100644 index 00000000000..a8975fa800a --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/Defaults-testInfo.plist @@ -0,0 +1,20 @@ + + + + + lastCheckTime + 2016-02-28T18:33:31Z + isPaidUser + + dataValue + Mi40 + New item + 2.4 + Languages + English + FileInfo + To setup default config. + format + key to value. + + diff --git a/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m b/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m new file mode 100644 index 00000000000..cf4b0862191 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/FIRRemoteConfigComponentTest.m @@ -0,0 +1,196 @@ +/* + * Copyright 2019 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 + +#import +#import +#import +#import +#import "FirebaseRemoteConfig/Sources/FIRRemoteConfigComponent.h" +#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" +#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" + +@interface FIRRemoteConfigComponentTest : XCTestCase +@end + +@implementation FIRRemoteConfigComponentTest + +- (void)tearDown { + [super tearDown]; + + // Clear out any apps that were called with `configure`. + [FIRApp resetApps]; +} + +- (void)testRCInstanceCreationAndCaching { + // Create the provider to vend Remote Config instances. + FIRRemoteConfigComponent *provider = [self providerForTest]; + + // Create a Remote Config instance from the provider. + NSString *sharedNamespace = @"some_namespace"; + FIRRemoteConfig *config = [provider remoteConfigForNamespace:sharedNamespace]; + XCTAssertNotNil(config); + + // Fetch an instance with the same namespace - should be the same instance. + FIRRemoteConfig *sameConfig = [provider remoteConfigForNamespace:sharedNamespace]; + XCTAssertNotNil(sameConfig); + XCTAssertEqual(config, sameConfig); +} + +- (void)testRCSeparateInstancesForDifferentNamespaces { + // Create the provider to vend Remote Config instances. + FIRRemoteConfigComponent *provider = [self providerForTest]; + + // Create a Remote Config instance from the provider. + FIRRemoteConfig *config = [provider remoteConfigForNamespace:@"namespace1"]; + XCTAssertNotNil(config); + + // Fetch another instance with a different namespace. + FIRRemoteConfig *config2 = [provider remoteConfigForNamespace:@"namespace2"]; + XCTAssertNotNil(config2); + XCTAssertNotEqual(config, config2); +} + +- (void)testRCSeparateInstancesForDifferentApps { + FIRRemoteConfigComponent *provider = [self providerForTest]; + + // Create a Remote Config instance from the provider. + NSString *sharedNamespace = @"some_namespace"; + FIRRemoteConfig *config = [provider remoteConfigForNamespace:sharedNamespace]; + XCTAssertNotNil(config); + + // Use a new app and new povider, ensure the instances with the same namespace are different. + NSString *secondAppName = [provider.app.name stringByAppendingString:@"2"]; + FIRApp *secondApp = [[FIRApp alloc] initInstanceWithName:secondAppName + options:[self fakeOptions]]; + FIRRemoteConfigComponent *separateProvider = + [[FIRRemoteConfigComponent alloc] initWithApp:secondApp]; + FIRRemoteConfig *separateConfig = [separateProvider remoteConfigForNamespace:sharedNamespace]; + XCTAssertNotNil(separateConfig); + XCTAssertNotEqual(config, separateConfig); +} + +- (void)testInitialization { + // Explicitly instantiate the component here in case the providerForTest ever changes to mock + // something. + NSString *appName = [self generatedTestAppName]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:appName options:[self fakeOptions]]; + FIRRemoteConfigComponent *provider = [[FIRRemoteConfigComponent alloc] initWithApp:app]; + XCTAssertNotNil(provider); + XCTAssertNotNil(provider.app); +} + +- (void)testRegistersAsLibrary { + XCTAssertEqual([FIRRemoteConfigComponent componentsToRegister].count, 1); + + // Configure a test FIRApp for fetching an instance of the FIRRemoteConfigProvider. + NSString *appName = [self generatedTestAppName]; + [FIRApp configureWithName:appName options:[self fakeOptions]]; + FIRApp *app = [FIRApp appNamed:appName]; + + // Attempt to fetch the component and verify it's a valid instance. + id provider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container); + XCTAssertNotNil(provider); + + // Ensure that the instance that comes from the container is cached. + id sameProvider = FIR_COMPONENT(FIRRemoteConfigProvider, app.container); + XCTAssertNotNil(sameProvider); + XCTAssertEqual(provider, sameProvider); +} + +- (void)testThrowsWithEmptyGoogleAppID { + FIROptions *options = [self fakeOptions]; + options.googleAppID = @""; + + // Create the provider to vend Remote Config instances. + NSString *appName = [self generatedTestAppName]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:appName options:options]; + FIRRemoteConfigComponent *component = [[FIRRemoteConfigComponent alloc] initWithApp:app]; + + // Creating a Remote Config instance should fail since the googleAppID is empty. + XCTAssertThrows([component remoteConfigForNamespace:@"some_namespace"]); +} + +- (void)testThrowsWithNilGoogleAppID { + FIROptions *options = [self fakeOptions]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + options.googleAppID = nil; +#pragma clang diagnostic pop + + // Create the provider to vend Remote Config instances. + NSString *appName = [self generatedTestAppName]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:appName options:options]; + FIRRemoteConfigComponent *component = [[FIRRemoteConfigComponent alloc] initWithApp:app]; + + // Creating a Remote Config instance should fail since the googleAppID is nil. + XCTAssertThrows([component remoteConfigForNamespace:@"some_namespace"]); +} + +- (void)testThrowsWithEmptyGCMSenderID { + FIROptions *options = [self fakeOptions]; + options.GCMSenderID = @""; + + // Create the provider to vend Remote Config instances. + NSString *appName = [self generatedTestAppName]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:appName options:options]; + FIRRemoteConfigComponent *component = [[FIRRemoteConfigComponent alloc] initWithApp:app]; + + // Creating a Remote Config instance should fail since the GCMSenderID is empty. + XCTAssertThrows([component remoteConfigForNamespace:@"some_namespace"]); +} + +- (void)testThrowsWithNilGCMSenderID { + FIROptions *options = [self fakeOptions]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wnonnull" + options.GCMSenderID = nil; +#pragma clang diagnostic pop + + // Create the provider to vend Remote Config instances. + NSString *appName = [self generatedTestAppName]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:appName options:options]; + FIRRemoteConfigComponent *component = [[FIRRemoteConfigComponent alloc] initWithApp:app]; + + // Creating a Remote Config instance should fail since the GCMSenderID is nil. + XCTAssertThrows([component remoteConfigForNamespace:@"some_namespace"]); +} + +#pragma mark - Helpers + +- (FIROptions *)fakeOptions { + return [[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:123abc" + GCMSenderID:@"correct_gcm_sender_id"]; +} + +- (NSString *)generatedTestAppName { + return [RCNTestUtilities generatedTestAppNameForTest:self.name]; +} + +- (FIRRemoteConfigComponent *)providerForTest { + // Create the provider to vend Remote Config instances. + NSString *appName = [self generatedTestAppName]; + FIROptions *options = [[self fakeOptions] copy]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:appName options:options]; + FIRRemoteConfigComponent *provider = [[FIRRemoteConfigComponent alloc] initWithApp:app]; + XCTAssertNotNil(provider); + XCTAssert(provider.app.options.googleAppID.length != 0); + XCTAssert(provider.app.options.GCMSenderID.length != 0); + return provider; +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigAnalyticsTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigAnalyticsTest.m new file mode 100644 index 00000000000..c874b5711a0 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigAnalyticsTest.m @@ -0,0 +1,87 @@ +/* + * Copyright 2019 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 + +#import +#import "RCNConfigAnalytics.h" + +@interface RCNConfigAnalyticsTest : XCTestCase { + id _mockAnalytics; + RCNConfigAnalytics *_configAnalytics; +} +@end + +@implementation RCNConfigAnalyticsTest + +- (void)setUp { + [super setUp]; + _mockAnalytics = OCMClassMock([FIRAnalytics class]); + _configAnalytics = [[RCNConfigAnalytics alloc] init]; +} + +- (void)testFetchUserProperty { + XCTestExpectation *fetchExpectation = + [self expectationWithDescription:@"Test fetch user property."]; + + // Mocks the user property fetching response. + OCMStub([_mockAnalytics userPropertiesIncludingInternal:NO + queue:[OCMArg any] + callback:([OCMArg invokeBlockWithArgs:@{ + @"user_property_event_name" : @"level up", + @"user_property_gold_amount" : @"1800", + @"user_property_level" : @20 + }, + nil])]); + + [_configAnalytics fetchUserPropertiesWithCompletionHandler:^(NSDictionary *userProperties) { + XCTAssertNotNil(userProperties); + XCTAssertEqual(userProperties.count, 3); + XCTAssertEqualObjects(@"level up", userProperties[@"user_property_event_name"]); + XCTAssertEqualObjects(@20, userProperties[@"user_property_level"]); + XCTAssertEqualObjects(@"1800", userProperties[@"user_property_gold_amount"]); + + [fetchExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1.0 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchUserPropertyWithEmptyResponse { + XCTestExpectation *fetchExpectation = + [self expectationWithDescription:@"Test fetch empty user property."]; + + // Mocks the user property fetching response. + OCMStub([_mockAnalytics userPropertiesIncludingInternal:NO + queue:[OCMArg any] + callback:([OCMArg invokeBlockWithArgs:@{}, nil])]); + + // Tests passing nil callback won't crash. + [_configAnalytics fetchUserPropertiesWithCompletionHandler:nil]; + + [_configAnalytics fetchUserPropertiesWithCompletionHandler:^(NSDictionary *userProperties) { + XCTAssertNotNil(userProperties); + XCTAssertEqual(userProperties.count, 0); + [fetchExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1.0 + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m new file mode 100644 index 00000000000..99397e3f244 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m @@ -0,0 +1,266 @@ +/* + * Copyright 2019 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 + +#import +#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" +#import "FirebaseRemoteConfig/Sources/Public/FIRRemoteConfig.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" +#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" + +@interface RCNConfigContent (Testing) +- (void)checkAndWaitForInitialDatabaseLoad; +@end + +@interface RCNConfigContentTest : XCTestCase { + NSTimeInterval _expectationTimeout; + RCNConfigContent *_configContent; + NSString *namespaceApp1, *namespaceApp2; +} +@end + +/// Unit Tests for RCNConfigContent methods. +@implementation RCNConfigContentTest +- (void)setUp { + [super setUp]; + _expectationTimeout = 1.0; + + namespaceApp1 = [NSString + stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsDefaultFIRAppName]; + namespaceApp2 = [NSString + stringWithFormat:@"%@:%@", FIRNamespaceGoogleMobilePlatform, RCNTestsSecondFIRAppName]; + + _configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; + + id partialMock = OCMPartialMock(_configContent); + OCMStub([partialMock checkAndWaitForInitialDatabaseLoad]).andDo(nil); +} + +/// Passing in a nil bundleID should not crash the app +- (void)testCrashShouldNotHappenWithoutMainBundleID { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([NSBundle mainBundle]).andReturn(mockBundle); + OCMStub([mockBundle bundleIdentifier]).andReturn(nil); + _configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; + [mockBundle stopMocking]; +} + +/// Standard test case of receiving updated config from fetch. +- (void)testUpdateConfigContentForMultipleApps { + NSMutableDictionary *config1ToSet = + [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; + NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"}; + [config1ToSet setValue:entries forKey:@"entries"]; + [_configContent updateConfigContentWithResponse:config1ToSet forNamespace:namespaceApp1]; + + // Update for second app. + NSMutableDictionary *config2ToSet = + [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; + NSDictionary *entries2 = @{@"key11" : @"value11", @"key21" : @"value21"}; + [config2ToSet setValue:entries2 forKey:@"entries"]; + [_configContent updateConfigContentWithResponse:config2ToSet forNamespace:namespaceApp2]; + + // Check config for first app. + + NSDictionary *fetchedConfig = _configContent.fetchedConfig; + XCTAssertNotNil(fetchedConfig[namespaceApp1][@"key1"]); + XCTAssertEqualObjects([fetchedConfig[namespaceApp1][@"key1"] stringValue], @"value1"); + XCTAssertNotNil(fetchedConfig[namespaceApp1][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[namespaceApp1][@"key2"] stringValue], @"value2"); + + // Check config for second app. + + fetchedConfig = _configContent.fetchedConfig; + XCTAssertNotNil(fetchedConfig[namespaceApp2][@"key11"]); + XCTAssertEqualObjects([fetchedConfig[namespaceApp2][@"key11"] stringValue], @"value11"); + XCTAssertNotNil(fetchedConfig[namespaceApp2][@"key21"]); + XCTAssertEqualObjects([fetchedConfig[namespaceApp2][@"key21"] stringValue], @"value21"); +} + +/// Standard test case of receiving updated config from fetch. +- (void)testUpdateConfigContentWithResponse { + NSMutableDictionary *configToSet = + [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; + NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"}; + [configToSet setValue:entries forKey:@"entries"]; + [_configContent updateConfigContentWithResponse:configToSet + forNamespace:FIRNamespaceGoogleMobilePlatform]; + + NSDictionary *fetchedConfig = _configContent.fetchedConfig; + XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]); + XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"] stringValue], + @"value1"); + XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue], + @"value2"); +} + +/// Verify that fetchedConfig is overwritten for a new fetch call. +- (void)testUpdateConfigContentWithStatusUpdateWithDifferentKeys { + NSMutableDictionary *configToSet = + [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; + NSDictionary *entries = @{@"key1" : @"value1"}; + [configToSet setValue:entries forKey:@"entries"]; + [_configContent updateConfigContentWithResponse:configToSet + forNamespace:FIRNamespaceGoogleMobilePlatform]; + configToSet = [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; + entries = @{@"key2" : @"value2", @"key3" : @"value3"}; + [configToSet setValue:entries forKey:@"entries"]; + [_configContent updateConfigContentWithResponse:configToSet + forNamespace:FIRNamespaceGoogleMobilePlatform]; + + NSDictionary *fetchedConfig = _configContent.fetchedConfig; + XCTAssertNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key1"]); + XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key2"] stringValue], + @"value2"); + XCTAssertNotNil(fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"]); + XCTAssertEqualObjects([fetchedConfig[FIRNamespaceGoogleMobilePlatform][@"key3"] stringValue], + @"value3"); +} + +/// Verify fetchedConfig is available across different namespaces. +- (void)testUpdateConfigContentWithStatusUpdateWithDifferentNamespaces { + NSMutableDictionary *configToSet = + [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; + NSMutableDictionary *configToSet2 = + [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; + NSDictionary *entries = @{@"key1" : @"value1"}; + NSDictionary *entries2 = @{@"key2" : @"value2"}; + [configToSet setValue:entries forKey:@"entries"]; + [configToSet2 setValue:entries2 forKey:@"entries"]; + [_configContent updateConfigContentWithResponse:configToSet forNamespace:@"namespace_1"]; + [_configContent updateConfigContentWithResponse:configToSet2 forNamespace:@"namespace_2"]; + [_configContent updateConfigContentWithResponse:configToSet forNamespace:@"namespace_3"]; + [_configContent updateConfigContentWithResponse:configToSet2 forNamespace:@"namespace_4"]; + + NSDictionary *fetchedConfig = _configContent.fetchedConfig; + + XCTAssertNotNil(fetchedConfig[@"namespace_1"][@"key1"]); + XCTAssertEqualObjects([fetchedConfig[@"namespace_1"][@"key1"] stringValue], @"value1"); + XCTAssertNotNil(fetchedConfig[@"namespace_2"][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[@"namespace_2"][@"key2"] stringValue], @"value2"); + XCTAssertNotNil(fetchedConfig[@"namespace_3"][@"key1"]); + XCTAssertEqualObjects([fetchedConfig[@"namespace_3"][@"key1"] stringValue], @"value1"); + XCTAssertNotNil(fetchedConfig[@"namespace_4"][@"key2"]); + XCTAssertEqualObjects([fetchedConfig[@"namespace_4"][@"key2"] stringValue], @"value2"); +} + +- (void)skip_testUpdateConfigContentWithStatusNoChange { + // TODO: Add test case once new eTag based logic is implemented. +} + +- (void)skip_testUpdateConfigContentWithRemoveNamespaceStatus { + // TODO: Add test case once new eTag based logic is implemented. +} + +- (void)skip_testUpdateConfigContentWithEmptyConfig { + // TODO: Add test case once new eTag based logic is implemented. +} + +- (void)testCopyFromDictionaryDoesNotUpdateFetchedConfig { + NSMutableDictionary *configToSet = + [[NSMutableDictionary alloc] initWithObjectsAndKeys:@"UPDATE", @"state", nil]; + NSDictionary *entries = @{@"key1" : @"value1", @"key2" : @"value2"}; + [configToSet setValue:entries forKey:@"entries"]; + [_configContent updateConfigContentWithResponse:configToSet forNamespace:@"dummy_namespace"]; + NSDictionary *namespaceToConfig = @{ + @"dummy_namespace" : @{ + @"new_key" : @"new_value", + + } + }; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceFetched + forNamespace:@"dummy_namespace"]; + XCTAssertEqual(((NSDictionary *)_configContent.fetchedConfig[@"dummy_namespace"]).count, 2); + XCTAssertEqual(_configContent.activeConfig.count, 0); + XCTAssertEqual(_configContent.defaultConfig.count, 0); +} + +- (void)testCopyFromDictionaryUpdatesDefaultConfig { + NSDictionary *embeddedDictionary = @{@"default_embedded_key" : @"default_embedded_Value"}; + NSData *dataValue = [NSJSONSerialization dataWithJSONObject:embeddedDictionary + options:NSJSONWritingPrettyPrinted + error:nil]; + NSDate *now = [NSDate date]; + NSError *error; + NSData *JSONData = [NSJSONSerialization dataWithJSONObject:@{@"key1" : @"value1"} + options:0 + error:&error]; + NSString *JSONString = [[NSString alloc] initWithData:JSONData encoding:NSUTF8StringEncoding]; + NSDictionary *namespaceToConfig = @{ + @"default_namespace" : @{ + @"new_string_key" : @"new_string_value", + @"new_number_key" : @1234, + @"new_data_key" : dataValue, + @"new_date_key" : now, + @"new_json_key" : JSONString + } + }; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceDefault + forNamespace:@"default_namespace"]; + NSDictionary *defaultConfig = _configContent.defaultConfig; + XCTAssertEqual(_configContent.fetchedConfig.count, 0); + XCTAssertEqual(_configContent.activeConfig.count, 0); + XCTAssertNotNil(defaultConfig[@"default_namespace"]); + XCTAssertEqual(((NSDictionary *)defaultConfig[@"default_namespace"]).count, 5); + XCTAssertEqualObjects(@"new_string_value", + [defaultConfig[@"default_namespace"][@"new_string_key"] stringValue]); + XCTAssertEqualObjects( + @1234, [((FIRRemoteConfigValue *)defaultConfig[@"default_namespace"][@"new_number_key"]) + numberValue]); + NSDictionary *sampleJSON = @{@"key1" : @"value1"}; + id configJSON = [(defaultConfig[@"default_namespace"][@"new_json_key"]) JSONValue]; + XCTAssertTrue([configJSON isKindOfClass:[NSDictionary class]]); + XCTAssertTrue([sampleJSON isKindOfClass:[NSDictionary class]]); + XCTAssertEqualObjects(sampleJSON, (NSDictionary *)configJSON); + XCTAssertEqualObjects(dataValue, + [defaultConfig[@"default_namespace"][@"new_data_key"] dataValue]); + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + NSString *strValueForDate = [dateFormatter stringFromDate:now]; + XCTAssertEqualObjects(strValueForDate, + [defaultConfig[@"default_namespace"][@"new_date_key"] stringValue]); +} + +- (void)testCopyFromDictionaryUpdatesActiveConfig { + // Active config values must be RCNConfigValue format + NSDictionary *embeddedDictionary = @{@"active_embedded_key" : @"active_embedded_Value"}; + NSData *dataValue = [NSJSONSerialization dataWithJSONObject:embeddedDictionary + options:NSJSONWritingPrettyPrinted + error:nil]; + + NSDictionary *namespaceToConfig = @{ + @"dummy_namespace" : @{ + @"new_key" : [[FIRRemoteConfigValue alloc] initWithData:dataValue source:-1], + } + }; + [_configContent copyFromDictionary:namespaceToConfig + toSource:RCNDBSourceActive + forNamespace:@"dummy_namespace"]; + XCTAssertEqual(((NSDictionary *)_configContent.activeConfig[@"dummy_namespace"]).count, 1); + XCTAssertEqual(_configContent.fetchedConfig.count, 0); + XCTAssertEqual(_configContent.defaultConfig.count, 0); + XCTAssertEqualObjects(dataValue, + [_configContent.activeConfig[@"dummy_namespace"][@"new_key"] dataValue]); +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m new file mode 100644 index 00000000000..c417f967d05 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigDBManagerTest.m @@ -0,0 +1,594 @@ +/* + * Copyright 2019 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 + +#import "sqlite3.h" + +#import +#import +#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" +#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" + +@interface RCNConfigDBManager (Test) +- (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path; +- (void)insertExperimentTableWithKey:(NSString *)key + value:(NSData *)serializedValue + completionHandler:(RCNDBCompletion)handler; +- (void)deleteExperimentTableForKey:(NSString *)key; +- (void)createOrOpenDatabase; +@end + +@interface RCNConfigDBManagerTest : XCTestCase { + NSString *_DBPath; +} +@property(nonatomic, strong) RCNConfigDBManager *DBManager; +@property(nonatomic, assign) NSTimeInterval expectionTimeout; +@end + +@implementation RCNConfigDBManagerTest + +- (void)setUp { + [super setUp]; + // always remove the database at the start of testing + _DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase]; + + _expectionTimeout = 10.0; + id classMock = OCMClassMock([RCNConfigDBManager class]); + OCMStub([classMock remoteConfigPathForDatabase]).andReturn(_DBPath); + _DBManager = [[RCNConfigDBManager alloc] init]; +} + +- (void)tearDown { + // Causes crash if main thread exits before the RCNConfigDB queue cleans up + // [_DBManager removeDatabaseOnDatabaseQueueAtPath:_DBPath]; +} + +- (void)testV1NamespaceMigrationToV2Namespace { + // Write v1 namespace. + XCTestExpectation *loadConfigContentExpectation = + [self expectationWithDescription:@"test v1 namespace migration to v2 namespace"]; + NSString *namespace_p = @"testNamespace"; + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + __block int count = 0; + for (int i = 0; i <= 100; ++i) { + // Check namespace is updated after database write is completed. + RCNDBCompletion insertCompletion = ^void(BOOL success, + NSDictionary *result) { + count++; + XCTAssertTrue(success); + if (count == 100) { + // Migrate to the new namespace. + [_DBManager createOrOpenDatabase]; + [_DBManager + loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:^( + BOOL loadSuccess, + NSDictionary *> *fetchedConfig, + NSDictionary *> *activeConfig, + NSDictionary *> + *defaultConfig) { + XCTAssertTrue(loadSuccess); + NSString *fullyQualifiedNamespace = + [NSString stringWithFormat:@"%@:%@", namespace_p, kFIRDefaultAppName]; + XCTAssertNotNil(fetchedConfig[fullyQualifiedNamespace]); + XCTAssertEqual([fetchedConfig[fullyQualifiedNamespace] count], 101U); + XCTAssertEqual([fetchedConfig[namespace_p] count], 0); + if (loadSuccess) { + [loadConfigContentExpectation fulfill]; + } + }]; + } + }; + NSString *value = [NSString stringWithFormat:@"value%d", i]; + NSString *key = [NSString stringWithFormat:@"key%d", i]; + NSArray *values = + @[ bundleIdentifier, namespace_p, key, [value dataUsingEncoding:NSUTF8StringEncoding] ]; + [_DBManager insertMainTableWithValues:values + fromSource:RCNDBSourceFetched + completionHandler:insertCompletion]; + } + + [self waitForExpectationsWithTimeout:_expectionTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testWriteAndLoadMainTableResult { + XCTestExpectation *loadConfigContentExpectation = + [self expectationWithDescription:@"Write and read metadata in database serailizedly"]; + NSString *namespace_p = @"namespace_1"; + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + __block int count = 0; + for (int i = 0; i <= 100; ++i) { + // check DB write correctly + RCNDBCompletion insertCompletion = ^void(BOOL success, NSDictionary *result) { + count++; + XCTAssertTrue(success); + if (count == 100) { + // check DB read correctly + [_DBManager loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:^(BOOL success, NSDictionary *fetchedConfig, + NSDictionary *activeConfig, + NSDictionary *defaultConfig) { + NSMutableDictionary *res = [fetchedConfig mutableCopy]; + XCTAssertTrue(success); + FIRRemoteConfigValue *value = res[namespace_p][@"key100"]; + XCTAssertEqualObjects(value.stringValue, @"value100"); + if (success) { + [loadConfigContentExpectation fulfill]; + } + }]; + } + }; + NSString *value = [NSString stringWithFormat:@"value%d", i]; + NSString *key = [NSString stringWithFormat:@"key%d", i]; + NSArray *values = + @[ bundleIdentifier, namespace_p, key, [value dataUsingEncoding:NSUTF8StringEncoding] ]; + [_DBManager insertMainTableWithValues:values + fromSource:RCNDBSourceFetched + completionHandler:insertCompletion]; + } + + [self waitForExpectationsWithTimeout:_expectionTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testWriteAndLoadInternalMetadataResult { + XCTestExpectation *loadConfigContentExpectation = [self + expectationWithDescription:@"Write and read internal metadata in database successfully"]; + __block int count = 0; + for (int i = 0; i <= 100; ++i) { + // check DB write correctly + RCNDBCompletion insertCompletion = ^void(BOOL success, NSDictionary *result) { + count++; + XCTAssertTrue(success); + if (count == 100) { + // check DB read correctly + NSDictionary *result = [_DBManager loadInternalMetadataTable]; + NSString *stringValue = [[NSString alloc] initWithData:result[@"key100"] + encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(stringValue, @"value100"); + if (success) { + [loadConfigContentExpectation fulfill]; + } + } + }; + NSString *value = [NSString stringWithFormat:@"value%d", i]; + NSString *key = [NSString stringWithFormat:@"key%d", i]; + + NSArray *values = @[ key, [value dataUsingEncoding:NSUTF8StringEncoding] ]; + [_DBManager insertInternalMetadataTableWithValues:values completionHandler:insertCompletion]; + } + + [self waitForExpectationsWithTimeout:_expectionTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testWriteAndLoadMetadataResult { + XCTestExpectation *writeAndLoadMetadataExpectation = + [self expectationWithDescription:@"Write and load metadata in database successfully"]; + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + NSTimeInterval lastFetchTimestamp = [NSDate date].timeIntervalSince1970; + + NSDictionary *deviceContext = @{@"app_version" : @"1.0.1", @"os_version" : @"iOS9.1"}; + NSDictionary *syncedDBCustomVariables = @{@"user_level" : @15, @"user_experiences" : @"2468"}; + NSArray *successFetchTimes = @[]; + NSTimeInterval now = [NSDate date].timeIntervalSince1970; + NSArray *failureFetchTimes = + @[ [NSNumber numberWithDouble:now - 200], [NSNumber numberWithDouble:now] ]; + + // serialize objects + NSError *error; + NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:syncedDBCustomVariables + options:NSJSONWritingPrettyPrinted + error:&error]; + NSData *serializedDeviceContext = + [NSJSONSerialization dataWithJSONObject:deviceContext + options:NSJSONWritingPrettyPrinted + error:&error]; + NSData *serializedDigestPerNamespace = + [NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error]; + NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:successFetchTimes + options:NSJSONWritingPrettyPrinted + error:&error]; + NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:failureFetchTimes + options:NSJSONWritingPrettyPrinted + error:&error]; + NSDictionary *columnNameToValue = @{ + RCNKeyBundleIdentifier : bundleIdentifier, + RCNKeyFetchTime : @(lastFetchTimestamp), + RCNKeyDigestPerNamespace : serializedDigestPerNamespace, + RCNKeyDeviceContext : serializedDeviceContext, + RCNKeyAppContext : serializedAppContext, + RCNKeySuccessFetchTime : serializedSuccessTime, + RCNKeyFailureFetchTime : serializedFailureTime, + RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess), + RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown), + RCNKeyLastApplyTime : @(now - 100), + RCNKeyLastSetDefaultsTime : @(now - 200) + }; + + RCNDBCompletion completion = ^(BOOL success, NSDictionary *result1) { + NSDictionary *result = [_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier]; + XCTAssertNotNil(result); + XCTAssertEqualObjects(result[RCNKeyBundleIdentifier], bundleIdentifier); + XCTAssertEqual([result[RCNKeyFetchTime] doubleValue], lastFetchTimestamp); + XCTAssertEqualObjects([result[RCNKeyDigestPerNamespace] copy], @{}); + XCTAssertEqualObjects([result[RCNKeyDeviceContext] copy], deviceContext); + XCTAssertEqualObjects([result[RCNKeyAppContext] copy], syncedDBCustomVariables); + XCTAssertEqualObjects([result[RCNKeySuccessFetchTime] copy], successFetchTimes); + // TODO(chliang): Fix the flakiness caused by the commented out test + // XCTAssertTrue([[result[RCNKeyFailureFetchTime] copy] isEqualToArray:failureFetchTimes]); + XCTAssertEqual([result[RCNKeyLastFetchStatus] intValue], + (int)FIRRemoteConfigFetchStatusSuccess); + XCTAssertEqual([result[RCNKeyLastFetchError] intValue], (int)FIRRemoteConfigErrorUnknown); + XCTAssertEqual([result[RCNKeyLastApplyTime] doubleValue], now - 100); + XCTAssertEqual([result[RCNKeyLastSetDefaultsTime] doubleValue], now - 200); + + [writeAndLoadMetadataExpectation fulfill]; + }; + + [_DBManager insertMetadataTableWithValues:columnNameToValue completionHandler:completion]; + [self waitForExpectationsWithTimeout:_expectionTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +// Create a key each for two namespaces, delete it from one namespace, read both namespaces. +- (void)testDeleteParamAndLoadMainTable { + XCTestExpectation *namespaceDeleteExpectation = + [self expectationWithDescription:@"Contents of 'namespace_delete' should be deleted."]; + XCTestExpectation *namespaceKeepExpectation = + [self expectationWithDescription:@"Write a key to namespace_keep and read back again."]; + NSString *namespaceToDelete = @"namespace_delete"; + NSString *namespaceToKeep = @"namespace_keep"; + NSString *bundleIdentifier = @"testBundleID"; + + // Write something to the database for both namespaces. + // Completion handler for the write to namespace_delete namespace. + RCNDBCompletion insertNamespace1Completion = ^void(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + + // Delete the key for given namespace. + [_DBManager deleteRecordFromMainTableWithNamespace:namespaceToDelete + bundleIdentifier:bundleIdentifier + fromSource:RCNDBSourceActive]; + + // Read from the database and verify expected values. + [_DBManager + loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:^(BOOL success, NSDictionary *fetchedConfig, + NSDictionary *activeConfig, NSDictionary *defaultConfig) { + NSMutableDictionary *res = [activeConfig mutableCopy]; + XCTAssertTrue(success); + FIRRemoteConfigValue *value = res[namespaceToDelete][@"keyToDelete"]; + XCTAssertNil(value); + + FIRRemoteConfigValue *value2 = res[namespaceToKeep][@"keyToRetain"]; + XCTAssertTrue([value2.stringValue isEqualToString:@"valueToRetain"]); + + [namespaceDeleteExpectation fulfill]; + }]; + }; + + // Insert a key into the second namespace. + RCNDBCompletion insertNamespace2Completion = ^void(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + + // Ensure DB read succeeds. + [_DBManager + loadMainWithBundleIdentifier:bundleIdentifier + completionHandler:^(BOOL success, NSDictionary *fetchedConfig, + NSDictionary *activeConfig, NSDictionary *defaultConfig) { + NSMutableDictionary *res = [activeConfig mutableCopy]; + XCTAssertTrue(success); + FIRRemoteConfigValue *value2 = res[namespaceToKeep][@"keyToRetain"]; + XCTAssertTrue([value2.stringValue isEqualToString:@"valueToRetain"]); + + [namespaceKeepExpectation fulfill]; + }]; + }; + // We will delete this key after storing in the database. + NSString *valueToDelete = @"valueToDelete"; + NSString *keyToDelete = @"keyToDelete"; + NSArray *items = @[ + bundleIdentifier, namespaceToDelete, keyToDelete, + [valueToDelete dataUsingEncoding:NSUTF8StringEncoding] + ]; + [_DBManager insertMainTableWithValues:items + fromSource:RCNDBSourceActive + completionHandler:insertNamespace1Completion]; + + // This key value will be retained. + NSString *valueToRetain = @"valueToRetain"; + NSString *keyToRetain = @"keyToRetain"; + NSArray *items2 = @[ + bundleIdentifier, namespaceToKeep, keyToRetain, + [valueToRetain dataUsingEncoding:NSUTF8StringEncoding] + ]; + [_DBManager insertMainTableWithValues:items2 + fromSource:RCNDBSourceActive + completionHandler:insertNamespace2Completion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testWriteAndLoadExperiments { + XCTestExpectation *updateAndLoadExperimentExpectation = + [self expectationWithDescription:@"Update and load experiment in database successfully"]; + + NSError *error; + NSArray *payload2 = @[ @"ab", @"cd" ]; + NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload2 + options:NSJSONWritingPrettyPrinted + error:&error]; + NSDictionary *payload3 = + @{@"experiment_ID" : @"35667", @"experiment_activate_name" : @"activate_game"}; + NSData *payloadData3 = [NSJSONSerialization dataWithJSONObject:payload3 + options:NSJSONWritingPrettyPrinted + error:&error]; + NSArray *payloads = @[ [[NSData alloc] init], payloadData2, payloadData3 ]; + + RCNDBCompletion writePayloadCompletion = ^(BOOL success, NSDictionary *result) { + NSDictionary *metadata = + @{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"}; + XCTAssertTrue(success); + RCNDBCompletion writeMetadataCompletion = ^(BOOL success, NSDictionary *result) { + XCTAssertTrue(success); + RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) { + XCTAssertTrue(success); + XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyPayload]); + XCTAssertEqualObjects(payloads, experimentResults[@RCNExperimentTableKeyPayload]); + + XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyMetadata]); + XCTAssertEqualWithAccuracy( + -11, + [experimentResults[@RCNExperimentTableKeyMetadata][@"last_known_start_time"] + doubleValue], + 1.0); + XCTAssertEqualObjects( + @"wonderful", + experimentResults[@RCNExperimentTableKeyMetadata][@"experiment_new_metadata"]); + [updateAndLoadExperimentExpectation fulfill]; + }; + [_DBManager loadExperimentWithCompletionHandler:readCompletion]; + }; + + NSError *error; + XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]); + NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata + options:NSJSONWritingPrettyPrinted + error:&error]; + + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata + value:serializedMetadata + completionHandler:writeMetadataCompletion]; + }; + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload + value:[[NSData alloc] init] + completionHandler:nil]; + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload + value:payloadData2 + completionHandler:nil]; + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyPayload + value:payloadData3 + completionHandler:writePayloadCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + +- (void)testWriteAndLoadMetadataMultipleTimes { + XCTestExpectation *updateAndLoadMetadataExpectation = [self + expectationWithDescription:@"Update and load experiment metadata in database successfully"]; + + RCNDBCompletion readCompletion = ^(BOOL success, NSDictionary *experimentResults) { + XCTAssertTrue(success); + XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyPayload]); + XCTAssertNotNil(experimentResults[@RCNExperimentTableKeyMetadata]); + XCTAssertEqualWithAccuracy( + 12345678, + [experimentResults[@RCNExperimentTableKeyMetadata][@"last_known_start_time"] doubleValue], + 1.0); + XCTAssertEqualObjects( + @"wonderful", + experimentResults[@RCNExperimentTableKeyMetadata][@"experiment_new_metadata"]); + + [updateAndLoadMetadataExpectation fulfill]; + }; + NSDictionary *metadata = + @{@"last_known_start_time" : @(-11), @"experiment_new_metadata" : @"wonderful"}; + NSError *error; + XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]); + NSData *serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata + options:NSJSONWritingPrettyPrinted + error:&error]; + + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata + value:serializedMetadata + completionHandler:nil]; + + metadata = @{@"last_known_start_time" : @(12345678), @"experiment_new_metadata" : @"wonderful"}; + XCTAssertTrue([NSJSONSerialization isValidJSONObject:metadata]); + serializedMetadata = [NSJSONSerialization dataWithJSONObject:metadata + options:NSJSONWritingPrettyPrinted + error:&error]; + + [_DBManager insertExperimentTableWithKey:@RCNExperimentTableKeyMetadata + value:serializedMetadata + completionHandler:nil]; + [_DBManager loadExperimentWithCompletionHandler:readCompletion]; + + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + +- (void)testUpdateAndloadLastFetchStatus { + XCTestExpectation *updateAndLoadMetadataExpectation = [self + expectationWithDescription:@"Update and load last fetch status in database successfully."]; + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + // Metadata row must exist before update + RCNDBCompletion createMetadataCompletion = ^(BOOL success, NSDictionary *createResult) { + NSDictionary *result = [_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier]; + XCTAssertTrue(success); + XCTAssertNotNil(result); + XCTAssertEqual([result[RCNKeyLastFetchStatus] intValue], + (int)FIRRemoteConfigFetchStatusSuccess); + XCTAssertEqual([result[RCNKeyLastFetchError] intValue], (int)FIRRemoteConfigErrorUnknown); + + RCNDBCompletion updateMetadataCompletion = ^(BOOL success, NSDictionary *updateResult) { + NSDictionary *result = [_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier]; + + XCTAssertTrue(success); + XCTAssertNotNil(result); + XCTAssertEqual([result[RCNKeyLastFetchStatus] intValue], + (int)FIRRemoteConfigFetchStatusThrottled); + XCTAssertEqual([result[RCNKeyLastFetchError] intValue], (int)FIRRemoteConfigErrorThrottled); + [updateAndLoadMetadataExpectation fulfill]; + }; + // Update with throttle status. + [_DBManager + updateMetadataWithOption:RCNUpdateOptionFetchStatus + values:@[ + @(FIRRemoteConfigFetchStatusThrottled), @(FIRRemoteConfigErrorThrottled) + ] + completionHandler:updateMetadataCompletion]; + }; + + [_DBManager insertMetadataTableWithValues:[self createSampleMetadata] + completionHandler:createMetadataCompletion]; + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + +/// TODO: Fix test case. +/// Tests that we can insert values in the database and can update them. +- (void)ignore_InsertAndUpdateApplyTime { + XCTestExpectation *updateAndLoadMetadataExpectation = + [self expectationWithDescription:@"Update and load apply time in database successfully."]; + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + NSTimeInterval lastApplyTimestamp = [NSDate date].timeIntervalSince1970; + + // Metadata row must exist before update + RCNDBCompletion createMetadataCompletion = ^(BOOL success, NSDictionary *createResult) { + XCTAssertTrue(success); + // Read newly created metadata. + NSDictionary *result = [_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier]; + XCTAssertNotNil(result); + XCTAssertEqual([result[RCNKeyLastApplyTime] doubleValue], (double)100); + RCNDBCompletion updateMetadataCompletion = ^(BOOL success, NSDictionary *updateResult) { + NSDictionary *result = [_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier]; + + XCTAssertTrue(success); + XCTAssertNotNil(result); + XCTAssertEqual([result[RCNKeyLastApplyTime] doubleValue], lastApplyTimestamp); + [updateAndLoadMetadataExpectation fulfill]; + }; + // Update apply config timestamp. + [_DBManager updateMetadataWithOption:RCNUpdateOptionApplyTime + values:@[ @(lastApplyTimestamp) ] + completionHandler:updateMetadataCompletion]; + }; + + [_DBManager insertMetadataTableWithValues:[self createSampleMetadata] + completionHandler:createMetadataCompletion]; + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + +- (void)testUpdateAndLoadSetDefaultsTime { + XCTestExpectation *updateAndLoadMetadataExpectation = [self + expectationWithDescription:@"Update and load set defaults time in database successfully."]; + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + NSTimeInterval lastSetDefaultsTimestamp = [NSDate date].timeIntervalSince1970; + + // Metadata row must exist before update + RCNDBCompletion createMetadataCompletion = ^(BOOL success, NSDictionary *createResult) { + NSDictionary *result = [_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier]; + XCTAssertTrue(success); + XCTAssertNotNil(result); + XCTAssertEqual([result[RCNKeyLastSetDefaultsTime] doubleValue], (double)200); + RCNDBCompletion updateMetadataCompletion = ^(BOOL success, NSDictionary *updateResult) { + NSDictionary *result = [_DBManager loadMetadataWithBundleIdentifier:bundleIdentifier]; + + XCTAssertTrue(success); + XCTAssertNotNil(result); + XCTAssertEqual([result[RCNKeyLastSetDefaultsTime] doubleValue], lastSetDefaultsTimestamp); + [updateAndLoadMetadataExpectation fulfill]; + }; + // Update setting default config timestamp. + [_DBManager updateMetadataWithOption:RCNUpdateOptionDefaultTime + values:@[ @(lastSetDefaultsTimestamp) ] + completionHandler:updateMetadataCompletion]; + }; + + [_DBManager insertMetadataTableWithValues:[self createSampleMetadata] + completionHandler:createMetadataCompletion]; + [self waitForExpectationsWithTimeout:_expectionTimeout handler:nil]; +} + +- (NSDictionary *)createSampleMetadata { + NSString *bundleIdentifier = [NSBundle mainBundle].bundleIdentifier; + + NSDictionary *deviceContext = @{}; + NSDictionary *syncedDBCustomVariables = @{}; + NSArray *successFetchTimes = @[]; + NSArray *failureFetchTimes = @[]; + + // serialize objects + NSError *error; + NSData *serializedAppContext = [NSJSONSerialization dataWithJSONObject:syncedDBCustomVariables + options:NSJSONWritingPrettyPrinted + error:&error]; + NSData *serializedDeviceContext = + [NSJSONSerialization dataWithJSONObject:deviceContext + options:NSJSONWritingPrettyPrinted + error:&error]; + NSData *serializedDigestPerNamespace = + [NSJSONSerialization dataWithJSONObject:@{} options:NSJSONWritingPrettyPrinted error:&error]; + NSData *serializedSuccessTime = [NSJSONSerialization dataWithJSONObject:successFetchTimes + options:NSJSONWritingPrettyPrinted + error:&error]; + NSData *serializedFailureTime = [NSJSONSerialization dataWithJSONObject:failureFetchTimes + options:NSJSONWritingPrettyPrinted + error:&error]; + return @{ + RCNKeyBundleIdentifier : bundleIdentifier, + RCNKeyFetchTime : @(0), + RCNKeyDigestPerNamespace : serializedDigestPerNamespace, + RCNKeyDeviceContext : serializedDeviceContext, + RCNKeyAppContext : serializedAppContext, + RCNKeySuccessFetchTime : serializedSuccessTime, + RCNKeyFailureFetchTime : serializedFailureTime, + RCNKeyLastFetchStatus : @(FIRRemoteConfigFetchStatusSuccess), + RCNKeyLastFetchError : @(FIRRemoteConfigErrorUnknown), + RCNKeyLastApplyTime : @(100), + RCNKeyLastSetDefaultsTime : @(200) + }; +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m new file mode 100644 index 00000000000..a8a7a5140b0 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigExperimentTest.m @@ -0,0 +1,261 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" + +#import + +#import +#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDefines.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" +#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" + +#import +#import + +#import +#import +#import "FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.h" + +// Surface the internal FIRExperimentController initializer. +@interface FIRExperimentController () +- (instancetype)initWithAnalytics:(nullable id)analytics; +@end + +@interface RCNConfigExperiment () +@property(nonatomic, copy) NSMutableArray *experimentPayloads; +@property(nonatomic, copy) NSMutableDictionary *experimentMetadata; +@property(nonatomic, strong) RCNConfigDBManager *DBManager; +- (NSTimeInterval)updateExperimentStartTime; +- (void)loadExperimentFromTable; +@end + +@interface RCNConfigExperimentTest : XCTestCase { + NSTimeInterval _expectationTimeout; + FIRExperimentController *_experimentController; + RCNConfigExperiment *_configExperiment; + id _DBManagerMock; + NSArray *> *_payloads; + NSArray *_payloadsData; + NSDictionary *_metadata; + NSString *_DBPath; +} +@end + +@implementation RCNConfigExperimentTest +- (void)setUp { + [super setUp]; + _expectationTimeout = 1.0; + _DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase]; + _DBManagerMock = OCMClassMock([RCNConfigDBManager class]); + OCMStub([_DBManagerMock remoteConfigPathForDatabase]).andReturn(_DBPath); + + // Mock all database operations. + NSDictionary *payload1 = @{@"experimentId" : @"DBValue1"}; + NSDictionary *payload2 = @{@"experimentId" : @"DBValue2"}; + _payloads = @[ payload1, payload2 ]; + NSError *error; + NSData *payloadData1 = [NSJSONSerialization dataWithJSONObject:payload1 options:0 error:&error]; + NSData *payloadData2 = [NSJSONSerialization dataWithJSONObject:payload2 options:0 error:&error]; + _payloadsData = @[ payloadData1, payloadData2 ]; + _metadata = @{@"last_know_start_time" : @12348765}; + NSDictionary *mockResults = @{ + @RCNExperimentTableKeyPayload : _payloadsData, + @RCNExperimentTableKeyMetadata : _metadata, + }; + OCMStub([_DBManagerMock + loadExperimentWithCompletionHandler:([OCMArg invokeBlockWithArgs:@YES, mockResults, nil])]); + OCMStub([_DBManagerMock deleteExperimentTableForKey:[OCMArg any]]).andDo(nil); + OCMStub([_DBManagerMock insertExperimentTableWithKey:[OCMArg any] + value:[OCMArg any] + completionHandler:nil]) + .andDo(nil); + + FIRExperimentController *experimentController = + [[FIRExperimentController alloc] initWithAnalytics:nil]; + _configExperiment = [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock + experimentController:experimentController]; +} + +- (void)tearDown { + [super tearDown]; +} + +- (void)testInitMethod { + OCMVerify([_DBManagerMock loadExperimentWithCompletionHandler:[OCMArg any]]); +} + +- (void)testLoadExperimentFromTable { + [_configExperiment loadExperimentFromTable]; + + int payloadIndex = 0; + for (NSData *payload in _configExperiment.experimentPayloads) { + ABTExperimentPayload *experimentPayload = [self deserializeABTData:payload]; + XCTAssertNotNil(experimentPayload); + XCTAssertEqualObjects(experimentPayload.experimentId, + _payloads[payloadIndex++][@"experimentId"]); + } + + XCTAssertEqualObjects(_metadata, _configExperiment.experimentMetadata); +} + +- (void)testUpdateExperiment { + RCNAppConfigTable *appTable = [[RCNAppConfigTable alloc] init]; + appTable.appName = [NSBundle mainBundle].bundleIdentifier; + NSDictionary *payload1 = @{@"experimentId" : @"exp1"}; + NSDictionary *payload2 = @{@"experimentId" : @"exp2"}; + NSDictionary *payload3 = @{@"experimentId" : @"exp3"}; + NSArray *> *originalPayloads = @[ payload1, payload2, payload3 ]; + + NSArray *> *response = @[ payload1, payload2, payload3 ]; + [_configExperiment updateExperimentsWithResponse:response]; + + // Serialized proto data. + int payloadIndex = 0; + for (NSData *payload in _configExperiment.experimentPayloads) { + ABTExperimentPayload *experimentPayload = [self deserializeABTData:payload]; + XCTAssertNotNil(experimentPayload); + XCTAssertEqualObjects(experimentPayload.experimentId, + originalPayloads[payloadIndex++][@"experimentId"]); + } +} + +- (void)testUpdateLastExperimentStartTime { + [_configExperiment updateExperimentStartTime]; + XCTAssertEqualObjects(_configExperiment.experimentMetadata[@"last_experiment_start_time"], @(0)); + + NSDictionary *payload = + @{@"experimentStartTime" : @"2019-04-04T21:54:38.555Z"}; + [_configExperiment updateExperimentsWithResponse:@[ payload ]]; + [_configExperiment updateExperimentStartTime]; + + int64_t originalTime = [self convertTimeToMillis:@"2019-04-04T21:54:38.555Z"] / 1000; + int64_t time = + ([_configExperiment.experimentMetadata[@"last_experiment_start_time"] doubleValue]); + XCTAssertEqual(time, originalTime); +} + +- (void)testMultipleUpdatesToLastExperimentStartTime { + [_configExperiment updateExperimentStartTime]; + XCTAssertEqualObjects(_configExperiment.experimentMetadata[@"last_experiment_start_time"], @(0)); + + NSDictionary *payload = + @{@"experimentStartTime" : @"2019-04-04T21:54:38.555Z"}; + [_configExperiment updateExperimentsWithResponse:@[ payload ]]; + [_configExperiment updateExperimentStartTime]; + + int64_t originalTime = [self convertTimeToMillis:@"2019-04-04T21:54:38.555Z"] / 1000; + int64_t time = + ([_configExperiment.experimentMetadata[@"last_experiment_start_time"] doubleValue]); + XCTAssertEqual(time, originalTime); + + // Update start time again. + payload = @{@"experimentStartTime" : @"2019-04-04T21:55:38.555Z"}; + [_configExperiment updateExperimentsWithResponse:@[ payload ]]; + [_configExperiment updateExperimentStartTime]; + + originalTime = [self convertTimeToMillis:@"2019-04-04T21:55:38.555Z"] / 1000; + time = ([_configExperiment.experimentMetadata[@"last_experiment_start_time"] doubleValue]); + XCTAssertEqual(time, originalTime); +} + +- (void)testUpdateLastExperimentStartTimeInThePast { + NSDictionary *payload = + @{@"experimentStartTime" : @"2019-04-04T21:55:38.555Z"}; + [_configExperiment updateExperimentsWithResponse:@[ payload ]]; + [_configExperiment updateExperimentStartTime]; + + int64_t originalTime = [self convertTimeToMillis:@"2019-04-04T21:55:38.555Z"] / 1000; + int64_t time = + ([_configExperiment.experimentMetadata[@"last_experiment_start_time"] doubleValue]); + XCTAssertEqual(time, originalTime); + + payload = @{@"experimentStartTime" : @"2018-04-04T21:55:38.555Z"}; + [_configExperiment updateExperimentsWithResponse:@[ payload ]]; + [_configExperiment updateExperimentStartTime]; + + originalTime = [self convertTimeToMillis:@"2019-04-04T21:55:38.555Z"] / 1000; + time = ([_configExperiment.experimentMetadata[@"last_experiment_start_time"] doubleValue]); + XCTAssertEqual(time, originalTime); +} + +- (void)testUpdateLastExperimentStartTimeInTheFuture { + NSDictionary *payload = + @{@"experimentStartTime" : @"2020-04-04T21:55:38.555Z"}; + [_configExperiment updateExperimentsWithResponse:@[ payload ]]; + [_configExperiment updateExperimentStartTime]; + + int64_t originalTime = [self convertTimeToMillis:@"2020-04-04T21:55:38.555Z"] / 1000; + int64_t time = + ([_configExperiment.experimentMetadata[@"last_experiment_start_time"] doubleValue]); + XCTAssertEqual(time, originalTime); +} + +- (void)testUpdateExperiments { + FIRExperimentController *experimentController = + [[FIRExperimentController alloc] initWithAnalytics:nil]; + id mockExperimentController = OCMPartialMock(experimentController); + RCNConfigExperiment *experiment = + [[RCNConfigExperiment alloc] initWithDBManager:_DBManagerMock + experimentController:mockExperimentController]; + + NSTimeInterval lastStartTime = + [experiment.experimentMetadata[@"last_experiment_start_time"] doubleValue]; + OCMStub( + [mockExperimentController + updateExperimentsWithServiceOrigin:[OCMArg any] + events:[OCMArg any] + policy: + ABTExperimentPayload_ExperimentOverflowPolicy_DiscardOldest // NOLINT + lastStartTime:lastStartTime + payloads:[OCMArg any]]) + .andDo(nil); + + ABTExperimentPayload *payload = [[ABTExperimentPayload alloc] init]; + payload.experimentStartTimeMillis = 12345678000; + + experiment.experimentPayloads = [@[ payload.data ] mutableCopy]; + + [experiment updateExperiments]; + XCTAssertEqualObjects(experiment.experimentMetadata[@"last_experiment_start_time"], @(12345678)); +} + +#pragma mark Helpers. + +- (ABTExperimentPayload *)deserializeABTData:(NSData *)payload { + NSError *error; + ABTExperimentPayload *experimentPayload = [ABTExperimentPayload parseFromData:payload + error:&error]; + if (error) { + return nil; + } + return experimentPayload; +} + +- (int64_t)convertTimeToMillis:(NSString *)time { + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]; + [dateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]]; + // Locale needs to be hardcoded. See + // https://developer.apple.com/library/ios/#qa/qa1480/_index.html for more details. + [dateFormatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]]; + [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; + NSDate *experimentStartTime = [dateFormatter dateFromString:time]; + return [@([experimentStartTime timeIntervalSince1970] * 1000) longLongValue]; +} +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigSettingsTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigSettingsTest.m new file mode 100644 index 00000000000..19fbdaa956a --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigSettingsTest.m @@ -0,0 +1,308 @@ +/* + * Copyright 2019 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 + +#import "FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.h" + +#import +#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" + +@interface RCNConfigSettings (ExposedTestCase) +- (RCNConfigFetchRequest *)nextRequestWithUserProperties:(NSDictionary *)userProperties + fetchedConfig:(NSDictionary *)fetchedConfig; +- (void)updateInternalContentWithResponse:(RCNConfigFetchResponse *)response; +- (void)updateConfigContentWithResponse:(RCNConfigFetchResponse *)response; +- (void)updateFetchTimeWithSuccessFetch:(BOOL)isSuccessfulFetch; +- (BOOL)hasCachedData; +- (BOOL)isCachedDataFresh; +@end + +@interface RCNConfigSettingsTest : XCTestCase { + RCNConfigSettings *_mockSettings; +} +@end + +@implementation RCNConfigSettingsTest +- (void)setUp { + [super setUp]; + // Mock the read/write DB operations, which are not needed in these tests. + _mockSettings = [[RCNConfigSettings alloc] initWithDatabaseManager:nil]; +} + +- (void)testCrashShouldNotHappenWithoutMainBundleID { + id mockBundle = OCMPartialMock([NSBundle mainBundle]); + OCMStub([NSBundle mainBundle]).andReturn(mockBundle); + OCMStub([mockBundle bundleIdentifier]).andReturn(nil); + _mockSettings = + [[RCNConfigSettings alloc] initWithDatabaseManager:[[RCNConfigDBManager alloc] init]]; + [mockBundle stopMocking]; +} + +#ifdef FIX_OR_DELETE +- (void)testUpdateInternalMetadata { + RCNConfigFetchResponse *response = [[RCNConfigFetchResponse alloc] init]; + // Mock internal metadata array with all_packages prefix key + response.internalMetadataArray = [RCNTestUtilities entryArrayWithKeyValuePair:@{ + [NSString stringWithFormat:@"%@:%@", RCNInternalMetadataAllPackagesPrefix, + RCNHTTPConnectionTimeoutInMillisecondsKey] : @"50", + [NSString stringWithFormat:@"%@:%@", RCNInternalMetadataAllPackagesPrefix, + RCNHTTPReadTimeoutInMillisecondsKey] : @"2000000", + [NSString stringWithFormat:@"%@:%@", RCNInternalMetadataAllPackagesPrefix, + RCNThrottledSuccessFetchTimeIntervalInSecondsKey] : @"300", + [NSString stringWithFormat:@"%@:%@", RCNInternalMetadataAllPackagesPrefix, + RCNThrottledSuccessFetchCountKey] : @"-6", + [NSString stringWithFormat:@"%@:%@", RCNInternalMetadataAllPackagesPrefix, + RCNThrottledFailureFetchTimeIntervalInSecondsKey] : @"10000000", + [NSString stringWithFormat:@"%@:%@", RCNInternalMetadataAllPackagesPrefix, + RCNThrottledFailureFetchCountKey] : @"21", + + }]; + [_mockSettings updateInternalContentWithResponse:response]; + XCTAssertEqual( + [_mockSettings internalMetadataValueForKey:RCNHTTPConnectionTimeoutInMillisecondsKey + minValue:RCNHTTPConnectionTimeoutInMillisecondsMin + maxValue:RCNHTTPConnectionTimeoutInMillisecondsMax + defaultValue:RCNHTTPConnectionTimeoutInMillisecondsDefault], + RCNHTTPConnectionTimeoutInMillisecondsMin, + @"HTTP Connection Timeout must be within the range."); + XCTAssertEqual( + [_mockSettings internalMetadataValueForKey:RCNHTTPReadTimeoutInMillisecondsKey + minValue:RCNHTTPReadTimeoutInMillisecondsMin + maxValue:RCNHTTPReadTimeoutInMillisecondsMax + defaultValue:RCNHTTPReadTimeoutInMillisecondsDefault], + RCNHTTPReadTimeoutInMillisecondsMax, @"HTTP Read Timeout must be within the range"); + XCTAssertEqual( + [_mockSettings + internalMetadataValueForKey:RCNThrottledSuccessFetchTimeIntervalInSecondsKey + minValue:RCNThrottledSuccessFetchTimeIntervalInSecondsMin + maxValue:RCNThrottledSuccessFetchTimeIntervalInSecondsMax + defaultValue:RCNThrottledSuccessFetchTimeIntervalInSecondsDefault], + RCNThrottledSuccessFetchTimeIntervalInSecondsMin, + @"Throttling success internal must be within the range"); + XCTAssertEqual([_mockSettings internalMetadataValueForKey:RCNThrottledSuccessFetchCountKey + minValue:RCNThrottledSuccessFetchCountMin + maxValue:RCNThrottledSuccessFetchCountMax + defaultValue:RCNThrottledSuccessFetchCountDefault], + RCNThrottledSuccessFetchCountMin); + XCTAssertEqual( + [_mockSettings + internalMetadataValueForKey:RCNThrottledFailureFetchTimeIntervalInSecondsKey + minValue:RCNThrottledFailureFetchTimeIntervalInSecondsMin + maxValue:RCNThrottledFailureFetchTimeIntervalInSecondsMax + defaultValue:RCNThrottledFailureFetchTimeIntervalInSecondsDefault], + RCNThrottledFailureFetchTimeIntervalInSecondsMax); + XCTAssertEqual([_mockSettings internalMetadataValueForKey:RCNThrottledFailureFetchCountKey + minValue:RCNThrottledFailureFetchCountMin + maxValue:RCNThrottledFailureFetchCountMax + defaultValue:RCNThrottledFailureFetchCountDefault], + RCNThrottledFailureFetchCountMax); + + // Mock internal metadata array with bundle_identifier prefix key. + // bundle_identifier prefixed key should override all_packages prefix key + NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier]; + response.internalMetadataArray = [RCNTestUtilities entryArrayWithKeyValuePair:@{ + [NSString stringWithFormat:@"%@:%@", bundleIdentifier, + RCNHTTPConnectionTimeoutInMillisecondsKey] : @"70000", + [NSString stringWithFormat:@"%@:%@", bundleIdentifier, RCNHTTPReadTimeoutInMillisecondsKey] : + @"70000", + [NSString stringWithFormat:@"%@:%@", bundleIdentifier, + RCNThrottledSuccessFetchTimeIntervalInSecondsKey] : @"1800", + [NSString stringWithFormat:@"%@:%@", bundleIdentifier, RCNThrottledSuccessFetchCountKey] : + @"100", + [NSString stringWithFormat:@"%@:%@", bundleIdentifier, + RCNThrottledFailureFetchTimeIntervalInSecondsKey] : @"1800", + [NSString stringWithFormat:@"%@:%@", bundleIdentifier, RCNThrottledFailureFetchCountKey] : @"0", + }]; + [_mockSettings updateInternalContentWithResponse:response]; + + XCTAssertEqual( + [_mockSettings internalMetadataValueForKey:RCNHTTPConnectionTimeoutInMillisecondsKey + minValue:RCNHTTPConnectionTimeoutInMillisecondsMin + maxValue:RCNHTTPConnectionTimeoutInMillisecondsMax + defaultValue:RCNHTTPConnectionTimeoutInMillisecondsDefault], + 70000); + XCTAssertEqual( + [_mockSettings internalMetadataValueForKey:RCNHTTPReadTimeoutInMillisecondsKey + minValue:RCNHTTPReadTimeoutInMillisecondsMin + maxValue:RCNHTTPReadTimeoutInMillisecondsMax + defaultValue:RCNHTTPReadTimeoutInMillisecondsDefault], + 70000); + XCTAssertEqual( + [_mockSettings + internalMetadataValueForKey:RCNThrottledSuccessFetchTimeIntervalInSecondsKey + minValue:RCNThrottledSuccessFetchTimeIntervalInSecondsMin + maxValue:RCNThrottledSuccessFetchTimeIntervalInSecondsMax + defaultValue:RCNThrottledSuccessFetchTimeIntervalInSecondsDefault], + 1800); + XCTAssertEqual([_mockSettings internalMetadataValueForKey:RCNThrottledSuccessFetchCountKey + minValue:RCNThrottledSuccessFetchCountMin + maxValue:RCNThrottledSuccessFetchCountMax + defaultValue:RCNThrottledSuccessFetchCountDefault], + 20); + + XCTAssertEqual( + [_mockSettings + internalMetadataValueForKey:RCNThrottledFailureFetchTimeIntervalInSecondsKey + minValue:RCNThrottledFailureFetchTimeIntervalInSecondsMin + maxValue:RCNThrottledFailureFetchTimeIntervalInSecondsMax + defaultValue:RCNThrottledFailureFetchTimeIntervalInSecondsDefault], + 1800); + XCTAssertEqual([_mockSettings internalMetadataValueForKey:RCNThrottledFailureFetchCountKey + minValue:RCNThrottledFailureFetchCountMin + maxValue:RCNThrottledFailureFetchCountMax + defaultValue:RCNThrottledFailureFetchCountDefault], + 1); +} + +- (void)testInternalMetadataOverride { + // Mock response after fetching. + RCNConfigFetchResponse *response = [[RCNConfigFetchResponse alloc] init]; + NSString *onePackageKey = + [NSString stringWithFormat:@"%@:%@", [[NSBundle mainBundle] bundleIdentifier], + RCNThrottledSuccessFetchCountKey]; + NSString *allPackageKey = + [NSString stringWithFormat:@"%@:%@", RCNInternalMetadataAllPackagesPrefix, + RCNThrottledSuccessFetchCountKey]; + + [_mockSettings updateInternalContentWithResponse:response]; + XCTAssertEqual([_mockSettings internalMetadataValueForKey:RCNThrottledSuccessFetchCountKey + minValue:RCNThrottledSuccessFetchCountMin + maxValue:RCNThrottledSuccessFetchCountMax + defaultValue:RCNThrottledSuccessFetchCountDefault], + RCNThrottledSuccessFetchCountDefault, + @"Fetch with no internal metadata, must return default value."); + + response.internalMetadataArray = + [RCNTestUtilities entryArrayWithKeyValuePair:@{onePackageKey : @"8", allPackageKey : @"9"}]; + [_mockSettings updateInternalContentWithResponse:response]; + XCTAssertEqual([_mockSettings internalMetadataValueForKey:RCNThrottledSuccessFetchCountKey + minValue:RCNThrottledSuccessFetchCountMin + maxValue:RCNThrottledSuccessFetchCountMax + defaultValue:RCNThrottledSuccessFetchCountDefault], + 8, @"Fetch with both keys, must return the one with package key."); + + [response.internalMetadataArray removeAllObjects]; + [_mockSettings updateInternalContentWithResponse:response]; + XCTAssertEqual( + [_mockSettings internalMetadataValueForKey:RCNThrottledSuccessFetchCountKey + minValue:RCNThrottledSuccessFetchCountMin + maxValue:RCNThrottledSuccessFetchCountMax + defaultValue:RCNThrottledSuccessFetchCountDefault], + 9, @"Fetch with no internal metadata, must return the one with previous all_packages key."); + + [response.internalMetadataArray removeAllObjects]; + response.internalMetadataArray = [RCNTestUtilities entryArrayWithKeyValuePair:@{ + onePackageKey : @"6", + }]; + + [_mockSettings updateInternalContentWithResponse:response]; + XCTAssertEqual([_mockSettings internalMetadataValueForKey:RCNThrottledSuccessFetchCountKey + minValue:RCNThrottledSuccessFetchCountMin + maxValue:RCNThrottledSuccessFetchCountMax + defaultValue:RCNThrottledSuccessFetchCountDefault], + 6, @"Fetch with one package key, must return the one with package key."); +} + +- (void)testThrottlingFresh { + NSTimeInterval endTimestamp = [_mockSettings cachedDataThrottledEndTimestamp]; + NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; + XCTAssertTrue(endTimestamp <= now); + + // Fetch failed once. + [_mockSettings updateFetchTimeWithSuccessFetch:NO]; + endTimestamp = [_mockSettings cachedDataThrottledEndTimestamp]; + now = [[NSDate date] timeIntervalSince1970]; + XCTAssertTrue(endTimestamp <= now); + + // Fetch succeeded once. + [_mockSettings updateFetchTimeWithSuccessFetch:YES]; + endTimestamp = [_mockSettings cachedDataThrottledEndTimestamp]; + now = [[NSDate date] timeIntervalSince1970]; + XCTAssertTrue(endTimestamp <= now); + + // The failure fetch rate is 5. Try another 3 times, and do not go over the limit. + for (int i = 0; i < 3; i++) { + [_mockSettings updateFetchTimeWithSuccessFetch:NO]; + } + endTimestamp = [_mockSettings cachedDataThrottledEndTimestamp]; + now = [[NSDate date] timeIntervalSince1970]; + XCTAssertTrue(endTimestamp <= now); + + // The success fetch rate is 5. Try another 4 times, which should go over the limit afterwards. + for (int i = 0; i < 4; i++) { + [_mockSettings updateFetchTimeWithSuccessFetch:YES]; + } + endTimestamp = [_mockSettings cachedDataThrottledEndTimestamp]; + now = [[NSDate date] timeIntervalSince1970]; + // Now it should go over the limit. + XCTAssertFalse(endTimestamp <= now); + XCTAssertTrue([_mockSettings hasCachedData]); +} +#endif + +- (void)testResetDigestInNextRequest { + NSDictionary *digestPerNamespace = @{@"firebase" : @"1234", @"p4" : @"5678"}; + [_mockSettings setNamespaceToDigest:digestPerNamespace]; + + RCNNamedValue *firebaseDigest = [[RCNNamedValue alloc] init]; + firebaseDigest.name = @"firebase"; + firebaseDigest.value = @"1234"; + + RCNNamedValue *p4Digest = [[RCNNamedValue alloc] init]; + p4Digest.name = @"p4"; + p4Digest.value = @"5678"; + + // Test where each namespace's fetched config is a non-empty dictionary, request should include + // the namespace's digest. + NSDictionary *fetchedConfig = + @{@"firebase" : @{@"a" : @"b", @"c" : @"d"}, @"p4" : @{@"p4key" : @"p4value"}}; + RCNConfigFetchRequest *request = [_mockSettings nextRequestWithUserProperties:nil + fetchedConfig:fetchedConfig]; + XCTAssertEqual(request.packageDataArray.count, 1); + NSArray *expectedArray = @[ firebaseDigest, p4Digest ]; + XCTAssertEqualObjects(request.packageDataArray[0].namespaceDigestArray, expectedArray); + + // Test when the namespace's fetched config doesn't exist, reset the digest by not included + // in the request. + fetchedConfig = @{@"firebase" : @{@"a" : @"b", @"c" : @"d"}}; + request = [_mockSettings nextRequestWithUserProperties:nil fetchedConfig:fetchedConfig]; + XCTAssertEqual(request.packageDataArray.count, 1); + XCTAssertEqualObjects(request.packageDataArray[0].namespaceDigestArray, @[ firebaseDigest ]); + + // Test when a namespace's fetched config is empty, reset the digest. + fetchedConfig = @{@"firebase" : @{@"a" : @"b", @"c" : @"d"}, @"p4" : @{}}; + request = [_mockSettings nextRequestWithUserProperties:nil fetchedConfig:fetchedConfig]; + XCTAssertEqual(request.packageDataArray.count, 1); + XCTAssertEqualObjects(request.packageDataArray[0].namespaceDigestArray, @[ firebaseDigest ]); + + // Test when a namespace's fetched config is a invalid format (non-dictionary), reset the digest. + fetchedConfig = @{@"firebase" : @{@"a" : @"b", @"c" : @"d"}, @"p4" : @[]}; + request = [_mockSettings nextRequestWithUserProperties:nil fetchedConfig:fetchedConfig]; + XCTAssertEqual(request.packageDataArray.count, 1); + XCTAssertEqualObjects(request.packageDataArray[0].namespaceDigestArray, @[ firebaseDigest ]); + + // Test when a namespace's fetched config is a invalid format (non-dictionary), reset the digest. + fetchedConfig = @{@"firebase" : @{@"a" : @"b", @"c" : @"d"}, @"p4" : @"wrong format of config"}; + request = [_mockSettings nextRequestWithUserProperties:nil fetchedConfig:fetchedConfig]; + XCTAssertEqual(request.packageDataArray.count, 1); + XCTAssertEqualObjects(request.packageDataArray[0].namespaceDigestArray, @[ firebaseDigest ]); +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m new file mode 100644 index 00000000000..c70fe182d2d --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigTest.m @@ -0,0 +1,476 @@ +/* + * Copyright 2019 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 + +#import "FirebaseRemoteConfig/Sources/Protos/wireless/android/config/proto/Config.pbobjc.h" + +#import +#import +#import +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigFetch.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" +#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" + +static NSString *const RCNFakeSenderID = @"855865492447"; +static NSString *const RCNFakeToken = @"ctToAh17Exk:" + @"APA91bFpX1aucYk5ONWt3MxyVxTyDKV8PKjaY2X3DPCZOOTHIBNf3ybxV5" + @"aMQ7G8zUTrduobNLEUoSvGsncthR27gDF_qqZELqp2eEi1BL8k_" + @"4AkeP2dLBq4f8MvuJTOEv2P5ChTdByr"; +static NSString *const RCNFakeDeviceID = @"4421690866479820589"; +static NSString *const RCNFakeSecretToken = @"6377571288467228941"; + +@interface RCNConfigFetch (ForTest) +// Exposes fetching user property method in the category. +//- (void)fetchWithUserPropertiesCompletionHandler:(RCNAnalyticsUserPropertiesCompletion)block; +- (void)refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler: + (FIRRemoteConfigFetchCompletion)completionHandler; +- (void)fetchCheckinInfoWithCompletionHandler:(FIRRemoteConfigFetchCompletion)completionHandler; +@end + +@interface RCNConfigTest : XCTestCase { + NSTimeInterval _expectationTimeout; + RCNConfigSettings *_settings; + RCNConfigFetchResponse *_response; + RCNConfigContent *_configContent; + RCNConfigExperiment *_experiment; + RCNConfigFetch *_configFetch; + dispatch_queue_t _queue; +} +@end + +@implementation RCNConfigTest +- (void)setUp { + [super setUp]; + _expectationTimeout = 1.0; + // Mock the singleton to an instance that is reset for each unit test + _configContent = [[RCNConfigContent alloc] initWithDBManager:nil]; + _settings = [[RCNConfigSettings alloc] initWithDatabaseManager:nil]; + _experiment = [[RCNConfigExperiment alloc] initWithDBManager:nil]; + _queue = dispatch_queue_create("com.google.GoogleConfigService.FIRRemoteConfigTest", + DISPATCH_QUEUE_CONCURRENT); + RCNConfigFetch *fetcher = [[RCNConfigFetch alloc] initWithContent:_configContent + settings:_settings + experiment:_experiment + queue:_queue]; + _configFetch = OCMPartialMock(fetcher); + // Fake a response with a default namespace and a custom namespace. + NSDictionary *namespaceToConfig = @{ + FIRNamespaceGoogleMobilePlatform : @{@"key1" : @"value1", @"key2" : @"value2"}, + FIRNamespaceGooglePlayPlatform : @{@"playerID" : @"36", @"gameLevel" : @"87"}, + }; + _response = + [RCNTestUtilities responseWithNamespaceToConfig:namespaceToConfig + statusArray:@[ + @(RCNAppNamespaceConfigTable_NamespaceStatus_Update), + @(RCNAppNamespaceConfigTable_NamespaceStatus_Update) + ]]; + NSData *responseData = [NSData gtm_dataByDeflatingData:_response.data error:nil]; + + // Mock successful network fetches with an empty config response. + RCNConfigFetcherTestBlock testBlock = ^(RCNConfigFetcherCompletion completion) { + completion(responseData, nil, nil); + }; + [RCNConfigFetch setGlobalTestBlock:testBlock]; + + // Mocks the user property fetch with a predefined dictionary. + NSDictionary *userProperties = @{@"userProperty1" : @"100", @"userProperty2" : @"200"}; + OCMStub([_configFetch + fetchWithUserPropertiesCompletionHandler:([OCMArg invokeBlockWithArgs:userProperties, nil])]); +} + +- (void)tearDown { + [RCNConfigFetch setGlobalTestBlock:nil]; + [super tearDown]; +} + +- (void)testInitMethod { + RCNConfigFetch *fetcher = [[RCNConfigFetch alloc] init]; + XCTAssertNotNil(fetcher); +} + +- (void)testFetchAllConfigsFailedWithoutCachedResult { + XCTestExpectation *fetchFailedExpectation = [self + expectationWithDescription:@"Test first config fetch failed without any cached result."]; + // Mock a failed network fetch. + NSError *error = [NSError errorWithDomain:@"testDomain" code:1 userInfo:nil]; + RCNConfigFetcherTestBlock testBlock = ^(RCNConfigFetcherCompletion completion) { + completion(nil, nil, error); + }; + [RCNConfigFetch setGlobalTestBlock:testBlock]; + + FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertNotNil(error); + XCTAssertEqual(self->_configContent.fetchedConfig.count, + 0); // There's no cached result yet since this is the first fetch. + XCTAssertEqual(status, FIRRemoteConfigFetchStatusFailure, + @"Fetch config failed, there is no cached config result yet. Status must " + @"equal to FIRRemoteConfigFetchStatusNotAvailable."); + + XCTAssertEqual(self->_settings.expirationInSeconds, 0, + @"expirationInSeconds is set successfully during fetch."); + XCTAssertEqual(self->_settings.lastFetchTimeInterval, 0, + @"last fetch time interval should not be set."); + XCTAssertEqual(self->_settings.lastApplyTimeInterval, 0, + @"last apply time interval should not be set."); + + [fetchFailedExpectation fulfill]; + }; + [_configFetch fetchAllConfigsWithExpirationDuration:0 + completionHandler:fetchAllConfigsCompletion]; + + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchAllConfigsSuccessfully { + XCTestExpectation *fetchAllConfigsExpectation = + [self expectationWithDescription:@"Test fetch all configs successfully."]; + + FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertNil(error); + NSDictionary *result = self->_configContent.fetchedConfig; + XCTAssertNotNil(result); + + [self checkConfigResult:result + withNamespace:FIRNamespaceGoogleMobilePlatform + key:@"key1" + value:@"value1"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGoogleMobilePlatform + key:@"key2" + value:@"value2"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGooglePlayPlatform + key:@"playerID" + value:@"36"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGooglePlayPlatform + key:@"gameLevel" + value:@"87"]; + XCTAssertEqual(self->_settings.expirationInSeconds, 43200, + @"expirationInSeconds is set successfully during fetch."); + XCTAssertGreaterThan(self->_settings.lastFetchTimeInterval, 0, + @"last fetch time interval should be set."); + XCTAssertEqual(self->_settings.lastApplyTimeInterval, 0, + @"last apply time interval should not be set."); + + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess, + @"Callback of first successful config " + @"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccess."); + [fetchAllConfigsExpectation fulfill]; + }; + + [_configFetch fetchAllConfigsWithExpirationDuration:43200 + completionHandler:fetchAllConfigsCompletion]; + + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchConfigInCachedResults { + XCTestExpectation *fetchConfigExpectation = + [self expectationWithDescription:@"Test fetch config within expiration duration, meaning " + @"use fresh cached result instead of fetching from server."]; + + FIRRemoteConfigFetchCompletion firstFetchCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertNil(error); + FIRRemoteConfigFetchCompletion secondFetchCompletion = ^void( + FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertNil(error); + NSDictionary *result = self->_configContent.fetchedConfig; + XCTAssertNotNil(result); + [self checkConfigResult:result + withNamespace:FIRNamespaceGoogleMobilePlatform + key:@"key1" + value:@"value1"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGoogleMobilePlatform + key:@"key2" + value:@"value2"]; + + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess, + "Config fetch's expiration duration is 43200 seconds, which means the" + "config cached data hasn't expired. Return cached result. Status must be " + "FIRRemoteConfigFetchStatusSuccess."); + [fetchConfigExpectation fulfill]; + }; + [_configFetch fetchAllConfigsWithExpirationDuration:43200 + completionHandler:secondFetchCompletion]; + }; + [_configFetch fetchAllConfigsWithExpirationDuration:43200 completionHandler:firstFetchCompletion]; + + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchFailedWithCachedResult { + XCTestExpectation *fetchFailedExpectation = + [self expectationWithDescription:@"Test fetch failed from server, use cached result."]; + // Mock a failed network fetch. + NSError *error = [NSError errorWithDomain:@"testDomain" code:1 userInfo:nil]; + RCNConfigFetcherTestBlock testBlock = ^(RCNConfigFetcherCompletion completion) { + completion(nil, nil, error); + }; + [RCNConfigFetch setGlobalTestBlock:testBlock]; + + // Mock previous fetch succeed with cached data. + [_settings updateMetadata:YES namespaceToDigest:nil]; + [_configContent updateConfigContentWithResponse:[_response copy]]; + + FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertNotNil(error); + NSDictionary *result = self->_configContent.fetchedConfig; + XCTAssertNotNil(result); + + [self checkConfigResult:result + withNamespace:FIRNamespaceGoogleMobilePlatform + key:@"key1" + value:@"value1"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGoogleMobilePlatform + key:@"key2" + value:@"value2"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGooglePlayPlatform + key:@"playerID" + value:@"36"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGooglePlayPlatform + key:@"gameLevel" + value:@"87"]; + + [fetchFailedExpectation fulfill]; + }; + // Expiration duration is set to 0, meaning always fetch from server because the cached result + // expired in 0 seconds. + [_configFetch fetchAllConfigsWithExpirationDuration:0 + completionHandler:fetchAllConfigsCompletion]; + + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchThrottledWithCachedResult { + XCTestExpectation *fetchAllConfigsExpectation = + [self expectationWithDescription: + @"Test fetch being throttled after exceeding throttling limit, use cached result."]; + // Fake a new response with a different custom namespace. + NSDictionary *namespaceToConfig = @{ + @"configns:MY_OWN_APP" : + @{@"columnID" : @"28", @"columnName" : @"height", @"columnValue" : @"2"} + }; + RCNConfigFetchResponse *newResponse = [RCNTestUtilities + responseWithNamespaceToConfig:namespaceToConfig + statusArray:@[ @(RCNAppNamespaceConfigTable_NamespaceStatus_Update) ]]; + // Mock 5 fetches ahead. + for (int i = 0; i < RCNThrottledSuccessFetchCountDefault; i++) { + [_settings updateMetadata:YES namespaceToDigest:nil]; + [_configContent updateConfigContentWithResponse:[newResponse copy]]; + } + + FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertNotNil(error); + NSDictionary *result = self->_configContent.fetchedConfig; + XCTAssertNotNil(result); // Cached result is not nil. + [self checkConfigResult:result + withNamespace:@"configns:MY_OWN_APP" + key:@"columnID" + value:@"28"]; + [self checkConfigResult:result + withNamespace:@"configns:MY_OWN_APP" + key:@"columnName" + value:@"height"]; + [self checkConfigResult:result + withNamespace:@"configns:MY_OWN_APP" + key:@"columnValue" + value:@"2"]; + + XCTAssertEqual(error.code, (int)FIRRemoteConfigErrorThrottled, + @"Default success throttling rate is 5. Mocked 5 successful fetches from " + @"server, this fetch will be throttled. Status must equal to " + @"FIRRemoteConfigFetchStatusFetchThrottled."); + [fetchAllConfigsExpectation fulfill]; + }; + [_configFetch fetchAllConfigsWithExpirationDuration:-1 + completionHandler:fetchAllConfigsCompletion]; + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchThrottledWithStaledCachedResult { + XCTestExpectation *fetchAllConfigsExpectation = + [self expectationWithDescription:@"Test fetch being throttled, use staled cache result."]; + // Mock 5 fetches ahead. + for (int i = 0; i < RCNThrottledSuccessFetchCountDefault; i++) { + [_settings updateMetadata:YES namespaceToDigest:nil]; + [_configContent updateConfigContentWithResponse:[_response copy]]; + } + + FIRRemoteConfigFetchCompletion fetchAllConfigsCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertNotNil(error); + NSDictionary *result = self->_configContent.fetchedConfig; + XCTAssertNotNil(result); + [self checkConfigResult:result + withNamespace:FIRNamespaceGoogleMobilePlatform + key:@"key1" + value:@"value1"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGoogleMobilePlatform + key:@"key2" + value:@"value2"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGooglePlayPlatform + key:@"playerID" + value:@"36"]; + [self checkConfigResult:result + withNamespace:FIRNamespaceGooglePlayPlatform + key:@"gameLevel" + value:@"87"]; + XCTAssertEqual( + error.code, FIRRemoteConfigErrorThrottled, + @"Request fetching within throttling time interval, so this fetch will still be " + @"throttled. " + @"However, the App context (custom variables) has changed, meaning the return cached " + @"result is staled. The status must equal to RCNConfigStatusFetchThrottledStale."); + [fetchAllConfigsExpectation fulfill]; + }; + // Fetch with new custom variables. + [_configFetch fetchAllConfigsWithExpirationDuration:0 + completionHandler:fetchAllConfigsCompletion]; + + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testRefreshInstanceIDToken { + XCTestExpectation *refreshTokenExpectation = + [self expectationWithDescription:@"Test refresh Instance ID token."]; + + FIRInstanceID *instanceID = [FIRInstanceID instanceID]; + _settings.senderID = RCNFakeSenderID; + id mock = OCMPartialMock(instanceID); + OCMStub([mock + tokenWithAuthorizedEntity:RCNFakeSenderID + scope:@"*" + options:nil + handler:([OCMArg invokeBlockWithArgs:RCNFakeToken, [NSNull null], nil])]); + + FIRRemoteConfigFetchCompletion completionHandler = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertEqualObjects(RCNFakeToken, _settings.configInstanceIDToken); + [mock stopMocking]; + [refreshTokenExpectation fulfill]; + }; + [_configFetch refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler:completionHandler]; + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testRefreshInstanceIDTokenWithError { + XCTestExpectation *refreshTokenExpectation = + [self expectationWithDescription:@"Test refresh Instance ID token."]; + + FIRInstanceID *instanceID = [FIRInstanceID instanceID]; + _settings.senderID = RCNFakeSenderID; + NSError *error = [NSError errorWithDomain:@"errorDomain" code:20 userInfo:nil]; + id mock = OCMPartialMock(instanceID); + OCMStub([mock + tokenWithAuthorizedEntity:RCNFakeSenderID + scope:@"*" + options:nil + handler:([OCMArg invokeBlockWithArgs:[NSNull null], error, nil])]); + + FIRRemoteConfigFetchCompletion completionHandler = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertNil(_settings.configInstanceIDToken); + XCTAssertNil(_settings.configInstanceID); + [mock stopMocking]; + [refreshTokenExpectation fulfill]; + }; + [_configFetch refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler:completionHandler]; + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchCheckin { + XCTestExpectation *refreshTokenExpectation = + [self expectationWithDescription:@"Test fetch checkin."]; + + FIRInstanceID *instanceID = [FIRInstanceID instanceID]; + _settings.senderID = RCNFakeSenderID; + id mock = OCMPartialMock(instanceID); + FIRInstanceIDCheckinPreferences *preferences = + [[FIRInstanceIDCheckinPreferences alloc] initWithDeviceID:RCNFakeDeviceID + secretToken:RCNFakeSecretToken]; + OCMStub([mock + fetchCheckinInfoWithHandler:([OCMArg invokeBlockWithArgs:preferences, [NSNull null], nil])]); + + FIRRemoteConfigFetchCompletion completionHandler = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertEqualObjects(RCNFakeDeviceID, _settings.deviceAuthID); + XCTAssertEqualObjects(RCNFakeSecretToken, _settings.secretToken); + [mock stopMocking]; + [refreshTokenExpectation fulfill]; + }; + [_configFetch refreshInstanceIDTokenAndFetchCheckInInfoWithCompletionHandler:completionHandler]; + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +#pragma mark - helpers + +- (void)checkConfigResult:(NSDictionary *)result + withNamespace:(NSString *)namespace + key:(NSString *)key + value:(NSString *)value { + if (result[namespace]) { + FIRRemoteConfigValue *configValue = result[namespace][key]; + XCTAssertEqualObjects(configValue.stringValue, value, + @"Config result missing the key value pair."); + } else { + XCTAssertNotNil(result[namespace], @"Config result missing the namespace."); + } +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNConfigValueTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNConfigValueTest.m new file mode 100644 index 00000000000..a12af9c13ee --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNConfigValueTest.m @@ -0,0 +1,82 @@ +/* + * Copyright 2019 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 + +#import "FirebaseRemoteConfig/Sources/RCNConfigValue_Internal.h" + +@interface FIRRemoteConfigValueTest : XCTestCase +@end + +@implementation FIRRemoteConfigValueTest +- (void)testConfigValueWithDifferentValueTypes { + NSString *valueA = @"0.33333"; + NSData *dataA = [valueA dataUsingEncoding:NSUTF8StringEncoding]; + + FIRRemoteConfigValue *configValueA = + [[FIRRemoteConfigValue alloc] initWithData:dataA source:FIRRemoteConfigSourceRemote]; + XCTAssertEqualObjects(configValueA.stringValue, valueA); + XCTAssertEqualObjects(configValueA.dataValue, dataA); + XCTAssertEqualObjects(configValueA.numberValue, configValueA.numberValue); + XCTAssertEqual(configValueA.boolValue, valueA.boolValue); + + NSString *valueB = @"NO"; + FIRRemoteConfigValue *configValueB = + [[FIRRemoteConfigValue alloc] initWithData:[valueB dataUsingEncoding:NSUTF8StringEncoding] + source:FIRRemoteConfigSourceRemote]; + XCTAssertEqual(configValueB.boolValue, valueB.boolValue); + + // Test JSON value. + NSDictionary *JSONDictionary = @{@"key1" : @"value1"}; + NSArray *> *JSONArray = + @[ @{@"key1" : @"value1"}, @{@"key2" : @"value2"} ]; + NSError *error; + NSData *JSONData = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error]; + FIRRemoteConfigValue *configValueC = + [[FIRRemoteConfigValue alloc] initWithData:JSONData source:FIRRemoteConfigSourceRemote]; + XCTAssertEqualObjects(configValueC.JSONValue, JSONDictionary); + + NSData *JSONArrayData = [NSJSONSerialization dataWithJSONObject:JSONArray options:0 error:&error]; + FIRRemoteConfigValue *configValueD = + [[FIRRemoteConfigValue alloc] initWithData:JSONArrayData source:FIRRemoteConfigSourceRemote]; + XCTAssertEqualObjects(configValueD.JSONValue, JSONArray); +} + +- (void)testFIRRemoteConfigValueToNumber { + FIRRemoteConfigValue *value; + + NSString *strValue = @"0.33"; + NSData *data = [strValue dataUsingEncoding:NSUTF8StringEncoding]; + value = [[FIRRemoteConfigValue alloc] initWithData:data source:FIRRemoteConfigSourceRemote]; + XCTAssertEqual(value.numberValue.floatValue, strValue.floatValue); + + strValue = @"3.14159265358979"; + data = [strValue dataUsingEncoding:NSUTF8StringEncoding]; + value = [[FIRRemoteConfigValue alloc] initWithData:data source:FIRRemoteConfigSourceRemote]; + XCTAssertEqual(value.numberValue.doubleValue, strValue.doubleValue); + + strValue = @"1000000000"; + data = [strValue dataUsingEncoding:NSUTF8StringEncoding]; + value = [[FIRRemoteConfigValue alloc] initWithData:data source:FIRRemoteConfigSourceRemote]; + XCTAssertEqual(value.numberValue.intValue, strValue.intValue); + + strValue = @"1000000000123"; + data = [strValue dataUsingEncoding:NSUTF8StringEncoding]; + value = [[FIRRemoteConfigValue alloc] initWithData:data source:FIRRemoteConfigSourceRemote]; + XCTAssertEqual(value.numberValue.longLongValue, strValue.longLongValue); +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfig+FIRAppTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfig+FIRAppTest.m new file mode 100644 index 00000000000..f24826acc46 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfig+FIRAppTest.m @@ -0,0 +1,113 @@ +/* + * Copyright 2019 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 + +//#import "FIRRemoteConfig+FIRApp.h" +#import +#import +#import +#import "FIRRemoteConfig_Private.h" +#import "third_party/firebase/ios/Releases/FirebaseCore/Tests/FIRTestCase.h" + +@interface RCNRemoteConfig_FIRAppTest : FIRTestCase + +@end + +@implementation RCNRemoteConfig_FIRAppTest + +- (void)setUp { + [super setUp]; + [FIRApp resetApps]; +} + +- (void)testConfigureConfigWithValidInput { + XCTAssertNoThrow([FIRApp configure]); + FIRRemoteConfig *rc = [FIRRemoteConfig remoteConfig]; + XCTAssertEqualObjects(rc.GMPProjectID, kGoogleAppID); + XCTAssertEqualObjects(rc.senderID, kGCMSenderID); +} + +- (void)testConfigureConfigWithEmptyGoogleAppID { + NSDictionary *optionsDictionary = @{kFIRGoogleAppID : @""}; + FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:kFIRDefaultAppName options:options]; + FIRRemoteConfig *rc = [FIRRemoteConfig remoteConfig]; + XCTAssertThrows([rc configureConfig:app]); +} + +- (void)testConfigureConfigWithNilGoogleAppID { + NSDictionary *optionsDictionary = @{}; + FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:kFIRDefaultAppName options:options]; + FIRRemoteConfig *rc = [FIRRemoteConfig remoteConfig]; + XCTAssertThrows([rc configureConfig:app]); +} + +- (void)testConfigureConfigWithEmptySenderID { + NSDictionary *optionsDictionary = @{kFIRGoogleAppID : kGoogleAppID, kFIRGCMSenderID : @""}; + FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:kFIRDefaultAppName options:options]; + FIRRemoteConfig *rc = [FIRRemoteConfig remoteConfig]; + XCTAssertThrows([rc configureConfig:app]); +} + +- (void)testConfigureConfigWithNilSenderID { + NSDictionary *optionsDictionary = @{kFIRGoogleAppID : kGoogleAppID}; + FIROptions *options = [[FIROptions alloc] initInternalWithOptionsDictionary:optionsDictionary]; + FIRApp *app = [[FIRApp alloc] initInstanceWithName:kFIRDefaultAppName options:options]; + FIRRemoteConfig *rc = [FIRRemoteConfig remoteConfig]; + XCTAssertThrows([rc configureConfig:app]); +} + +- (void)testConfigureConfigNotIntallingSenderID { + id settingsMock = OCMClassMock([RCNConfigSettings class]); + OCMStub([settingsMock instancesRespondToSelector:@selector(senderID)]).andReturn(NO); + + XCTAssertNoThrow([FIRApp configure]); + FIRRemoteConfig *rc = [FIRRemoteConfig remoteConfig]; + XCTAssertEqualObjects(rc.GMPProjectID, kGoogleAppID); + XCTAssertEqualObjects(rc.senderID, nil); + + [settingsMock stopMocking]; +} + +- (void)testConfigureWithOptions { + FIROptions *options = + [[FIROptions alloc] initWithGoogleAppID:@"1:966600170131:ios:a750b5ff97fbf47d" + GCMSenderID:@"966600170131"]; + XCTAssertNoThrow([FIRApp configureWithOptions:options]); + FIRRemoteConfig *rc = [FIRRemoteConfig remoteConfig]; + XCTAssertEqualObjects(rc.GMPProjectID, @"1:966600170131:ios:a750b5ff97fbf47d"); + XCTAssertEqualObjects(rc.senderID, @"966600170131"); +} + +- (void)testConfigureWithMultipleProjects { + XCTAssertNoThrow([FIRApp configure]); + FIRRemoteConfig *rc = [FIRRemoteConfig remoteConfig]; + XCTAssertEqualObjects(rc.GMPProjectID, kGoogleAppID); + XCTAssertEqualObjects(rc.senderID, kGCMSenderID); + + FIROptions *options = + [[FIROptions alloc] initWithGoogleAppID:@"1:966600170131:ios:a750b5ff97fbf47d" + GCMSenderID:@"966600170131"]; + [FIRApp configureWithName:@"nonDefault" options:options]; + + XCTAssertEqualObjects(rc.GMPProjectID, kGoogleAppID); + XCTAssertEqualObjects(rc.senderID, kGCMSenderID); +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m new file mode 100644 index 00000000000..7e9a45fb612 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNRemoteConfigTest.m @@ -0,0 +1,1187 @@ +/* + * Copyright 2019 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 + +#import +#import "FirebaseRemoteConfig/Sources/Private/FIRRemoteConfig_Private.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigConstants.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigFetch.h" +#import "FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h" + +#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" + +#import +#import +#import +#import +#import + +@interface RCNConfigFetch (ForTest) +- (instancetype)initWithContent:(RCNConfigContent *)content + DBManager:(RCNConfigDBManager *)DBManager + settings:(RCNConfigSettings *)settings + experiment:(RCNConfigExperiment *)experiment + queue:(dispatch_queue_t)queue + namespace:firebaseNamespace + app:firebaseApp; +/// Skip fetching user properties from analytics because we cannot mock the action here. Instead +/// overriding the method to skip. +- (void)fetchWithUserPropertiesCompletionHandler:(FIRAInteropUserPropertiesCallback)block; +- (NSURLSessionDataTask *)URLSessionDataTaskWithContent:(NSData *)content + completionHandler: + (RCNConfigFetcherCompletion)fetcherCompletion; + +- (void)fetchWithUserProperties:(NSDictionary *)userProperties + completionHandler:(FIRRemoteConfigFetchCompletion)completionHandler; +- (NSString *)constructServerURL; +- (NSURLSession *)currentNetworkSession; +@end + +@interface FIRRemoteConfig (ForTest) +- (void)updateWithNewInstancesForConfigFetch:(RCNConfigFetch *)configFetch + configContent:(RCNConfigContent *)configContent + configSettings:(RCNConfigSettings *)configSettings + configExperiment:(RCNConfigExperiment *)configExperiment; +@end + +@implementation FIRRemoteConfig (ForTest) +- (void)updateWithNewInstancesForConfigFetch:(RCNConfigFetch *)configFetch + configContent:(RCNConfigContent *)configContent + configSettings:(RCNConfigSettings *)configSettings + configExperiment:(RCNConfigExperiment *)configExperiment { + [self setValue:configFetch forKey:@"_configFetch"]; + [self setValue:configContent forKey:@"_configContent"]; + [self setValue:configSettings forKey:@"_settings"]; + [self setValue:configExperiment forKey:@"_configExperiment"]; +} +@end + +@interface RCNConfigDBManager (Test) +- (void)removeDatabaseOnDatabaseQueueAtPath:(NSString *)path; +@end + +@interface RCNUserDefaultsManager (Test) ++ (NSUserDefaults *)sharedUserDefaultsForBundleIdentifier:(NSString *)bundleIdentifier; +@end + +typedef NS_ENUM(NSInteger, RCNTestRCInstance) { + RCNTestRCInstanceDefault, + RCNTestRCInstanceSecondNamespace, + RCNTestRCInstanceSecondApp, + RCNTestRCNumTotalInstances +}; + +@interface RCNRemoteConfigTest : XCTestCase { + NSTimeInterval _expectationTimeout; + NSTimeInterval _checkCompletionTimeout; + NSMutableArray *_configInstances; + NSMutableArray *> *_entries; + NSMutableArray *> *_response; + NSMutableArray *_responseData; + NSMutableArray *_URLResponse; + NSMutableArray *_configFetch; + RCNConfigDBManager *_DBManager; + NSUserDefaults *_userDefaults; + NSString *_userDefaultsSuiteName; + NSString *_DBPath; +} +@end + +@implementation RCNRemoteConfigTest +- (void)setUp { + [super setUp]; + FIRSetLoggerLevel(FIRLoggerLevelMax); + + _expectationTimeout = 5; + _checkCompletionTimeout = 1.0; + + // Always remove the database at the start of testing. + _DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase]; + id classMock = OCMClassMock([RCNConfigDBManager class]); + OCMStub([classMock remoteConfigPathForDatabase]).andReturn(_DBPath); + _DBManager = [[RCNConfigDBManager alloc] init]; + + _userDefaultsSuiteName = [RCNTestUtilities userDefaultsSuiteNameForTestSuite]; + _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:_userDefaultsSuiteName]; + id userDefaultsClassMock = OCMClassMock([RCNUserDefaultsManager class]); + OCMStub([userDefaultsClassMock sharedUserDefaultsForBundleIdentifier:[OCMArg any]]) + .andReturn(_userDefaults); + + RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager]; + _configInstances = [[NSMutableArray alloc] initWithCapacity:3]; + _entries = [[NSMutableArray alloc] initWithCapacity:3]; + _response = [[NSMutableArray alloc] initWithCapacity:3]; + _responseData = [[NSMutableArray alloc] initWithCapacity:3]; + _URLResponse = [[NSMutableArray alloc] initWithCapacity:3]; + _configFetch = [[NSMutableArray alloc] initWithCapacity:3]; + + // Populate the default, second app, second namespace instances. + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + // Fake a response for default instance. + NSMutableDictionary *valuesDict = [[NSMutableDictionary alloc] init]; + for (int count = 1; count <= 100; count++) { + NSString *key = [NSString stringWithFormat:@"key%d-%d", count, i]; + NSString *value = [NSString stringWithFormat:@"value%d-%d", count, i]; + valuesDict[key] = value; + } + _entries[i] = valuesDict; + + NSString *currentAppName = nil; + FIROptions *currentOptions = nil; + NSString *currentNamespace = nil; + switch (i) { + case RCNTestRCInstanceSecondNamespace: + currentAppName = RCNTestsDefaultFIRAppName; + currentOptions = [self firstAppOptions]; + currentNamespace = RCNTestsPerfNamespace; + break; + case RCNTestRCInstanceSecondApp: + currentAppName = RCNTestsSecondFIRAppName; + currentOptions = [self secondAppOptions]; + currentNamespace = FIRNamespaceGoogleMobilePlatform; + break; + case RCNTestRCInstanceDefault: + default: + currentAppName = RCNTestsDefaultFIRAppName; + currentOptions = [self firstAppOptions]; + currentNamespace = RCNTestsFIRNamespace; + break; + } + NSString *fullyQualifiedNamespace = + [NSString stringWithFormat:@"%@:%@", currentNamespace, currentAppName]; + FIRRemoteConfig *config = + OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:currentAppName + FIROptions:currentOptions + namespace:currentNamespace + DBManager:_DBManager + configContent:configContent + analytics:nil]); + + _configInstances[i] = config; + RCNConfigSettings *settings = + [[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager + namespace:fullyQualifiedNamespace + firebaseAppName:currentAppName + googleAppID:currentOptions.googleAppID]; + dispatch_queue_t queue = dispatch_queue_create( + [[NSString stringWithFormat:@"testqueue: %d", i] cStringUsingEncoding:NSUTF8StringEncoding], + DISPATCH_QUEUE_SERIAL); + _configFetch[i] = OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent + DBManager:_DBManager + settings:settings + analytics:nil + experiment:nil + queue:queue + namespace:fullyQualifiedNamespace + options:currentOptions]); + + OCMStub([_configFetch[i] fetchAllConfigsWithExpirationDuration:43200 + completionHandler:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + void (^handler)(FIRRemoteConfigFetchStatus status, NSError *_Nullable error) = nil; + // void (^handler)(FIRRemoteConfigFetchCompletion); + [invocation getArgument:&handler atIndex:3]; + [_configFetch[i] fetchWithUserProperties:[[NSDictionary alloc] init] + completionHandler:handler]; + }); + + _response[i] = @{@"state" : @"UPDATE", @"entries" : _entries[i]}; + + _responseData[i] = [NSJSONSerialization dataWithJSONObject:_response[i] options:0 error:nil]; + + _URLResponse[i] = [[NSHTTPURLResponse alloc] + initWithURL:[NSURL URLWithString:@"https://firebase.com"] + statusCode:200 + HTTPVersion:nil + headerFields:@{@"etag" : [NSString stringWithFormat:@"etag1-%d", i]}]; + + id completionBlock = + [OCMArg invokeBlockWithArgs:_responseData[i], _URLResponse[i], [NSNull null], nil]; + + OCMExpect([_configFetch[i] URLSessionDataTaskWithContent:[OCMArg any] + completionHandler:completionBlock]) + .andReturn(nil); + [_configInstances[i] updateWithNewInstancesForConfigFetch:_configFetch[i] + configContent:configContent + configSettings:settings + configExperiment:nil]; + } +} + +- (void)tearDown { + [_DBManager removeDatabaseOnDatabaseQueueAtPath:_DBPath]; + [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:_userDefaultsSuiteName]; + [super tearDown]; +} + +- (void)testFetchConfigWithNilCallback { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = [self + expectationWithDescription: + [NSString stringWithFormat:@"Set defaults no callback expectation - instance %d", i]]; + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet); + + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:nil]; + + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess); + [expectations[i] fulfill]; + }); + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + +- (void)testFetchConfigsSuccessfully { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = + [self expectationWithDescription: + [NSString stringWithFormat:@"Test fetch configs successfully - instance %d", i]]; + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet); + FIRRemoteConfigFetchCompletion fetchCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + XCTAssertTrue([_configInstances[i] activateFetched]); +#pragma clang diagnostic pop + NSString *key1 = [NSString stringWithFormat:@"key1-%d", i]; + NSString *key2 = [NSString stringWithFormat:@"key2-%d", i]; + NSString *value1 = [NSString stringWithFormat:@"value1-%d", i]; + NSString *value2 = [NSString stringWithFormat:@"value2-%d", i]; + XCTAssertEqualObjects(_configInstances[i][key1].stringValue, value1); + XCTAssertEqualObjects(_configInstances[i][key2].stringValue, value2); + + OCMVerify([_configInstances[i] objectForKeyedSubscript:key1]); + + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess, + @"Callback of first successful config " + @"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccessFresh."); + + XCTAssertNotNil(_configInstances[i].lastFetchTime); + XCTAssertGreaterThan(_configInstances[i].lastFetchTime.timeIntervalSince1970, 0, + @"last fetch time interval should be set."); + [expectations[i] fulfill]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchAndActivate { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = + [self expectationWithDescription: + [NSString stringWithFormat:@"Test fetch configs successfully - instance %d", i]]; + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet); + FIRRemoteConfigFetchAndActivateCompletion fetchAndActivateCompletion = ^void( + FIRRemoteConfigFetchAndActivateStatus status, NSError *error) { + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); + + NSString *key1 = [NSString stringWithFormat:@"key1-%d", i]; + NSString *key2 = [NSString stringWithFormat:@"key2-%d", i]; + NSString *value1 = [NSString stringWithFormat:@"value1-%d", i]; + NSString *value2 = [NSString stringWithFormat:@"value2-%d", i]; + XCTAssertEqualObjects(_configInstances[i][key1].stringValue, value1); + XCTAssertEqualObjects(_configInstances[i][key2].stringValue, value2); + + OCMVerify([_configInstances[i] objectForKeyedSubscript:key1]); + + XCTAssertEqual( + status, FIRRemoteConfigFetchAndActivateStatusSuccessFetchedFromRemote, + @"Callback of first successful config " + @"fetchAndActivate status must equal to FIRRemoteConfigFetchAndStatusSuccessFromRemote."); + + XCTAssertNotNil(_configInstances[i].lastFetchTime); + XCTAssertGreaterThan(_configInstances[i].lastFetchTime.timeIntervalSince1970, 0, + @"last fetch time interval should be set."); + [expectations[i] fulfill]; + }; + // Update the minimum fetch interval to 0. This disables the cache and forces a remote fetch + // request. + FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init]; + settings.minimumFetchInterval = 0; + [_configInstances[i] setConfigSettings:settings]; + [_configInstances[i] fetchAndActivateWithCompletionHandler:fetchAndActivateCompletion]; + } + + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +// TODO: Try splitting into smaller tests. +- (void)testFetchConfigsSuccessfullyWithNewActivateMethodSignature { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = + [self expectationWithDescription: + [NSString stringWithFormat:@"Test fetch configs successfully - instance %d", i]]; + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet); + FIRRemoteConfigFetchCompletion fetchCompletion = + ^(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); + [_configInstances[i] activateWithCompletionHandler:^(NSError *_Nullable error) { + XCTAssertNil(error); + NSString *key1 = [NSString stringWithFormat:@"key1-%d", i]; + NSString *key2 = [NSString stringWithFormat:@"key2-%d", i]; + NSString *value1 = [NSString stringWithFormat:@"value1-%d", i]; + NSString *value2 = [NSString stringWithFormat:@"value2-%d", i]; + XCTAssertEqualObjects(_configInstances[i][key1].stringValue, value1); + XCTAssertEqualObjects(_configInstances[i][key2].stringValue, value2); + + OCMVerify([_configInstances[i] objectForKeyedSubscript:key1]); + + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess, + @"Callback of first successful config " + @"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccessFresh."); + + XCTAssertNotNil(_configInstances[i].lastFetchTime); + XCTAssertGreaterThan(_configInstances[i].lastFetchTime.timeIntervalSince1970, 0, + @"last fetch time interval should be set."); + // A second activate should return an error. + [_configInstances[i] activateWithCompletionHandler:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRRemoteConfigErrorDomain); + }]; + [expectations[i] fulfill]; + }]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testEnumeratingConfigResults { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = [self + expectationWithDescription: + [NSString stringWithFormat:@"Test enumerating configs successfully - instance %d", i]]; + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet); + FIRRemoteConfigFetchCompletion fetchCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + XCTAssertTrue([_configInstances[i] activateFetched]); +#pragma clang diagnostic pop + NSString *key5 = [NSString stringWithFormat:@"key5-%d", i]; + NSString *key19 = [NSString stringWithFormat:@"key19-%d", i]; + NSString *value5 = [NSString stringWithFormat:@"value5-%d", i]; + NSString *value19 = [NSString stringWithFormat:@"value19-%d", i]; + + XCTAssertEqualObjects(_configInstances[i][key5].stringValue, value5); + XCTAssertEqualObjects(_configInstances[i][key19].stringValue, value19); + + // Test enumerating config values. + for (NSString *key in _configInstances[i]) { + if ([key isEqualToString:key5]) { + XCTAssertEqualObjects(_configInstances[i][key5].stringValue, value5); + } + if ([key isEqualToString:key19]) { + XCTAssertEqualObjects(_configInstances[i][key19].stringValue, value19); + } + } + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess, + @"Callback of first successful config " + @"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccessFresh."); + + XCTAssertNotNil(_configInstances[i].lastFetchTime); + XCTAssertGreaterThan(_configInstances[i].lastFetchTime.timeIntervalSince1970, 0, + @"last fetch time interval should be set."); + + [expectations[i] fulfill]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchConfigsFailed { + // Override the setup values to return back an error status. + RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:_DBManager]; + // Populate the default, second app, second namespace instances. + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + NSString *currentAppName = nil; + FIROptions *currentOptions = nil; + NSString *currentNamespace = nil; + switch (i) { + case RCNTestRCInstanceSecondNamespace: + currentAppName = RCNTestsDefaultFIRAppName; + currentOptions = [self firstAppOptions]; + currentNamespace = RCNTestsPerfNamespace; + break; + case RCNTestRCInstanceSecondApp: + currentAppName = RCNTestsSecondFIRAppName; + currentOptions = [self secondAppOptions]; + currentNamespace = FIRNamespaceGoogleMobilePlatform; + break; + case RCNTestRCInstanceDefault: + default: + currentAppName = RCNTestsDefaultFIRAppName; + currentOptions = [self firstAppOptions]; + currentNamespace = RCNTestsFIRNamespace; + break; + } + NSString *fullyQualifiedNamespace = + [NSString stringWithFormat:@"%@:%@", currentNamespace, currentAppName]; + RCNUserDefaultsManager *userDefaultsManager = + [[RCNUserDefaultsManager alloc] initWithAppName:currentAppName + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:fullyQualifiedNamespace]; + userDefaultsManager.lastFetchTime = 0; + + FIRRemoteConfig *config = + OCMPartialMock([[FIRRemoteConfig alloc] initWithAppName:currentAppName + FIROptions:currentOptions + namespace:currentNamespace + DBManager:_DBManager + configContent:configContent + analytics:nil]); + + _configInstances[i] = config; + RCNConfigSettings *settings = + [[RCNConfigSettings alloc] initWithDatabaseManager:_DBManager + namespace:fullyQualifiedNamespace + firebaseAppName:currentAppName + googleAppID:currentOptions.googleAppID]; + dispatch_queue_t queue = dispatch_queue_create( + [[NSString stringWithFormat:@"testqueue: %d", i] cStringUsingEncoding:NSUTF8StringEncoding], + DISPATCH_QUEUE_SERIAL); + _configFetch[i] = OCMPartialMock([[RCNConfigFetch alloc] initWithContent:configContent + DBManager:_DBManager + settings:settings + analytics:nil + experiment:nil + queue:queue + namespace:fullyQualifiedNamespace + options:currentOptions]); + + OCMStub([_configFetch[i] fetchAllConfigsWithExpirationDuration:43200 + completionHandler:OCMOCK_ANY]) + .andDo(^(NSInvocation *invocation) { + void (^handler)(FIRRemoteConfigFetchStatus status, NSError *_Nullable error) = nil; + // void (^handler)(FIRRemoteConfigFetchCompletion); + [invocation getArgument:&handler atIndex:3]; + [_configFetch[i] fetchWithUserProperties:[[NSDictionary alloc] init] + completionHandler:handler]; + }); + + _response[i] = @{}; + + _responseData[i] = [NSJSONSerialization dataWithJSONObject:_response[i] options:0 error:nil]; + + _URLResponse[i] = + [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"https://firebase.com"] + statusCode:500 + HTTPVersion:nil + headerFields:@{@"etag" : @"etag1"}]; + + id completionBlock = + [OCMArg invokeBlockWithArgs:_responseData[i], _URLResponse[i], [NSNull null], nil]; + + OCMExpect([_configFetch[i] URLSessionDataTaskWithContent:[OCMArg any] + completionHandler:completionBlock]) + .andReturn(nil); + [_configInstances[i] updateWithNewInstancesForConfigFetch:_configFetch[i] + configContent:configContent + configSettings:settings + configExperiment:nil]; + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // Make the fetch calls for all instances. + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = [self + expectationWithDescription: + [NSString stringWithFormat:@"Test enumerating configs successfully - instance %d", i]]; + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet); + FIRRemoteConfigFetchCompletion fetchCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusFailure); + XCTAssertFalse([_configInstances[i] activateFetched]); + XCTAssertNotNil(error); + // No such key, still return a static value. + FIRRemoteConfigValue *value = _configInstances[RCNTestRCInstanceDefault][@"key1"]; + XCTAssertEqual((int)value.source, (int)FIRRemoteConfigSourceStatic); + XCTAssertEqualObjects(value.stringValue, @""); + XCTAssertEqual(value.boolValue, NO); + [expectations[i] fulfill]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testConfigValueForKey { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = + [self expectationWithDescription: + [NSString stringWithFormat:@"Test configValueForKey: method - instance %d", i]]; + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusNoFetchYet); + FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status, + NSError *error) { + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); + XCTAssertTrue([_configInstances[i] activateFetched]); + + NSString *key1 = [NSString stringWithFormat:@"key1-%d", i]; + NSString *key2 = [NSString stringWithFormat:@"key2-%d", i]; + NSString *key3 = [NSString stringWithFormat:@"key3-%d", i]; + NSString *key7 = [NSString stringWithFormat:@"key7-%d", i]; + NSString *value1 = [NSString stringWithFormat:@"value1-%d", i]; + NSString *value2 = [NSString stringWithFormat:@"value2-%d", i]; + NSString *value3 = [NSString stringWithFormat:@"value3-%d", i]; + NSString *value7 = [NSString stringWithFormat:@"value7-%d", i]; + XCTAssertEqualObjects(_configInstances[i][key1].stringValue, value1); + XCTAssertEqualObjects(_configInstances[i][key2].stringValue, value2); + OCMVerify([_configInstances[i] objectForKeyedSubscript:key1]); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:key3].stringValue, value3); + if (i == RCNTestRCInstanceDefault) { + XCTAssertEqualObjects( + [_configInstances[i] configValueForKey:key7 namespace:FIRNamespaceGoogleMobilePlatform] + .stringValue, + value7); + } + + XCTAssertEqualObjects([_configInstances[i] configValueForKey:key7].stringValue, value7); + XCTAssertNotNil([_configInstances[i] configValueForKey:nil]); + XCTAssertEqual([_configInstances[i] configValueForKey:nil].source, + FIRRemoteConfigSourceStatic); + XCTAssertEqual([_configInstances[i] configValueForKey:nil namespace:nil].source, + FIRRemoteConfigSourceStatic); + XCTAssertEqual([_configInstances[i] configValueForKey:nil namespace:nil source:-1].source, + FIRRemoteConfigSourceStatic); + + [expectations[i] fulfill]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testFetchConfigWithDefaultSets { + NSMutableArray *fetchConfigsExpectation = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + fetchConfigsExpectation[i] = [self + expectationWithDescription: + [NSString stringWithFormat:@"Test fetch configs with defaults set - instance %d", i]]; + NSString *key1 = [NSString stringWithFormat:@"key1-%d", i]; + NSString *key2 = [NSString stringWithFormat:@"key2-%d", i]; + NSString *key0 = [NSString stringWithFormat:@"key0-%d", i]; + NSString *value1 = [NSString stringWithFormat:@"value1-%d", i]; + NSString *value2 = [NSString stringWithFormat:@"value2-%d", i]; + + NSDictionary *defaults = @{key1 : @"default key1", key0 : @"value0-0"}; + [_configInstances[i] setDefaults:defaults]; + + FIRRemoteConfigFetchCompletion fetchCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); + XCTAssertEqualObjects(_configInstances[i][key1].stringValue, @"default key1"); + XCTAssertEqual(_configInstances[i][key1].source, FIRRemoteConfigSourceDefault); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + XCTAssertTrue([_configInstances[i] activateFetched]); +#pragma clang diagnostic pop + XCTAssertEqualObjects(_configInstances[i][key1].stringValue, value1); + XCTAssertEqual(_configInstances[i][key1].source, FIRRemoteConfigSourceRemote); + XCTAssertEqualObjects([_configInstances[i] defaultValueForKey:key1].stringValue, + @"default key1"); + XCTAssertEqualObjects(_configInstances[i][key2].stringValue, value2); + XCTAssertEqualObjects(_configInstances[i][key0].stringValue, @"value0-0"); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + XCTAssertNil([_configInstances[i] defaultValueForKey:nil namespace:nil]); +#pragma clang diagnostic pop + OCMVerify([_configInstances[i] objectForKeyedSubscript:key1]); + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess, + @"Callback of first successful config " + @"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccess."); + [fetchConfigsExpectation[i] fulfill]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testDefaultsSetsOnly { + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + NSString *key1 = [NSString stringWithFormat:@"key1-%d", i]; + NSString *key2 = [NSString stringWithFormat:@"key2-%d", i]; + NSString *key3 = [NSString stringWithFormat:@"key3-%d", i]; + NSString *key4 = [NSString stringWithFormat:@"key4-%d", i]; + NSString *key5 = [NSString stringWithFormat:@"key5-%d", i]; + + NSString *defaultValue1 = @"default value1"; + NSData *defaultValue2 = [defaultValue1 dataUsingEncoding:NSUTF8StringEncoding]; + NSNumber *defaultValue3 = [NSNumber numberWithFloat:3.1415926]; + NSDate *defaultValue4 = [NSDate date]; + BOOL defaultValue5 = NO; + + NSMutableDictionary *defaults = [NSMutableDictionary dictionaryWithDictionary:@{ + key1 : defaultValue1, + key2 : defaultValue2, + key3 : defaultValue3, + key4 : defaultValue4, + key5 : @(defaultValue5), + }]; + [_configInstances[i] setDefaults:defaults]; + if (i == RCNTestRCInstanceSecondNamespace) { + [defaults setObject:@"2860" forKey:@"experience"]; + [_configInstances[i] setDefaults:defaults namespace:RCNTestsPerfNamespace]; + } + // Remove objects right away to make sure dispatch_async gets the copy. + [defaults removeAllObjects]; + + FIRRemoteConfig *config = _configInstances[i]; + XCTAssertEqualObjects(config[key1].stringValue, defaultValue1, @"Should support string format"); + XCTAssertEqualObjects(config[key2].dataValue, defaultValue2, @"Should support data format"); + XCTAssertEqual(config[key3].numberValue.longValue, defaultValue3.longValue, + @"Should support number format"); + + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + NSString *strValueOfDate = [dateFormatter stringFromDate:(NSDate *)defaultValue4]; + XCTAssertEqualObjects( + config[key4].stringValue, strValueOfDate, + @"Date format can be set as an input from plist, but output coming out of " + @"defaultConfig as string."); + + XCTAssertEqual(config[key5].boolValue, defaultValue5, @"Should support bool format"); + + if (i == RCNTestRCInstanceSecondNamespace) { + XCTAssertEqualObjects( + [_configInstances[i] configValueForKey:@"experience" namespace:RCNTestsPerfNamespace] + .stringValue, + @"2860", @"Only default config has the key, must equal to default config value."); + } + + // Reset default sets + [_configInstances[i] setDefaults:nil]; + XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceDefault].count, 0); + } +} + +- (void)testSetDefaultsWithNilParams { + NSMutableArray *expectations = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + expectations[i] = [self + expectationWithDescription: + [NSString stringWithFormat:@"Set defaults no callback expectation - instance %d", i]]; + // Should work when passing nil. + [_configInstances[i] setDefaults:nil]; + [_configInstances[i] setDefaults:nil namespace:nil]; + + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_checkCompletionTimeout * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceDefault + namespace:FIRNamespaceGoogleMobilePlatform] + .count, + 0); + [expectations[i] fulfill]; + }); + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + +- (void)testFetchConfigOverwriteDefaultSet { + NSMutableArray *fetchConfigsExpectation = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + fetchConfigsExpectation[i] = [self + expectationWithDescription: + [NSString stringWithFormat:@"Test fetch configs with defaults set - instance %d", i]]; + NSString *key1 = [NSString stringWithFormat:@"key1-%d", i]; + NSString *value1 = [NSString stringWithFormat:@"value1-%d", i]; + + [_configInstances[i] setDefaults:@{key1 : @"default key1"}]; + + FIRRemoteConfigValue *value = _configInstances[i][key1]; + XCTAssertEqualObjects(value.stringValue, @"default key1"); + XCTAssertEqual(value.source, FIRRemoteConfigSourceDefault); + + value = _configInstances[i][@"A key doesn't exist"]; + XCTAssertEqual(value.source, FIRRemoteConfigSourceStatic); + + FIRRemoteConfigFetchCompletion fetchCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); + XCTAssertTrue([_configInstances[i] activateFetched]); + XCTAssertEqualObjects(_configInstances[i][key1].stringValue, value1); + XCTAssertEqual(_configInstances[i][key1].source, FIRRemoteConfigSourceRemote); + XCTAssertEqualObjects([_configInstances[i] defaultValueForKey:key1].stringValue, + @"default key1"); + OCMVerify([_configInstances[i] objectForKeyedSubscript:key1]); + + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess, + @"Callback of first successful config " + @"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccess."); + [fetchConfigsExpectation[i] fulfill]; + }; + + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testGetConfigValueBySource { + NSMutableArray *fetchConfigsExpectation = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + fetchConfigsExpectation[i] = + [self expectationWithDescription: + [NSString stringWithFormat:@"Test get config value by source - instance %d", i]]; + NSString *key1 = [NSString stringWithFormat:@"key1-%d", i]; + NSString *value1 = [NSString stringWithFormat:@"value1-%d", i]; + + NSDictionary *defaults = @{key1 : @"default value1"}; + [_configInstances[i] setDefaults:defaults]; + + FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status, + NSError *error) { + XCTAssertEqual(_configInstances[i].lastFetchStatus, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); + XCTAssertEqualObjects(_configInstances[i][key1].stringValue, @"default value1"); + XCTAssertEqual(_configInstances[i][key1].source, FIRRemoteConfigSourceDefault); + XCTAssertTrue([_configInstances[i] activateFetched]); + XCTAssertEqualObjects(_configInstances[i][key1].stringValue, value1); + XCTAssertEqual(_configInstances[i][key1].source, FIRRemoteConfigSourceRemote); + FIRRemoteConfigValue *value; + if (i == RCNTestRCInstanceDefault) { + value = [_configInstances[i] configValueForKey:key1 + namespace:FIRNamespaceGoogleMobilePlatform + source:FIRRemoteConfigSourceRemote]; + XCTAssertEqualObjects(value.stringValue, value1); + value = [_configInstances[i] configValueForKey:key1 + namespace:FIRNamespaceGoogleMobilePlatform + source:FIRRemoteConfigSourceDefault]; + XCTAssertEqualObjects(value.stringValue, @"default value1"); + value = [_configInstances[i] configValueForKey:key1 + namespace:FIRNamespaceGoogleMobilePlatform + source:FIRRemoteConfigSourceStatic]; + } else { + value = [_configInstances[i] configValueForKey:key1 source:FIRRemoteConfigSourceRemote]; + XCTAssertEqualObjects(value.stringValue, value1); + value = [_configInstances[i] configValueForKey:key1 source:FIRRemoteConfigSourceDefault]; + XCTAssertEqualObjects(value.stringValue, @"default value1"); + value = [_configInstances[i] configValueForKey:key1 source:FIRRemoteConfigSourceStatic]; + } + XCTAssertEqualObjects(value.stringValue, @""); + XCTAssertEqualObjects(value.numberValue, @(0)); + XCTAssertEqual(value.boolValue, NO); + + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess, + @"Callback of first successful config " + @"fetch. Status must equal to FIRRemoteConfigFetchStatusSuccess."); + [fetchConfigsExpectation[i] fulfill]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout handler:nil]; +} + +- (void)testInvalidKeyOrNamespace { + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + FIRRemoteConfigValue *value = [_configInstances[i] configValueForKey:nil]; + XCTAssertNotNil(value); + XCTAssertEqual(value.source, FIRRemoteConfigSourceStatic); + + value = [_configInstances[i] configValueForKey:nil namespace:nil]; + XCTAssertNotNil(value); + XCTAssertEqual(value.source, FIRRemoteConfigSourceStatic); + + value = [_configInstances[i] configValueForKey:nil namespace:nil source:5]; + XCTAssertNotNil(value); + XCTAssertEqual(value.source, FIRRemoteConfigSourceStatic); + } +} + +// Remote Config converts UTC times in the plists to local times. This utility function makes it +// possible to check the times when running the tests in any timezone. +static NSString *UTCToLocal(NSString *utcTime) { + // Create a UTC dateFormatter. + NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"]; + [dateFormatter setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"UTC"]]; + NSDate *date = [dateFormatter dateFromString:utcTime]; + [dateFormatter setTimeZone:[NSTimeZone localTimeZone]]; + return [dateFormatter stringFromDate:date]; +} + +- (void)testSetDefaultsFromPlist { + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + FIRRemoteConfig *config = _configInstances[i]; + [config setDefaultsFromPlistFileName:@"Defaults-testInfo"]; + XCTAssertEqualObjects(_configInstances[i][@"lastCheckTime"].stringValue, + UTCToLocal(@"2016-02-28 18:33:31")); + XCTAssertEqual(_configInstances[i][@"isPaidUser"].boolValue, YES); + XCTAssertEqualObjects(_configInstances[i][@"dataValue"].stringValue, @"2.4"); + XCTAssertEqualObjects(_configInstances[i][@"New item"].numberValue, @(2.4)); + XCTAssertEqualObjects(_configInstances[i][@"Languages"].stringValue, @"English"); + XCTAssertEqualObjects(_configInstances[i][@"FileInfo"].stringValue, + @"To setup default config."); + XCTAssertEqualObjects(_configInstances[i][@"format"].stringValue, @"key to value."); + + // If given a wrong file name, the default will not be set and kept as previous results. + [_configInstances[i] setDefaultsFromPlistFileName:@""]; + XCTAssertEqualObjects(_configInstances[i][@"lastCheckTime"].stringValue, + UTCToLocal(@"2016-02-28 18:33:31")); + [_configInstances[i] setDefaultsFromPlistFileName:@"non-existed_file"]; + XCTAssertEqualObjects(_configInstances[i][@"dataValue"].stringValue, @"2.4"); + [_configInstances[i] setDefaultsFromPlistFileName:nil namespace:nil]; + XCTAssertEqualObjects(_configInstances[i][@"New item"].numberValue, @(2.4)); + [_configInstances[i] setDefaultsFromPlistFileName:@"" namespace:@""]; + XCTAssertEqualObjects(_configInstances[i][@"Languages"].stringValue, @"English"); + } +} + +- (void)testSetDefaultsAndNamespaceFromPlist { + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + if (i == RCNTestRCInstanceDefault) { + [_configInstances[i] setDefaultsFromPlistFileName:@"Defaults-testInfo" + namespace:RCNTestsPerfNamespace]; + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"lastCheckTime" + namespace:RCNTestsPerfNamespace] + .stringValue, + UTCToLocal(@"2016-02-28 18:33:31")); + XCTAssertEqual([_configInstances[i] configValueForKey:@"isPaidUser" + namespace:RCNTestsPerfNamespace] + .boolValue, + YES); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"dataValue" + namespace:RCNTestsPerfNamespace] + .stringValue, + @"2.4"); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"New item" + namespace:RCNTestsPerfNamespace] + .numberValue, + @(2.4)); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"Languages" + namespace:RCNTestsPerfNamespace] + .stringValue, + @"English"); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"FileInfo" + namespace:RCNTestsPerfNamespace] + .stringValue, + @"To setup default config."); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"format" + namespace:RCNTestsPerfNamespace] + .stringValue, + @"key to value."); + } else { + [_configInstances[i] setDefaultsFromPlistFileName:@"Defaults-testInfo"]; + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"lastCheckTime"].stringValue, + UTCToLocal(@"2016-02-28 18:33:31")); + XCTAssertEqual([_configInstances[i] configValueForKey:@"isPaidUser"].boolValue, YES); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"dataValue"].stringValue, + @"2.4"); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"New item"].numberValue, + @(2.4)); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"Languages"].stringValue, + @"English"); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"FileInfo"].stringValue, + @"To setup default config."); + XCTAssertEqualObjects([_configInstances[i] configValueForKey:@"format"].stringValue, + @"key to value."); + } + } +} + +- (void)testSetDeveloperMode { + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + XCTAssertFalse(_configInstances[i].configSettings.isDeveloperModeEnabled); + FIRRemoteConfigSettings *settings = + [[FIRRemoteConfigSettings alloc] initWithDeveloperModeEnabled:YES]; + + _configInstances[i].configSettings = settings; + XCTAssertTrue(_configInstances[i].configSettings.isDeveloperModeEnabled); + } +} + +- (void)testAllKeysFromSource { + NSMutableArray *fetchConfigsExpectation = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + fetchConfigsExpectation[i] = [self + expectationWithDescription:[NSString + stringWithFormat:@"Test allKeys methods - instance %d", i]]; + NSString *key1 = [NSString stringWithFormat:@"key1-%d", i]; + NSString *key0 = [NSString stringWithFormat:@"key0-%d", i]; + NSDictionary *defaults = @{key1 : @"default key1", key0 : @"value0-0"}; + [_configInstances[i] setDefaults:defaults]; + + FIRRemoteConfigFetchCompletion fetchCompletion = ^void(FIRRemoteConfigFetchStatus status, + NSError *error) { + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); + XCTAssertTrue([_configInstances[i] activateFetched]); + + if (i == RCNTestRCInstanceDefault) { + XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceRemote + namespace:FIRNamespaceGoogleMobilePlatform] + .count, + 100); + XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceDefault + namespace:FIRNamespaceGoogleMobilePlatform] + .count, + 2); + XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceStatic + namespace:FIRNamespaceGoogleMobilePlatform] + .count, + 0); + } else { + XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceRemote].count, + 100); + XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceDefault].count, + 2); + XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceStatic].count, + 0); + } + + XCTAssertNotNil([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceRemote + namespace:@"invalid namespace"]); + XCTAssertEqual([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceRemote + namespace:@"invalid namespace"] + .count, + 0); + XCTAssertNotNil([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceRemote + namespace:nil]); + XCTAssertEqual( + [_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceRemote namespace:nil].count, + 0); + XCTAssertNotNil([_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceDefault + namespace:nil]); + XCTAssertEqual( + [_configInstances[i] allKeysFromSource:FIRRemoteConfigSourceDefault namespace:nil].count, + 0); + + [fetchConfigsExpectation[i] fulfill]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testAllKeysWithPrefix { + NSMutableArray *fetchConfigsExpectation = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + fetchConfigsExpectation[i] = [self + expectationWithDescription:[NSString + stringWithFormat:@"Test allKeys methods - instance %d", i]]; + FIRRemoteConfigFetchCompletion fetchCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertEqual(status, FIRRemoteConfigFetchStatusSuccess); + XCTAssertNil(error); + NSLog(@"Testing _configInstances %d", i); +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + XCTAssertTrue([_configInstances[i] activateFetched]); + + // Test keysWithPrefix:namespace: method. + if (i == RCNTestRCInstanceDefault) { + XCTAssertEqual([_configInstances[i] keysWithPrefix:@"key" + namespace:FIRNamespaceGoogleMobilePlatform] + .count, + 100); + } else { + XCTAssertEqual([_configInstances[i] keysWithPrefix:@"key"].count, 100); + } + + XCTAssertEqual( + [_configInstances[i] keysWithPrefix:@"pl" namespace:@"invalid namespace"].count, 0); + XCTAssertEqual([_configInstances[i] keysWithPrefix:@"pl" namespace:nil].count, 0); + XCTAssertEqual([_configInstances[i] keysWithPrefix:@"pl" namespace:@""].count, 0); + + XCTAssertNotNil([_configInstances[i] keysWithPrefix:nil namespace:nil]); + XCTAssertEqual([_configInstances[i] keysWithPrefix:nil namespace:nil].count, 0); +#pragma clang diagnostic pop + + // Test keysWithPrefix: method. + XCTAssertEqual([_configInstances[i] keysWithPrefix:@"key1"].count, 12); + XCTAssertEqual([_configInstances[i] keysWithPrefix:@"key"].count, 100); + + XCTAssertEqual([_configInstances[i] keysWithPrefix:@"invalid key"].count, 0); + XCTAssertEqual([_configInstances[i] keysWithPrefix:nil].count, 100); + XCTAssertEqual([_configInstances[i] keysWithPrefix:@""].count, 100); + + [fetchConfigsExpectation[i] fulfill]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +- (void)testSetDeveloperModeConfigSetting { + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + FIRRemoteConfigSettings *settings = + [[FIRRemoteConfigSettings alloc] initWithDeveloperModeEnabled:YES]; + [_configInstances[i] setConfigSettings:settings]; + XCTAssertTrue([_configInstances[i] configSettings].isDeveloperModeEnabled); + + settings = [[FIRRemoteConfigSettings alloc] initWithDeveloperModeEnabled:NO]; + [_configInstances[i] setConfigSettings:settings]; + XCTAssertFalse([_configInstances[i] configSettings].isDeveloperModeEnabled); +#pragma clang diagnostic pop + } +} + +/// Test the minimum fetch interval is applied and read back correctly. +- (void)testSetMinimumFetchIntervalConfigSetting { + NSMutableArray *fetchConfigsExpectation = + [[NSMutableArray alloc] initWithCapacity:RCNTestRCNumTotalInstances]; + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + fetchConfigsExpectation[i] = [self + expectationWithDescription: + [NSString stringWithFormat:@"Test minimumFetchInterval expectation - instance %d", i]]; + FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init]; + settings.minimumFetchInterval = 123; + [_configInstances[i] setConfigSettings:settings]; + XCTAssertEqual([_configInstances[i] configSettings].minimumFetchInterval, 123); + + FIRRemoteConfigFetchCompletion fetchCompletion = + ^void(FIRRemoteConfigFetchStatus status, NSError *error) { + XCTAssertFalse([_configInstances[i].settings hasMinimumFetchIntervalElapsed:43200]); + + // Update minimum fetch interval. + FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init]; + settings.minimumFetchInterval = 0; + [_configInstances[i] setConfigSettings:settings]; + XCTAssertEqual([_configInstances[i] configSettings].minimumFetchInterval, 0); + XCTAssertTrue([_configInstances[i].settings hasMinimumFetchIntervalElapsed:0]); + [fetchConfigsExpectation[i] fulfill]; + }; + [_configInstances[i] fetchWithExpirationDuration:43200 completionHandler:fetchCompletion]; + } + [self waitForExpectationsWithTimeout:_expectationTimeout + handler:^(NSError *error) { + XCTAssertNil(error); + }]; +} + +/// Test the fetch timeout is properly set and read back. +- (void)testSetFetchTimeoutConfigSetting { + for (int i = 0; i < RCNTestRCNumTotalInstances; i++) { + FIRRemoteConfigSettings *settings = [[FIRRemoteConfigSettings alloc] init]; + settings.fetchTimeout = 1; + [_configInstances[i] setConfigSettings:settings]; + XCTAssertEqual([_configInstances[i] configSettings].fetchTimeout, 1); + NSURLSession *networkSession = [_configFetch[i] currentNetworkSession]; + XCTAssertNotNil(networkSession); + XCTAssertEqual(networkSession.configuration.timeoutIntervalForResource, 1); + XCTAssertEqual(networkSession.configuration.timeoutIntervalForRequest, 1); + } +} + +#pragma mark - Public Factory Methods + +- (void)testConfigureConfigWithValidInput { + // Configure the default app with our options and ensure the Remote Config instance is set up + // properly. + XCTAssertNoThrow([FIRApp configureWithOptions:[self firstAppOptions]]); + XCTAssertNoThrow([FIRRemoteConfig remoteConfig]); + FIRRemoteConfig *config = [FIRRemoteConfig remoteConfig]; + XCTAssertNotNil(config); + + // Ensure the same instance is returned from the singleton. + FIRRemoteConfig *sameConfig = [FIRRemoteConfig remoteConfig]; + XCTAssertNotNil(sameConfig); + XCTAssertEqual(config, sameConfig); + + // Ensure the app name is stored properly. + XCTAssertEqual([config valueForKey:@"_appName"], kFIRDefaultAppName); +} + +#pragma mark - Test Helpers + +- (FIROptions *)firstAppOptions { + // TODO: Evaluate if we want to hardcode things here instead. + FIROptions *options = [[FIROptions alloc] initWithGoogleAppID:@"1:123:ios:123abc" + GCMSenderID:@"correct_gcm_sender_id"]; + options.APIKey = @"correct_api_key"; + options.projectID = @"abc-xyz-123"; + return options; +} + +- (FIROptions *)secondAppOptions { + FIROptions *options = + [[FIROptions alloc] initWithContentsOfFile:[[NSBundle bundleForClass:[self class]] + pathForResource:@"SecondApp-GoogleService-Info" + ofType:@"plist"]]; + XCTAssertNotNil(options); + return options; +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h b/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h new file mode 100644 index 00000000000..e1024182970 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const FIRNamespaceGooglePlayPlatform; +extern NSString *const RCNTestsFIRNamespace; +extern NSString *const RCNTestsPerfNamespace; +extern NSString *const RCNTestsDefaultFIRAppName; +extern NSString *const RCNTestsSecondFIRAppName; + +@interface RCNTestUtilities : NSObject + +/// Returns the name of the test that's compatible with configuring a FIRApp name to ensure +/// uniqueness. `testName` is the `name` property of the `XCTest` running. ++ (NSString *)generatedTestAppNameForTest:(NSString *)testName; + +/// Creates a new path for a test database. ++ (NSString *)remoteConfigPathForTestDatabase; + +/// Creates a new suite name for a test userdefaults suite. ++ (NSString *)userDefaultsSuiteNameForTestSuite; + +/// Deletes the database at a given path. ++ (void)removeDatabaseAtPath:(NSString *)DBPath; + +@end + +NS_ASSUME_NONNULL_END diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.m b/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.m new file mode 100644 index 00000000000..f82bb0945fa --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.m @@ -0,0 +1,72 @@ +/* + * Copyright 2019 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 "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" + +NSString *const RCNTestsPerfNamespace = @"fireperf"; +NSString *const RCNTestsFIRNamespace = @"firebase"; +NSString *const RCNTestsDefaultFIRAppName = @"__FIRAPP_DEFAULT"; +NSString *const RCNTestsSecondFIRAppName = @"secondFIRApp"; + +/// The application support sub-directory that the Remote Config database resides in. +static NSString *const RCNRemoteConfigApplicationSupportSubDirectory = @"Google/RemoteConfig"; + +@implementation RCNTestUtilities + ++ (NSString *)generatedTestAppNameForTest:(NSString *)testName { + // Filter out any characters not valid for FIRApp's naming scheme. + NSCharacterSet *invalidCharacters = [[NSCharacterSet alphanumericCharacterSet] invertedSet]; + + // This will result in a string with the class name, a space, and the test name. We only care + // about the test name so split it into components and return the last item. + NSString *friendlyTestName = [testName stringByTrimmingCharactersInSet:invalidCharacters]; + NSArray *components = [friendlyTestName componentsSeparatedByString:@" "]; + return [components lastObject]; +} + +/// Remote Config database path for test version ++ (NSString *)remoteConfigPathForTestDatabase { + NSArray *dirPaths = + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString *appSupportPath = dirPaths.firstObject; + NSArray *components = @[ + appSupportPath, RCNRemoteConfigApplicationSupportSubDirectory, + [NSString stringWithFormat:@"test-%f.sqlite3", [[NSDate date] timeIntervalSince1970] * 1000] + ]; + NSString *dbPath = [NSString pathWithComponents:components]; + [RCNTestUtilities removeDatabaseAtPath:dbPath]; + return dbPath; +} + ++ (void)removeDatabaseAtPath:(NSString *)DBPath { + // Remove existing database if exists. + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:DBPath]) { + NSError *error; + [fileManager removeItemAtPath:DBPath error:&error]; + } +} + +#pragma mark UserDefaults +/// Remote Config database path for test version ++ (NSString *)userDefaultsSuiteNameForTestSuite { + NSString *suiteName = + [NSString stringWithFormat:@"group.%@.test-%f", [NSBundle mainBundle].bundleIdentifier, + [[NSDate date] timeIntervalSince1970] * 1000]; + return suiteName; +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m new file mode 100644 index 00000000000..35953249511 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNThrottlingTests.m @@ -0,0 +1,198 @@ +/* + * Copyright 2019 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 + +#import "FirebaseRemoteConfig/Sources/Private/RCNConfigSettings.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigContent.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigDBManager.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigExperiment.h" +#import "FirebaseRemoteConfig/Sources/RCNConfigFetch.h" +#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h" + +#import +#import +#import + +@interface RCNThrottlingTests : XCTestCase { + RCNConfigContent *_configContentMock; + RCNConfigSettings *_settings; + RCNConfigExperiment *_experimentMock; + RCNConfigFetch *_configFetch; + NSString *_DBPath; +} + +@end + +@implementation RCNThrottlingTests + +- (void)setUp { + [super setUp]; + // Put setup code here. This method is called before the invocation of each test method in the + // class. + if (![FIRApp defaultApp]) { + [FIRApp configure]; + } + [[FIRConfiguration sharedInstance] setLoggerLevel:FIRLoggerLevelMax]; + // Get a test database. + _DBPath = [RCNTestUtilities remoteConfigPathForTestDatabase]; + id classMock = OCMClassMock([RCNConfigDBManager class]); + OCMStub([classMock remoteConfigPathForDatabase]).andReturn(_DBPath); + RCNConfigDBManager *DBManager = [[RCNConfigDBManager alloc] init]; + + _configContentMock = OCMClassMock([RCNConfigContent class]); + _settings = [[RCNConfigSettings alloc] initWithDatabaseManager:DBManager + namespace:FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; + _experimentMock = OCMClassMock([RCNConfigExperiment class]); + dispatch_queue_t _queue = dispatch_queue_create( + "com.google.GoogleConfigService.FIRRemoteConfigTest", DISPATCH_QUEUE_SERIAL); + + _configFetch = [[RCNConfigFetch alloc] initWithContent:_configContentMock + DBManager:DBManager + settings:_settings + experiment:_experimentMock + queue:_queue + namespace:FIRNamespaceGoogleMobilePlatform + app:[FIRApp defaultApp]]; +} + +- (void)mockFetchResponseWithStatusCode:(NSInteger)statusCode { + // Mock successful network fetches with an empty config response. + RCNConfigFetcherTestBlock testBlock = ^(RCNConfigFetcherCompletion completion) { + NSURL *url = [[NSURL alloc] initWithString:@"https://google.com"]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:url + statusCode:statusCode + HTTPVersion:nil + headerFields:@{@"etag" : @"etag1"}]; + NSData *data = + [NSJSONSerialization dataWithJSONObject:@{@"key1" : @"val1", @"state" : @"UPDATE"} + options:0 + error:nil]; + completion(data, response, nil); + }; + [RCNConfigFetch setGlobalTestBlock:testBlock]; +} + +/// Regular case of calling fetch should succeed. +- (void)testRegularFetchDoesNotGetThrottled { + [self mockFetchResponseWithStatusCode:200]; + XCTestExpectation *expectation = [self expectationWithDescription:@"throttlingExpectation"]; + [_configFetch fetchAllConfigsWithExpirationDuration:0 + completionHandler:^(FIRRemoteConfigFetchStatus status, + NSError *_Nullable error) { + XCTAssertNil(error); + XCTAssertEqual(FIRRemoteConfigFetchStatusSuccess, status); + [expectation fulfill]; + }]; + // TODO(dmandar): Investigate using a smaller timeout. b/122674668 + [self waitForExpectationsWithTimeout:4.0 handler:nil]; +} + +- (void)testServerThrottleHTTP429ReturnsAThrottledError { + [self mockFetchResponseWithStatusCode:429]; + XCTestExpectation *expectation = [self expectationWithDescription:@"throttlingExpectation"]; + + [_configFetch + fetchAllConfigsWithExpirationDuration:0 + completionHandler:^(FIRRemoteConfigFetchStatus status, + NSError *_Nullable error) { + XCTAssertNotNil(error); + NSNumber *endTime = error.userInfo[@"error_throttled_end_time_seconds"]; + XCTAssertGreaterThanOrEqual([endTime doubleValue], + [[NSDate date] timeIntervalSinceNow]); + XCTAssertEqual(FIRRemoteConfigFetchStatusThrottled, status); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:4.0 handler:nil]; +} + +- (void)testServerInternalError500ReturnsAThrottledError { + [self mockFetchResponseWithStatusCode:500]; + XCTestExpectation *expectation = [self expectationWithDescription:@"throttlingExpectation"]; + + [_configFetch + fetchAllConfigsWithExpirationDuration:0 + completionHandler:^(FIRRemoteConfigFetchStatus status, + NSError *_Nullable error) { + XCTAssertNotNil(error); + NSNumber *endTime = error.userInfo[@"error_throttled_end_time_seconds"]; + XCTAssertGreaterThanOrEqual([endTime doubleValue], + [[NSDate date] timeIntervalSinceNow]); + XCTAssertEqual(FIRRemoteConfigFetchStatusThrottled, status); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:4.0 handler:nil]; +} + +- (void)testServerUnavailableError503ReturnsAThrottledError { + [self mockFetchResponseWithStatusCode:503]; + XCTestExpectation *expectation = [self expectationWithDescription:@"throttlingExpectation"]; + + [_configFetch + fetchAllConfigsWithExpirationDuration:0 + completionHandler:^(FIRRemoteConfigFetchStatus status, + NSError *_Nullable error) { + XCTAssertNotNil(error); + NSNumber *endTime = error.userInfo[@"error_throttled_end_time_seconds"]; + XCTAssertGreaterThanOrEqual([endTime doubleValue], + [[NSDate date] timeIntervalSinceNow]); + XCTAssertEqual(FIRRemoteConfigFetchStatusThrottled, status); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:4.0 handler:nil]; +} + +- (void)testThrottleReturnsAThrottledErrorAndThrottlesSubsequentRequests { + [self mockFetchResponseWithStatusCode:429]; + XCTestExpectation *expectation = [self expectationWithDescription:@"throttlingExpectation"]; + XCTestExpectation *expectation2 = [self expectationWithDescription:@"throttlingExpectation2"]; + + [_configFetch + fetchAllConfigsWithExpirationDuration:0 + completionHandler:^(FIRRemoteConfigFetchStatus status, + NSError *_Nullable error) { + XCTAssertNotNil(error); + NSNumber *endTime = error.userInfo[@"error_throttled_end_time_seconds"]; + XCTAssertGreaterThanOrEqual([endTime doubleValue], + [[NSDate date] timeIntervalSinceNow]); + XCTAssertEqual(FIRRemoteConfigFetchStatusThrottled, status); + [expectation fulfill]; + + // follow-up request. + [_configFetch + fetchAllConfigsWithExpirationDuration:0 + completionHandler:^( + FIRRemoteConfigFetchStatus status, + NSError *_Nullable error) { + XCTAssertNotNil(error); + NSNumber *endTime = + error.userInfo + [@"error_throttled_end_time_seconds"]; + XCTAssertGreaterThanOrEqual( + [endTime doubleValue], + [[NSDate date] timeIntervalSinceNow]); + XCTAssertEqual( + FIRRemoteConfigFetchStatusThrottled, + status); + [expectation2 fulfill]; + }]; + }]; + + [self waitForExpectationsWithTimeout:4.0 handler:nil]; +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m new file mode 100644 index 00000000000..a3096860fdd --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/RCNUserDefaultsManagerTests.m @@ -0,0 +1,161 @@ +/* + * Copyright 2019 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 + +#import "FirebaseRemoteConfig/Sources/RCNUserDefaultsManager.h" + +static NSTimeInterval RCNUserDefaultsSampleTimeStamp = 0; + +@interface RCNUserDefaultsManagerTests : XCTestCase + +@end + +@implementation RCNUserDefaultsManagerTests + +- (void)setUp { + [super setUp]; + + [[NSUserDefaults standardUserDefaults] + removePersistentDomainForName:[NSBundle mainBundle].bundleIdentifier]; + RCNUserDefaultsSampleTimeStamp = [[NSDate date] timeIntervalSince1970]; +} + +- (void)testUserDefaultsEtagWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:@"TESTING" + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:@"testNamespace1"]; + [manager setLastETag:@"eTag1"]; + XCTAssertEqualObjects([manager lastETag], @"eTag1"); + + [manager setLastETag:@"eTag2"]; + XCTAssertEqualObjects([manager lastETag], @"eTag2"); +} + +- (void)testUserDefaultsLastFetchTimeWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:@"TESTING" + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:@"testNamespace1"]; + [manager setLastFetchTime:RCNUserDefaultsSampleTimeStamp]; + XCTAssertEqual([manager lastFetchTime], RCNUserDefaultsSampleTimeStamp); + + [manager setLastFetchTime:RCNUserDefaultsSampleTimeStamp - 1000]; + XCTAssertEqual([manager lastFetchTime], RCNUserDefaultsSampleTimeStamp - 1000); +} + +- (void)testUserDefaultsLastFetchStatusWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:@"TESTING" + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:@"testNamespace1"]; + [manager setLastFetchStatus:@"Success"]; + XCTAssertEqualObjects([manager lastFetchStatus], @"Success"); + + [manager setLastFetchStatus:@"Error"]; + XCTAssertEqualObjects([manager lastFetchStatus], @"Error"); +} + +- (void)testUserDefaultsisClientThrottledWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:@"TESTING" + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:@"testNamespace1"]; + [manager setIsClientThrottledWithExponentialBackoff:YES]; + XCTAssertEqual([manager isClientThrottledWithExponentialBackoff], YES); + + [manager setIsClientThrottledWithExponentialBackoff:NO]; + XCTAssertEqual([manager isClientThrottledWithExponentialBackoff], NO); +} + +- (void)testUserDefaultsThrottleEndTimeWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:@"TESTING" + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:@"testNamespace1"]; + [manager setThrottleEndTime:RCNUserDefaultsSampleTimeStamp - 7.0]; + XCTAssertEqual([manager throttleEndTime], RCNUserDefaultsSampleTimeStamp - 7.0); + + [manager setThrottleEndTime:RCNUserDefaultsSampleTimeStamp - 8.0]; + XCTAssertEqual([manager throttleEndTime], RCNUserDefaultsSampleTimeStamp - 8.0); +} + +- (void)testUserDefaultsCurrentThrottlingRetryIntervalWriteAndRead { + RCNUserDefaultsManager* manager = + [[RCNUserDefaultsManager alloc] initWithAppName:@"TESTING" + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:@"testNamespace1"]; + [manager setCurrentThrottlingRetryIntervalSeconds:RCNUserDefaultsSampleTimeStamp - 1.0]; + XCTAssertEqual([manager currentThrottlingRetryIntervalSeconds], + RCNUserDefaultsSampleTimeStamp - 1.0); + + [manager setCurrentThrottlingRetryIntervalSeconds:RCNUserDefaultsSampleTimeStamp - 2.0]; + XCTAssertEqual([manager currentThrottlingRetryIntervalSeconds], + RCNUserDefaultsSampleTimeStamp - 2.0); +} + +- (void)testUserDefaultsForMultipleNamespaces { + RCNUserDefaultsManager* manager1 = + [[RCNUserDefaultsManager alloc] initWithAppName:@"TESTING" + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:@"testNamespace1"]; + + RCNUserDefaultsManager* manager2 = + [[RCNUserDefaultsManager alloc] initWithAppName:@"TESTING" + bundleID:[NSBundle mainBundle].bundleIdentifier + namespace:@"testNamespace2"]; + + /// Last ETag. + [manager1 setLastETag:@"eTag1ForNamespace1"]; + [manager2 setLastETag:@"eTag1ForNamespace2"]; + XCTAssertEqualObjects([manager1 lastETag], @"eTag1ForNamespace1"); + XCTAssertEqualObjects([manager2 lastETag], @"eTag1ForNamespace2"); + + /// Last fetch time. + [manager1 setLastFetchTime:RCNUserDefaultsSampleTimeStamp - 1000.0]; + [manager2 setLastFetchTime:RCNUserDefaultsSampleTimeStamp - 7000.0]; + XCTAssertEqual([manager1 lastFetchTime], RCNUserDefaultsSampleTimeStamp - 1000); + XCTAssertEqual([manager2 lastFetchTime], RCNUserDefaultsSampleTimeStamp - 7000); + + /// Last fetch status. + [manager1 setLastFetchStatus:@"Success"]; + [manager2 setLastFetchStatus:@"Error"]; + XCTAssertEqualObjects([manager1 lastFetchStatus], @"Success"); + XCTAssertEqualObjects([manager2 lastFetchStatus], @"Error"); + + /// Is client throttled. + [manager1 setIsClientThrottledWithExponentialBackoff:YES]; + [manager2 setIsClientThrottledWithExponentialBackoff:NO]; + XCTAssertEqual([manager1 isClientThrottledWithExponentialBackoff], YES); + XCTAssertEqual([manager2 isClientThrottledWithExponentialBackoff], NO); + + /// Throttle end time. + [manager1 setThrottleEndTime:RCNUserDefaultsSampleTimeStamp - 7.0]; + [manager2 setThrottleEndTime:RCNUserDefaultsSampleTimeStamp - 8.0]; + XCTAssertEqual([manager1 throttleEndTime], RCNUserDefaultsSampleTimeStamp - 7.0); + XCTAssertEqual([manager2 throttleEndTime], RCNUserDefaultsSampleTimeStamp - 8.0); + + /// Throttling retry interval. + [manager1 setCurrentThrottlingRetryIntervalSeconds:RCNUserDefaultsSampleTimeStamp - 1.0]; + [manager2 setCurrentThrottlingRetryIntervalSeconds:RCNUserDefaultsSampleTimeStamp - 2.0]; + XCTAssertEqual([manager1 currentThrottlingRetryIntervalSeconds], + RCNUserDefaultsSampleTimeStamp - 1.0); + XCTAssertEqual([manager2 currentThrottlingRetryIntervalSeconds], + RCNUserDefaultsSampleTimeStamp - 2.0); +} + +@end diff --git a/FirebaseRemoteConfig/Tests/Unit/SecondApp-GoogleService-Info.plist b/FirebaseRemoteConfig/Tests/Unit/SecondApp-GoogleService-Info.plist new file mode 100644 index 00000000000..883c2b77660 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/Unit/SecondApp-GoogleService-Info.plist @@ -0,0 +1,36 @@ + + + + + CLIENT_ID + 123456.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.abc + API_KEY + My String + GCM_SENDER_ID + 1234 + PLIST_VERSION + 1 + BUNDLE_ID + com.me.mybundleid + PROJECT_ID + my-project + STORAGE_BUCKET + my.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:1234:ios:44444 + DATABASE_URL + https://my.firebaseio.com + + \ No newline at end of file diff --git a/FirebaseStorage.podspec b/FirebaseStorage.podspec index 1b142963351..71c5c634cbd 100644 --- a/FirebaseStorage.podspec +++ b/FirebaseStorage.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'FirebaseStorage' - s.version = '3.4.0' + s.version = '3.4.1' s.summary = 'Firebase Storage for iOS (plus community support for macOS and tvOS)' s.description = <<-DESC diff --git a/Firestore/CHANGELOG.md b/Firestore/CHANGELOG.md index 66fb8c5cae1..805ebd88c87 100644 --- a/Firestore/CHANGELOG.md +++ b/Firestore/CHANGELOG.md @@ -1,5 +1,25 @@ # Unreleased +# v1.5.1 +- [fixed] Fixed a memory access error discovered using the sanitizers in Xcode + 11. + +# v1.5.0 +- [changed] Transactions now perform exponential backoff before retrying. + This means transactions on highly contended documents are more likely to + succeed. +- [feature] Added a `waitForPendingWrites()` method to `FIRFirestore` class + which allows users to wait on a promise that resolves when all pending + writes are acknowledged by the Firestore backend. +- [feature] Added a `terminate()` method to `FIRFirestore` which terminates + the instance, releasing any held resources. Once it completes, you can + optionally call `clearPersistence()` to wipe persisted Firestore data + from disk. + +# v1.4.5 +- [fixed] Fixed a crash that would happen when changing networks or going from + online to offline. (#3661). + # v1.4.4 - [changed] Internal improvements. diff --git a/Firestore/CMakeLists.txt b/Firestore/CMakeLists.txt index 17cb17de744..51c5d42f0af 100644 --- a/Firestore/CMakeLists.txt +++ b/Firestore/CMakeLists.txt @@ -15,5 +15,6 @@ add_subdirectory(Example/Benchmarks) add_subdirectory(Protos) +add_subdirectory(Source) add_subdirectory(core) add_subdirectory(fuzzing) diff --git a/Firestore/Example/Benchmarks/FSTLevelDBBenchmarkTests.mm b/Firestore/Example/Benchmarks/FSTLevelDBBenchmarkTests.mm index 73d684672c8..2208d4ddd4e 100644 --- a/Firestore/Example/Benchmarks/FSTLevelDBBenchmarkTests.mm +++ b/Firestore/Example/Benchmarks/FSTLevelDBBenchmarkTests.mm @@ -19,7 +19,6 @@ #include -#import "Firestore/Source/Local/FSTLevelDB.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" @@ -86,8 +85,8 @@ void SetUp(benchmark::State &state) override { } void TearDown(benchmark::State &state) override { - [db_ shutdown]; - db_ = nil; + db_->Shutdown(); + db_.reset(); } void FillDB() { diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 2749f98749c..7896f1ad268 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -20,10 +20,10 @@ 020AFD89BB40E5175838BB76 /* local_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F8043813A5D16963EC02B182 /* local_serializer_test.cc */; }; 022BA1619A576F6818B212C5 /* remote_store_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B843E4A1F3930A400548890 /* remote_store_spec_test.json */; }; 023829DB2198383927233318 /* FSTLocalSerializerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08A2021552A00B64F25 /* FSTLocalSerializerTests.mm */; }; - 0265CCC8BBB76AE013F52411 /* FSTViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05E202154B900B64F25 /* FSTViewTests.mm */; }; 02B83EB79020AE6CBA60A410 /* FIRTimestampTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */; }; 02C953A7B0FA5EF87DB0361A /* FSTIntegrationTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */; }; 02EB33CC2590E1484D462912 /* annotations.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9520B89AAC00B5BCE7 /* annotations.pb.cc */; }; + 036381635A5E42A666757192 /* event_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = A28C9DBBA0D9DCD288652DC2 /* event_manager_test.mm */; }; 036F975093414351FE952F08 /* index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F73B2210F3D800E1F692 /* index_manager_test.mm */; }; 041CF73F67F6A22BF317625A /* FIRTimestampTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */; }; 0455FC6E2A281BD755FD933A /* precondition_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5520A36E1F00BCEB75 /* precondition_test.cc */; }; @@ -32,7 +32,9 @@ 051D3E20184AF195266EF678 /* no_document_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB6B908720322E8800CC290A /* no_document_test.cc */; }; 0535C1B65DADAE1CE47FA3CA /* string_format_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */; }; 056542AD1D0F78E29E22EFA9 /* grpc_connection_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D9649021544D4F00EB9CFB /* grpc_connection_test.cc */; }; + 05D99904EA713414928DD920 /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; 06A3926F89C847846BE4D6BE /* http.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */; }; + 06BCEB9C65DFAA142F3D3F0B /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; 072D805A94E767DE4D371881 /* FSTSyncEngineTestDriver.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02E20213FFC00B64F25 /* FSTSyncEngineTestDriver.mm */; }; 079E63E270F3EFCA175D2705 /* cc_compilation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1B342370EAE3AA02393E33EB /* cc_compilation_test.cc */; }; 07A64E6C4EB700E3AF3FD496 /* document_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB6B908320322E4D00CC290A /* document_test.cc */; }; @@ -44,6 +46,7 @@ 08839E1CEAAC07E350257E9D /* collection_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129C1F315EE100DD57A1 /* collection_spec_test.json */; }; 08A9C531265B5E4C5367346E /* cc_compilation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 1B342370EAE3AA02393E33EB /* cc_compilation_test.cc */; }; 08D853C9D3A4DC919C55671A /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; + 08E3D48B3651E4908D75B23A /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; }; 08F44F7DF9A3EF0D35C8FB57 /* FIRNumericTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = D5B25E7E7D6873CBA4571841 /* FIRNumericTransformTests.mm */; }; 08FA4102AD14452E9587A1F2 /* leveldb_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 332485C4DCC6BA0DBB5E31B7 /* leveldb_util_test.cc */; }; 0963F6D7B0F9AE1E24B82866 /* path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 403DBF6EFB541DFD01582AA3 /* path_test.cc */; }; @@ -63,6 +66,7 @@ 0CEE93636BA4852D3C5EC428 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; }; 0D2D25522A94AA8195907870 /* status.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */; }; 0D67722B43147F775891EA43 /* FSTSerializerBetaTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0C12021557E00B64F25 /* FSTSerializerBetaTests.mm */; }; + 0D88B4CB916A4752B08E5B42 /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; 0DAA255C2FEB387895ADEE12 /* bits_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380D01201BC69F00D97691 /* bits_test.cc */; }; 0DDEE9FE08845BB7CA4607DE /* grpc_connection_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D9649021544D4F00EB9CFB /* grpc_connection_test.cc */; }; 0E4C94369FFF7EC0C9229752 /* iterator_adaptors_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */; }; @@ -73,6 +77,7 @@ 10CA552415BE0954221A1626 /* FSTLRUGarbageCollectorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5CC9650220A0E93200A2D6A1 /* FSTLRUGarbageCollectorTests.mm */; }; 1115DB1F1DCE93B63E03BA8C /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; 113190791F42202FDE1ABC14 /* FIRQuerySnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */; }; + 11BC867491A6631D37DE56A8 /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; }; 11CFC7F545012C246B3484FD /* FSTLevelDBMigrationsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0862021552A00B64F25 /* FSTLevelDBMigrationsTests.mm */; }; 11F8EE69182C9699E90A9E3D /* database_info_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D92E20235D22000A432D /* database_info_test.cc */; }; 12158DFCEE09D24B7988A340 /* maybe_document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7E20B89AAC00B5BCE7 /* maybe_document.pb.cc */; }; @@ -105,6 +110,7 @@ 16F52ECC6FA8A0587CD779EB /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D93220239654000A432D /* user_test.cc */; }; 16FE432587C1B40AF08613D2 /* objc_type_traits_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2A0CF41BA5AED6049B0BEB2C /* objc_type_traits_apple_test.mm */; }; 1733601ECCEA33E730DEAF45 /* autoid_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54740A521FC913E500713A1A /* autoid_test.cc */; }; + 17473086EBACB98CDC3CC65C /* view_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7429071B33BDF80A7FA2F8A /* view_test.cc */; }; 17638F813B9B556FE7718C0C /* FIRQuerySnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */; }; 17DFF30CF61D87883986E8B6 /* executor_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4687208F9B9100554BA2 /* executor_std_test.cc */; }; 1817DEF8FF479D218381C541 /* FSTGoogleTestTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 54764FAE1FAA21B90085E60A /* FSTGoogleTestTests.mm */; }; @@ -117,6 +123,7 @@ 198F193BD9484E49375A7BE7 /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; 199B778D5820495797E0BE02 /* filesystem_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F51859B394D01C0C507282F1 /* filesystem_test.cc */; }; 1A1927621A8593B0A69AA2F3 /* FSTLevelDBMigrationsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0862021552A00B64F25 /* FSTLevelDBMigrationsTests.mm */; }; + 1A82B6A8C3B0E69D259C514C /* event_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = A28C9DBBA0D9DCD288652DC2 /* event_manager_test.mm */; }; 1B6E74BA33B010D76DB1E2F9 /* FIRGeoPointTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */; }; 1C19D796DB6715368407387A /* annotations.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9520B89AAC00B5BCE7 /* annotations.pb.cc */; }; 1C4D8915AE94323AD1024D74 /* token_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABC1D7DF2023A3EF00BA84F0 /* token_test.cc */; }; @@ -148,7 +155,6 @@ 215643858470A449D3A3E168 /* stream_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B66D8995213609EE0086DA0C /* stream_test.mm */; }; 21836C4D9D48F962E7A3A244 /* ordered_code_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380D03201BC6E400D97691 /* ordered_code_test.cc */; }; 21A2A881F71CB825299DF06E /* hard_assert_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 444B7AB3F5A2929070CB1363 /* hard_assert_test.cc */; }; - 21F821BF241244BA7BF070D9 /* FSTEventManagerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */; }; 227CFA0B2A01884C277E4F1D /* hashing_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54511E8D209805F8005BD28F /* hashing_test.cc */; }; 229D1A9381F698D71F229471 /* string_win_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 79507DF8378D3C42F5B36268 /* string_win_test.cc */; }; 22A00AC39CAB3426A943E037 /* query.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D621C2DDC800EFB9CC /* query.pb.cc */; }; @@ -182,11 +188,12 @@ 2B7764F81BA762BE1D791851 /* objc_class_test_helper.mm in Sources */ = {isa = PBXBuildFile; fileRef = B5748BD89DF96FB1B20272F3 /* objc_class_test_helper.mm */; }; 2BA71CDFB97B8F854F5E7AD7 /* field_transform_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352320A3AEC3003E0143 /* field_transform_test.mm */; }; 2BBFAD893295881057E6C1FD /* FSTMockDatastore.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02D20213FFC00B64F25 /* FSTMockDatastore.mm */; }; - 2BCFA42FEA5657A17C5439B3 /* FSTQueryListenerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */; }; + 2C5E4D9FDE7615AD0F63909E /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; }; 2C66492A0099ED7C8B6CDEC6 /* FSTLevelDBRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0922021552B00B64F25 /* FSTLevelDBRemoteDocumentCacheTests.mm */; }; 2CD379584D1D35AAEA271D21 /* sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4E20A36DBB00BCEB75 /* sorted_map_test.cc */; }; 2D220B9ABFA36CD7AC43D0A7 /* time_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 5497CB76229DECDE000FB92F /* time_testing.cc */; }; 2D3401180516B739494C7EFC /* field_value_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB356EF6200EA5EB0089B766 /* field_value_test.cc */; }; + 2D65D31D71A75B046C47B0EB /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; 2DB56B6DED2C93014AE5C51A /* write_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A51F315EE100DD57A1 /* write_spec_test.json */; }; 2E0BBA7E627EB240BA11B0D0 /* exponential_backoff_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */; }; 2E169CF1E9E499F054BB873A /* FSTEventAccumulator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0392021401F00B64F25 /* FSTEventAccumulator.mm */; }; @@ -197,7 +204,6 @@ 2F7D76FF225B550F83B95A72 /* FSTUserDataConverterTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 548180A4228DEF1A004F70CD /* FSTUserDataConverterTests.mm */; }; 2F8FDF35BBB549A6F4D2118E /* FSTMemorySpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02F20213FFC00B64F25 /* FSTMemorySpecTests.mm */; }; 2FA0BAE32D587DF2EA5EEB97 /* async_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB467B208E9A8200554BA2 /* async_queue_test.cc */; }; - 300D9D215F4128E69068B863 /* FSTQueryListenerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */; }; 3021937CBABFD9270A051900 /* FSTViewSnapshotTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05C202154B800B64F25 /* FSTViewSnapshotTest.mm */; }; 3040FD156E1B7C92B0F2A70C /* ordered_code_benchmark.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0473AFFF5567E667A125347B /* ordered_code_benchmark.cc */; }; 306E762DC6B829CED4FD995D /* target_id_generator_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CF82019382300D97691 /* target_id_generator_test.cc */; }; @@ -215,11 +221,12 @@ 339CFFD1323BDCA61EAAFE31 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; }; 342724CA250A65E23CB133AC /* async_queue_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4681208EA0BE00554BA2 /* async_queue_std_test.cc */; }; 34387C13A92D31B212BC0CA9 /* FSTMemoryLRUGarbageCollectorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5CC9650420A0E9BD00A2D6A1 /* FSTMemoryLRUGarbageCollectorTests.mm */; }; + 3451DC1712D7BF5D288339A2 /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; 34D69886DAD4A2029BFC5C63 /* precondition_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5520A36E1F00BCEB75 /* precondition_test.cc */; }; 355A9171EF3F7AD44A9C60CB /* document_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB6B908320322E4D00CC290A /* document_test.cc */; }; 358DBA8B2560C65D9EB23C35 /* Pods_Firestore_IntegrationTests_macOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39B832380209CC5BAF93BC52 /* Pods_Firestore_IntegrationTests_macOS.framework */; }; + 35C330499D50AC415B24C580 /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; }; 36E174A66C323891AEA16A2A /* FIRTimestampTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */; }; - 36F9381F2663A238CDD30E9D /* FSTEventManagerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */; }; 36FD4CE79613D18BC783C55B /* string_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0EE5300F8233D14025EF0456 /* string_apple_test.mm */; }; 37282216FF1CF33E15B3D363 /* stream_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B66D8995213609EE0086DA0C /* stream_test.mm */; }; 3737D67E59DD506291A4C4D0 /* FSTLevelDBLRUGarbageCollectorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5CC9650620A0E9C600A2D6A1 /* FSTLevelDBLRUGarbageCollectorTests.mm */; }; @@ -255,7 +262,6 @@ 3DF174BBCE4F032D27BB5196 /* FSTLocalSerializerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08A2021552A00B64F25 /* FSTLocalSerializerTests.mm */; }; 3DF1AB74036BD8AEF4430FA6 /* firebase_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = ABC1D7E22023CDC500BA84F0 /* firebase_credentials_provider_test.mm */; }; 3DFBA7413965F3E6F366E923 /* grpc_unary_call_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964942163E63900EB9CFB /* grpc_unary_call_test.cc */; }; - 3E0C71810093ADFBAD9B453F /* FSTEventManagerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */; }; 3F2DF1DDDF7F5830F0669992 /* datastore_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 546854A820A36867004BDBD5 /* datastore_test.mm */; }; 3F3C2DAD9F9326BF789B1C96 /* serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 61F72C5520BC48FD001A68CB /* serializer_test.cc */; }; 3F4B6300198FD78E7B19BC5A /* strerror_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 358C3B5FE573B1D60A4F7592 /* strerror_test.cc */; }; @@ -288,6 +294,7 @@ 4781186C01D33E67E07F0D0D /* orderby_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A21F315EE100DD57A1 /* orderby_spec_test.json */; }; 4809D7ACAA9414E3192F04FF /* FIRGeoPointTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E048202154AA00B64F25 /* FIRGeoPointTests.mm */; }; 489D672CAA09B9BC66798E9F /* status.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9920B89AAC00B5BCE7 /* status.pb.cc */; }; + 48D1B38B93D34F1B82320577 /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; 49794806F3D5052E5F61A40D /* http.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */; }; 498A45B1EEBAC97A1C547BAC /* grpc_unary_call_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964942163E63900EB9CFB /* grpc_unary_call_test.cc */; }; 49C04B97AB282FFA82FD98CD /* latlng.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9220B89AAC00B5BCE7 /* latlng.pb.cc */; }; @@ -392,9 +399,6 @@ 5492E058202154AB00B64F25 /* FSTAPIHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */; }; 5492E059202154AB00B64F25 /* FIRQuerySnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */; }; 5492E063202154B900B64F25 /* FSTViewSnapshotTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05C202154B800B64F25 /* FSTViewSnapshotTest.mm */; }; - 5492E064202154B900B64F25 /* FSTQueryListenerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */; }; - 5492E065202154B900B64F25 /* FSTViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05E202154B900B64F25 /* FSTViewTests.mm */; }; - 5492E067202154B900B64F25 /* FSTEventManagerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */; }; 5492E072202154D600B64F25 /* FIRQueryTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E069202154D500B64F25 /* FIRQueryTests.mm */; }; 5492E073202154D600B64F25 /* FIRFieldsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */; }; 5492E074202154D600B64F25 /* FIRListenerRegistrationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06B202154D500B64F25 /* FIRListenerRegistrationTests.mm */; }; @@ -478,7 +482,6 @@ 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 */; }; - 550FB7562D0CF9C3E1984000 /* FSTQueryListenerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */; }; 5542632E76A0C17B61399B54 /* objc_class_test_helper.mm in Sources */ = {isa = PBXBuildFile; fileRef = B5748BD89DF96FB1B20272F3 /* objc_class_test_helper.mm */; }; 555161D6DB2DDC8B57F72A70 /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; 5556B648B9B1C2F79A706B4F /* common.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D221C2DDC800EFB9CC /* common.pb.cc */; }; @@ -581,6 +584,7 @@ 6B8806528FD3757D33D8B8AE /* FSTMemoryQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08B2021552B00B64F25 /* FSTMemoryQueryCacheTests.mm */; }; 6B94E0AE1002C5C9EA0F5582 /* log_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54C2294E1FECABAE007D065B /* log_test.cc */; }; 6C143182916AC638707DB854 /* FIRQuerySnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */; }; + 6C92AD45A3619A18ECCA5B1F /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; 6D578695E8E03988820D401C /* string_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CFC201A2EE200D97691 /* string_util_test.cc */; }; 6DBB3DB3FD6B4981B7F26A55 /* FIRQuerySnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */; }; 6DCA8E54E652B78EFF3EEDAC /* XCTestCase+Await.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0372021401E00B64F25 /* XCTestCase+Await.mm */; }; @@ -608,7 +612,6 @@ 70AB665EB6A473FF6C4CFD31 /* CodableTimestampTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B65C996438B84DBC7616640 /* CodableTimestampTests.swift */; }; 70D96C9129976DB01AC58BAC /* FSTMemoryMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0972021552C00B64F25 /* FSTMemoryMutationQueueTests.mm */; }; 70E78AA49D365D31B211CB0B /* memory_index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F7392210F3D800E1F692 /* memory_index_manager_test.mm */; }; - 7158ED9601DDBCE317E3BAAF /* FSTEventManagerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */; }; 716289F99B5316B3CC5E5CE9 /* FIRSnapshotMetadataTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */; }; 7166078E05192A41206045D8 /* FSTQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0892021552A00B64F25 /* FSTQueryCacheTests.mm */; }; 71702588BFBF5D3A670508E7 /* ordered_code_benchmark.cc in Sources */ = {isa = PBXBuildFile; fileRef = 0473AFFF5567E667A125347B /* ordered_code_benchmark.cc */; }; @@ -664,6 +667,7 @@ 7E851838D105F8FBD4EEC7DB /* FSTQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0892021552A00B64F25 /* FSTQueryCacheTests.mm */; }; 7ECE019D317D808FB3022A31 /* FSTMemoryQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08B2021552B00B64F25 /* FSTMemoryQueryCacheTests.mm */; }; 7EF540911720DAAF516BEDF0 /* query_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B9C261C26C5D311E1E3C0CB9 /* query_test.cc */; }; + 7F771EB980D9CFAAB4764233 /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; 7FF39B8BD834F8267BDCBCC6 /* status_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5493A423225F9990006DE7BA /* status_apple_test.mm */; }; 804B0C6CCE3933CF3948F249 /* grpc_streaming_reader_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964922154AB8F00EB9CFB /* grpc_streaming_reader_test.cc */; }; 8146D5979B2A0B63C79B7AC4 /* FSTUserDataConverterTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 548180A4228DEF1A004F70CD /* FSTUserDataConverterTests.mm */; }; @@ -678,7 +682,6 @@ 8403D519C916C72B9C7F2FA1 /* FIRValidationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06D202154D600B64F25 /* FIRValidationTests.mm */; }; 8413BD9958F6DD52C466D70F /* sorted_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA4C20A36DBB00BCEB75 /* sorted_set_test.cc */; }; 8460C97C9209D7DAF07090BD /* FIRFieldsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */; }; - 84F5EC2591503B7A55BA113C /* FSTViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05E202154B900B64F25 /* FSTViewTests.mm */; }; 85B8918FC8C5DC62482E39C3 /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; }; 85BC2AB572A400114BF59255 /* limbo_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129E1F315EE100DD57A1 /* limbo_spec_test.json */; }; 85D301119D7175F82E12892E /* field_value_benchmark.cc in Sources */ = {isa = PBXBuildFile; fileRef = 6D0EE49C1D5AF75664D0EBE4 /* field_value_benchmark.cc */; }; @@ -699,10 +702,10 @@ 88FD82A1FC5FEC5D56B481D8 /* maybe_document.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE7E20B89AAC00B5BCE7 /* maybe_document.pb.cc */; }; 8943A7C0750CEB0B98D21209 /* FSTPersistenceTestHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08D2021552B00B64F25 /* FSTPersistenceTestHelpers.mm */; }; 897F3C1936612ACB018CA1DD /* http.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE9720B89AAC00B5BCE7 /* http.pb.cc */; }; + 89C71AEAA5316836BB1D5A01 /* view_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7429071B33BDF80A7FA2F8A /* view_test.cc */; }; 8A6C809B9F81C30B7333FCAA /* FIRFirestoreSourceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */; }; 8A79DDB4379A063C30A76329 /* iterator_adaptors_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0353420A3D8CB003E0143 /* iterator_adaptors_test.cc */; }; 8AA7A1FCEE6EC309399978AD /* leveldb_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54995F6E205B6E12004EFFA0 /* leveldb_key_test.cc */; }; - 8ACCBD975DDA23712ED2ACEF /* FSTViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05E202154B900B64F25 /* FSTViewTests.mm */; }; 8B31F63673F3B5238DE95AFB /* geo_point_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB7BAB332012B519001E0872 /* geo_point_test.cc */; }; 8C39F6D4B3AA9074DF00CFB8 /* string_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380CFC201A2EE200D97691 /* string_util_test.cc */; }; 8C602DAD4E8296AB5EFB962A /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; @@ -734,6 +737,7 @@ 939C898FE9D129F6A2EA259C /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; 93E5620E3884A431A14500B0 /* document_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6152AD5202A5385000E5744 /* document_key_test.cc */; }; 9424DA0353DCC7B5EDDEEF5D /* FSTQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0892021552A00B64F25 /* FSTQueryCacheTests.mm */; }; + 95622D4B4C50B4613E739AD5 /* event_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = A28C9DBBA0D9DCD288652DC2 /* event_manager_test.mm */; }; 95C0F55813DA51E6B8C439E1 /* status_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5493A423225F9990006DE7BA /* status_apple_test.mm */; }; 95DCD082374F871A86EF905F /* to_string_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B68B1E002213A764008977EF /* to_string_apple_test.mm */; }; 95ED06D2B0078D3CDB821B68 /* FIRArrayTransformTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73866A9F2082B069009BB4FF /* FIRArrayTransformTests.mm */; }; @@ -781,7 +785,7 @@ A57EC303CD2D6AA4F4745551 /* FIRFieldValueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04A202154AA00B64F25 /* FIRFieldValueTests.mm */; }; A585BD0F31E90980B5F5FBCA /* local_serializer_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = F8043813A5D16963EC02B182 /* local_serializer_test.cc */; }; A5AB1815C45FFC762981E481 /* write.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D921C2DDC800EFB9CC /* write.pb.cc */; }; - A6067B7CE24861A5E8FCC2EB /* FSTQueryListenerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */; }; + A5B8C273593D1BB6E8AE4CBA /* view_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7429071B33BDF80A7FA2F8A /* view_test.cc */; }; A61AE3D94C975A87EFA82ADA /* firebase_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = ABC1D7E22023CDC500BA84F0 /* firebase_credentials_provider_test.mm */; }; A64B1CD2776BC118C74503A7 /* leveldb_index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F7402211FEF300E1F692 /* leveldb_index_manager_test.mm */; }; A6D29E15ED1221352DBE0CF2 /* FSTPersistenceTestHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08D2021552B00B64F25 /* FSTPersistenceTestHelpers.mm */; }; @@ -821,13 +825,14 @@ 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 */; }; ABFD599019CF312CFF96B3EC /* perf_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = D5B2593BCB52957D62F1C9D3 /* perf_spec_test.json */; }; + AC03C4F1456FB1C0D88E94FF /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; AC6C1E57B18730428CB15E03 /* executor_libdispatch_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.mm */; }; ACC9369843F5ED3BD2284078 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; }; AD3C26630E33BE59C49BEB0D /* grpc_unary_call_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D964942163E63900EB9CFB /* grpc_unary_call_test.cc */; }; AD74843082C6465A676F16A7 /* async_queue_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB467B208E9A8200554BA2 /* async_queue_test.cc */; }; AD86162AC78673BA969F3467 /* FSTQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0892021552A00B64F25 /* FSTQueryCacheTests.mm */; }; + AD8F0393B276B2934D251AAC /* view_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7429071B33BDF80A7FA2F8A /* view_test.cc */; }; AE0CFFC34A423E1B80D07418 /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; }; - AE789535A401F3BCE1AE0BDA /* FSTQueryListenerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */; }; AEBF3F80ACC01AA8A27091CD /* FSTIntegrationTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5491BC711FB44593008B3588 /* FSTIntegrationTestCase.mm */; }; AECCD9663BB3DC52199F954A /* executor_std_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4687208F9B9100554BA2 /* executor_std_test.cc */; }; AF332E7926A8766FCFFB04CA /* FSTLevelDBQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0982021552C00B64F25 /* FSTLevelDBQueryCacheTests.mm */; }; @@ -844,6 +849,7 @@ B1BD0A7EC48C7B7AF09437D5 /* FSTLocalStoreTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0832021552A00B64F25 /* FSTLocalStoreTests.mm */; }; B220E091D8F4E6DE1EA44F57 /* executor_libdispatch_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.mm */; }; B235E260EA0DCB7BAC04F69B /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; }; + B28ACC69EB1F232AE612E77B /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; }; B371628DA91E80B64AE53085 /* FIRFieldPathTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04C202154AA00B64F25 /* FIRFieldPathTests.mm */; }; B3A309CCF5D75A555C7196E1 /* path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 403DBF6EFB541DFD01582AA3 /* path_test.cc */; }; B3B8608727430210C4405AC0 /* FSTMemorySpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02F20213FFC00B64F25 /* FSTMemorySpecTests.mm */; }; @@ -860,9 +866,9 @@ B5AEF7E4EBC29653DEE856A2 /* strerror_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 358C3B5FE573B1D60A4F7592 /* strerror_test.cc */; }; B60894F72170207200EBC644 /* fake_credentials_provider.cc in Sources */ = {isa = PBXBuildFile; fileRef = B60894F62170207100EBC644 /* fake_credentials_provider.cc */; }; B6152AD7202A53CB000E5744 /* document_key_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6152AD5202A5385000E5744 /* document_key_test.cc */; }; + B63D84B2980C7DEE7E6E4708 /* view_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7429071B33BDF80A7FA2F8A /* view_test.cc */; }; B65D34A9203C995B0076A5E1 /* FIRTimestampTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */; }; B66D8996213609EE0086DA0C /* stream_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B66D8995213609EE0086DA0C /* stream_test.mm */; }; - B67BB1DA1E247A87B4755C26 /* FSTViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05E202154B900B64F25 /* FSTViewTests.mm */; }; B67BF449216EB43000CA9097 /* create_noop_connectivity_monitor.cc in Sources */ = {isa = PBXBuildFile; fileRef = B67BF448216EB43000CA9097 /* create_noop_connectivity_monitor.cc */; }; 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 */; }; @@ -902,6 +908,7 @@ BC2D0A8EA272A0058F6C2B9E /* FIRFirestoreSourceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */; }; BC549E3F3F119D80741D8612 /* leveldb_util_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 332485C4DCC6BA0DBB5E31B7 /* leveldb_util_test.cc */; }; BC5AC8890974E0821431267E /* limit_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA129F1F315EE100DD57A1 /* limit_spec_test.json */; }; + BC8DFBCB023DBD914E27AA7D /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; BCD9AEA4A890E804922BF72F /* FSTRemoteEventTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0C32021557E00B64F25 /* FSTRemoteEventTests.mm */; }; BD6CC8614970A3D7D2CF0D49 /* exponential_backoff_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B6D1B68420E2AB1A00B35856 /* exponential_backoff_test.cc */; }; BDD2D1812BAD962E3C81A53F /* hashing_test_apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = B69CF3F02227386500B281C8 /* hashing_test_apple.mm */; }; @@ -918,6 +925,7 @@ C1AA536F90A0A576CA2816EB /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB92EB03E3F92485023F64ED /* Pods_Firestore_Example_iOS_Firestore_SwiftTests_iOS.framework */; }; C1B4621C0820EEB0AC9CCD22 /* bits_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB380D01201BC69F00D97691 /* bits_test.cc */; }; C1E35BCE2CFF9B56C28545A2 /* Pods_Firestore_Example_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 62E103B28B48A81D682A0DE9 /* Pods_Firestore_Example_tvOS.framework */; }; + C1F196EC5A7C112D2F7C7724 /* view_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C7429071B33BDF80A7FA2F8A /* view_test.cc */; }; C21B3A1CCB3AD42E57EA14FC /* Pods_Firestore_Tests_macOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 759E964B6A03E6775C992710 /* Pods_Firestore_Tests_macOS.framework */; }; C25F321AC9BF8D1CFC8543AF /* reference_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 132E32997D781B896672D30A /* reference_set_test.cc */; }; C393D6984614D8E4D8C336A2 /* mutation.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */; }; @@ -935,6 +943,7 @@ C5DEDF6148FD41B3000DDD5C /* FSTMemoryRemoteDocumentCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E08C2021552B00B64F25 /* FSTMemoryRemoteDocumentCacheTests.mm */; }; C5F1E2220E30ED5EAC9ABD9E /* mutation.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 618BBE8220B89AAC00B5BCE7 /* mutation.pb.cc */; }; C663A8B74B57FD84717DEA21 /* delayed_constructor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */; }; + C71385CC8843932F658BF21E /* event_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = A28C9DBBA0D9DCD288652DC2 /* event_manager_test.mm */; }; C71AD99EE8D176614E742FD7 /* string_apple_benchmark.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C0CC6F62A90D8573F383 /* string_apple_benchmark.mm */; }; C7F174164D7C55E35A526009 /* resource_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2B02024FFD70028D6BE /* resource_path_test.cc */; }; C80B10E79CDD7EF7843C321E /* objc_type_traits_apple_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2A0CF41BA5AED6049B0BEB2C /* objc_type_traits_apple_test.mm */; }; @@ -949,6 +958,7 @@ CBE3E77B645BCA9A7A827DED /* FSTLevelDBTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 132E36BB104830BD806351AC /* FSTLevelDBTransactionTests.mm */; }; CC94A33318F983907E9ED509 /* resume_token_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A41F315EE100DD57A1 /* resume_token_spec_test.json */; }; CD0AA9E5D83C00CAAE7C2F67 /* FIRTimestampTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B65D34A7203C99090076A5E1 /* FIRTimestampTest.m */; }; + CD226D868CEFA9D557EF33A1 /* query_listener_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */; }; CD78EEAA1CD36BE691CA3427 /* hashing_test_apple.mm in Sources */ = {isa = PBXBuildFile; fileRef = B69CF3F02227386500B281C8 /* hashing_test_apple.mm */; }; CE222EAE5BD7D4CDEA8528BF /* objc_class_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B3CC6B2ACFDC873F06AE9E3F /* objc_class_test.cc */; }; CEDDC6DB782989587D0139B2 /* datastore_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 546854A820A36867004BDBD5 /* datastore_test.mm */; }; @@ -962,6 +972,7 @@ D148475D7F26BFEE6E05CCDA /* firebase_credentials_provider_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = ABC1D7E22023CDC500BA84F0 /* firebase_credentials_provider_test.mm */; }; D18DBCE3FE34BF5F14CF8ABD /* mutation_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = C8522DE226C467C54E6788D8 /* mutation_test.cc */; }; D22B96C19A0F3DE998D4320C /* delayed_constructor_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */; }; + D268E8770462354725981C25 /* event_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = A28C9DBBA0D9DCD288652DC2 /* event_manager_test.mm */; }; D39F0216BF1EA8CD54C76CF8 /* FIRQueryUnitTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = FF73B39D04D1760190E6B84A /* FIRQueryUnitTests.mm */; }; D43F7601F3F3DE3125346D42 /* user_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB38D93220239654000A432D /* user_test.cc */; }; D44DA2F61B854E8771E4E446 /* memory_index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F7392210F3D800E1F692 /* memory_index_manager_test.mm */; }; @@ -985,6 +996,7 @@ D73BBA4AB42940AB187169E3 /* listen_spec_test.json in Resources */ = {isa = PBXBuildFile; fileRef = 54DA12A01F315EE100DD57A1 /* listen_spec_test.json */; }; D756A1A63E626572EE8DF592 /* firestore.pb.cc in Sources */ = {isa = PBXBuildFile; fileRef = 544129D421C2DDC800EFB9CC /* firestore.pb.cc */; }; D77941FD93DBE862AEF1F623 /* FSTTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E07B202154EB00B64F25 /* FSTTransactionTests.mm */; }; + D8673F8C7CE8172FE7AD9DEC /* event_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = A28C9DBBA0D9DCD288652DC2 /* event_manager_test.mm */; }; D911D382B321AE24190F609F /* stream_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = B66D8995213609EE0086DA0C /* stream_test.mm */; }; D91D86B29B86A60C05879A48 /* timestamp_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = ABF6506B201131F8005F2C74 /* timestamp_test.cc */; }; D9366A834BFF13246DC3AF9E /* field_path_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B686F2AD2023DDB20028D6BE /* field_path_test.cc */; }; @@ -1014,6 +1026,7 @@ DD24F8DDFFA8C6D5A2315A59 /* FSTLevelDBQueryCacheTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0982021552C00B64F25 /* FSTLevelDBQueryCacheTests.mm */; }; DD5976A45071455FF3FE74B8 /* string_win_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 79507DF8378D3C42F5B36268 /* string_win_test.cc */; }; DDBC6DB41D1A43CFF01288A2 /* field_value_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB356EF6200EA5EB0089B766 /* field_value_test.cc */; }; + DDDE74C752E65DE7D39A7166 /* view_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = A5466E7809AD2871FFDE6C76 /* view_testing.cc */; }; 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 */; }; @@ -1074,7 +1087,6 @@ EC7A44792A5513FBB6F501EE /* comparison_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 548DB928200D59F600E00ABC /* comparison_test.cc */; }; EC7C09704D2E9305F4AE431E /* FSTMemoryLRUGarbageCollectorTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5CC9650420A0E9BD00A2D6A1 /* FSTMemoryLRUGarbageCollectorTests.mm */; }; EC80A217F3D66EB0272B36B0 /* FSTLevelDBSpecTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E02C20213FFB00B64F25 /* FSTLevelDBSpecTests.mm */; }; - ECEAED9FE1AAA9C748834757 /* FSTEventManagerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */; }; ECED3B60C5718B085AAB14FB /* to_string_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = B696858D2214B53900271095 /* to_string_test.cc */; }; ED420D8F49DA5C41EEF93913 /* FIRSnapshotMetadataTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */; }; ED4E2AC80CAF2A8FDDAC3DEE /* field_mask_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 549CCA5320A36E1F00BCEB75 /* field_mask_test.cc */; }; @@ -1102,6 +1114,7 @@ F66650439F7B638789EFB810 /* memory_index_manager_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = 73F1F7392210F3D800E1F692 /* memory_index_manager_test.mm */; }; F72DF72447EA7AB9D100816A /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; F731A0CCD0220B370BC1BE8B /* BasicCompileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0761F61F2FE68D003233AF /* BasicCompileTests.swift */; }; + F73471529D36DD48ABD8AAE8 /* async_testing.cc in Sources */ = {isa = PBXBuildFile; fileRef = 872C92ABD71B12784A1C5520 /* async_testing.cc */; }; F74AA9DA46B7B2820B5AC79F /* FSTMemoryMutationQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0972021552C00B64F25 /* FSTMemoryMutationQueueTests.mm */; }; F7718C43D3A8FCCDB4BB0071 /* geo_point_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = AB7BAB332012B519001E0872 /* geo_point_test.cc */; }; F8038FD17F7CDB7C6510D8DB /* watch_change_test.mm in Sources */ = {isa = PBXBuildFile; fileRef = DD9224E7AB303B920105EF13 /* watch_change_test.mm */; }; @@ -1111,7 +1124,6 @@ FA63B7521A07F1EB2F999859 /* FSTLevelDBMigrationsTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E0862021552A00B64F25 /* FSTLevelDBMigrationsTests.mm */; }; FA7837C5CDFB273DE447E447 /* FIRServerTimestampTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E06E202154D600B64F25 /* FIRServerTimestampTests.mm */; }; FABE084FA7DA6E216A41EE80 /* status_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54A0352C20A3B3D7003E0143 /* status_test.cc */; }; - FAF50FBED03B089269307943 /* FSTViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E05E202154B900B64F25 /* FSTViewTests.mm */; }; FB2111D9205822CC8E7368C2 /* FIRDocumentReferenceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E049202154AA00B64F25 /* FIRDocumentReferenceTests.mm */; }; FBBB13329D3B5827C21AE7AB /* reference_set_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 132E32997D781B896672D30A /* reference_set_test.cc */; }; FCA48FB54FC50BFDFDA672CD /* array_sorted_map_test.cc in Sources */ = {isa = PBXBuildFile; fileRef = 54EB764C202277B30088B8F3 /* array_sorted_map_test.cc */; }; @@ -1267,11 +1279,7 @@ 5492E04D202154AA00B64F25 /* FIRSnapshotMetadataTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRSnapshotMetadataTests.mm; sourceTree = ""; }; 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTAPIHelpers.mm; sourceTree = ""; }; 5492E04F202154AA00B64F25 /* FIRQuerySnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRQuerySnapshotTests.mm; sourceTree = ""; }; - 5492E05A202154B800B64F25 /* FSTSyncEngine+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "FSTSyncEngine+Testing.h"; sourceTree = ""; }; 5492E05C202154B800B64F25 /* FSTViewSnapshotTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTViewSnapshotTest.mm; sourceTree = ""; }; - 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTQueryListenerTests.mm; sourceTree = ""; }; - 5492E05E202154B900B64F25 /* FSTViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTViewTests.mm; sourceTree = ""; }; - 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FSTEventManagerTests.mm; sourceTree = ""; }; 5492E069202154D500B64F25 /* FIRQueryTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRQueryTests.mm; sourceTree = ""; }; 5492E06A202154D500B64F25 /* FIRFieldsTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFieldsTests.mm; sourceTree = ""; }; 5492E06B202154D500B64F25 /* FIRListenerRegistrationTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRListenerRegistrationTests.mm; sourceTree = ""; }; @@ -1378,6 +1386,7 @@ 6003F5AF195388D20070C39A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 6003F5B7195388D20070C39A /* Tests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Tests-Info.plist"; sourceTree = ""; }; 6003F5B9195388D20070C39A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 600A7D7D821CE84E0CA8CB89 /* async_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = async_testing.h; sourceTree = ""; }; 6161B5012047140400A99DBB /* FIRFirestoreSourceTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FIRFirestoreSourceTests.mm; sourceTree = ""; }; 618BBE7D20B89AAC00B5BCE7 /* target.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = target.pb.cc; sourceTree = ""; }; 618BBE7E20B89AAC00B5BCE7 /* maybe_document.pb.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = maybe_document.pb.cc; sourceTree = ""; }; @@ -1417,14 +1426,19 @@ 74AC2ADBF1BAD9A8EF30CF41 /* Pods-Firestore_IntegrationTests_tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_IntegrationTests_tvOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_IntegrationTests_tvOS/Pods-Firestore_IntegrationTests_tvOS.debug.xcconfig"; sourceTree = ""; }; 759E964B6A03E6775C992710 /* Pods_Firestore_Tests_macOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_Tests_macOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 79507DF8378D3C42F5B36268 /* string_win_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = string_win_test.cc; sourceTree = ""; }; + 79D4CD6A707ED3F7A6D2ECF5 /* view_testing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = view_testing.h; sourceTree = ""; }; 7B65C996438B84DBC7616640 /* CodableTimestampTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CodableTimestampTests.swift; sourceTree = ""; }; + 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = query_listener_test.cc; sourceTree = ""; }; 84434E57CA72951015FC71BC /* Pods-Firestore_FuzzTests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS.debug.xcconfig"; sourceTree = ""; }; + 872C92ABD71B12784A1C5520 /* async_testing.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = async_testing.cc; sourceTree = ""; }; 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 8E002F4AD5D9B6197C940847 /* Firestore.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = Firestore.podspec; path = ../Firestore.podspec; sourceTree = ""; }; 97C492D2524E92927C11F425 /* Pods-Firestore_FuzzTests_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_FuzzTests_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_FuzzTests_iOS/Pods-Firestore_FuzzTests_iOS.release.xcconfig"; sourceTree = ""; }; 98366480BD1FD44A1FEDD982 /* Pods-Firestore_Example_macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_macOS/Pods-Firestore_Example_macOS.debug.xcconfig"; sourceTree = ""; }; 9B7F2784838799FBBD3C80F5 /* objc_class_test_helper.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = objc_class_test_helper.h; sourceTree = ""; }; 9CFD366B783AE27B9E79EE7A /* string_format_apple_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; path = string_format_apple_test.mm; sourceTree = ""; }; + A28C9DBBA0D9DCD288652DC2 /* event_manager_test.mm */ = {isa = PBXFileReference; includeInIndex = 1; path = event_manager_test.mm; sourceTree = ""; }; + A5466E7809AD2871FFDE6C76 /* view_testing.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = view_testing.cc; sourceTree = ""; }; A5FA86650A18F3B7A8162287 /* Pods-Firestore_Benchmarks_iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Benchmarks_iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Benchmarks_iOS/Pods-Firestore_Benchmarks_iOS.release.xcconfig"; sourceTree = ""; }; A70E82DD627B162BEF92B8ED /* Pods-Firestore_Example_tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Example_tvOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Example_tvOS/Pods-Firestore_Example_tvOS.debug.xcconfig"; sourceTree = ""; }; AB356EF6200EA5EB0089B766 /* field_value_test.cc */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = field_value_test.cc; sourceTree = ""; }; @@ -1471,7 +1485,6 @@ B6FB467B208E9A8200554BA2 /* async_queue_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = async_queue_test.cc; sourceTree = ""; }; B6FB4680208EA0BE00554BA2 /* async_queue_libdispatch_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = async_queue_libdispatch_test.mm; sourceTree = ""; }; B6FB4681208EA0BE00554BA2 /* async_queue_std_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = async_queue_std_test.cc; sourceTree = ""; }; - B6FB4686208F9B9100554BA2 /* async_tests_util.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = async_tests_util.h; sourceTree = ""; }; B6FB4687208F9B9100554BA2 /* executor_std_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = executor_std_test.cc; sourceTree = ""; }; B6FB4688208F9B9100554BA2 /* executor_test.cc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = executor_test.cc; sourceTree = ""; }; B6FB4689208F9B9100554BA2 /* executor_libdispatch_test.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = executor_libdispatch_test.mm; sourceTree = ""; }; @@ -1483,6 +1496,7 @@ 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; }; BC3C788D290A935C353CEAA1 /* writer_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; name = writer_test.cc; path = nanopb/writer_test.cc; sourceTree = ""; }; BD01F0E43E4E2A07B8B05099 /* Pods-Firestore_Tests_macOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_macOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_macOS/Pods-Firestore_Tests_macOS.debug.xcconfig"; sourceTree = ""; }; + C7429071B33BDF80A7FA2F8A /* view_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; path = view_test.cc; sourceTree = ""; }; C8522DE226C467C54E6788D8 /* mutation_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = mutation_test.cc; sourceTree = ""; }; CD422AF3E4515FB8E9BE67A0 /* equals_tester.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = equals_tester.h; sourceTree = ""; }; D0A6E9136804A41CEC9D55D4 /* delayed_constructor_test.cc */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.cpp; path = delayed_constructor_test.cc; sourceTree = ""; }; @@ -1683,11 +1697,15 @@ children = ( 5467FB06203E6A44009C9584 /* app_testing.h */, 5467FB07203E6A44009C9584 /* app_testing.mm */, + 872C92ABD71B12784A1C5520 /* async_testing.cc */, + 600A7D7D821CE84E0CA8CB89 /* async_testing.h */, CD422AF3E4515FB8E9BE67A0 /* equals_tester.h */, 54A0352820A3B3BD003E0143 /* testutil.cc */, 54A0352920A3B3BD003E0143 /* testutil.h */, 5497CB76229DECDE000FB92F /* time_testing.cc */, 5497CB75229DECDE000FB92F /* time_testing.h */, + A5466E7809AD2871FFDE6C76 /* view_testing.cc */, + 79D4CD6A707ED3F7A6D2ECF5 /* view_testing.h */, BA6E5B9D53CCF301F58A62D7 /* xcgmock.h */, 4425A513895DEC60325A139E /* xcgmock_test.mm */, ); @@ -1717,7 +1735,6 @@ 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 */, @@ -2155,9 +2172,12 @@ isa = PBXGroup; children = ( AB38D92E20235D22000A432D /* database_info_test.cc */, + A28C9DBBA0D9DCD288652DC2 /* event_manager_test.mm */, E8551D6C6FB0B1BACE9E5BAD /* field_filter_test.cc */, + 7C3F995E040E9E9C5E8514BB /* query_listener_test.cc */, B9C261C26C5D311E1E3C0CB9 /* query_test.cc */, AB380CF82019382300D97691 /* target_id_generator_test.cc */, + C7429071B33BDF80A7FA2F8A /* view_test.cc */, ); path = core; sourceTree = ""; @@ -2313,11 +2333,7 @@ DE51B1A81F0D48AC0013853F /* Core */ = { isa = PBXGroup; children = ( - 5492E060202154B900B64F25 /* FSTEventManagerTests.mm */, - 5492E05D202154B900B64F25 /* FSTQueryListenerTests.mm */, - 5492E05A202154B800B64F25 /* FSTSyncEngine+Testing.h */, 5492E05C202154B800B64F25 /* FSTViewSnapshotTest.mm */, - 5492E05E202154B900B64F25 /* FSTViewTests.mm */, ); path = Core; sourceTree = ""; @@ -3337,7 +3353,6 @@ CD0AA9E5D83C00CAAE7C2F67 /* FIRTimestampTest.m in Sources */, 9D71628E38D9F64C965DF29E /* FSTAPIHelpers.mm in Sources */, F4F00BF4E87D7F0F0F8831DB /* FSTEventAccumulator.mm in Sources */, - 21F821BF241244BA7BF070D9 /* FSTEventManagerTests.mm in Sources */, 0A6FBE65A7FE048BAD562A15 /* FSTGoogleTestTests.mm in Sources */, 939C898FE9D129F6A2EA259C /* FSTHelpers.mm in Sources */, C4055D868A38221B332CD03D /* FSTIntegrationTestCase.mm in Sources */, @@ -3362,7 +3377,6 @@ 38F973FA8ADEAFE9541C25EA /* FSTMutationQueueTests.mm in Sources */, 8943A7C0750CEB0B98D21209 /* FSTPersistenceTestHelpers.mm in Sources */, AD86162AC78673BA969F3467 /* FSTQueryCacheTests.mm in Sources */, - 550FB7562D0CF9C3E1984000 /* FSTQueryListenerTests.mm in Sources */, D063F56AC89E074F9AB05DD3 /* FSTRemoteDocumentCacheTests.mm in Sources */, BCD9AEA4A890E804922BF72F /* FSTRemoteEventTests.mm in Sources */, 0D67722B43147F775891EA43 /* FSTSerializerBetaTests.mm in Sources */, @@ -3370,7 +3384,6 @@ 072D805A94E767DE4D371881 /* FSTSyncEngineTestDriver.mm in Sources */, 548180A6228DEF1A004F70CD /* FSTUserDataConverterTests.mm in Sources */, 3021937CBABFD9270A051900 /* FSTViewSnapshotTest.mm in Sources */, - B67BB1DA1E247A87B4755C26 /* FSTViewTests.mm in Sources */, 6DCA8E54E652B78EFF3EEDAC /* XCTestCase+Await.mm in Sources */, 45939AFF906155EA27D281AB /* annotations.pb.cc in Sources */, FF3405218188DFCE586FB26B /* app_testing.mm in Sources */, @@ -3379,6 +3392,7 @@ 4F857404731D45F02C5EE4C3 /* async_queue_libdispatch_test.mm in Sources */, 83A9CD3B6E791A860CE81FA1 /* async_queue_std_test.cc in Sources */, 0B7B24194E2131F5C325FE0E /* async_queue_test.cc in Sources */, + B28ACC69EB1F232AE612E77B /* async_testing.cc in Sources */, 1733601ECCEA33E730DEAF45 /* autoid_test.cc in Sources */, 0DAA255C2FEB387895ADEE12 /* bits_test.cc in Sources */, EBE4A7B6A57BCE02B389E8A6 /* byte_string_test.cc in Sources */, @@ -3396,6 +3410,7 @@ 547E9A4422F9EA7300A275E0 /* document_set_test.cc in Sources */, 355A9171EF3F7AD44A9C60CB /* document_test.cc in Sources */, 3BCEBA50E9678123245C0272 /* empty_credentials_provider_test.cc in Sources */, + C71385CC8843932F658BF21E /* event_manager_test.mm in Sources */, AC6C1E57B18730428CB15E03 /* executor_libdispatch_test.mm in Sources */, E7D415B8717701B952C344E5 /* executor_std_test.cc in Sources */, 470A37727BBF516B05ED276A /* executor_test.cc in Sources */, @@ -3442,6 +3457,7 @@ DB7E9C5A59CCCDDB7F0C238A /* path_test.cc in Sources */, 0455FC6E2A281BD755FD933A /* precondition_test.cc in Sources */, 938F2AF6EC5CD0B839300DB0 /* query.pb.cc in Sources */, + AC03C4F1456FB1C0D88E94FF /* query_listener_test.cc in Sources */, 7EF540911720DAAF516BEDF0 /* query_test.cc in Sources */, 37EC6C6EA9169BB99078CA96 /* reference_set_test.cc in Sources */, C7F174164D7C55E35A526009 /* resource_path_test.cc in Sources */, @@ -3473,6 +3489,8 @@ 8DA258092DD856D829D973B5 /* transform_operations_test.mm in Sources */, 5F19F66D8B01BA2B97579017 /* tree_sorted_map_test.cc in Sources */, 16F52ECC6FA8A0587CD779EB /* user_test.cc in Sources */, + AD8F0393B276B2934D251AAC /* view_test.cc in Sources */, + 2D65D31D71A75B046C47B0EB /* view_testing.cc in Sources */, 5C315A03862BB73A7C3FF0CD /* watch_change_test.mm in Sources */, 53AB47E44D897C81A94031F6 /* write.pb.cc in Sources */, 59E6941008253D4B0F77C2BA /* writer_test.cc in Sources */, @@ -3507,7 +3525,6 @@ 36E174A66C323891AEA16A2A /* FIRTimestampTest.m in Sources */, 6E4854B19B120C6F0F8192CC /* FSTAPIHelpers.mm in Sources */, 73E42D984FB36173A2BDA57C /* FSTEventAccumulator.mm in Sources */, - 3E0C71810093ADFBAD9B453F /* FSTEventManagerTests.mm in Sources */, E375FBA0632EFB4D14C4E5A9 /* FSTGoogleTestTests.mm in Sources */, F72DF72447EA7AB9D100816A /* FSTHelpers.mm in Sources */, AEBF3F80ACC01AA8A27091CD /* FSTIntegrationTestCase.mm in Sources */, @@ -3532,7 +3549,6 @@ 239B9B357E67036BEA831E3A /* FSTMutationQueueTests.mm in Sources */, A6D29E15ED1221352DBE0CF2 /* FSTPersistenceTestHelpers.mm in Sources */, BBFCCD960DD2937EE278D7B6 /* FSTQueryCacheTests.mm in Sources */, - 300D9D215F4128E69068B863 /* FSTQueryListenerTests.mm in Sources */, 9664E5831CE35D515CDBC12A /* FSTRemoteDocumentCacheTests.mm in Sources */, 61D1EB3438B92F61F6CAC191 /* FSTRemoteEventTests.mm in Sources */, F58A4EE0A1A77F61EF41E5ED /* FSTSerializerBetaTests.mm in Sources */, @@ -3540,7 +3556,6 @@ D69B97FF4C065EACEDD91886 /* FSTSyncEngineTestDriver.mm in Sources */, 548180A7228DEF1A004F70CD /* FSTUserDataConverterTests.mm in Sources */, 4BB325E8B87A2FA4483AA070 /* FSTViewSnapshotTest.mm in Sources */, - 0265CCC8BBB76AE013F52411 /* FSTViewTests.mm in Sources */, AAC15E7CCAE79619B2ABB972 /* XCTestCase+Await.mm in Sources */, 1C19D796DB6715368407387A /* annotations.pb.cc in Sources */, 6EEA00A737690EF82A3C91C6 /* app_testing.mm in Sources */, @@ -3549,6 +3564,7 @@ 4AD9809C9CE9FA09AC40992F /* async_queue_libdispatch_test.mm in Sources */, 38208AC761FF994BA69822BE /* async_queue_std_test.cc in Sources */, 900D0E9F18CE3DB954DD0D1E /* async_queue_test.cc in Sources */, + F73471529D36DD48ABD8AAE8 /* async_testing.cc in Sources */, 5D5E24E3FA1128145AA117D2 /* autoid_test.cc in Sources */, B6FDE6F91D3F81D045E962A0 /* bits_test.cc in Sources */, E1264B172412967A09993EC6 /* byte_string_test.cc in Sources */, @@ -3566,6 +3582,7 @@ 547E9A4622F9EA7300A275E0 /* document_set_test.cc in Sources */, 07A64E6C4EB700E3AF3FD496 /* document_test.cc in Sources */, 2B1E95FAFD350C191B525F3B /* empty_credentials_provider_test.cc in Sources */, + 1A82B6A8C3B0E69D259C514C /* event_manager_test.mm in Sources */, B220E091D8F4E6DE1EA44F57 /* executor_libdispatch_test.mm in Sources */, BAB43C839445782040657239 /* executor_std_test.cc in Sources */, 3A7CB01751697ED599F2D9A1 /* executor_test.cc in Sources */, @@ -3612,6 +3629,7 @@ 0963F6D7B0F9AE1E24B82866 /* path_test.cc in Sources */, 152543FD706D5E8851C8DA92 /* precondition_test.cc in Sources */, 5FA3DB52A478B01384D3A2ED /* query.pb.cc in Sources */, + 0D88B4CB916A4752B08E5B42 /* query_listener_test.cc in Sources */, F481368DB694B3B4D0C8E4A2 /* query_test.cc in Sources */, 7DBE7DB90CF83B589A94980F /* reference_set_test.cc in Sources */, 85B8918FC8C5DC62482E39C3 /* resource_path_test.cc in Sources */, @@ -3643,6 +3661,8 @@ 5C7FAF228D0F52CFFE9E41B5 /* transform_operations_test.mm in Sources */, 627253FDEC6BB5549FE77F4E /* tree_sorted_map_test.cc in Sources */, 596C782EFB68131380F8EEF8 /* user_test.cc in Sources */, + C1F196EC5A7C112D2F7C7724 /* view_test.cc in Sources */, + 3451DC1712D7BF5D288339A2 /* view_testing.cc in Sources */, F8038FD17F7CDB7C6510D8DB /* watch_change_test.mm in Sources */, A5AB1815C45FFC762981E481 /* write.pb.cc in Sources */, A21819C437C3C80450D7EEEE /* writer_test.cc in Sources */, @@ -3685,7 +3705,6 @@ D550446303227FB1B381133C /* FSTAPIHelpers.mm in Sources */, A4ECA8335000CBDF94586C94 /* FSTDatastoreTests.mm in Sources */, 2E169CF1E9E499F054BB873A /* FSTEventAccumulator.mm in Sources */, - 7158ED9601DDBCE317E3BAAF /* FSTEventManagerTests.mm in Sources */, 1817DEF8FF479D218381C541 /* FSTGoogleTestTests.mm in Sources */, 086E10B1B37666FB746D56BC /* FSTHelpers.mm in Sources */, 02C953A7B0FA5EF87DB0361A /* FSTIntegrationTestCase.mm in Sources */, @@ -3710,7 +3729,6 @@ CF51E8910ED6E5F6D87941BC /* FSTMutationQueueTests.mm in Sources */, 9E160C50C43AB91C64710773 /* FSTPersistenceTestHelpers.mm in Sources */, 7E851838D105F8FBD4EEC7DB /* FSTQueryCacheTests.mm in Sources */, - A6067B7CE24861A5E8FCC2EB /* FSTQueryListenerTests.mm in Sources */, 26F59942DA1B72E3B8460CF7 /* FSTRemoteDocumentCacheTests.mm in Sources */, 2F6432A121BD52D6934C29C6 /* FSTRemoteEventTests.mm in Sources */, 518C1013F53C1BD1D0023C6C /* FSTSerializerBetaTests.mm in Sources */, @@ -3720,7 +3738,6 @@ D77941FD93DBE862AEF1F623 /* FSTTransactionTests.mm in Sources */, E9B704651F9783B70F2D5E86 /* FSTUserDataConverterTests.mm in Sources */, FDCAB270789A24CDA9457D2C /* FSTViewSnapshotTest.mm in Sources */, - 84F5EC2591503B7A55BA113C /* FSTViewTests.mm in Sources */, 3B1E27D951407FD237E64D07 /* FirestoreEncoderTests.swift in Sources */, 4D42E5C756229C08560DD731 /* XCTestCase+Await.mm in Sources */, 276A563D546698B6AAC20164 /* annotations.pb.cc in Sources */, @@ -3730,6 +3747,7 @@ 9B2CD4CBB1DFE8BC3C81A335 /* async_queue_libdispatch_test.mm in Sources */, 342724CA250A65E23CB133AC /* async_queue_std_test.cc in Sources */, DA1D665B12AA1062DCDEA6BD /* async_queue_test.cc in Sources */, + 08E3D48B3651E4908D75B23A /* async_testing.cc in Sources */, B842780CF42361ACBBB381A9 /* autoid_test.cc in Sources */, 146C140B254F3837A4DD7AE8 /* bits_test.cc in Sources */, D658E6DA5A218E08810E1688 /* byte_string_test.cc in Sources */, @@ -3747,6 +3765,7 @@ 547E9A4722F9EA7300A275E0 /* document_set_test.cc in Sources */, 13E264F840239C8C99865921 /* document_test.cc in Sources */, 3A8C29BF47A62B7BADCBA6F5 /* empty_credentials_provider_test.cc in Sources */, + 036381635A5E42A666757192 /* event_manager_test.mm in Sources */, 5F6CE37B34C542704C5605A4 /* executor_libdispatch_test.mm in Sources */, AECCD9663BB3DC52199F954A /* executor_std_test.cc in Sources */, 18F644E6AA98E6D6F3F1F809 /* executor_test.cc in Sources */, @@ -3793,6 +3812,7 @@ 70A171FC43BE328767D1B243 /* path_test.cc in Sources */, 34D69886DAD4A2029BFC5C63 /* precondition_test.cc in Sources */, 22A00AC39CAB3426A943E037 /* query.pb.cc in Sources */, + 05D99904EA713414928DD920 /* query_listener_test.cc in Sources */, 339CFFD1323BDCA61EAAFE31 /* query_test.cc in Sources */, C25F321AC9BF8D1CFC8543AF /* reference_set_test.cc in Sources */, AE0CFFC34A423E1B80D07418 /* resource_path_test.cc in Sources */, @@ -3824,6 +3844,8 @@ 5F9B5702D6356DBC9E1E7D2C /* transform_operations_test.mm in Sources */, 54B91B921DA757C64CC67C90 /* tree_sorted_map_test.cc in Sources */, 8D5A9E6E43B6F47431841FE2 /* user_test.cc in Sources */, + 89C71AEAA5316836BB1D5A01 /* view_test.cc in Sources */, + 06BCEB9C65DFAA142F3D3F0B /* view_testing.cc in Sources */, 561A5E47C117CBA1AEEC8762 /* watch_change_test.mm in Sources */, FCF8E7F5268F6842C07B69CF /* write.pb.cc in Sources */, B0D10C3451EDFB016A6EAF03 /* writer_test.cc in Sources */, @@ -3866,7 +3888,6 @@ 881E55152AB34465412F8542 /* FSTAPIHelpers.mm in Sources */, 4A64A339BCA77B9F875D1D8B /* FSTDatastoreTests.mm in Sources */, 1C7254742A9F6F7042C9D78E /* FSTEventAccumulator.mm in Sources */, - 36F9381F2663A238CDD30E9D /* FSTEventManagerTests.mm in Sources */, 8D0EF43F1B7B156550E65C20 /* FSTGoogleTestTests.mm in Sources */, 198F193BD9484E49375A7BE7 /* FSTHelpers.mm in Sources */, 0F54634745BA07B09BDC14D7 /* FSTIntegrationTestCase.mm in Sources */, @@ -3891,7 +3912,6 @@ 1628C9CE4A1640E73761806B /* FSTMutationQueueTests.mm in Sources */, 32CD5A8F4E44DF71181DEBCE /* FSTPersistenceTestHelpers.mm in Sources */, 9424DA0353DCC7B5EDDEEF5D /* FSTQueryCacheTests.mm in Sources */, - AE789535A401F3BCE1AE0BDA /* FSTQueryListenerTests.mm in Sources */, E75A2ADBD689EC2CDC4CC30A /* FSTRemoteDocumentCacheTests.mm in Sources */, D5D577C25F3B4C735AFF0918 /* FSTRemoteEventTests.mm in Sources */, 930EA7B9B13602DEFFF9CC85 /* FSTSerializerBetaTests.mm in Sources */, @@ -3901,7 +3921,6 @@ 5E5B3B8B3A41C8EB70035A6B /* FSTTransactionTests.mm in Sources */, 8146D5979B2A0B63C79B7AC4 /* FSTUserDataConverterTests.mm in Sources */, 86442CD280F3C92CA179A0C4 /* FSTViewSnapshotTest.mm in Sources */, - 8ACCBD975DDA23712ED2ACEF /* FSTViewTests.mm in Sources */, 5E89B1A5A5430713C79C4854 /* FirestoreEncoderTests.swift in Sources */, 736C4E82689F1CA1859C4A3F /* XCTestCase+Await.mm in Sources */, EA46611779C3EEF12822508C /* annotations.pb.cc in Sources */, @@ -3911,6 +3930,7 @@ 1CB8AEFBF3E9565FF9955B50 /* async_queue_libdispatch_test.mm in Sources */, AB2BAB0BD77FF05CC26FCF75 /* async_queue_std_test.cc in Sources */, 2FA0BAE32D587DF2EA5EEB97 /* async_queue_test.cc in Sources */, + 2C5E4D9FDE7615AD0F63909E /* async_testing.cc in Sources */, 6AF739DDA9D33DF756DE7CDE /* autoid_test.cc in Sources */, C1B4621C0820EEB0AC9CCD22 /* bits_test.cc in Sources */, 297DC2B3C1EB136D58F4BA9C /* byte_string_test.cc in Sources */, @@ -3928,6 +3948,7 @@ 547E9A4522F9EA7300A275E0 /* document_set_test.cc in Sources */, 8ECDF2AFCF1BCA1A2CDAAD8A /* document_test.cc in Sources */, 18688026A6F1E9404F63B243 /* empty_credentials_provider_test.cc in Sources */, + 95622D4B4C50B4613E739AD5 /* event_manager_test.mm in Sources */, 49C593017B5438B216FAF593 /* executor_libdispatch_test.mm in Sources */, 17DFF30CF61D87883986E8B6 /* executor_std_test.cc in Sources */, 814724DE70EFC3DDF439CD78 /* executor_test.cc in Sources */, @@ -3974,6 +3995,7 @@ B3A309CCF5D75A555C7196E1 /* path_test.cc in Sources */, 9EE1447AA8E68DF98D0590FF /* precondition_test.cc in Sources */, 7B0F073BDB6D0D6E542E23D4 /* query.pb.cc in Sources */, + 6C92AD45A3619A18ECCA5B1F /* query_listener_test.cc in Sources */, 9617B75E9E27E7BA46D87EF3 /* query_test.cc in Sources */, FBBB13329D3B5827C21AE7AB /* reference_set_test.cc in Sources */, 2634E1C1971C05790B505824 /* resource_path_test.cc in Sources */, @@ -4005,6 +4027,8 @@ 2153BB93FDAD158FB61E068A /* transform_operations_test.mm in Sources */, 3D22F56C0DE7C7256C75DC06 /* tree_sorted_map_test.cc in Sources */, 918E3D35942CE493690C45CE /* user_test.cc in Sources */, + A5B8C273593D1BB6E8AE4CBA /* view_test.cc in Sources */, + 7F771EB980D9CFAAB4764233 /* view_testing.cc in Sources */, 774EF991E24E6215D99E3D52 /* watch_change_test.mm in Sources */, B592DB7DB492B1C1D5E67D01 /* write.pb.cc in Sources */, E51957EDECF741E1D3C3968A /* writer_test.cc in Sources */, @@ -4061,7 +4085,6 @@ B65D34A9203C995B0076A5E1 /* FIRTimestampTest.m in Sources */, 5492E058202154AB00B64F25 /* FSTAPIHelpers.mm in Sources */, 5492E03E2021401F00B64F25 /* FSTEventAccumulator.mm in Sources */, - 5492E067202154B900B64F25 /* FSTEventManagerTests.mm in Sources */, 54764FAF1FAA21B90085E60A /* FSTGoogleTestTests.mm in Sources */, 5492E03F2021401F00B64F25 /* FSTHelpers.mm in Sources */, 5491BC721FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */, @@ -4086,7 +4109,6 @@ 5492E0AC2021552D00B64F25 /* FSTMutationQueueTests.mm in Sources */, 5492E0A62021552D00B64F25 /* FSTPersistenceTestHelpers.mm in Sources */, 5492E0A22021552D00B64F25 /* FSTQueryCacheTests.mm in Sources */, - 5492E064202154B900B64F25 /* FSTQueryListenerTests.mm in Sources */, 5492E0B12021552D00B64F25 /* FSTRemoteDocumentCacheTests.mm in Sources */, 5492E0C92021557E00B64F25 /* FSTRemoteEventTests.mm in Sources */, 5492E0C72021557E00B64F25 /* FSTSerializerBetaTests.mm in Sources */, @@ -4094,7 +4116,6 @@ 5492E03320213FFC00B64F25 /* FSTSyncEngineTestDriver.mm in Sources */, 548180A5228DEF1A004F70CD /* FSTUserDataConverterTests.mm in Sources */, 5492E063202154B900B64F25 /* FSTViewSnapshotTest.mm in Sources */, - 5492E065202154B900B64F25 /* FSTViewTests.mm in Sources */, 5492E03C2021401F00B64F25 /* XCTestCase+Await.mm in Sources */, 618BBEAF20B89AAC00B5BCE7 /* annotations.pb.cc in Sources */, 5467FB08203E6A44009C9584 /* app_testing.mm in Sources */, @@ -4103,6 +4124,7 @@ B6FB4684208EA0EC00554BA2 /* async_queue_libdispatch_test.mm in Sources */, B6FB4685208EA0F000554BA2 /* async_queue_std_test.cc in Sources */, B6FB467D208E9D3C00554BA2 /* async_queue_test.cc in Sources */, + 11BC867491A6631D37DE56A8 /* async_testing.cc in Sources */, 54740A581FC914F000713A1A /* autoid_test.cc in Sources */, AB380D02201BC69F00D97691 /* bits_test.cc in Sources */, 7B86B1B21FD0EF2A67547F66 /* byte_string_test.cc in Sources */, @@ -4120,6 +4142,7 @@ 547E9A4222F9EA7300A275E0 /* document_set_test.cc in Sources */, AB6B908420322E4D00CC290A /* document_test.cc in Sources */, ABC1D7DD2023A04F00BA84F0 /* empty_credentials_provider_test.cc in Sources */, + D268E8770462354725981C25 /* event_manager_test.mm in Sources */, B6FB468E208F9BAB00554BA2 /* executor_libdispatch_test.mm in Sources */, B6FB468F208F9BAE00554BA2 /* executor_std_test.cc in Sources */, B6FB4690208F9BB300554BA2 /* executor_test.cc in Sources */, @@ -4166,6 +4189,7 @@ 5A080105CCBFDB6BF3F3772D /* path_test.cc in Sources */, 549CCA5920A36E1F00BCEB75 /* precondition_test.cc in Sources */, 544129DC21C2DDC800EFB9CC /* query.pb.cc in Sources */, + CD226D868CEFA9D557EF33A1 /* query_listener_test.cc in Sources */, 6F3CAC76D918D6B0917EDF92 /* query_test.cc in Sources */, 132E3483789344640A52F223 /* reference_set_test.cc in Sources */, B686F2B22025000D0028D6BE /* resource_path_test.cc in Sources */, @@ -4197,6 +4221,8 @@ 54A0352720A3AED0003E0143 /* transform_operations_test.mm in Sources */, 549CCA5120A36DBC00BCEB75 /* tree_sorted_map_test.cc in Sources */, ABC1D7DE2023A05300BA84F0 /* user_test.cc in Sources */, + 17473086EBACB98CDC3CC65C /* view_test.cc in Sources */, + DDDE74C752E65DE7D39A7166 /* view_testing.cc in Sources */, EB394AFBD8174C8921B6A0AD /* watch_change_test.mm in Sources */, 544129DE21C2DDC800EFB9CC /* write.pb.cc in Sources */, 3BA4EEA6153B3833F86B8104 /* writer_test.cc in Sources */, @@ -4258,7 +4284,6 @@ D9EF7FC0E3F8646B272B427E /* FSTAPIHelpers.mm in Sources */, 5492E082202154EC00B64F25 /* FSTDatastoreTests.mm in Sources */, 5492E041202143E700B64F25 /* FSTEventAccumulator.mm in Sources */, - ECEAED9FE1AAA9C748834757 /* FSTEventManagerTests.mm in Sources */, 1E6E2AE74B7C9DEDFC07E76B /* FSTGoogleTestTests.mm in Sources */, 5492E0422021440500B64F25 /* FSTHelpers.mm in Sources */, 5491BC731FB44593008B3588 /* FSTIntegrationTestCase.mm in Sources */, @@ -4283,7 +4308,6 @@ 4D85E7D0647904018F040545 /* FSTMutationQueueTests.mm in Sources */, C11E3349BA96A5DC07C94611 /* FSTPersistenceTestHelpers.mm in Sources */, 7166078E05192A41206045D8 /* FSTQueryCacheTests.mm in Sources */, - 2BCFA42FEA5657A17C5439B3 /* FSTQueryListenerTests.mm in Sources */, D52ABD7683382E2C8C60338A /* FSTRemoteDocumentCacheTests.mm in Sources */, E4BB7A34D8BA9AFE62B1F628 /* FSTRemoteEventTests.mm in Sources */, 5692A7F69B935A3D75624478 /* FSTSerializerBetaTests.mm in Sources */, @@ -4293,7 +4317,6 @@ 5492E07F202154EC00B64F25 /* FSTTransactionTests.mm in Sources */, 2F7D76FF225B550F83B95A72 /* FSTUserDataConverterTests.mm in Sources */, 795395CD088806F2012794C6 /* FSTViewSnapshotTest.mm in Sources */, - FAF50FBED03B089269307943 /* FSTViewTests.mm in Sources */, 6F45846C159D3C063DBD3CBE /* FirestoreEncoderTests.swift in Sources */, 5492E0442021457E00B64F25 /* XCTestCase+Await.mm in Sources */, 02EB33CC2590E1484D462912 /* annotations.pb.cc in Sources */, @@ -4303,6 +4326,7 @@ 45A5504D33D39C6F80302450 /* async_queue_libdispatch_test.mm in Sources */, 6F914209F46E6552B5A79570 /* async_queue_std_test.cc in Sources */, AD74843082C6465A676F16A7 /* async_queue_test.cc in Sources */, + 35C330499D50AC415B24C580 /* async_testing.cc in Sources */, 8F781F527ED72DC6C123689E /* autoid_test.cc in Sources */, 0B9BD73418289EFF91917934 /* bits_test.cc in Sources */, 52967C3DD7896BFA48840488 /* byte_string_test.cc in Sources */, @@ -4320,6 +4344,7 @@ 547E9A4322F9EA7300A275E0 /* document_set_test.cc in Sources */, A5175CA2E677E13CC5F23D72 /* document_test.cc in Sources */, 0A1B97E51BDE36DE4F6E3787 /* empty_credentials_provider_test.cc in Sources */, + D8673F8C7CE8172FE7AD9DEC /* event_manager_test.mm in Sources */, B6BF6EFEF887B072068BA658 /* executor_libdispatch_test.mm in Sources */, 125B1048ECB755C2106802EB /* executor_std_test.cc in Sources */, DABB9FB61B1733F985CBF713 /* executor_test.cc in Sources */, @@ -4366,6 +4391,7 @@ 6105A1365831B79A7DEEA4F3 /* path_test.cc in Sources */, 4194B7BB8B0352E1AC5D69B9 /* precondition_test.cc in Sources */, 63B91FC476F3915A44F00796 /* query.pb.cc in Sources */, + BC8DFBCB023DBD914E27AA7D /* query_listener_test.cc in Sources */, DE435F33CE563E238868D318 /* query_test.cc in Sources */, B921A4F35B58925D958DD9A6 /* reference_set_test.cc in Sources */, 5DDEC1A08F13226271FE636E /* resource_path_test.cc in Sources */, @@ -4397,6 +4423,8 @@ 26C8485FF6BDF35CCDDAC79E /* transform_operations_test.mm in Sources */, 5DA343D28AE05B0B2FE9FFB3 /* tree_sorted_map_test.cc in Sources */, D43F7601F3F3DE3125346D42 /* user_test.cc in Sources */, + B63D84B2980C7DEE7E6E4708 /* view_test.cc in Sources */, + 48D1B38B93D34F1B82320577 /* view_testing.cc in Sources */, A8FFAF70D6E7ECA09605A625 /* watch_change_test.mm in Sources */, E435450184AEB51EE8435F66 /* write.pb.cc in Sources */, AFB0ACCF130713DF6495E110 /* writer_test.cc in Sources */, diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests_iOS.xcscheme index 03e9133eefe..f4896bc192e 100644 --- a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests_iOS.xcscheme +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_IntegrationTests_iOS.xcscheme @@ -26,7 +26,17 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES" + enableASanStackUseAfterReturn = "YES"> + + + + @@ -39,17 +49,6 @@ - - - - - - - - ../..` declaration to use local sources if the +# Podfile has been configured to operate that way. +def maybe_local_pod(name) + if use_local_sources() + pod name, :path => '../..' + end +end + +# Adds local pod declarations for all Firestore's transitive dependencies if +# required. +def configure_local_pods() + # Firestore is always local; that's what's under development here. + pod 'FirebaseFirestore', :path => '../../' + + # FirebaseCore must always be a local pod so that CI builds that make changes + # to its podspec can still function. See Firestore-*-xcodebuild in + # scripts/install_prereqs.sh for more details. + pod 'FirebaseCore', :path => '../..' + + # Pull in local sources conditionally. + maybe_local_pod 'FirebaseAuth' + maybe_local_pod 'FirebaseAuthInterop' + maybe_local_pod 'GoogleUtilities' + + if xcode_major_version() >= 9 + # Firestore still compiles with Xcode 8 to help verify general conformance + # with C++11 by using an older compiler that doesn't have as many + # extensions from later versions of the language. However, Firebase as a + # whole does not support this environment and @available checks in + # GoogleDataTransport would otherwise break this build. + # + # Firestore doesn't depend on GoogleDataTransport directly--it comes in as + # a dependency of FirebaseCoreDiagnostics. Luckily, FirebaseCore does not + # strongly depend on this, so we can edit the dependency out in the podspec + # and then avoid adding those dependencies here to prevent CocoaPods from + # importing it. + # + # This list should include the transitive closure of all dependencies of + # FirebaseCoreDiagnostics, except GoogleUtilities which we otherwise need. + maybe_local_pod 'FirebaseCoreDiagnostics' + maybe_local_pod 'FirebaseCoreDiagnosticsInterop' + maybe_local_pod 'GoogleDataTransport' + maybe_local_pod 'GoogleDataTransportCCTSupport' + end +end 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/CoreOnly', '6.6.0' + # corresponding podspecs. + pod 'Firebase/CoreOnly', '6.9.0' - pod 'FirebaseFirestore', :path => '../../' + configure_local_pods() target 'Firestore_Tests_iOS' do inherit! :search_paths @@ -85,7 +145,7 @@ end target 'Firestore_Example_macOS' do platform :osx, '10.11' - pod 'FirebaseFirestore', :path => '../../' + configure_local_pods() target 'Firestore_Tests_macOS' do inherit! :search_paths @@ -114,7 +174,7 @@ end target 'Firestore_Example_tvOS' do platform :tvos, '10.0' - pod 'FirebaseFirestore', :path => '../../' + configure_local_pods() target 'Firestore_Tests_tvOS' do inherit! :search_paths diff --git a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm index 8ea301b275e..3e6cb71040c 100644 --- a/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm +++ b/Firestore/Example/Tests/API/FIRQuerySnapshotTests.mm @@ -98,8 +98,8 @@ - (void)testIncludeMetadataChanges { DocumentSet oldDocuments = DocSet(DocumentComparator::ByKey(), {doc1Old, doc2Old}); DocumentSet newDocuments = DocSet(DocumentComparator::ByKey(), {doc2New, doc2New}); std::vector documentChanges{ - DocumentViewChange(doc1New, DocumentViewChange::Type::kMetadata), - DocumentViewChange(doc2New, DocumentViewChange::Type::kModified), + DocumentViewChange(doc1New, DocumentViewChange::Type::Metadata), + DocumentViewChange(doc2New, DocumentViewChange::Type::Modified), }; std::shared_ptr firestore = FSTTestFirestore().wrapped; diff --git a/Firestore/Example/Tests/API/FSTAPIHelpers.mm b/Firestore/Example/Tests/API/FSTAPIHelpers.mm index 46441c7539e..82a793e6d11 100644 --- a/Firestore/Example/Tests/API/FSTAPIHelpers.mm +++ b/Firestore/Example/Tests/API/FSTAPIHelpers.mm @@ -138,7 +138,7 @@ Doc(documentKey, 1, doc, hasPendingWrites ? DocumentState::kLocalMutations : DocumentState::kSynced); newDocuments = newDocuments.insert(docToAdd); - documentChanges.emplace_back(docToAdd, DocumentViewChange::Type::kAdded); + documentChanges.emplace_back(docToAdd, DocumentViewChange::Type::Added); if (hasPendingWrites) { mutatedKeys = mutatedKeys.insert(testutil::Key(documentKey)); } diff --git a/Firestore/Example/Tests/API/FSTUserDataConverterTests.mm b/Firestore/Example/Tests/API/FSTUserDataConverterTests.mm index b4414ffa5e5..98497067ea4 100644 --- a/Firestore/Example/Tests/API/FSTUserDataConverterTests.mm +++ b/Firestore/Example/Tests/API/FSTUserDataConverterTests.mm @@ -137,7 +137,7 @@ - (void)testConvertsGeoPoints { } - (void)testConvertsBlobs { - NSArray *values = @[ FSTTestData(1, 2, 3), FSTTestData(1, 2) ]; + NSArray *values = @[ FSTTestData(1, 2, 3, -1), FSTTestData(1, 2, -1) ]; for (NSData *value in values) { FieldValue wrapped = FSTTestFieldValue(value); XCTAssertEqual(wrapped.type(), FieldValue::Type::Blob); diff --git a/Firestore/Example/Tests/Core/FSTEventManagerTests.mm b/Firestore/Example/Tests/Core/FSTEventManagerTests.mm deleted file mode 100644 index b33fcbd1f3c..00000000000 --- a/Firestore/Example/Tests/Core/FSTEventManagerTests.mm +++ /dev/null @@ -1,179 +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 -#import - -#include -#include -#include - -#import "Firestore/Source/Core/FSTSyncEngine.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -#include "Firestore/core/src/firebase/firestore/core/event_manager.h" -#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" -#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" -#include "Firestore/core/src/firebase/firestore/model/document_set.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" -#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" -#include "Firestore/core/test/firebase/firestore/testutil/xcgmock.h" - -using firebase::firestore::core::EventListener; -using firebase::firestore::core::EventManager; -using firebase::firestore::core::ListenOptions; -using firebase::firestore::core::QueryListener; -using firebase::firestore::core::SyncEngineCallback; -using firebase::firestore::core::ViewSnapshot; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::DocumentSet; -using firebase::firestore::model::OnlineState; -using firebase::firestore::util::StatusOr; -using firebase::firestore::util::StatusOrCallback; - -using firebase::firestore::testutil::Query; -using testing::ElementsAre; - -NS_ASSUME_NONNULL_BEGIN - -namespace { - -ViewSnapshot::Listener NoopViewSnapshotHandler() { - return EventListener::Create([](const StatusOr &) {}); -} - -std::shared_ptr NoopQueryListener(core::Query query) { - return QueryListener::Create(std::move(query), ListenOptions::DefaultOptions(), - NoopViewSnapshotHandler()); -} - -} // namespace - -@interface FSTEventManagerTests : XCTestCase -@end - -@implementation FSTEventManagerTests - -// TODO(wilhuff): re-enable once FSTSyncEngine has been ported to C++ -- (void)DISABLED_testHandlesManyListenersPerQuery { - core::Query query = Query("foo/bar"); - auto listener1 = NoopQueryListener(query); - auto listener2 = NoopQueryListener(query); - - FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); - OCMExpect([syncEngineMock setCallback:static_cast([OCMArg anyPointer])]); - EventManager eventManager(syncEngineMock); - - OCMExpect([syncEngineMock listenToQuery:query]); - eventManager.AddQueryListener(listener1); - OCMVerifyAll((id)syncEngineMock); - - eventManager.AddQueryListener(listener2); - eventManager.RemoveQueryListener(listener2); - - OCMExpect([syncEngineMock stopListeningToQuery:query]); - eventManager.RemoveQueryListener(listener1); - OCMVerifyAll((id)syncEngineMock); -} - -- (void)testHandlesUnlistenOnUnknownListenerGracefully { - core::Query query = Query("foo/bar"); - auto listener = NoopQueryListener(query); - - FSTSyncEngine *syncEngineMock = OCMStrictClassMock([FSTSyncEngine class]); - OCMExpect([syncEngineMock setCallback:static_cast([OCMArg anyPointer])]); - EventManager eventManager(syncEngineMock); - - eventManager.RemoveQueryListener(listener); - OCMVerifyAll((id)syncEngineMock); -} - -- (ViewSnapshot)makeEmptyViewSnapshotWithQuery:(const core::Query &)query { - DocumentSet emptyDocs{query.Comparator()}; - // sync_state_changed has to be `true` to prevent an assertion about a meaningless view snapshot. - return ViewSnapshot{ - query, emptyDocs, emptyDocs, {}, DocumentKeySet{}, false, /*sync_state_changed=*/true, false}; -} - -// TODO(wilhuff): re-enable once FSTSyncEngine has been ported to C++ -- (void)DISABLED_testNotifiesListenersInTheRightOrder { - core::Query query1 = Query("foo/bar"); - core::Query query2 = Query("bar/baz"); - NSMutableArray *eventOrder = [NSMutableArray array]; - - auto listener1 = QueryListener::Create( - query1, [eventOrder](StatusOr) { [eventOrder addObject:@"listener1"]; }); - - auto listener2 = QueryListener::Create( - query2, [eventOrder](StatusOr) { [eventOrder addObject:@"listener2"]; }); - - auto listener3 = QueryListener::Create( - query1, [eventOrder](StatusOr) { [eventOrder addObject:@"listener3"]; }); - - FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); - EventManager eventManager(syncEngineMock); - - eventManager.AddQueryListener(listener1); - eventManager.AddQueryListener(listener2); - eventManager.AddQueryListener(listener3); - OCMVerify([syncEngineMock listenToQuery:query1]); - OCMVerify([syncEngineMock listenToQuery:query2]); - - ViewSnapshot snapshot1 = [self makeEmptyViewSnapshotWithQuery:query1]; - ViewSnapshot snapshot2 = [self makeEmptyViewSnapshotWithQuery:query2]; - eventManager.OnViewSnapshots({snapshot1, snapshot2}); - - NSArray *expected = @[ @"listener1", @"listener3", @"listener2" ]; - XCTAssertEqualObjects(eventOrder, expected); -} - -- (void)testWillForwardOnlineStateChanges { - core::Query query = Query("foo/bar"); - - class FakeQueryListener : public QueryListener { - public: - explicit FakeQueryListener(core::Query query) - : QueryListener( - std::move(query), ListenOptions::DefaultOptions(), NoopViewSnapshotHandler()) { - } - - void OnOnlineStateChanged(OnlineState online_state) override { - events.push_back(online_state); - } - - std::vector events; - }; - - auto fake_listener = std::make_shared(query); - - FSTSyncEngine *syncEngineMock = OCMClassMock([FSTSyncEngine class]); - OCMExpect([syncEngineMock setCallback:static_cast([OCMArg anyPointer])]); - EventManager eventManager(syncEngineMock); - - eventManager.AddQueryListener(fake_listener); - XC_ASSERT_THAT(fake_listener->events, ElementsAre(OnlineState::Unknown)); - - eventManager.HandleOnlineStateChange(OnlineState::Online); - XC_ASSERT_THAT(fake_listener->events, ElementsAre(OnlineState::Unknown, OnlineState::Online)); - - OCMVerifyAll((id)syncEngineMock); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm b/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm deleted file mode 100644 index a2e35261080..00000000000 --- a/Firestore/Example/Tests/Core/FSTQueryListenerTests.mm +++ /dev/null @@ -1,502 +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 - -#include -#include -#include - -#import "Firestore/Source/Core/FSTView.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -#include "Firestore/core/include/firebase/firestore/firestore_errors.h" -#include "Firestore/core/src/firebase/firestore/core/event_listener.h" -#include "Firestore/core/src/firebase/firestore/core/listen_options.h" -#include "Firestore/core/src/firebase/firestore/core/query_listener.h" -#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" -#include "Firestore/core/src/firebase/firestore/model/document_set.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" -#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" -#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.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 "Firestore/core/test/firebase/firestore/testutil/xcgmock.h" - -using firebase::firestore::Error; -using firebase::firestore::core::AsyncEventListener; -using firebase::firestore::core::EventListener; -using firebase::firestore::core::DocumentViewChange; -using firebase::firestore::core::EventListener; -using firebase::firestore::core::ListenOptions; -using firebase::firestore::core::QueryListener; -using firebase::firestore::core::ViewSnapshot; -using firebase::firestore::model::Document; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::DocumentSet; -using firebase::firestore::model::DocumentState; -using firebase::firestore::model::OnlineState; -using firebase::firestore::remote::TargetChange; -using firebase::firestore::util::DelayedConstructor; -using firebase::firestore::util::ExecutorLibdispatch; -using firebase::firestore::util::Status; -using firebase::firestore::util::StatusOr; - -using firebase::firestore::testutil::Doc; -using firebase::firestore::testutil::Map; -using firebase::firestore::testutil::Query; -using testing::ElementsAre; -using testing::IsEmpty; - -NS_ASSUME_NONNULL_BEGIN - -namespace { - -ViewSnapshot ExcludingMetadataChanges(const ViewSnapshot &snapshot) { - return ViewSnapshot{ - snapshot.query(), - snapshot.documents(), - snapshot.old_documents(), - snapshot.document_changes(), - snapshot.mutated_keys(), - snapshot.from_cache(), - snapshot.sync_state_changed(), - /*excludes_metadata_changes=*/true, - }; -} - -ViewSnapshot::Listener Accumulating(std::vector *values) { - return EventListener::Create( - [values](const StatusOr &maybe_snapshot) { - values->push_back(maybe_snapshot.ValueOrDie()); - }); -} - -} // namespace - -@interface FSTQueryListenerTests : XCTestCase -@end - -@implementation FSTQueryListenerTests { - std::shared_ptr _executor; - ListenOptions _includeMetadataChanges; -} - -- (void)setUp { - _executor = std::make_shared( - dispatch_queue_create("FSTQueryListenerTests Queue", DISPATCH_QUEUE_SERIAL)); - _includeMetadataChanges = ListenOptions::FromIncludeMetadataChanges(true); -} - -- (void)testRaisesCollectionEvents { - std::vector accum; - std::vector otherAccum; - - core::Query query = Query("rooms"); - Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros")); - Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); - Document doc2prime = Doc("rooms/Hades", 3, Map("name", "Hades", "owner", "Jonny")); - - auto listener = QueryListener::Create(query, _includeMetadataChanges, Accumulating(&accum)); - auto otherListener = QueryListener::Create(query, Accumulating(&otherAccum)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {doc1, doc2}, absl::nullopt).value(); - ViewSnapshot snap2 = FSTTestApplyChanges(view, {doc2prime}, absl::nullopt).value(); - - DocumentViewChange change1{doc1, DocumentViewChange::Type::kAdded}; - DocumentViewChange change2{doc2, DocumentViewChange::Type::kAdded}; - DocumentViewChange change3{doc2prime, DocumentViewChange::Type::kModified}; - DocumentViewChange change4{doc2prime, DocumentViewChange::Type::kAdded}; - - listener->OnViewSnapshot(snap1); - listener->OnViewSnapshot(snap2); - otherListener->OnViewSnapshot(snap2); - - XC_ASSERT_THAT(accum, ElementsAre(snap1, snap2)); - XC_ASSERT_THAT(accum[0].document_changes(), ElementsAre(change1, change2)); - XC_ASSERT_THAT(accum[1].document_changes(), ElementsAre(change3)); - - ViewSnapshot expectedSnap2{snap2.query(), - snap2.documents(), - /*old_documents=*/DocumentSet{snap2.query().Comparator()}, - /*document_changes=*/{change1, change4}, - snap2.mutated_keys(), - snap2.from_cache(), - /*sync_state_changed=*/true, - /*excludes_metadata_changes=*/true}; - XC_ASSERT_THAT(otherAccum, ElementsAre(expectedSnap2)); -} - -- (void)testRaisesErrorEvent { - __block std::vector accum; - core::Query query = Query("rooms/Eros"); - - auto listener = QueryListener::Create(query, ^(const StatusOr &maybe_snapshot) { - accum.push_back(maybe_snapshot.status()); - }); - - Status testError{Error::Unauthenticated, "Some info"}; - listener->OnError(testError); - - XC_ASSERT_THAT(accum, ElementsAre(testError)); -} - -- (void)testRaisesEventForEmptyCollectionAfterSync { - std::vector accum; - core::Query query = Query("rooms"); - - auto listener = QueryListener::Create(query, _includeMetadataChanges, Accumulating(&accum)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {}, absl::nullopt).value(); - ViewSnapshot snap2 = FSTTestApplyChanges(view, {}, FSTTestTargetChangeMarkCurrent()).value(); - - listener->OnViewSnapshot(snap1); - XC_ASSERT_THAT(accum, IsEmpty()); - - listener->OnViewSnapshot(snap2); - XC_ASSERT_THAT(accum, ElementsAre(snap2)); -} - -- (void)testMutingAsyncListenerPreventsAllSubsequentEvents { - std::vector accum; - - core::Query query = Query("rooms/Eros"); - Document doc1 = Doc("rooms/Eros", 3, Map("name", "Eros")); - Document doc2 = Doc("rooms/Eros", 4, Map("name", "Eros2")); - - std::shared_ptr> listener = - AsyncEventListener::Create( - _executor, EventListener::Create( - [&accum, &listener](const StatusOr &maybe_snapshot) { - accum.push_back(maybe_snapshot.ValueOrDie()); - listener->Mute(); - })); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot viewSnapshot1 = FSTTestApplyChanges(view, {doc1}, absl::nullopt).value(); - ViewSnapshot viewSnapshot2 = FSTTestApplyChanges(view, {doc2}, absl::nullopt).value(); - - listener->OnEvent(viewSnapshot1); - listener->OnEvent(viewSnapshot2); - - // Drain queue - XCTestExpectation *expectation = [self expectationWithDescription:@"Queue drained"]; - _executor->Execute([=] { [expectation fulfill]; }); - - [self waitForExpectationsWithTimeout:4.0 - handler:^(NSError *_Nullable expectationError) { - if (expectationError) { - XCTFail(@"Error waiting for timeout: %@", expectationError); - } - }]; - - // We should get the first snapshot but not the second. - XC_ASSERT_THAT(accum, ElementsAre(viewSnapshot1)); -} - -- (void)testDoesNotRaiseEventsForMetadataChangesUnlessSpecified { - std::vector filteredAccum; - std::vector fullAccum; - - core::Query query = Query("rooms"); - Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros")); - Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); - - auto filteredListener = QueryListener::Create(query, Accumulating(&filteredAccum)); - auto fullListener = - QueryListener::Create(query, _includeMetadataChanges, Accumulating(&fullAccum)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {doc1}, absl::nullopt).value(); - - TargetChange ackTarget = FSTTestTargetChangeAckDocuments({doc1.key()}); - ViewSnapshot snap2 = FSTTestApplyChanges(view, {}, ackTarget).value(); - ViewSnapshot snap3 = FSTTestApplyChanges(view, {doc2}, absl::nullopt).value(); - - filteredListener->OnViewSnapshot(snap1); // local event - filteredListener->OnViewSnapshot(snap2); // no event - filteredListener->OnViewSnapshot(snap3); // doc2 update - - fullListener->OnViewSnapshot(snap1); // local event - fullListener->OnViewSnapshot(snap2); // state change event - fullListener->OnViewSnapshot(snap3); // doc2 update - - XC_ASSERT_THAT(filteredAccum, - ElementsAre(ExcludingMetadataChanges(snap1), ExcludingMetadataChanges(snap3))); - XC_ASSERT_THAT(fullAccum, ElementsAre(snap1, snap2, snap3)); -} - -- (void)testRaisesDocumentMetadataEventsOnlyWhenSpecified { - std::vector filteredAccum; - std::vector fullAccum; - - core::Query query = Query("rooms"); - Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros"), DocumentState::kLocalMutations); - Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); - Document doc1Prime = Doc("rooms/Eros", 1, Map("name", "Eros")); - Document doc3 = Doc("rooms/Other", 3, Map("name", "Other")); - - ListenOptions options( - /*include_query_metadata_changes=*/false, - /*include_document_metadata_changes=*/true, - /*wait_for_sync_when_online=*/false); - - auto filteredListener = QueryListener::Create(query, Accumulating(&filteredAccum)); - auto fullListener = QueryListener::Create(query, options, Accumulating(&fullAccum)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {doc1, doc2}, absl::nullopt).value(); - ViewSnapshot snap2 = FSTTestApplyChanges(view, {doc1Prime}, absl::nullopt).value(); - ViewSnapshot snap3 = FSTTestApplyChanges(view, {doc3}, absl::nullopt).value(); - - DocumentViewChange change1{doc1, DocumentViewChange::Type::kAdded}; - DocumentViewChange change2{doc2, DocumentViewChange::Type::kAdded}; - DocumentViewChange change3{doc1Prime, DocumentViewChange::Type::kMetadata}; - DocumentViewChange change4{doc3, DocumentViewChange::Type::kAdded}; - - filteredListener->OnViewSnapshot(snap1); - filteredListener->OnViewSnapshot(snap2); - filteredListener->OnViewSnapshot(snap3); - fullListener->OnViewSnapshot(snap1); - fullListener->OnViewSnapshot(snap2); - fullListener->OnViewSnapshot(snap3); - - XC_ASSERT_THAT(filteredAccum, - ElementsAre(ExcludingMetadataChanges(snap1), ExcludingMetadataChanges(snap3))); - XC_ASSERT_THAT(filteredAccum[0].document_changes(), ElementsAre(change1, change2)); - XC_ASSERT_THAT(filteredAccum[1].document_changes(), ElementsAre(change4)); - - XC_ASSERT_THAT(fullAccum, ElementsAre(snap1, snap2, snap3)); - XC_ASSERT_THAT(fullAccum[0].document_changes(), ElementsAre(change1, change2)); - XC_ASSERT_THAT(fullAccum[1].document_changes(), ElementsAre(change3)); - XC_ASSERT_THAT(fullAccum[2].document_changes(), ElementsAre(change4)); -} - -- (void)testRaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges { - std::vector fullAccum; - - core::Query query = Query("rooms"); - Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros"), DocumentState::kLocalMutations); - Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades"), DocumentState::kLocalMutations); - Document doc1Prime = Doc("rooms/Eros", 1, Map("name", "Eros")); - Document doc2Prime = Doc("rooms/Hades", 2, Map("name", "Hades")); - Document doc3 = Doc("rooms/Other", 3, Map("name", "Other")); - - ListenOptions options( - /*include_query_metadata_changes=*/true, - /*include_document_metadata_changes=*/false, - /*wait_for_sync_when_online=*/false); - auto fullListener = QueryListener::Create(query, options, Accumulating(&fullAccum)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {doc1, doc2}, absl::nullopt).value(); - ViewSnapshot snap2 = FSTTestApplyChanges(view, {doc1Prime}, absl::nullopt).value(); - ViewSnapshot snap3 = FSTTestApplyChanges(view, {doc3}, absl::nullopt).value(); - ViewSnapshot snap4 = FSTTestApplyChanges(view, {doc2Prime}, absl::nullopt).value(); - - fullListener->OnViewSnapshot(snap1); - fullListener->OnViewSnapshot(snap2); // Emits no events. - fullListener->OnViewSnapshot(snap3); - fullListener->OnViewSnapshot(snap4); // Metadata change event. - - ViewSnapshot expectedSnap4{ - snap4.query(), - snap4.documents(), - snap3.documents(), - /*document_changes=*/{}, - snap4.mutated_keys(), - snap4.from_cache(), - snap4.sync_state_changed(), - /*excludes_metadata_changes=*/true // This test excludes document metadata changes - }; - - XC_ASSERT_THAT(fullAccum, ElementsAre(ExcludingMetadataChanges(snap1), - ExcludingMetadataChanges(snap3), expectedSnap4)); -} - -- (void)testMetadataOnlyDocumentChangesAreFilteredOutWhenIncludeDocumentMetadataChangesIsFalse { - std::vector filteredAccum; - - core::Query query = Query("rooms"); - Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros"), DocumentState::kLocalMutations); - Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); - Document doc1Prime = Doc("rooms/Eros", 1, Map("name", "Eros")); - Document doc3 = Doc("rooms/Other", 3, Map("name", "Other")); - - auto filteredListener = QueryListener::Create(query, Accumulating(&filteredAccum)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {doc1, doc2}, absl::nullopt).value(); - ViewSnapshot snap2 = FSTTestApplyChanges(view, {doc1Prime, doc3}, absl::nullopt).value(); - - DocumentViewChange change3{doc3, DocumentViewChange::Type::kAdded}; - - filteredListener->OnViewSnapshot(snap1); - filteredListener->OnViewSnapshot(snap2); - - ViewSnapshot expectedSnap2{snap2.query(), - snap2.documents(), - snap1.documents(), - /*document_changes=*/{change3}, - snap2.mutated_keys(), - snap2.from_cache(), - snap2.sync_state_changed(), - /*excludes_metadata_changes=*/true}; - XC_ASSERT_THAT(filteredAccum, ElementsAre(ExcludingMetadataChanges(snap1), expectedSnap2)); -} - -- (void)testWillWaitForSyncIfOnline { - std::vector events; - - core::Query query = Query("rooms"); - Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros")); - Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); - - ListenOptions options( - /*include_query_metadata_changes=*/false, - /*include_document_metadata_changes=*/false, - /*wait_for_sync_when_online=*/true); - auto listener = QueryListener::Create(query, options, Accumulating(&events)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {doc1}, absl::nullopt).value(); - ViewSnapshot snap2 = FSTTestApplyChanges(view, {doc2}, absl::nullopt).value(); - ViewSnapshot snap3 = - FSTTestApplyChanges(view, {}, FSTTestTargetChangeAckDocuments({doc1.key(), doc2.key()})) - .value(); - - listener->OnOnlineStateChanged(OnlineState::Online); // no event - listener->OnViewSnapshot(snap1); - listener->OnOnlineStateChanged(OnlineState::Unknown); - listener->OnOnlineStateChanged(OnlineState::Online); - listener->OnViewSnapshot(snap2); - listener->OnViewSnapshot(snap3); - - DocumentViewChange change1{doc1, DocumentViewChange::Type::kAdded}; - DocumentViewChange change2{doc2, DocumentViewChange::Type::kAdded}; - ViewSnapshot expectedSnap{snap3.query(), - snap3.documents(), - /*old_documents=*/DocumentSet{snap3.query().Comparator()}, - /*document_changes=*/{change1, change2}, - snap3.mutated_keys(), - /*from_cache=*/false, - /*sync_state_changed=*/true, - /*excludes_metadata_changes=*/true}; - XC_ASSERT_THAT(events, ElementsAre(expectedSnap)); -} - -- (void)testWillRaiseInitialEventWhenGoingOffline { - std::vector events; - - core::Query query = Query("rooms"); - Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros")); - Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); - - ListenOptions options( - /*include_query_metadata_changes=*/false, - /*include_document_metadata_changes=*/false, - /*wait_for_sync_when_online=*/true); - - auto listener = QueryListener::Create(query, options, Accumulating(&events)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {doc1}, absl::nullopt).value(); - ViewSnapshot snap2 = FSTTestApplyChanges(view, {doc2}, absl::nullopt).value(); - - listener->OnOnlineStateChanged(OnlineState::Online); // no event - listener->OnViewSnapshot(snap1); // no event - listener->OnOnlineStateChanged(OnlineState::Offline); // event - listener->OnOnlineStateChanged(OnlineState::Unknown); // no event - listener->OnOnlineStateChanged(OnlineState::Offline); // no event - listener->OnViewSnapshot(snap2); // another event - - DocumentViewChange change1{doc1, DocumentViewChange::Type::kAdded}; - DocumentViewChange change2{doc2, DocumentViewChange::Type::kAdded}; - ViewSnapshot expectedSnap1{query, - /*documents=*/snap1.documents(), - /*old_documents=*/DocumentSet{snap1.query().Comparator()}, - /*document_changes=*/{change1}, - snap1.mutated_keys(), - /*from_cache=*/true, - /*sync_state_changed=*/true, - /*excludes_metadata_changes=*/true}; - - ViewSnapshot expectedSnap2{query, - /*documents=*/snap2.documents(), - /*old_documents=*/snap1.documents(), - /*document_changes=*/{change2}, - snap2.mutated_keys(), - /*from_cache=*/true, - /*sync_state_changed=*/false, - /*excludes_metadata_changes=*/true}; - XC_ASSERT_THAT(events, ElementsAre(expectedSnap1, expectedSnap2)); -} - -- (void)testWillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs { - std::vector events; - - core::Query query = Query("rooms"); - auto listener = QueryListener::Create(query, Accumulating(&events)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {}, absl::nullopt).value(); - - listener->OnOnlineStateChanged(OnlineState::Online); // no event - listener->OnViewSnapshot(snap1); // no event - listener->OnOnlineStateChanged(OnlineState::Offline); // event - - ViewSnapshot expectedSnap{query, - /*documents=*/snap1.documents(), - /*old_documents=*/DocumentSet{snap1.query().Comparator()}, - /*document_changes=*/{}, - snap1.mutated_keys(), - /*from_cache=*/true, - /*sync_state_changed=*/true, - /*excludes_metadata_changes=*/true}; - XC_ASSERT_THAT(events, ElementsAre(expectedSnap)); -} - -- (void)testWillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs { - std::vector events; - - core::Query query = Query("rooms"); - auto listener = QueryListener::Create(query, Accumulating(&events)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - ViewSnapshot snap1 = FSTTestApplyChanges(view, {}, absl::nullopt).value(); - - listener->OnOnlineStateChanged(OnlineState::Offline); // no event - listener->OnViewSnapshot(snap1); // event - - ViewSnapshot expectedSnap{query, - /*documents=*/snap1.documents(), - /*old_documents=*/DocumentSet{snap1.query().Comparator()}, - /*document_changes=*/{}, - snap1.mutated_keys(), - /*from_cache=*/true, - /*sync_state_changed=*/true, - /*excludes_metadata_changes=*/true}; - XC_ASSERT_THAT(events, ElementsAre(expectedSnap)); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h b/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h deleted file mode 100644 index 5d5c9813276..00000000000 --- a/Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h +++ /dev/null @@ -1,35 +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 - -#include - -#import "Firestore/Source/Core/FSTSyncEngine.h" - -#include "Firestore/core/src/firebase/firestore/model/document_key.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTSyncEngine (Testing) - -/** Returns the current set of limbo document keys and their associated target IDs. */ -- (std::map) - currentLimboDocuments; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm index c31f982313a..9088b596850 100644 --- a/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm +++ b/Firestore/Example/Tests/Core/FSTViewSnapshotTest.mm @@ -46,7 +46,7 @@ @implementation FSTViewSnapshotTests - (void)testDocumentChangeConstructor { Document doc = Doc("a/b", 0, Map()); - DocumentViewChange::Type type = DocumentViewChange::Type::kModified; + DocumentViewChange::Type type = DocumentViewChange::Type::Modified; DocumentViewChange change{doc, type}; XCTAssertEqual(change.document(), doc); XCTAssertEqual(change.type(), type); @@ -65,43 +65,43 @@ - (void)testTrack { Document docModifiedThenRemoved = Doc("b/4", 0, Map()); Document docModifiedThenModified = Doc("b/5", 0, Map()); - set.AddChange(DocumentViewChange{docAdded, DocumentViewChange::Type::kAdded}); - set.AddChange(DocumentViewChange{docRemoved, DocumentViewChange::Type::kRemoved}); - set.AddChange(DocumentViewChange{docModified, DocumentViewChange::Type::kModified}); - set.AddChange(DocumentViewChange{docAddedThenModified, DocumentViewChange::Type::kAdded}); - set.AddChange(DocumentViewChange{docAddedThenModified, DocumentViewChange::Type::kModified}); - set.AddChange(DocumentViewChange{docAddedThenRemoved, DocumentViewChange::Type::kAdded}); - set.AddChange(DocumentViewChange{docAddedThenRemoved, DocumentViewChange::Type::kRemoved}); - set.AddChange(DocumentViewChange{docRemovedThenAdded, DocumentViewChange::Type::kRemoved}); - set.AddChange(DocumentViewChange{docRemovedThenAdded, DocumentViewChange::Type::kAdded}); - set.AddChange(DocumentViewChange{docModifiedThenRemoved, DocumentViewChange::Type::kModified}); - set.AddChange(DocumentViewChange{docModifiedThenRemoved, DocumentViewChange::Type::kRemoved}); - set.AddChange(DocumentViewChange{docModifiedThenModified, DocumentViewChange::Type::kModified}); - set.AddChange(DocumentViewChange{docModifiedThenModified, DocumentViewChange::Type::kModified}); + set.AddChange(DocumentViewChange{docAdded, DocumentViewChange::Type::Added}); + set.AddChange(DocumentViewChange{docRemoved, DocumentViewChange::Type::Removed}); + set.AddChange(DocumentViewChange{docModified, DocumentViewChange::Type::Modified}); + set.AddChange(DocumentViewChange{docAddedThenModified, DocumentViewChange::Type::Added}); + set.AddChange(DocumentViewChange{docAddedThenModified, DocumentViewChange::Type::Modified}); + set.AddChange(DocumentViewChange{docAddedThenRemoved, DocumentViewChange::Type::Added}); + set.AddChange(DocumentViewChange{docAddedThenRemoved, DocumentViewChange::Type::Removed}); + set.AddChange(DocumentViewChange{docRemovedThenAdded, DocumentViewChange::Type::Removed}); + set.AddChange(DocumentViewChange{docRemovedThenAdded, DocumentViewChange::Type::Added}); + set.AddChange(DocumentViewChange{docModifiedThenRemoved, DocumentViewChange::Type::Modified}); + set.AddChange(DocumentViewChange{docModifiedThenRemoved, DocumentViewChange::Type::Removed}); + set.AddChange(DocumentViewChange{docModifiedThenModified, DocumentViewChange::Type::Modified}); + set.AddChange(DocumentViewChange{docModifiedThenModified, DocumentViewChange::Type::Modified}); std::vector changes = set.GetChanges(); XCTAssertEqual(changes.size(), 7); XCTAssertEqual(changes[0].document(), docAdded); - XCTAssertEqual(changes[0].type(), DocumentViewChange::Type::kAdded); + XCTAssertEqual(changes[0].type(), DocumentViewChange::Type::Added); XCTAssertEqual(changes[1].document(), docRemoved); - XCTAssertEqual(changes[1].type(), DocumentViewChange::Type::kRemoved); + XCTAssertEqual(changes[1].type(), DocumentViewChange::Type::Removed); XCTAssertEqual(changes[2].document(), docModified); - XCTAssertEqual(changes[2].type(), DocumentViewChange::Type::kModified); + XCTAssertEqual(changes[2].type(), DocumentViewChange::Type::Modified); XCTAssertEqual(changes[3].document(), docAddedThenModified); - XCTAssertEqual(changes[3].type(), DocumentViewChange::Type::kAdded); + XCTAssertEqual(changes[3].type(), DocumentViewChange::Type::Added); XCTAssertEqual(changes[4].document(), docRemovedThenAdded); - XCTAssertEqual(changes[4].type(), DocumentViewChange::Type::kModified); + XCTAssertEqual(changes[4].type(), DocumentViewChange::Type::Modified); XCTAssertEqual(changes[5].document(), docModifiedThenRemoved); - XCTAssertEqual(changes[5].type(), DocumentViewChange::Type::kRemoved); + XCTAssertEqual(changes[5].type(), DocumentViewChange::Type::Removed); XCTAssertEqual(changes[6].document(), docModifiedThenModified); - XCTAssertEqual(changes[6].type(), DocumentViewChange::Type::kModified); + XCTAssertEqual(changes[6].type(), DocumentViewChange::Type::Modified); } - (void)testViewSnapshotConstructor { @@ -110,7 +110,7 @@ - (void)testViewSnapshotConstructor { DocumentSet oldDocuments = documents; documents = documents.insert(Doc("c/a", 1, Map())); std::vector documentChanges{ - DocumentViewChange{Doc("c/a", 1, Map()), DocumentViewChange::Type::kAdded}}; + DocumentViewChange{Doc("c/a", 1, Map()), DocumentViewChange::Type::Added}}; bool fromCache = true; DocumentKeySet mutatedKeys; diff --git a/Firestore/Example/Tests/Core/FSTViewTests.mm b/Firestore/Example/Tests/Core/FSTViewTests.mm deleted file mode 100644 index 648dfafd8d2..00000000000 --- a/Firestore/Example/Tests/Core/FSTViewTests.mm +++ /dev/null @@ -1,678 +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/FSTView.h" - -#import - -#include -#include -#include - -#import "Firestore/Source/API/FIRFirestore+Internal.h" - -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -#include "Firestore/core/src/firebase/firestore/core/field_filter.h" -#include "Firestore/core/src/firebase/firestore/core/filter.h" -#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" -#include "Firestore/core/src/firebase/firestore/model/document_set.h" -#include "Firestore/core/src/firebase/firestore/model/resource_path.h" -#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" -#include "Firestore/core/test/firebase/firestore/testutil/xcgmock.h" -#include "absl/types/optional.h" - -namespace testutil = firebase::firestore::testutil; -using firebase::firestore::core::Direction; -using firebase::firestore::core::DocumentViewChange; -using firebase::firestore::core::FieldFilter; -using firebase::firestore::core::Filter; -using firebase::firestore::core::Query; -using firebase::firestore::core::ViewSnapshot; -using firebase::firestore::model::Document; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::DocumentSet; -using firebase::firestore::model::DocumentState; -using firebase::firestore::model::FieldValue; -using firebase::firestore::model::ResourcePath; - -using testing::ElementsAre; -using testutil::DeletedDoc; -using testutil::Doc; -using testutil::Field; -using testutil::Filter; -using testutil::Map; -using testutil::OrderBy; - -NS_ASSUME_NONNULL_BEGIN - -/** - * A custom matcher that verifies that the subject has the same keys as the given documents without - * verifying that the contents are the same. - */ -MATCHER_P(ContainsDocs, expected, "") { - if (expected.size() != arg.size()) { - return false; - } - for (const Document &doc : expected) { - if (!arg.ContainsKey(doc.key())) { - return false; - } - } - return true; -} - -/** Constructs `ContainsDocs` instances with an initializer list. */ -inline ContainsDocsMatcherP> ContainsDocs(std::vector docs) { - return ContainsDocsMatcherP>(std::move(docs)); -} - -/** Returns a new empty query to use for testing. */ -inline Query QueryForMessages() { - return testutil::Query("rooms/eros/messages"); -} - -@interface FSTViewTests : XCTestCase -@end - -@implementation FSTViewTests - -- (void)testAddsDocumentsBasedOnQuery { - Query query = QueryForMessages(); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - Document doc1 = Doc("rooms/eros/messages/1", 0, Map("text", "msg1")); - Document doc2 = Doc("rooms/eros/messages/2", 0, Map("text", "msg2")); - Document doc3 = Doc("rooms/other/messages/1", 0, Map("text", "msg3")); - - absl::optional maybe_snapshot = - FSTTestApplyChanges(view, {doc1, doc2, doc3}, - FSTTestTargetChangeAckDocuments({doc1.key(), doc2.key(), doc3.key()})); - XCTAssertTrue(maybe_snapshot.has_value()); - ViewSnapshot snapshot = std::move(maybe_snapshot).value(); - - XCTAssertEqual(snapshot.query(), query); - - XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc2)); - - XCTAssertTrue(( - snapshot.document_changes() == - std::vector{DocumentViewChange{doc1, DocumentViewChange::Type::kAdded}, - DocumentViewChange{doc2, DocumentViewChange::Type::kAdded}})); - - XCTAssertFalse(snapshot.from_cache()); - XCTAssertFalse(snapshot.has_pending_writes()); - XCTAssertTrue(snapshot.sync_state_changed()); -} - -- (void)testRemovesDocuments { - Query query = QueryForMessages(); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - Document doc1 = Doc("rooms/eros/messages/1", 0, Map("text", "msg1")); - Document doc2 = Doc("rooms/eros/messages/2", 0, Map("text", "msg2")); - Document doc3 = Doc("rooms/eros/messages/3", 0, Map("text", "msg3")); - - // initial state - FSTTestApplyChanges(view, {doc1, doc2}, absl::nullopt); - - // delete doc2, add doc3 - absl::optional maybe_snapshot = - FSTTestApplyChanges(view, {DeletedDoc("rooms/eros/messages/2"), doc3}, - FSTTestTargetChangeAckDocuments({doc1.key(), doc3.key()})); - XCTAssertTrue(maybe_snapshot.has_value()); - ViewSnapshot snapshot = std::move(maybe_snapshot).value(); - - XCTAssertEqual(snapshot.query(), query); - - XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc3)); - - XCTAssertTrue(( - snapshot.document_changes() == - std::vector{DocumentViewChange{doc2, DocumentViewChange::Type::kRemoved}, - DocumentViewChange{doc3, DocumentViewChange::Type::kAdded}})); - - XCTAssertFalse(snapshot.from_cache()); - XCTAssertTrue(snapshot.sync_state_changed()); -} - -- (void)testReturnsNilIfThereAreNoChanges { - Query query = QueryForMessages(); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - Document doc1 = Doc("rooms/eros/messages/1", 0, Map("text", "msg1")); - Document doc2 = Doc("rooms/eros/messages/2", 0, Map("text", "msg2")); - - // initial state - FSTTestApplyChanges(view, {doc1, doc2}, absl::nullopt); - - // reapply same docs, no changes - absl::optional snapshot = FSTTestApplyChanges(view, {doc1, doc2}, absl::nullopt); - XCTAssertFalse(snapshot.has_value()); -} - -- (void)testDoesNotReturnNilForFirstChanges { - Query query = QueryForMessages(); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - absl::optional snapshot = FSTTestApplyChanges(view, {}, absl::nullopt); - XCTAssertTrue(snapshot.has_value()); -} - -- (void)testFiltersDocumentsBasedOnQueryWithFilter { - Query query = QueryForMessages().AddingFilter(Filter("sort", "<=", 2)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - Document doc1 = Doc("rooms/eros/messages/1", 0, Map("sort", 1)); - Document doc2 = Doc("rooms/eros/messages/2", 0, Map("sort", 2)); - Document doc3 = Doc("rooms/eros/messages/3", 0, Map("sort", 3)); - Document doc4 = Doc("rooms/eros/messages/4", 0, Map()); // no sort, no match - Document doc5 = Doc("rooms/eros/messages/5", 0, Map("sort", 1)); - - absl::optional maybe_snapshot = - FSTTestApplyChanges(view, {doc1, doc2, doc3, doc4, doc5}, absl::nullopt); - XCTAssertTrue(maybe_snapshot.has_value()); - ViewSnapshot snapshot = std::move(maybe_snapshot).value(); - - XCTAssertEqual(snapshot.query(), query); - - XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc5, doc2)); - - XCTAssertTrue(( - snapshot.document_changes() == - std::vector{DocumentViewChange{doc1, DocumentViewChange::Type::kAdded}, - DocumentViewChange{doc5, DocumentViewChange::Type::kAdded}, - DocumentViewChange{doc2, DocumentViewChange::Type::kAdded}})); - - XCTAssertTrue(snapshot.from_cache()); - XCTAssertTrue(snapshot.sync_state_changed()); -} - -- (void)testUpdatesDocumentsBasedOnQueryWithFilter { - Query query = QueryForMessages().AddingFilter(Filter("sort", "<=", 2)); - - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - Document doc1 = Doc("rooms/eros/messages/1", 0, Map("sort", 1)); - Document doc2 = Doc("rooms/eros/messages/2", 0, Map("sort", 3)); - Document doc3 = Doc("rooms/eros/messages/3", 0, Map("sort", 2)); - Document doc4 = Doc("rooms/eros/messages/4", 0, Map()); - - ViewSnapshot snapshot = - FSTTestApplyChanges(view, {doc1, doc2, doc3, doc4}, absl::nullopt).value(); - - XCTAssertEqual(snapshot.query(), query); - - XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc3)); - - Document newDoc2 = Doc("rooms/eros/messages/2", 1, Map("sort", 2)); - Document newDoc3 = Doc("rooms/eros/messages/3", 1, Map("sort", 3)); - Document newDoc4 = Doc("rooms/eros/messages/4", 1, Map("sort", 0)); - - snapshot = FSTTestApplyChanges(view, {newDoc2, newDoc3, newDoc4}, absl::nullopt).value(); - - XCTAssertEqual(snapshot.query(), query); - - XC_ASSERT_THAT(snapshot.documents(), ElementsAre(newDoc4, doc1, newDoc2)); - - XC_ASSERT_THAT(snapshot.document_changes(), - ElementsAre(DocumentViewChange{doc3, DocumentViewChange::Type::kRemoved}, - DocumentViewChange{newDoc4, DocumentViewChange::Type::kAdded}, - DocumentViewChange{newDoc2, DocumentViewChange::Type::kAdded})); - - XCTAssertTrue(snapshot.from_cache()); - XCTAssertFalse(snapshot.sync_state_changed()); -} - -- (void)testRemovesDocumentsForQueryWithLimit { - Query query = QueryForMessages().WithLimit(2); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - Document doc1 = Doc("rooms/eros/messages/1", 0, Map("text", "msg1")); - Document doc2 = Doc("rooms/eros/messages/2", 0, Map("text", "msg2")); - Document doc3 = Doc("rooms/eros/messages/3", 0, Map("text", "msg3")); - - // initial state - FSTTestApplyChanges(view, {doc1, doc3}, absl::nullopt); - - // add doc2, which should push out doc3 - ViewSnapshot snapshot = - FSTTestApplyChanges(view, {doc2}, - FSTTestTargetChangeAckDocuments({doc1.key(), doc2.key(), doc3.key()})) - .value(); - - XCTAssertEqual(snapshot.query(), query); - - XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc2)); - - XCTAssertTrue(( - snapshot.document_changes() == - std::vector{DocumentViewChange{doc3, DocumentViewChange::Type::kRemoved}, - DocumentViewChange{doc2, DocumentViewChange::Type::kAdded}})); - - XCTAssertFalse(snapshot.from_cache()); - XCTAssertTrue(snapshot.sync_state_changed()); -} - -- (void)testDoesntReportChangesForDocumentBeyondLimitOfQuery { - Query query = QueryForMessages().AddingOrderBy(OrderBy("num")).WithLimit(2); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - Document doc1 = Doc("rooms/eros/messages/1", 0, Map("num", 1)); - Document doc2 = Doc("rooms/eros/messages/2", 0, Map("num", 2)); - Document doc3 = Doc("rooms/eros/messages/3", 0, Map("num", 3)); - Document doc4 = Doc("rooms/eros/messages/4", 0, Map("num", 4)); - - // initial state - FSTTestApplyChanges(view, {doc1, doc2}, absl::nullopt); - - // change doc2 to 5, and add doc3 and doc4. - // doc2 will be modified + removed = removed - // doc3 will be added - // doc4 will be added + removed = nothing - doc2 = Doc("rooms/eros/messages/2", 1, Map("num", 5)); - FSTViewDocumentChanges *viewDocChanges = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc2, doc3, doc4})]; - XCTAssertTrue(viewDocChanges.needsRefill); - // Verify that all the docs still match. - viewDocChanges = [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2, doc3, doc4}) - previousChanges:viewDocChanges]; - absl::optional maybe_snapshot = - [view applyChangesToDocuments:viewDocChanges - targetChange:FSTTestTargetChangeAckDocuments( - {doc1.key(), doc2.key(), doc3.key(), doc4.key()})] - .snapshot; - XCTAssertTrue(maybe_snapshot.has_value()); - ViewSnapshot snapshot = std::move(maybe_snapshot).value(); - - XCTAssertEqual(snapshot.query(), query); - - XC_ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc3)); - - XC_ASSERT_THAT(snapshot.document_changes(), - ElementsAre(DocumentViewChange{doc2, DocumentViewChange::Type::kRemoved}, - DocumentViewChange{doc3, DocumentViewChange::Type::kAdded})); - - XCTAssertFalse(snapshot.from_cache()); - XCTAssertTrue(snapshot.sync_state_changed()); -} - -- (void)testKeepsTrackOfLimboDocuments { - Query query = QueryForMessages(); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); - Document doc3 = Doc("rooms/eros/messages/2", 0, Map()); - - FSTViewChange *change = - [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates({doc1})]]; - XCTAssertEqualObjects(change.limboChanges, @[]); - - change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates({})] - targetChange:FSTTestTargetChangeMarkCurrent()]; - XCTAssertEqualObjects(change.limboChanges, - @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded - key:doc1.key()] ]); - - change = [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates({})] - targetChange:FSTTestTargetChangeAckDocuments({doc1.key()})]; - XCTAssertEqualObjects(change.limboChanges, - @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved - key:doc1.key()] ]); - - change = - [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates({doc2})] - targetChange:FSTTestTargetChangeAckDocuments({doc2.key()})]; - XCTAssertEqualObjects(change.limboChanges, @[]); - - change = - [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates({doc3})]]; - XCTAssertEqualObjects(change.limboChanges, - @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded - key:doc3.key()] ]); - - change = [view applyChangesToDocuments: - [view computeChangesWithDocuments:FSTTestDocUpdates({DeletedDoc( - "rooms/eros/messages/2")})]]; // remove - XCTAssertEqualObjects(change.limboChanges, - @[ [FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved - key:doc3.key()] ]); -} - -- (void)testResumingQueryCreatesNoLimbos { - Query query = QueryForMessages(); - - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); - - // 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:DocumentKeySet{doc1.key(), doc2.key()}]; - - FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates({})]; - FSTViewChange *change = [view applyChangesToDocuments:changes - targetChange:FSTTestTargetChangeMarkCurrent()]; - XCTAssertEqualObjects(change.limboChanges, @[]); -} - -- (void)testReturnsNeedsRefillOnDeleteInLimitQuery { - Query query = QueryForMessages().WithLimit(2); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; - - // Remove one of the docs. - changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({DeletedDoc("rooms/eros/messages/0")})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc2})); - XCTAssertTrue(changes.needsRefill); - XCTAssertEqual(1, changes.changeSet.GetChanges().size()); - // Refill it with just the one doc remaining. - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc2}) previousChanges:changes]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc2})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(1, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; -} - -- (void)testReturnsNeedsRefillOnReorderInLimitQuery { - Query query = QueryForMessages().AddingOrderBy(OrderBy("order")).WithLimit(2); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map("order", 1)); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map("order", 2)); - Document doc3 = Doc("rooms/eros/messages/2", 0, Map("order", 3)); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2, doc3})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; - - // Move one of the docs. - doc2 = Doc("rooms/eros/messages/1", 1, Map("order", 2000)); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc2})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); - XCTAssertTrue(changes.needsRefill); - XCTAssertEqual(1, changes.changeSet.GetChanges().size()); - // Refill it with all three current docs. - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2, doc3}) - previousChanges:changes]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc3})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; -} - -- (void)testDoesntNeedRefillOnReorderWithinLimit { - Query query = QueryForMessages().AddingOrderBy(OrderBy("order")).WithLimit(3); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map("order", 1)); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map("order", 2)); - Document doc3 = Doc("rooms/eros/messages/2", 0, Map("order", 3)); - Document doc4 = Doc("rooms/eros/messages/3", 0, Map("order", 4)); - Document doc5 = Doc("rooms/eros/messages/4", 0, Map("order", 5)); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2, doc3, doc4, doc5})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2, doc3})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(3, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; - - // Move one of the docs. - doc1 = Doc("rooms/eros/messages/0", 1, Map("order", 3)); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc1})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc2, doc3, doc1})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(1, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; -} - -- (void)testDoesntNeedRefillOnReorderAfterLimitQuery { - Query query = QueryForMessages().AddingOrderBy(OrderBy("order")).WithLimit(3); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map("order", 1)); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map("order", 2)); - Document doc3 = Doc("rooms/eros/messages/2", 0, Map("order", 3)); - Document doc4 = Doc("rooms/eros/messages/3", 0, Map("order", 4)); - Document doc5 = Doc("rooms/eros/messages/4", 0, Map("order", 5)); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2, doc3, doc4, doc5})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2, doc3})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(3, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; - - // Move one of the docs. - doc4 = Doc("rooms/eros/messages/3", 1, Map("order", 6)); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc4})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2, doc3})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(0, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; -} - -- (void)testDoesntNeedRefillForAdditionAfterTheLimit { - Query query = QueryForMessages().WithLimit(2); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; - - // Add a doc that is past the limit. - Document doc3 = Doc("rooms/eros/messages/2", 1, Map()); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc3})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(0, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; -} - -- (void)testDoesntNeedRefillForDeletionsWhenNotNearTheLimit { - Query query = QueryForMessages().WithLimit(20); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; - - // Remove one of the docs. - changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({DeletedDoc("rooms/eros/messages/1")})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(1, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; -} - -- (void)testHandlesApplyingIrrelevantDocs { - Query query = QueryForMessages().WithLimit(2); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(2, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; - - // Remove a doc that isn't even in the results. - changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({DeletedDoc("rooms/eros/messages/2")})]; - XC_ASSERT_THAT(changes.documentSet, ContainsDocs({doc1, doc2})); - XCTAssertFalse(changes.needsRefill); - XCTAssertEqual(0, changes.changeSet.GetChanges().size()); - [view applyChangesToDocuments:changes]; -} - -- (void)testComputesMutatedKeys { - Query query = QueryForMessages(); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2})]; - [view applyChangesToDocuments:changes]; - XCTAssertEqual(changes.mutatedKeys, DocumentKeySet{}); - - Document doc3 = Doc("rooms/eros/messages/2", 0, Map(), DocumentState::kLocalMutations); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc3})]; - XCTAssertEqual(changes.mutatedKeys, DocumentKeySet{doc3.key()}); -} - -- (void)testRemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges { - Query query = QueryForMessages(); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map(), DocumentState::kLocalMutations); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2})]; - [view applyChangesToDocuments:changes]; - XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key()})); - - Document doc2Prime = Doc("rooms/eros/messages/1", 0, Map()); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc2Prime})]; - [view applyChangesToDocuments:changes]; - XCTAssertEqual(changes.mutatedKeys, DocumentKeySet{}); -} - -- (void)testRemembersLocalMutationsFromPreviousSnapshot { - Query query = QueryForMessages(); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map(), DocumentState::kLocalMutations); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2})]; - [view applyChangesToDocuments:changes]; - XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key()})); - - Document doc3 = Doc("rooms/eros/messages/2", 0, Map()); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc3})]; - [view applyChangesToDocuments:changes]; - XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key()})); -} - -- (void)testRemembersLocalMutationsFromPreviousCallToComputeChangesWithDocuments { - Query query = QueryForMessages(); - Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); - Document doc2 = Doc("rooms/eros/messages/1", 0, Map(), DocumentState::kLocalMutations); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - - // Start with a full view. - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2})]; - XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key()})); - - Document doc3 = Doc("rooms/eros/messages/2", 0, Map()); - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc3}) previousChanges:changes]; - XCTAssertEqual(changes.mutatedKeys, (DocumentKeySet{doc2.key()})); -} - -- (void)testRaisesHasPendingWritesForPendingMutationsInInitialSnapshot { - Query query = QueryForMessages(); - Document doc1 = Doc("rooms/eros/messages/1", 0, Map(), DocumentState::kLocalMutations); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc1})]; - FSTViewChange *viewChange = [view applyChangesToDocuments:changes]; - XCTAssertTrue(viewChange.snapshot.value().has_pending_writes()); -} - -- (void)testDoesntRaiseHasPendingWritesForCommittedMutationsInInitialSnapshot { - Query query = QueryForMessages(); - Document doc1 = Doc("rooms/eros/messages/1", 0, Map(), DocumentState::kCommittedMutations); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewDocumentChanges *changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc1})]; - FSTViewChange *viewChange = [view applyChangesToDocuments:changes]; - XCTAssertFalse(viewChange.snapshot.value().has_pending_writes()); -} - -- (void)testSuppressesWriteAcknowledgementIfWatchHasNotCaughtUp { - // This test verifies that we don't get three events for an FSTServerTimestamp mutation. We - // suppress the event generated by the write acknowledgement and instead wait for Watch to catch - // up. - - Query query = QueryForMessages(); - Document doc1 = Doc("rooms/eros/messages/1", 1, Map("time", 1), DocumentState::kLocalMutations); - Document doc1Committed = - Doc("rooms/eros/messages/1", 2, Map("time", 2), DocumentState::kCommittedMutations); - Document doc1Acknowledged = Doc("rooms/eros/messages/1", 2, Map("time", 2)); - Document doc2 = Doc("rooms/eros/messages/2", 1, Map("time", 1), DocumentState::kLocalMutations); - Document doc2Modified = - Doc("rooms/eros/messages/2", 2, Map("time", 3), DocumentState::kLocalMutations); - Document doc2Acknowledged = Doc("rooms/eros/messages/2", 2, Map("time", 3)); - FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:DocumentKeySet{}]; - FSTViewDocumentChanges *changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1, doc2})]; - FSTViewChange *viewChange = [view applyChangesToDocuments:changes]; - - XC_ASSERT_THAT(viewChange.snapshot.value().document_changes(), - ElementsAre(DocumentViewChange{doc1, DocumentViewChange::Type::kAdded}, - DocumentViewChange{doc2, DocumentViewChange::Type::kAdded})); - - changes = [view computeChangesWithDocuments:FSTTestDocUpdates({doc1Committed, doc2Modified})]; - viewChange = [view applyChangesToDocuments:changes]; - // The 'doc1Committed' update is suppressed - XC_ASSERT_THAT( - viewChange.snapshot.value().document_changes(), - ElementsAre(DocumentViewChange{doc2Modified, DocumentViewChange::Type::kModified})); - - changes = - [view computeChangesWithDocuments:FSTTestDocUpdates({doc1Acknowledged, doc2Acknowledged})]; - viewChange = [view applyChangesToDocuments:changes]; - XC_ASSERT_THAT( - viewChange.snapshot.value().document_changes(), - ElementsAre(DocumentViewChange{doc1Acknowledged, DocumentViewChange::Type::kModified}, - DocumentViewChange{doc2Acknowledged, DocumentViewChange::Type::kMetadata})); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm index cce274ff9e1..53bb196c19e 100644 --- a/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRDatabaseTests.mm @@ -22,7 +22,8 @@ #import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" + +#include "Firestore/core/src/firebase/firestore/core/firestore_client.h" #include "Firestore/core/test/firebase/firestore/testutil/app_testing.h" namespace testutil = firebase::firestore::testutil; @@ -530,6 +531,42 @@ - (void)testAddingToACollectionYieldsTheCorrectDocumentReference { [self awaitExpectations]; } +- (void)testSnapshotsInSyncListenerFiresAfterListenersInSync { + FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; + FIRDocumentReference *ref = [coll addDocumentWithData:@{@"foo" : @1}]; + NSMutableArray *events = [NSMutableArray array]; + + XCTestExpectation *gotInitialSnapshot = [self expectationWithDescription:@"gotInitialSnapshot"]; + __block bool setupComplete = false; + [ref addSnapshotListener:^(FIRDocumentSnapshot *snapshot, NSError *error) { + XCTAssertNil(error); + [events addObject:@"doc"]; + // Wait for the initial event from the backend so that we know we'll get exactly one snapshot + // event for our local write below. + if (!setupComplete) { + setupComplete = true; + [gotInitialSnapshot fulfill]; + } + }]; + + [self awaitExpectations]; + [events removeAllObjects]; + + XCTestExpectation *done = [self expectationWithDescription:@"SnapshotsInSyncListenerDone"]; + [ref.firestore addSnapshotsInSyncListener:^() { + [events addObject:@"snapshots-in-sync"]; + if ([events count] == 3) { + // We should have an initial snapshots-in-sync event, then a snapshot event + // for set(), then another event to indicate we're in sync again. + NSArray *expected = @[ @"snapshots-in-sync", @"doc", @"snapshots-in-sync" ]; + XCTAssertEqualObjects(events, expected); + [done fulfill]; + } + }]; + + [self writeDocumentRef:ref data:@{@"foo" : @3}]; +} + - (void)testListenCanBeCalledMultipleTimes { FIRCollectionReference *coll = [self.db collectionWithPath:@"collection"]; FIRDocumentReference *doc = [coll documentWithAutoID]; @@ -1223,13 +1260,13 @@ - (void)testCanDisableNetwork { [self awaitExpectations]; } -- (void)testClientCallsAfterShutdownFail { +- (void)testClientCallsAfterTerminationFail { FIRDocumentReference *doc = [self documentRef]; FIRFirestore *firestore = doc.firestore; [firestore enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable network"]]; [self awaitExpectations]; - [firestore shutdownWithCompletion:[self completionForExpectationWithName:@"Shutdown"]]; + [firestore terminateWithCompletion:[self completionForExpectationWithName:@"Terminate"]]; [self awaitExpectations]; XCTAssertThrowsSpecific( @@ -1237,7 +1274,7 @@ - (void)testClientCallsAfterShutdownFail { [firestore disableNetworkWithCompletion:^(NSError *error){ }]; }, - NSException, @"The client has already been shutdown."); + NSException, @"The client has already been terminated."); } - (void)testMaintainsPersistenceAfterRestarting { @@ -1250,9 +1287,9 @@ - (void)testMaintainsPersistenceAfterRestarting { NSDictionary *initialData = @{@"foo" : @"42"}; [self writeDocumentRef:doc data:initialData]; - // -clearPersistence() requires Firestore to be shut down. Shutdown FIRApp and remove the + // -clearPersistence() requires Firestore to be terminated. Shutdown FIRApp and remove the // firestore instance to emulate the way an end user would do this. - [self shutdownFirestore:firestore]; + [self terminateFirestore:firestore]; [self.firestores removeObject:firestore]; [self deleteApp:app]; @@ -1277,9 +1314,9 @@ - (void)testCanClearPersistenceAfterRestarting { NSDictionary *initialData = @{@"foo" : @"42"}; [self writeDocumentRef:doc data:initialData]; - // -clearPersistence() requires Firestore to be shut down. Shutdown FIRApp and remove the + // -clearPersistence() requires Firestore to be terminated. Shutdown FIRApp and remove the // firestore instance to emulate the way an end user would do this. - [self shutdownFirestore:firestore]; + [self terminateFirestore:firestore]; [self.firestores removeObject:firestore]; [firestore clearPersistenceWithCompletion:[self completionForExpectationWithName:@"Enable network"]]; @@ -1331,7 +1368,7 @@ - (void)testRestartFirestoreLeadsToNewInstance { @{@"owner" : @{@"name" : @"Jonny", @"email" : @"abc@xyz.com"}}; [self writeDocumentRef:[firestore documentWithPath:@"abc/123"] data:data]; - [self shutdownFirestore:firestore]; + [self terminateFirestore:firestore]; // Create a new instance, check it's a different instance. FIRFirestore *newInstance = [FIRFirestore firestoreForApp:app]; @@ -1344,7 +1381,7 @@ - (void)testRestartFirestoreLeadsToNewInstance { XCTAssertTrue([data isEqualToDictionary:[snapshot data]]); } -- (void)testAppDeleteLeadsToFirestoreShutdown { +- (void)testAppDeleteLeadsToFirestoreTermination { FIRApp *app = testutil::AppForUnitTesting(util::MakeString([FSTIntegrationTestCase projectID])); FIRFirestore *firestore = [FIRFirestore firestoreForApp:app]; firestore.settings = [FSTIntegrationTestCase settings]; @@ -1354,34 +1391,33 @@ - (void)testAppDeleteLeadsToFirestoreShutdown { [self deleteApp:app]; - FSTFirestoreClient *client = firestore.wrapped->client(); - XCTAssertTrue([client isShutdown]); + XCTAssertTrue(firestore.wrapped->client()->is_terminated()); } -- (void)testShutdownCanBeCalledMultipleTimes { +- (void)testTerminateCanBeCalledMultipleTimes { FIRApp *app = testutil::AppForUnitTesting(util::MakeString([FSTIntegrationTestCase projectID])); FIRFirestore *firestore = [FIRFirestore firestoreForApp:app]; - [firestore shutdownWithCompletion:[self completionForExpectationWithName:@"Shutdown1"]]; + [firestore terminateWithCompletion:[self completionForExpectationWithName:@"Terminate1"]]; [self awaitExpectations]; XCTAssertThrowsSpecific( { [firestore disableNetworkWithCompletion:^(NSError *error){ }]; }, - NSException, @"The client has already been shutdown."); + NSException, @"The client has already been terminated."); - [firestore shutdownWithCompletion:[self completionForExpectationWithName:@"Shutdown2"]]; + [firestore terminateWithCompletion:[self completionForExpectationWithName:@"Terminate2"]]; [self awaitExpectations]; XCTAssertThrowsSpecific( { [firestore enableNetworkWithCompletion:^(NSError *error){ }]; }, - NSException, @"The client has already been shutdown."); + NSException, @"The client has already been terminated."); } -- (void)testCanRemoveListenerAfterShutdown { +- (void)testCanRemoveListenerAfterTermination { FIRApp *app = testutil::AppForUnitTesting(util::MakeString([FSTIntegrationTestCase projectID])); FIRFirestore *firestore = [FIRFirestore firestoreForApp:app]; firestore.settings = [FSTIntegrationTestCase settings]; @@ -1393,7 +1429,7 @@ - (void)testCanRemoveListenerAfterShutdown { [doc addSnapshotListener:[accumulator valueEventHandler]]; [accumulator awaitEventWithName:@"Snapshot"]; - [firestore shutdownWithCompletion:[self completionForExpectationWithName:@"shutdown"]]; + [firestore terminateWithCompletion:[self completionForExpectationWithName:@"terminate"]]; [self awaitExpectations]; // This should proceed without error. @@ -1402,4 +1438,50 @@ - (void)testCanRemoveListenerAfterShutdown { [listenerRegistration remove]; } +- (void)testWaitForPendingWritesCompletes { + FIRDocumentReference *doc = [self documentRef]; + FIRFirestore *firestore = doc.firestore; + + [self disableNetwork]; + + [doc setData:@{@"foo" : @"bar"}]; + [firestore waitForPendingWritesWithCompletion: + [self completionForExpectationWithName:@"Wait for pending writes"]]; + + [firestore enableNetworkWithCompletion:[self completionForExpectationWithName:@"Enable network"]]; + [self awaitExpectations]; +} + +- (void)testWaitForPendingWritesFailsWhenUserChanges { + FIRFirestore *firestore = self.db; + + [self disableNetwork]; + + // Writes to local to prevent immediate call to the completion of waitForPendingWrites. + NSDictionary *data = + @{@"owner" : @{@"name" : @"Andy", @"email" : @"abc@example.com"}}; + [[self documentRef] setData:data]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"waitForPendingWrites"]; + [firestore waitForPendingWritesWithCompletion:^(NSError *_Nullable error) { + XCTAssertNotNil(error); + XCTAssertEqualObjects(error.domain, FIRFirestoreErrorDomain); + XCTAssertEqual(error.code, FIRFirestoreErrorCodeCancelled); + [expectation fulfill]; + }]; + + [self triggerUserChangeWithUid:@"user-to-fail-pending-writes"]; + [self awaitExpectations]; +} + +- (void)testWaitForPendingWritesCompletesWhenOfflineIfNoPending { + FIRFirestore *firestore = self.db; + + [self disableNetwork]; + + [firestore waitForPendingWritesWithCompletion: + [self completionForExpectationWithName:@"Wait for pending writes"]]; + [self awaitExpectations]; +} + @end diff --git a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm index adb33b7a30b..ed5cd2aca01 100644 --- a/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm +++ b/Firestore/Example/Tests/Integration/API/FIRWriteBatchTests.mm @@ -400,7 +400,6 @@ - (void)testReasonableMemoryUsageForLotsOfMutations { XCTAssertNil(error); const int64_t memoryUsedAfterCommitMb = GetCurrentMemoryUsedInMb(); XCTAssertNotEqual(memoryUsedAfterCommitMb, -1); - const int64_t memoryDeltaMb = memoryUsedAfterCommitMb - memoryUsedBeforeCommitMb; #if !defined(THREAD_SANITIZER) && !defined(ADDRESS_SANITIZER) // This by its nature cannot be a precise value. Runs on simulator seem to give an increase of @@ -408,7 +407,7 @@ - (void)testReasonableMemoryUsageForLotsOfMutations { // // This check is disabled under the thread sanitizer because it introduces an overhead of // 5x-10x. - XCTAssertLessThan(memoryDeltaMb, 20); + XCTAssertLessThan(memoryUsedAfterCommitMb - memoryUsedBeforeCommitMb, 20); #endif [expectation fulfill]; diff --git a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm index 9ce99a64e3b..404aa237898 100644 --- a/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm +++ b/Firestore/Example/Tests/Integration/FSTDatastoreTests.mm @@ -24,13 +24,14 @@ #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" #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/local/local_store.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.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/precondition.h" @@ -38,10 +39,10 @@ #include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/remote_store.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "absl/memory/memory.h" @@ -50,12 +51,19 @@ using firebase::Timestamp; using firebase::firestore::auth::EmptyCredentialsProvider; +using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; +using firebase::firestore::local::LocalStore; +using firebase::firestore::local::MemoryPersistence; +using firebase::firestore::local::Persistence; +using firebase::firestore::local::QueryData; using firebase::firestore::model::BatchId; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::FieldValue; +using firebase::firestore::model::MutationBatch; +using firebase::firestore::model::MutationBatchResult; using firebase::firestore::model::Precondition; using firebase::firestore::model::OnlineState; using firebase::firestore::model::TargetId; @@ -63,17 +71,17 @@ using firebase::firestore::remote::GrpcConnection; using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::RemoteStore; +using firebase::firestore::remote::RemoteStoreCallback; using firebase::firestore::testutil::Map; using firebase::firestore::testutil::WrapObject; using firebase::firestore::util::AsyncQueue; -using firebase::firestore::util::ExecutorLibdispatch; using firebase::firestore::util::Status; NS_ASSUME_NONNULL_BEGIN #pragma mark - FSTRemoteStoreEventCapture -@interface FSTRemoteStoreEventCapture : NSObject +@interface FSTRemoteStoreEventCapture : NSObject - (instancetype)init __attribute__((unavailable("Use initWithTestCase:"))); @@ -83,18 +91,17 @@ - (void)expectWriteEventWithDescription:(NSString *)description; - (void)expectListenEventWithDescription:(NSString *)description; @property(nonatomic, weak, nullable) XCTestCase *testCase; -@property(nonatomic, strong) NSMutableArray *writeEvents; @property(nonatomic, strong) NSMutableArray *writeEventExpectations; @property(nonatomic, strong) NSMutableArray *listenEventExpectations; @end @implementation FSTRemoteStoreEventCapture { std::vector _listenEvents; + std::vector _writeEvents; } - (instancetype)initWithTestCase:(XCTestCase *_Nullable)testCase { if (self = [super init]) { - _writeEvents = [NSMutableArray array]; _testCase = testCase; _writeEventExpectations = [NSMutableArray array]; _listenEventExpectations = [NSMutableArray array]; @@ -124,8 +131,8 @@ - (void)expectListenEventWithDescription:(NSString *)description { description]]]; } -- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { - [self.writeEvents addObject:batchResult]; +- (void)applySuccessfulWriteWithResult:(const MutationBatchResult &)batchResult { + _writeEvents.push_back(batchResult); XCTestExpectation *expectation = [self.writeEventExpectations objectAtIndex:0]; [self.writeEventExpectations removeObjectAtIndex:0]; [expectation fulfill]; @@ -152,6 +159,48 @@ - (void)rejectListenWithTargetID:(const TargetId)targetID error:(NSError *)error @end +class RemoteStoreEventCapture : public RemoteStoreCallback { + public: + explicit RemoteStoreEventCapture(XCTestCase *test_case) { + underlying_capture_ = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:test_case]; + } + + void ExpectWriteEvent(NSString *description) { + [underlying_capture_ expectWriteEventWithDescription:description]; + } + + void ExpectListenEvent(NSString *description) { + [underlying_capture_ expectListenEventWithDescription:description]; + } + + void ApplyRemoteEvent(const RemoteEvent &remote_event) override { + [underlying_capture_ applyRemoteEvent:remote_event]; + } + + void HandleRejectedListen(TargetId target_id, Status error) override { + [underlying_capture_ rejectListenWithTargetID:target_id error:error.ToNSError()]; + } + + void HandleSuccessfulWrite(const MutationBatchResult &batch_result) override { + [underlying_capture_ applySuccessfulWriteWithResult:batch_result]; + } + + void HandleRejectedWrite(BatchId batch_id, Status error) override { + [underlying_capture_ rejectFailedWriteWithBatchID:batch_id error:error.ToNSError()]; + } + + void HandleOnlineStateChange(OnlineState online_state) override { + HARD_FAIL("Not implemented"); + } + + model::DocumentKeySet GetRemoteKeys(TargetId target_id) const override { + return [underlying_capture_ remoteKeysForTarget:target_id]; + } + + private: + FSTRemoteStoreEventCapture *underlying_capture_; +}; + #pragma mark - FSTDatastoreTests @interface FSTDatastoreTests : XCTestCase @@ -160,7 +209,8 @@ @interface FSTDatastoreTests : XCTestCase @implementation FSTDatastoreTests { std::shared_ptr _testWorkerQueue; - FSTLocalStore *_localStore; + std::unique_ptr _localStore; + std::unique_ptr _persistence; DatabaseInfo _databaseInfo; std::shared_ptr _datastore; @@ -181,14 +231,15 @@ - (void)setUp { _databaseInfo = DatabaseInfo(database_id, "test-key", util::MakeString(settings.host), settings.sslEnabled); - dispatch_queue_t queue = dispatch_queue_create( - "com.google.firestore.FSTDatastoreTestsWorkerQueue", DISPATCH_QUEUE_SERIAL); - _testWorkerQueue = std::make_shared(absl::make_unique(queue)); + _testWorkerQueue = testutil::AsyncQueueForTesting(); _datastore = std::make_shared(_databaseInfo, _testWorkerQueue, std::make_shared()); - _remoteStore = - absl::make_unique(_localStore, _datastore, _testWorkerQueue, [](OnlineState) {}); + _persistence = MemoryPersistence::WithEagerGarbageCollector(); + _localStore = absl::make_unique(_persistence.get(), User::Unauthenticated()); + + _remoteStore = absl::make_unique(_localStore.get(), _datastore, _testWorkerQueue, + [](OnlineState) {}); _testWorkerQueue->Enqueue([=] { _remoteStore->Start(); }); } @@ -216,16 +267,13 @@ - (void)testCommit { } - (void)testStreamingWrite { - FSTRemoteStoreEventCapture *capture = [[FSTRemoteStoreEventCapture alloc] initWithTestCase:self]; - [capture expectWriteEventWithDescription:@"write mutations"]; + RemoteStoreEventCapture capture = RemoteStoreEventCapture(self); + capture.ExpectWriteEvent(@"write mutations"); - _remoteStore->set_sync_engine(capture); + _remoteStore->set_sync_engine(&capture); auto mutation = testutil::SetMutation("rooms/eros", Map("name", "Eros")); - FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:23 - localWriteTime:Timestamp::Now() - baseMutations:{} - mutations:{mutation}]; + MutationBatch batch = MutationBatch(23, Timestamp::Now(), {}, {mutation}); _testWorkerQueue->Enqueue([=] { _remoteStore->AddToWritePipeline(batch); // The added batch won't be written immediately because write stream wasn't yet open -- diff --git a/Firestore/Example/Tests/Integration/FSTTransactionTests.mm b/Firestore/Example/Tests/Integration/FSTTransactionTests.mm index 56b60d54080..2b6fd97c0ac 100644 --- a/Firestore/Example/Tests/Integration/FSTTransactionTests.mm +++ b/Firestore/Example/Tests/Integration/FSTTransactionTests.mm @@ -17,11 +17,14 @@ #import #import -#include +#include #import "Firestore/Example/Tests/Util/FSTIntegrationTestCase.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/Util/FSTClasses.h" +using firebase::firestore::util::TimerId; + @interface FSTTransactionTests : FSTIntegrationTestCase @end @@ -412,12 +415,16 @@ - (void)testSetDocumentWithMerge { - (void)testIncrementTransactionally { // A barrier to make sure every transaction reaches the same spot. dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); - __block volatile int32_t counter = 0; + auto counter_unique = absl::make_unique(0); + auto counter = counter_unique.get(); FIRFirestore *firestore = [self firestore]; FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; [self writeDocumentRef:doc data:@{@"count" : @(5.0)}]; + // Skip backoff delays. + [firestore workerQueue] -> SkipDelaysForTimerId(TimerId::RetryTransaction); + // Make 3 transactions that will all increment. int total = 3; for (int i = 0; i < total; i++) { @@ -426,9 +433,9 @@ - (void)testIncrementTransactionally { runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; XCTAssertNil(*error); - int32_t nowStarted = OSAtomicIncrement32(&counter); + (*counter)++; // Once all of the transactions have read, allow the first write. - if (nowStarted == total) { + if (*counter == total) { dispatch_semaphore_signal(writeBarrier); } @@ -454,25 +461,29 @@ - (void)testIncrementTransactionally { - (void)testUpdateTransactionally { // A barrier to make sure every transaction reaches the same spot. dispatch_semaphore_t writeBarrier = dispatch_semaphore_create(0); - __block volatile int32_t counter = 0; + auto counter_unique = absl::make_unique(0); + auto counter = counter_unique.get(); FIRFirestore *firestore = [self firestore]; FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; [self writeDocumentRef:doc data:@{@"count" : @(5.0), @"other" : @"yes"}]; + // Skip backoff delays. + [firestore workerQueue] -> SkipDelaysForTimerId(TimerId::RetryTransaction); + // Make 3 transactions that will all increment. int total = 3; for (int i = 0; i < total; i++) { XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - int32_t nowStarted = OSAtomicIncrement32(&counter); + int32_t nowStarted = ++(*counter); FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; XCTAssertNil(*error); // Once all of the transactions have read, allow the first write. There should be 3 // initial transaction runs. if (nowStarted == total) { - XCTAssertEqual(3, (int)counter); + XCTAssertEqual(3, (int)(*counter)); dispatch_semaphore_signal(writeBarrier); } @@ -491,7 +502,7 @@ - (void)testUpdateTransactionally { [self awaitExpectations]; // There should be a maximum of 3 retries: once for the 2nd update, and twice for the 3rd update. - XCTAssertLessThanOrEqual((int)counter, 6); + XCTAssertLessThanOrEqual((int)(*counter), 6); // Now all transaction should be completed, so check the result. FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; XCTAssertEqualObjects(@(5.0 + total), snapshot[@"count"]); @@ -505,6 +516,9 @@ - (void)testHandleReadingOneDocAndWritingAnother { [self writeDocumentRef:doc1 data:@{@"count" : @(15.0)}]; + // Skip backoff delays. + [firestore workerQueue] -> SkipDelaysForTimerId(TimerId::RetryTransaction); + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { @@ -550,13 +564,18 @@ - (void)testHandleReadingOneDocAndWritingAnother { - (void)testReadingADocTwiceWithDifferentVersions { FIRFirestore *firestore = [self firestore]; FIRDocumentReference *doc = [[firestore collectionWithPath:@"counters"] documentWithAutoID]; - __block volatile int32_t counter = 0; + auto counter_unique = absl::make_unique(0); + auto counter = counter_unique.get(); [self writeDocumentRef:doc data:@{@"count" : @(15.0)}]; + + // Skip backoff delays. + [firestore workerQueue] -> SkipDelaysForTimerId(TimerId::RetryTransaction); + XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - OSAtomicIncrement32(&counter); + ++(*counter); // Get the doc once. FIRDocumentSnapshot *snapshot = [transaction getDocument:doc error:error]; XCTAssertNil(*error); @@ -564,7 +583,7 @@ - (void)testReadingADocTwiceWithDifferentVersions { // document to a different value each time. dispatch_semaphore_t writeSemaphore = dispatch_semaphore_create(0); [doc setData:@{ - @"count" : @(1234 + (int)counter) + @"count" : @(1234 + (int)(*counter)) } completion:^(NSError *_Nullable error) { dispatch_semaphore_signal(writeSemaphore); @@ -646,12 +665,13 @@ - (void)testCannotHaveAGetWithoutMutations { - (void)testDoesNotRetryOnPermanentError { FIRFirestore *firestore = [self firestore]; - __block volatile int32_t counter = 0; + auto counter_unique = absl::make_unique(0); + auto counter = counter_unique.get(); // Make a transaction that should fail with a permanent error XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - OSAtomicIncrement32(&counter); + ++(*counter); // Get and update a document that doesn't exist so that the transaction fails. FIRDocumentReference *doc = [[firestore collectionWithPath:@"nonexistent"] documentWithAutoID]; @@ -663,7 +683,7 @@ - (void)testDoesNotRetryOnPermanentError { [expectation fulfill]; XCTAssertNotNil(error); XCTAssertEqual(error.code, FIRFirestoreErrorCodeInvalidArgument); - XCTAssertEqual(1, (int)counter); + XCTAssertEqual(1, (int)(*counter)); }]; [self awaitExpectations]; } @@ -686,11 +706,12 @@ - (void)testSuccessWithNoTransactionOperations { - (void)testCancellationOnError { FIRFirestore *firestore = [self firestore]; FIRDocumentReference *doc = [[firestore collectionWithPath:@"towns"] documentWithAutoID]; - __block volatile int32_t counter = 0; + auto counter_unique = absl::make_unique(0); + auto counter = counter_unique.get(); XCTestExpectation *expectation = [self expectationWithDescription:@"transaction"]; [firestore runTransactionWithBlock:^id _Nullable(FIRTransaction *transaction, NSError **error) { - OSAtomicIncrement32(&counter); + ++(*counter); [transaction setData:@{@"foo" : @"bar"} forDocument:doc]; if (error) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:35 userInfo:@{}]; @@ -704,7 +725,7 @@ - (void)testCancellationOnError { [expectation fulfill]; }]; [self awaitExpectations]; - XCTAssertEqual(1, (int)counter); + XCTAssertEqual(1, (int)(*counter)); FIRDocumentSnapshot *snapshot = [self readDocumentForRef:doc]; XCTAssertFalse(snapshot.exists); } diff --git a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h index 8f12e0306da..232daa5a072 100644 --- a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h +++ b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h @@ -16,17 +16,28 @@ #import -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" +#include -@protocol FSTPersistence; +namespace firebase { +namespace firestore { +namespace local { + +class LruParams; +class Persistence; + +} // namespace local +} // namespace firestore +} // namespace firebase + +namespace local = firebase::firestore::local; NS_ASSUME_NONNULL_BEGIN @interface FSTLRUGarbageCollectorTests : XCTestCase -- (id)newPersistenceWithLruParams:(firebase::firestore::local::LruParams)lruParams; +- (std::unique_ptr)newPersistenceWithLruParams:(local::LruParams)lruParams; -@property(nonatomic, strong) id persistence; +- (local::Persistence *)persistence; @end diff --git a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm index d9520df60b6..fa2ed9bf99c 100644 --- a/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm +++ b/Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.mm @@ -24,14 +24,16 @@ #include #import "Firestore/Example/Tests/Util/FSTHelpers.h" -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTPersistence.h" #import "Firestore/Source/Util/FSTClasses.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/core/query.h" +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" #include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" #include "Firestore/core/src/firebase/firestore/local/query_cache.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @@ -42,13 +44,19 @@ #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "absl/strings/str_cat.h" +namespace core = firebase::firestore::core; namespace testutil = firebase::firestore::testutil; using firebase::Timestamp; using firebase::firestore::auth::User; +using firebase::firestore::local::LruDelegate; +using firebase::firestore::local::LruGarbageCollector; using firebase::firestore::local::LruParams; using firebase::firestore::local::LruResults; using firebase::firestore::local::MutationQueue; +using firebase::firestore::local::Persistence; using firebase::firestore::local::QueryCache; +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::local::ReferenceSet; using firebase::firestore::local::RemoteDocumentCache; using firebase::firestore::model::Document; @@ -74,12 +82,12 @@ @implementation FSTLRUGarbageCollectorTests { int _previousDocNum; ObjectValue _testValue; ObjectValue _bigObjectValue; - id _persistence; + std::unique_ptr _persistence; QueryCache *_queryCache; RemoteDocumentCache *_documentCache; MutationQueue *_mutationQueue; - id _lruDelegate; - FSTLRUGarbageCollector *_gc; + LruDelegate *_lruDelegate; + LruGarbageCollector *_gc; ListenSequenceNumber _initialSequenceNumber; User _user; ReferenceSet _additionalReferences; @@ -100,18 +108,22 @@ - (BOOL)isTestBaseClass { return ([self class] == [FSTLRUGarbageCollectorTests class]); } +- (Persistence *)persistence { + return _persistence.get(); +} + - (void)newTestResourcesWithLruParams:(LruParams)lruParams { HARD_ASSERT(_persistence == nil, "Persistence already created"); _persistence = [self newPersistenceWithLruParams:lruParams]; - [_persistence.referenceDelegate addInMemoryPins:&_additionalReferences]; - _queryCache = [_persistence queryCache]; - _documentCache = [_persistence remoteDocumentCache]; - _mutationQueue = [_persistence mutationQueueForUser:_user]; - _lruDelegate = (id)_persistence.referenceDelegate; - _initialSequenceNumber = _persistence.run("start querycache", [&]() -> ListenSequenceNumber { + _persistence->reference_delegate()->AddInMemoryPins(&_additionalReferences); + _queryCache = _persistence->query_cache(); + _documentCache = _persistence->remote_document_cache(); + _mutationQueue = _persistence->GetMutationQueueForUser(_user); + _lruDelegate = static_cast(_persistence->reference_delegate()); + _initialSequenceNumber = _persistence->Run("start querycache", [&] { _mutationQueue->Start(); - _gc = _lruDelegate.gc; - return _persistence.currentSequenceNumber; + _gc = _lruDelegate->garbage_collector(); + return _persistence->current_sequence_number(); }); } @@ -119,7 +131,7 @@ - (void)newTestResources { [self newTestResourcesWithLruParams:LruParams::Default()]; } -- (id)newPersistenceWithLruParams:(LruParams)lruParams { +- (std::unique_ptr)newPersistenceWithLruParams:(LruParams)lruParams { @throw FSTAbstractMethodException(); // NOLINT } @@ -134,59 +146,46 @@ - (void)expectSentinelRemoved:(const DocumentKey &)key { #pragma mark - helpers - (ListenSequenceNumber)sequenceNumberForQueryCount:(int)queryCount { - return _persistence.run( - "gc", [&]() -> ListenSequenceNumber { return [_gc sequenceNumberForQueryCount:queryCount]; }); + return _persistence->Run("gc", [&] { return _gc->SequenceNumberForQueryCount(queryCount); }); } - (int)queryCountForPercentile:(int)percentile { - return _persistence.run("query count", - [&]() -> int { return [_gc queryCountForPercentile:percentile]; }); + return _persistence->Run("query count", [&] { return _gc->QueryCountForPercentile(percentile); }); } - (int)removeQueriesThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber - liveQueries:(const std::unordered_map &) - liveQueries { - return _persistence.run("gc", [&]() -> int { - return [_gc removeQueriesUpThroughSequenceNumber:sequenceNumber liveQueries:liveQueries]; - }); + liveQueries: + (const std::unordered_map &)liveQueries { + return _persistence->Run("gc", [&] { return _gc->RemoveTargets(sequenceNumber, liveQueries); }); } // Removes documents that are not part of a target or a mutation and have a sequence number // less than or equal to the given sequence number. - (int)removeOrphanedDocumentsThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber { - return _persistence.run("gc", [&]() -> int { - return [_gc removeOrphanedDocumentsThroughSequenceNumber:sequenceNumber]; - }); + return _persistence->Run("gc", [&] { return _gc->RemoveOrphanedDocuments(sequenceNumber); }); } -- (FSTQueryData *)nextTestQuery { +- (QueryData)nextTestQuery { TargetId targetID = ++_previousTargetID; - ListenSequenceNumber listenSequenceNumber = _persistence.currentSequenceNumber; + ListenSequenceNumber listenSequenceNumber = _persistence->current_sequence_number(); core::Query query = Query(absl::StrCat("path", targetID)); - return [[FSTQueryData alloc] initWithQuery:std::move(query) - targetID:targetID - listenSequenceNumber:listenSequenceNumber - purpose:FSTQueryPurposeListen]; + return QueryData(std::move(query), targetID, listenSequenceNumber, QueryPurpose::Listen); } -- (FSTQueryData *)addNextQueryInTransaction { - FSTQueryData *queryData = [self nextTestQuery]; +- (QueryData)addNextQueryInTransaction { + QueryData queryData = [self nextTestQuery]; _queryCache->AddTarget(queryData); return queryData; } -- (void)updateTargetInTransaction:(FSTQueryData *)queryData { - NSData *token = [@"hello" dataUsingEncoding:NSUTF8StringEncoding]; - FSTQueryData *updated = - [queryData queryDataByReplacingSnapshotVersion:queryData.snapshotVersion - resumeToken:token - sequenceNumber:_persistence.currentSequenceNumber]; +- (void)updateTargetInTransaction:(const QueryData &)queryData { + QueryData updated = queryData.Copy(queryData.snapshot_version(), testutil::ResumeToken(7), + _persistence->current_sequence_number()); _queryCache->UpdateTarget(updated); } -- (FSTQueryData *)addNextQuery { - return _persistence.run("adding query", - [&]() -> FSTQueryData * { return [self addNextQueryInTransaction]; }); +- (QueryData)addNextQuery { + return _persistence->Run("adding query", [&] { return [self addNextQueryInTransaction]; }); } // Simulates a document being mutated and then having that mutation ack'd. @@ -200,8 +199,8 @@ - (DocumentKey)markADocumentEligibleForGC { } - (void)markDocumentEligibleForGC:(const DocumentKey &)docKey { - _persistence.run("Removing mutation reference", - [&]() { [self markDocumentEligibleForGCInTransaction:docKey]; }); + _persistence->Run("Removing mutation reference", + [&] { [self markDocumentEligibleForGCInTransaction:docKey]; }); } - (DocumentKey)markADocumentEligibleForGCInTransaction { @@ -211,7 +210,7 @@ - (DocumentKey)markADocumentEligibleForGCInTransaction { } - (void)markDocumentEligibleForGCInTransaction:(const DocumentKey &)docKey { - [_persistence.referenceDelegate removeMutationReference:docKey]; + _persistence->reference_delegate()->RemoveMutationReference(docKey); } - (void)addDocument:(const DocumentKey &)docKey toTarget:(TargetId)targetId { @@ -276,8 +275,8 @@ - (void)testPickSequenceNumberPercentile { } int tenth = [self queryCountForPercentile:10]; XCTAssertEqual(expectedTenthPercentile, tenth, @"Total query count: %i", numQueries); - [_persistence shutdown]; - _persistence = nil; + _persistence->Shutdown(); + _persistence.reset(); } } @@ -286,8 +285,8 @@ - (void)testSequenceNumberNoQueries { // No queries... should get invalid sequence number (-1) [self newTestResources]; - XCTAssertEqual(kFSTListenSequenceNumberInvalid, [self sequenceNumberForQueryCount:0]); - [_persistence shutdown]; + XCTAssertEqual(local::kListenSequenceNumberInvalid, [self sequenceNumberForQueryCount:0]); + _persistence->Shutdown(); } - (void)testSequenceNumberForFiftyQueries { @@ -299,7 +298,7 @@ - (void)testSequenceNumberForFiftyQueries { [self addNextQuery]; } XCTAssertEqual(_initialSequenceNumber + 10, [self sequenceNumberForQueryCount:10]); - [_persistence shutdown]; + _persistence->Shutdown(); } - (void)testSequenceNumberForMultipleQueriesInATransaction { @@ -307,7 +306,7 @@ - (void)testSequenceNumberForMultipleQueriesInATransaction { // 50 queries, 9 with one transaction, incrementing from there. Should get second sequence number. [self newTestResources]; - _persistence.run("9 queries in a batch", [&]() { + _persistence->Run("9 queries in a batch", [&] { for (int i = 0; i < 9; i++) { [self addNextQueryInTransaction]; } @@ -316,7 +315,7 @@ - (void)testSequenceNumberForMultipleQueriesInATransaction { [self addNextQuery]; } XCTAssertEqual(2 + _initialSequenceNumber, [self sequenceNumberForQueryCount:10]); - [_persistence shutdown]; + _persistence->Shutdown(); } // Ensure that even if all of the queries are added in a single transaction, we still @@ -327,7 +326,7 @@ - (void)testAllCollectedQueriesInSingleTransaction { // 50 queries, 11 with one transaction, incrementing from there. Should get first sequence number. [self newTestResources]; - _persistence.run("11 queries in a transaction", [&]() { + _persistence->Run("11 queries in a transaction", [&] { for (int i = 0; i < 11; i++) { [self addNextQueryInTransaction]; } @@ -338,7 +337,7 @@ - (void)testAllCollectedQueriesInSingleTransaction { // We expect to GC the targets from the first transaction, since they account for // at least the first 10 of the targets. XCTAssertEqual(1 + _initialSequenceNumber, [self sequenceNumberForQueryCount:10]); - [_persistence shutdown]; + _persistence->Shutdown(); } - (void)testSequenceNumbersWithMutationAndSequentialQueries { @@ -352,7 +351,7 @@ - (void)testSequenceNumbersWithMutationAndSequentialQueries { [self addNextQuery]; } XCTAssertEqual(10 + _initialSequenceNumber, [self sequenceNumberForQueryCount:10]); - [_persistence shutdown]; + _persistence->Shutdown(); } - (void)testSequenceNumbersWithMutationsInQueries { @@ -362,7 +361,7 @@ - (void)testSequenceNumbersWithMutationsInQueries { // Expect 3 past the initial value: the mutations not part of a query, and two queries [self newTestResources]; Document docInQuery = [self nextTestDocument]; - _persistence.run("mark mutations", [&]() { + _persistence->Run("mark mutations", [&] { // Adding 9 doc keys in a transaction. If we remove one of them, we'll have room for two actual // queries. [self markDocumentEligibleForGCInTransaction:docInQuery.key()]; @@ -373,27 +372,27 @@ - (void)testSequenceNumbersWithMutationsInQueries { for (int i = 0; i < 49; i++) { [self addNextQuery]; } - _persistence.run("query with mutation", [&]() { - FSTQueryData *queryData = [self addNextQueryInTransaction]; + _persistence->Run("query with mutation", [&] { + QueryData queryData = [self addNextQueryInTransaction]; // This should keep the document from getting GC'd, since it is no longer orphaned. - [self addDocument:docInQuery.key() toTarget:queryData.targetID]; + [self addDocument:docInQuery.key() toTarget:queryData.target_id()]; }); // This should catch the remaining 8 documents, plus the first two queries we added. XCTAssertEqual(3 + _initialSequenceNumber, [self sequenceNumberForQueryCount:10]); - [_persistence shutdown]; + _persistence->Shutdown(); } - (void)testRemoveQueriesUpThroughSequenceNumber { if ([self isTestBaseClass]) return; [self newTestResources]; - std::unordered_map liveQueries; + std::unordered_map liveQueries; for (int i = 0; i < 100; i++) { - FSTQueryData *queryData = [self addNextQuery]; + QueryData queryData = [self addNextQuery]; // Mark odd queries as live so we can test filtering out live queries. - if (queryData.targetID % 2 == 1) { - liveQueries[queryData.targetID] = queryData; + if (queryData.target_id() % 2 == 1) { + liveQueries[queryData.target_id()] = queryData; } } // GC up through 20th query, which is 20%. @@ -402,12 +401,12 @@ - (void)testRemoveQueriesUpThroughSequenceNumber { liveQueries:liveQueries]; XCTAssertEqual(10, removed); // Make sure we removed the even targets with targetID <= 20. - _persistence.run("verify remaining targets are > 20 or odd", [&]() { - _queryCache->EnumerateTargets([&](FSTQueryData *queryData) { - XCTAssertTrue(queryData.targetID > 20 || queryData.targetID % 2 == 1); + _persistence->Run("verify remaining targets are > 20 or odd", [&] { + _queryCache->EnumerateTargets([&](const QueryData &queryData) { + XCTAssertTrue(queryData.target_id() > 20 || queryData.target_id() % 2 == 1); }); }); - [_persistence shutdown]; + _persistence->Shutdown(); } - (void)testRemoveOrphanedDocuments { @@ -423,29 +422,29 @@ - (void)testRemoveOrphanedDocuments { // Add a target and add two documents to it. The documents are expected to be // retained, since their membership in the target keeps them alive. - _persistence.run("add a target and add two documents to it", [&]() { + _persistence->Run("add a target and add two documents to it", [&] { // Add two documents to first target, queue a mutation on the second document - FSTQueryData *queryData = [self addNextQueryInTransaction]; + QueryData queryData = [self addNextQueryInTransaction]; Document doc1 = [self cacheADocumentInTransaction]; - [self addDocument:doc1.key() toTarget:queryData.targetID]; + [self addDocument:doc1.key() toTarget:queryData.target_id()]; expectedRetained.insert(doc1.key()); Document doc2 = [self cacheADocumentInTransaction]; - [self addDocument:doc2.key() toTarget:queryData.targetID]; + [self addDocument:doc2.key() toTarget:queryData.target_id()]; expectedRetained.insert(doc2.key()); mutations.push_back([self mutationForDocument:doc2.key()]); }); // Add a second query and register a third document on it - _persistence.run("second query", [&]() { - FSTQueryData *queryData = [self addNextQueryInTransaction]; + _persistence->Run("second query", [&] { + QueryData queryData = [self addNextQueryInTransaction]; Document doc3 = [self cacheADocumentInTransaction]; expectedRetained.insert(doc3.key()); - [self addDocument:doc3.key() toTarget:queryData.targetID]; + [self addDocument:doc3.key() toTarget:queryData.target_id()]; }); // cache another document and prepare a mutation on it. - _persistence.run("queue a mutation", [&]() { + _persistence->Run("queue a mutation", [&] { Document doc4 = [self cacheADocumentInTransaction]; mutations.push_back([self mutationForDocument:doc4.key()]); expectedRetained.insert(doc4.key()); @@ -453,7 +452,7 @@ - (void)testRemoveOrphanedDocuments { // Insert the mutations. These operations don't have a sequence number, they just // serve to keep the mutated documents from being GC'd while the mutations are outstanding. - _persistence.run("actually register the mutations", [&]() { + _persistence->Run("actually register the mutations", [&] { Timestamp writeTime = Timestamp::Now(); _mutationQueue->AddMutationBatch(writeTime, {}, std::move(mutations)); }); @@ -462,7 +461,7 @@ - (void)testRemoveOrphanedDocuments { // Since they were ack'd, they are no longer in a mutation queue, and there is nothing keeping // them alive. std::unordered_set toBeRemoved; - _persistence.run("add orphaned docs (previously mutated, then ack'd)", [&]() { + _persistence->Run("add orphaned docs (previously mutated, then ack'd)", [&] { for (int i = 0; i < 5; i++) { Document doc = [self cacheADocumentInTransaction]; toBeRemoved.insert(doc.key()); @@ -475,7 +474,7 @@ - (void)testRemoveOrphanedDocuments { // use a large sequence number to remove as much as possible int removed = [self removeOrphanedDocumentsThroughSequenceNumber:1000]; XCTAssertEqual(toBeRemoved.size(), removed); - _persistence.run("verify", [&]() { + _persistence->Run("verify", [&] { for (const DocumentKey &key : toBeRemoved) { XCTAssertEqual(_documentCache->Get(key), absl::nullopt); XCTAssertFalse(_queryCache->Contains(key)); @@ -485,7 +484,7 @@ - (void)testRemoveOrphanedDocuments { key.ToString().c_str()); } }); - [_persistence shutdown]; + _persistence->Shutdown(); } // TODO(gsoltis): write a test that includes limbo documents @@ -521,83 +520,81 @@ - (void)testRemoveTargetsThenGC { // Add oldest target, 5 documents, and add those documents to the target. // This target will not be removed, so all documents that are part of it will // be retained. - FSTQueryData *oldestTarget = - _persistence.run("Add oldest target and docs", [&]() -> FSTQueryData * { - FSTQueryData *queryData = [self addNextQueryInTransaction]; - for (int i = 0; i < 5; i++) { - Document doc = [self cacheADocumentInTransaction]; - expectedRetained.insert(doc.key()); - [self addDocument:doc.key() toTarget:queryData.targetID]; - } - return queryData; - }); + QueryData oldestTarget = _persistence->Run("Add oldest target and docs", [&] { + QueryData queryData = [self addNextQueryInTransaction]; + for (int i = 0; i < 5; i++) { + Document doc = [self cacheADocumentInTransaction]; + expectedRetained.insert(doc.key()); + [self addDocument:doc.key() toTarget:queryData.target_id()]; + } + return queryData; + }); // Add middle target and docs. Some docs will be removed from this target later, // which we track here. DocumentKeySet middleDocsToRemove; // This will be the document in this target that gets an update later DocumentKey middleDocToUpdate; - FSTQueryData *middleTarget = - _persistence.run("Add middle target and docs", [&]() -> FSTQueryData * { - FSTQueryData *middleTarget = [self addNextQueryInTransaction]; - // these docs will be removed from this target later, triggering a bump - // to their sequence numbers. Since they will not be a part of the target, we - // expect them to be removed. - for (int i = 0; i < 2; i++) { - Document doc = [self cacheADocumentInTransaction]; - expectedRemoved.insert(doc.key()); - [self addDocument:doc.key() toTarget:middleTarget.targetID]; - middleDocsToRemove = middleDocsToRemove.insert(doc.key()); - } - // these docs stay in this target and only this target. There presence in this - // target prevents them from being GC'd, so they are also expected to be retained. - for (int i = 2; i < 4; i++) { - Document doc = [self cacheADocumentInTransaction]; - expectedRetained.insert(doc.key()); - [self addDocument:doc.key() toTarget:middleTarget.targetID]; - } - // This doc stays in this target, but gets updated. - { - Document doc = [self cacheADocumentInTransaction]; - expectedRetained.insert(doc.key()); - [self addDocument:doc.key() toTarget:middleTarget.targetID]; - middleDocToUpdate = doc.key(); - } - return middleTarget; - }); + QueryData middleTarget = _persistence->Run("Add middle target and docs", [&] { + QueryData middleTarget = [self addNextQueryInTransaction]; + // these docs will be removed from this target later, triggering a bump + // to their sequence numbers. Since they will not be a part of the target, we + // expect them to be removed. + for (int i = 0; i < 2; i++) { + Document doc = [self cacheADocumentInTransaction]; + expectedRemoved.insert(doc.key()); + [self addDocument:doc.key() toTarget:middleTarget.target_id()]; + middleDocsToRemove = middleDocsToRemove.insert(doc.key()); + } + // these docs stay in this target and only this target. There presence in this + // target prevents them from being GC'd, so they are also expected to be retained. + for (int i = 2; i < 4; i++) { + Document doc = [self cacheADocumentInTransaction]; + expectedRetained.insert(doc.key()); + [self addDocument:doc.key() toTarget:middleTarget.target_id()]; + } + // This doc stays in this target, but gets updated. + { + Document doc = [self cacheADocumentInTransaction]; + expectedRetained.insert(doc.key()); + [self addDocument:doc.key() toTarget:middleTarget.target_id()]; + middleDocToUpdate = doc.key(); + } + return middleTarget; + }); // Add the newest target and add 5 documents to it. Some of those documents will // additionally be added to the oldest target, which will cause those documents to // be retained. The remaining documents are expected to be removed, since this target // will be removed. DocumentKeySet newestDocsToAddToOldest; - _persistence.run("Add newest target and docs", [&]() { - FSTQueryData *newestTarget = [self addNextQueryInTransaction]; + _persistence->Run("Add newest target and docs", [&] { + QueryData newestTarget = [self addNextQueryInTransaction]; // These documents are only in this target. They are expected to be removed // because this target will also be removed. for (int i = 0; i < 3; i++) { Document doc = [self cacheADocumentInTransaction]; expectedRemoved.insert(doc.key()); - [self addDocument:doc.key() toTarget:newestTarget.targetID]; + [self addDocument:doc.key() toTarget:newestTarget.target_id()]; } // docs to add to the oldest target in addition to this target. They will be retained for (int i = 3; i < 5; i++) { Document doc = [self cacheADocumentInTransaction]; expectedRetained.insert(doc.key()); - [self addDocument:doc.key() toTarget:newestTarget.targetID]; + [self addDocument:doc.key() toTarget:newestTarget.target_id()]; newestDocsToAddToOldest = newestDocsToAddToOldest.insert(doc.key()); } }); // 2 doc writes, add one of them to the oldest target. - _persistence.run("2 doc writes, add one of them to the oldest target", [&]() { + _persistence->Run("2 doc writes, add one of them to the oldest target", [&] { // write two docs and have them ack'd by the server. can skip mutation queue // and set them in document cache. Add potentially orphaned first, also add one // doc to a target. Document doc1 = [self cacheADocumentInTransaction]; [self markDocumentEligibleForGCInTransaction:doc1.key()]; [self updateTargetInTransaction:oldestTarget]; - [self addDocument:doc1.key() toTarget:oldestTarget.targetID]; + [self addDocument:doc1.key() toTarget:oldestTarget.target_id()]; // doc1 should be retained by being added to oldestTarget. expectedRetained.insert(doc1.key()); @@ -608,27 +605,27 @@ - (void)testRemoveTargetsThenGC { }); // Remove some documents from the middle target. - _persistence.run("Remove some documents from the middle target", [&]() { + _persistence->Run("Remove some documents from the middle target", [&] { [self updateTargetInTransaction:middleTarget]; for (const DocumentKey &docKey : middleDocsToRemove) { - [self removeDocument:docKey fromTarget:middleTarget.targetID]; + [self removeDocument:docKey fromTarget:middleTarget.target_id()]; } }); // Add a couple docs from the newest target to the oldest (preserves them past the point where // newest was removed) // upperBound is the sequence number right before middleTarget is updated, then removed. - ListenSequenceNumber upperBound = _persistence.run( - "Add a couple docs from the newest target to the oldest", [&]() -> ListenSequenceNumber { + ListenSequenceNumber upperBound = + _persistence->Run("Add a couple docs from the newest target to the oldest", [&] { [self updateTargetInTransaction:oldestTarget]; for (const DocumentKey &docKey : newestDocsToAddToOldest) { - [self addDocument:docKey toTarget:oldestTarget.targetID]; + [self addDocument:docKey toTarget:oldestTarget.target_id()]; } - return _persistence.currentSequenceNumber; + return _persistence->current_sequence_number(); }); // Update a doc in the middle target - _persistence.run("Update a doc in the middle target", [&]() { + _persistence->Run("Update a doc in the middle target", [&] { FSTTestSnapshotVersion version = 3; Document doc(ObjectValue(_testValue), middleDocToUpdate, Version(version), DocumentState::kSynced); @@ -639,7 +636,7 @@ Document doc(ObjectValue(_testValue), middleDocToUpdate, Version(version), // middleTarget removed here, no update needed // Write a doc and get an ack, not part of a target. - _persistence.run("Write a doc and get an ack, not part of a target", [&]() { + _persistence->Run("Write a doc and get an ack, not part of a target", [&] { Document doc = [self cacheADocumentInTransaction]; // Mark it as eligible for GC, but this is after our upper bound for what we will collect. [self markDocumentEligibleForGCInTransaction:doc.key()]; @@ -648,13 +645,13 @@ Document doc(ObjectValue(_testValue), middleDocToUpdate, Version(version), }); // Finally, do the garbage collection, up to but not including the removal of middleTarget - std::unordered_map liveQueries{{oldestTarget.targetID, oldestTarget}}; + std::unordered_map liveQueries{{oldestTarget.target_id(), oldestTarget}}; int queriesRemoved = [self removeQueriesThroughSequenceNumber:upperBound liveQueries:liveQueries]; XCTAssertEqual(1, queriesRemoved, @"Expected to remove newest target"); int docsRemoved = [self removeOrphanedDocumentsThroughSequenceNumber:upperBound]; XCTAssertEqual(expectedRemoved.size(), docsRemoved); - _persistence.run("verify results", [&]() { + _persistence->Run("verify results", [&] { for (const DocumentKey &key : expectedRemoved) { XCTAssertEqual(_documentCache->Get(key), absl::nullopt, @"Did not expect to find %s in document cache", key.ToString().c_str()); @@ -668,7 +665,7 @@ Document doc(ObjectValue(_testValue), middleDocToUpdate, Version(version), } }); - [_persistence shutdown]; + _persistence->Shutdown(); } - (void)testGetsSize { @@ -676,9 +673,9 @@ - (void)testGetsSize { [self newTestResources]; - size_t initialSize = [_gc byteSize]; + size_t initialSize = _gc->CalculateByteSize(); - _persistence.run("fill cache", [&]() { + _persistence->Run("fill cache", [&] { // Simulate a bunch of ack'd mutations for (int i = 0; i < 50; i++) { Document doc = [self cacheADocumentInTransaction]; @@ -686,10 +683,10 @@ - (void)testGetsSize { } }); - size_t finalSize = [_gc byteSize]; + size_t finalSize = _gc->CalculateByteSize(); XCTAssertGreaterThan(finalSize, initialSize); - [_persistence shutdown]; + _persistence->Shutdown(); } - (void)testDisabled { @@ -698,7 +695,7 @@ - (void)testDisabled { LruParams params = LruParams::Disabled(); [self newTestResourcesWithLruParams:params]; - _persistence.run("fill cache", [&]() { + _persistence->Run("fill cache", [&] { // Simulate a bunch of ack'd mutations for (int i = 0; i < 500; i++) { Document doc = [self cacheADocumentInTransaction]; @@ -706,11 +703,10 @@ - (void)testDisabled { } }); - LruResults results = - _persistence.run("GC", [&]() -> LruResults { return [_gc collectWithLiveTargets:{}]; }); - XCTAssertFalse(results.didRun); + LruResults results = _persistence->Run("GC", [&] { return _gc->Collect({}); }); + XCTAssertFalse(results.did_run); - [_persistence shutdown]; + _persistence->Shutdown(); } - (void)testCacheTooSmall { @@ -719,7 +715,7 @@ - (void)testCacheTooSmall { LruParams params = LruParams::Default(); [self newTestResourcesWithLruParams:params]; - _persistence.run("fill cache", [&]() { + _persistence->Run("fill cache", [&] { // Simulate a bunch of ack'd mutations for (int i = 0; i < 50; i++) { Document doc = [self cacheADocumentInTransaction]; @@ -727,16 +723,15 @@ - (void)testCacheTooSmall { } }); - int cacheSize = (int)[_gc byteSize]; + int cacheSize = (int)_gc->CalculateByteSize(); // Verify that we don't have enough in our cache to warrant collection - XCTAssertLessThan(cacheSize, params.minBytesThreshold); + XCTAssertLessThan(cacheSize, params.min_bytes_threshold); // Try collection and verify that it didn't run - LruResults results = - _persistence.run("GC", [&]() -> LruResults { return [_gc collectWithLiveTargets:{}]; }); - XCTAssertFalse(results.didRun); + LruResults results = _persistence->Run("GC", [&] { return _gc->Collect({}); }); + XCTAssertFalse(results.did_run); - [_persistence shutdown]; + _persistence->Shutdown(); } - (void)testGCRan { @@ -744,32 +739,31 @@ - (void)testGCRan { LruParams params = LruParams::Default(); // Set a low threshold so we will definitely run - params.minBytesThreshold = 100; + params.min_bytes_threshold = 100; [self newTestResourcesWithLruParams:params]; // Add 100 targets and 10 documents to each for (int i = 0; i < 100; i++) { // Use separate transactions so that each target and associated documents get their own // sequence number. - _persistence.run("Add a target and some documents", [&]() { - FSTQueryData *queryData = [self addNextQueryInTransaction]; + _persistence->Run("Add a target and some documents", [&] { + QueryData queryData = [self addNextQueryInTransaction]; for (int j = 0; j < 10; j++) { Document doc = [self cacheADocumentInTransaction]; - [self addDocument:doc.key() toTarget:queryData.targetID]; + [self addDocument:doc.key() toTarget:queryData.target_id()]; } }); } // Mark nothing as live, so everything is eligible. - LruResults results = - _persistence.run("GC", [&]() -> LruResults { return [_gc collectWithLiveTargets:{}]; }); + LruResults results = _persistence->Run("GC", [&] { return _gc->Collect({}); }); // By default, we collect 10% of the sequence numbers. Since we added 100 targets, // that should be 10 targets with 10 documents each, for a total of 100 documents. - XCTAssertTrue(results.didRun); - XCTAssertEqual(10, results.targetsRemoved); - XCTAssertEqual(100, results.documentsRemoved); - [_persistence shutdown]; + XCTAssertTrue(results.did_run); + XCTAssertEqual(10, results.targets_removed); + XCTAssertEqual(100, results.documents_removed); + _persistence->Shutdown(); } @end diff --git a/Firestore/Example/Tests/Local/FSTLevelDBLRUGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBLRUGarbageCollectorTests.mm index d680e1fecf6..ad26c79233b 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBLRUGarbageCollectorTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBLRUGarbageCollectorTests.mm @@ -19,12 +19,16 @@ #import "Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h" #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTLevelDB.h" + #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" using firebase::firestore::local::LevelDbDocumentTargetKey; +using firebase::firestore::local::LevelDbPersistence; +using firebase::firestore::local::Persistence; using firebase::firestore::model::DocumentKey; using firebase::firestore::local::LruParams; @@ -36,15 +40,15 @@ @interface FSTLevelDBLRUGarbageCollectorTests : FSTLRUGarbageCollectorTests @implementation FSTLevelDBLRUGarbageCollectorTests -- (id)newPersistenceWithLruParams:(LruParams)lruParams { +- (std::unique_ptr)newPersistenceWithLruParams:(LruParams)lruParams { return [FSTPersistenceTestHelpers levelDBPersistenceWithLruParams:lruParams]; } - (BOOL)sentinelExists:(const DocumentKey &)key { - FSTLevelDB *db = (FSTLevelDB *)self.persistence; + auto db = static_cast(self.persistence); std::string sentinelKey = LevelDbDocumentTargetKey::SentinelKey(key); std::string unusedValue; - return !db.currentTransaction->Get(sentinelKey, &unusedValue).IsNotFound(); + return !db->current_transaction()->Get(sentinelKey, &unusedValue).IsNotFound(); } @end diff --git a/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.mm index c097bf8a806..99b2129f64f 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBLocalStoreTests.mm @@ -16,15 +16,17 @@ #import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" -#import "Firestore/Source/Local/FSTLevelDB.h" - #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" + NS_ASSUME_NONNULL_BEGIN +using firebase::firestore::local::Persistence; + /** * The tests for FSTLevelDBLocalStore are performed on the FSTLocalStore protocol in - * FSTLocalStoreTests. This class is merely responsible for creating a new FSTPersistence + * FSTLocalStoreTests. This class is merely responsible for creating a new Persistence * implementation on demand. */ @interface FSTLevelDBLocalStoreTests : FSTLocalStoreTests @@ -32,7 +34,7 @@ @interface FSTLevelDBLocalStoreTests : FSTLocalStoreTests @implementation FSTLevelDBLocalStoreTests -- (id)persistence { +- (std::unique_ptr)persistence { return [FSTPersistenceTestHelpers levelDBPersistence]; } diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm index 2efc7da0e7c..03370a85017 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMigrationsTests.mm @@ -23,7 +23,6 @@ #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" -#import "Firestore/Source/Local/FSTLevelDB.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_migrations.h" diff --git a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm index d24e24a0818..c0d9de1e3ca 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBMutationQueueTests.mm @@ -22,11 +22,11 @@ #import "Firestore/Example/Tests/Local/FSTMutationQueueTests.h" #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Source/Local/FSTLevelDB.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/util/ordered_code.h" #include "absl/strings/string_view.h" @@ -37,6 +37,7 @@ using firebase::firestore::auth::User; using firebase::firestore::local::LevelDbMutationKey; using firebase::firestore::local::LevelDbMutationQueue; +using firebase::firestore::local::LevelDbPersistence; using firebase::firestore::local::LoadNextBatchIdFromDb; using firebase::firestore::local::ReferenceSet; using firebase::firestore::model::BatchId; @@ -70,38 +71,38 @@ @interface FSTLevelDBMutationQueueTests : FSTMutationQueueTests } @implementation FSTLevelDBMutationQueueTests { - FSTLevelDB *_db; + std::unique_ptr _db; ReferenceSet _additionalReferences; } - (void)setUp { [super setUp]; _db = [FSTPersistenceTestHelpers levelDBPersistence]; - [_db.referenceDelegate addInMemoryPins:&_additionalReferences]; + _db->reference_delegate()->AddInMemoryPins(&_additionalReferences); - self.mutationQueue = [_db mutationQueueForUser:User("user")]; - self.persistence = _db; + self.mutationQueue = _db->GetMutationQueueForUser(User("user")); + self.persistence = _db.get(); - self.persistence.run("Setup", [&]() { self.mutationQueue->Start(); }); + self.persistence->Run("Setup", [&]() { self.mutationQueue->Start(); }); } - (void)testLoadNextBatchID_zeroWhenTotallyEmpty { // Initial seek is invalid - XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 0); + XCTAssertEqual(LoadNextBatchIdFromDb(_db->ptr()), 1); } - (void)testLoadNextBatchID_zeroWhenNoMutations { // Initial seek finds no mutations [self setDummyValueForKey:MutationLikeKey("mutationr", "foo", 20)]; [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)]; - XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 0); + XCTAssertEqual(LoadNextBatchIdFromDb(_db->ptr()), 1); } - (void)testLoadNextBatchID_findsSingleRow { // Seeks off the end of the table altogether [self setDummyValueForKey:LevelDbMutationKey::Key("foo", 6)]; - XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 7); + XCTAssertEqual(LoadNextBatchIdFromDb(_db->ptr()), 7); } - (void)testLoadNextBatchID_findsSingleRowAmongNonMutations { @@ -109,7 +110,7 @@ - (void)testLoadNextBatchID_findsSingleRowAmongNonMutations { [self setDummyValueForKey:LevelDbMutationKey::Key("foo", 6)]; [self setDummyValueForKey:MutationLikeKey("mutationsa", "foo", 10)]; - XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 7); + XCTAssertEqual(LoadNextBatchIdFromDb(_db->ptr()), 7); } - (void)testLoadNextBatchID_findsMaxAcrossUsers { @@ -120,7 +121,7 @@ - (void)testLoadNextBatchID_findsMaxAcrossUsers { [self setDummyValueForKey:LevelDbMutationKey::Key("foo", 2)]; [self setDummyValueForKey:LevelDbMutationKey::Key("foo", 1)]; - XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 7); + XCTAssertEqual(LoadNextBatchIdFromDb(_db->ptr()), 7); } - (void)testLoadNextBatchID_onlyFindsMutations { @@ -137,7 +138,7 @@ - (void)testLoadNextBatchID_onlyFindsMutations { // None of the higher tables should match -- this is the only entry that's in the mutations // table - XCTAssertEqual(LoadNextBatchIdFromDb(_db.ptr), 4); + XCTAssertEqual(LoadNextBatchIdFromDb(_db->ptr()), 4); } - (void)testEmptyProtoCanBeUpgraded { @@ -159,7 +160,7 @@ - (void)testEmptyProtoCanBeUpgraded { } - (void)setDummyValueForKey:(const std::string &)key { - _db.ptr->Put(WriteOptions(), key, kDummy); + _db->ptr()->Put(WriteOptions(), key, kDummy); } @end diff --git a/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.mm index ec1ebb4fc1c..08387dcc8b5 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBQueryCacheTests.mm @@ -14,23 +14,30 @@ * limitations under the License. */ -#import "Firestore/Source/Local/FSTLevelDB.h" -#import "Firestore/Source/Local/FSTQueryData.h" - #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" #import "Firestore/Example/Tests/Local/FSTQueryCacheTests.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #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" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +namespace core = firebase::firestore::core; namespace testutil = firebase::firestore::testutil; + using firebase::Timestamp; +using firebase::firestore::local::LevelDbPersistence; using firebase::firestore::local::LevelDbQueryCache; +using firebase::firestore::local::Persistence; +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::local::ReferenceSet; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::ListenSequenceNumber; @@ -51,35 +58,38 @@ @interface FSTLevelDBQueryCacheTests : FSTQueryCacheTests * @a queryCache. */ @implementation FSTLevelDBQueryCacheTests { + std::unique_ptr _db; ReferenceSet _additionalReferences; } -- (LevelDbQueryCache *)getCache:(id)persistence { - return static_cast(persistence.queryCache); +- (LevelDbQueryCache *)getCache:(Persistence *)persistence { + return static_cast(persistence->query_cache()); } - (void)setUp { [super setUp]; - self.persistence = [FSTPersistenceTestHelpers levelDBPersistence]; - self.queryCache = [self getCache:self.persistence]; - [self.persistence.referenceDelegate addInMemoryPins:&_additionalReferences]; + _db = [FSTPersistenceTestHelpers levelDBPersistence]; + self.persistence = _db.get(); + self.queryCache = [self getCache:_db.get()]; + self.persistence->reference_delegate()->AddInMemoryPins(&_additionalReferences); } - (void)tearDown { [super tearDown]; - self.persistence = nil; - self.queryCache = nil; + self.persistence = nullptr; + self.queryCache = nullptr; } - (void)testMetadataPersistedAcrossRestarts { - [self.persistence shutdown]; - self.persistence = nil; + self.persistence->Shutdown(); + _db.reset(); + self.persistence = nullptr; Path dir = [FSTPersistenceTestHelpers levelDBDir]; - FSTLevelDB *db1 = [FSTPersistenceTestHelpers levelDBPersistenceWithDir:dir]; - LevelDbQueryCache *queryCache = [self getCache:db1]; + auto db1 = [FSTPersistenceTestHelpers levelDBPersistenceWithDir:dir]; + LevelDbQueryCache *queryCache = [self getCache:db1.get()]; XCTAssertEqual(0, queryCache->highest_listen_sequence_number()); XCTAssertEqual(0, queryCache->highest_target_id()); @@ -90,36 +100,34 @@ - (void)testMetadataPersistedAcrossRestarts { TargetId lastTargetId = 5; SnapshotVersion lastVersion(Timestamp(1, 2)); - db1.run("add query data", [&]() { + db1->Run("add query data", [&] { core::Query query = Query("some/path"); - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:std::move(query) - targetID:lastTargetId - listenSequenceNumber:minimumSequenceNumber - purpose:FSTQueryPurposeListen]; + QueryData queryData(std::move(query), lastTargetId, minimumSequenceNumber, + QueryPurpose::Listen); queryCache->AddTarget(queryData); queryCache->SetLastRemoteSnapshotVersion(lastVersion); }); - [db1 shutdown]; - db1 = nil; + db1->Shutdown(); + db1.reset(); - FSTLevelDB *db2 = [FSTPersistenceTestHelpers levelDBPersistenceWithDir:dir]; - db2.run("verify sequence number", [&]() { + auto db2 = [FSTPersistenceTestHelpers levelDBPersistenceWithDir:dir]; + db2->Run("verify sequence number", [&] { // We should remember the previous sequence number, and the next transaction should // have a higher one. - XCTAssertGreaterThan(db2.currentSequenceNumber, minimumSequenceNumber); + XCTAssertGreaterThan(db2->current_sequence_number(), minimumSequenceNumber); }); - LevelDbQueryCache *queryCache2 = [self getCache:db2]; + LevelDbQueryCache *queryCache2 = [self getCache:db2.get()]; XCTAssertEqual(lastTargetId, queryCache2->highest_target_id()); XCTAssertEqual(lastVersion, queryCache2->GetLastRemoteSnapshotVersion()); - [db2 shutdown]; - db2 = nil; + db2->Shutdown(); + db2.reset(); } - (void)testRemoveMatchingKeysForTargetID { - self.persistence.run("testRemoveMatchingKeysForTargetID", [&]() { + self.persistence->Run("testRemoveMatchingKeysForTargetID", [&]() { DocumentKey key1 = testutil::Key("foo/bar"); DocumentKey key2 = testutil::Key("foo/baz"); DocumentKey key3 = testutil::Key("foo/blah"); diff --git a/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm index ac106caad0d..4528d77cd85 100644 --- a/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTLevelDBRemoteDocumentCacheTests.mm @@ -19,7 +19,8 @@ #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" #import "Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h" -#import "Firestore/Source/Local/FSTLevelDB.h" + +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" @@ -30,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN using leveldb::WriteOptions; +using firebase::firestore::local::LevelDbPersistence; using firebase::firestore::local::LevelDbRemoteDocumentCache; using firebase::firestore::local::RemoteDocumentCache; using firebase::firestore::util::OrderedCode; @@ -46,16 +48,16 @@ @interface FSTLevelDBRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests @end @implementation FSTLevelDBRemoteDocumentCacheTests { - FSTLevelDB *_db; - std::unique_ptr _cache; + std::unique_ptr _db; + LevelDbRemoteDocumentCache *_cache; } - (void)setUp { [super setUp]; _db = [FSTPersistenceTestHelpers levelDBPersistence]; - self.persistence = _db; + self.persistence = _db.get(); HARD_ASSERT(!_cache, "Previous cache not torn down"); - _cache = absl::make_unique(_db, _db.serializer); + _cache = _db->remote_document_cache(); // Write a couple dummy rows that should appear before/after the remote_documents table to make // sure the tests are unaffected. @@ -64,15 +66,15 @@ - (void)setUp { } - (RemoteDocumentCache *_Nullable)remoteDocumentCache { - return _cache.get(); + return _cache; } - (void)tearDown { [super tearDown]; self.remoteDocumentCache = nil; self.persistence = nil; - _cache.reset(); - _db = nil; + _cache = nullptr; + _db.reset(); } - (void)writeDummyRowWithSegments:(NSArray *)segments { @@ -81,7 +83,7 @@ - (void)writeDummyRowWithSegments:(NSArray *)segments { OrderedCode::WriteString(&key, segment.UTF8String); } - _db.ptr->Put(WriteOptions(), key, kDummy); + _db->ptr()->Put(WriteOptions(), key, kDummy); } @end diff --git a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm index 8aa2ae8a35b..610ca300bf9 100644 --- a/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalSerializerTests.mm @@ -31,36 +31,41 @@ #import "Firestore/Protos/objc/google/firestore/v1/Query.pbobjc.h" #import "Firestore/Protos/objc/google/firestore/v1/Write.pbobjc.h" #import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #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/field_mask.h" #include "Firestore/core/src/firebase/firestore/model/maybe_document.h" #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/unknown_document.h" +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.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; using firebase::Timestamp; +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::Document; using firebase::firestore::model::DocumentState; using firebase::firestore::model::FieldMask; using firebase::firestore::model::MaybeDocument; using firebase::firestore::model::Mutation; +using firebase::firestore::model::MutationBatch; using firebase::firestore::model::NoDocument; using firebase::firestore::model::PatchMutation; using firebase::firestore::model::Precondition; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; using firebase::firestore::model::UnknownDocument; +using firebase::firestore::nanopb::ByteString; +using firebase::firestore::nanopb::MakeNSData; using firebase::firestore::testutil::Field; using firebase::firestore::testutil::Query; using firebase::firestore::testutil::Version; @@ -98,10 +103,7 @@ - (void)testEncodesMutationBatch { Mutation del = testutil::DeleteMutation("baz/quux"); Timestamp writeTime = Timestamp::Now(); - FSTMutationBatch *model = [[FSTMutationBatch alloc] initWithBatchID:42 - localWriteTime:writeTime - baseMutations:{base} - mutations:{set, patch, del}]; + MutationBatch model = MutationBatch(42, writeTime, {base}, {set, patch, del}); GCFSWrite *baseProto = [GCFSWrite message]; baseProto.update.name = @"projects/p/databases/d/documents/bar/baz"; @@ -141,12 +143,12 @@ - (void)testEncodesMutationBatch { batchProto.localWriteTime = writeTimeProto; XCTAssertEqualObjects([self.serializer encodedMutationBatch:model], batchProto); - FSTMutationBatch *decoded = [self.serializer decodedMutationBatch:batchProto]; - XCTAssertEqual(decoded.batchID, model.batchID); - XCTAssertEqual(decoded.localWriteTime, model.localWriteTime); - XCTAssertEqual(decoded.baseMutations, model.baseMutations); - XCTAssertEqual(decoded.mutations, model.mutations); - XCTAssertEqual([decoded keys], [model keys]); + MutationBatch decoded = [self.serializer decodedMutationBatch:batchProto]; + XCTAssertEqual(decoded.batch_id(), model.batch_id()); + XCTAssertEqual(decoded.local_write_time(), model.local_write_time()); + XCTAssertEqual(decoded.base_mutations(), model.base_mutations()); + XCTAssertEqual(decoded.mutations(), model.mutations()); + XCTAssertEqual(decoded.keys(), model.keys()); } - (void)testEncodesDocumentAsMaybeDocument { @@ -199,14 +201,9 @@ - (void)testEncodesQueryData { core::Query query = Query("room"); TargetId targetID = 42; SnapshotVersion version = Version(1039); - NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1039); + ByteString resumeToken = testutil::ResumeToken(1039); - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:targetID - listenSequenceNumber:10 - purpose:FSTQueryPurposeListen - snapshotVersion:version - resumeToken:resumeToken]; + QueryData queryData(query, targetID, 10, QueryPurpose::Listen, version, resumeToken); // Let the RPC serializer test various permutations of query serialization. GCFSTarget_QueryTarget *queryTarget = [self.remoteSerializer encodedQueryTarget:query]; @@ -215,13 +212,13 @@ - (void)testEncodesQueryData { expected.targetId = targetID; expected.lastListenSequenceNumber = 10; expected.snapshotVersion.nanos = 1039000; - expected.resumeToken = [resumeToken copy]; + expected.resumeToken = MakeNullableNSData(resumeToken); expected.query.parent = queryTarget.parent; expected.query.structuredQuery = queryTarget.structuredQuery; XCTAssertEqualObjects([self.serializer encodedQueryData:queryData], expected); - FSTQueryData *decoded = [self.serializer decodedQueryData:expected]; - XCTAssertEqualObjects(decoded, queryData); + QueryData decoded = [self.serializer decodedQueryData:expected]; + XCTAssertEqual(decoded, queryData); } @end diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.h b/Firestore/Example/Tests/Local/FSTLocalStoreTests.h index e514954e1fc..efbdaa2c9f3 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.h +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.h @@ -16,9 +16,11 @@ #import -#import "Firestore/Source/Local/FSTLocalStore.h" +#include -@protocol FSTPersistence; +#include "Firestore/core/src/firebase/firestore/local/persistence.h" + +namespace local = firebase::firestore::local; NS_ASSUME_NONNULL_BEGIN @@ -28,12 +30,12 @@ NS_ASSUME_NONNULL_BEGIN * To test a specific implementation of FSTLocalStore: * * + Subclass FSTLocalStoreTests - * + override -persistence, creating a new instance of FSTPersistence. + * + override -persistence, creating a new instance of Persistence. */ @interface FSTLocalStoreTests : XCTestCase -/** Creates and returns an appropriate id implementation. */ -- (id)persistence; +/** Creates and returns an appropriate Persistence implementation. */ +- (std::unique_ptr)persistence; @end diff --git a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm index c2fba8f4c87..78b5dc86c2f 100644 --- a/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTLocalStoreTests.mm @@ -13,8 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -#import "Firestore/Source/Local/FSTLocalStore.h" +// TODO(wuandy): Move `local_store.h` here once this test is ported to C++. #import #import @@ -24,9 +23,6 @@ #include #import "Firestore/Source/API/FIRFieldValue+Internal.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Source/Util/FSTClasses.h" #import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" @@ -34,10 +30,14 @@ #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/local_store.h" #include "Firestore/core/src/firebase/firestore/local/local_view_changes.h" #include "Firestore/core/src/firebase/firestore/local/local_write_result.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch_result.h" #include "Firestore/core/src/firebase/firestore/remote/remote_event.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/util/status.h" @@ -46,8 +46,11 @@ namespace testutil = firebase::firestore::testutil; using firebase::Timestamp; using firebase::firestore::auth::User; +using firebase::firestore::local::LocalStore; using firebase::firestore::local::LocalViewChanges; using firebase::firestore::local::LocalWriteResult; +using firebase::firestore::local::Persistence; +using firebase::firestore::local::QueryData; using firebase::firestore::model::Document; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; @@ -56,11 +59,14 @@ using firebase::firestore::model::ListenSequenceNumber; using firebase::firestore::model::MaybeDocument; using firebase::firestore::model::Mutation; +using firebase::firestore::model::MutationBatch; +using firebase::firestore::model::MutationBatchResult; using firebase::firestore::model::MutationResult; using firebase::firestore::model::DocumentMap; using firebase::firestore::model::MaybeDocumentMap; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::nanopb::ByteString; using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::TestTargetMetadataProvider; using firebase::firestore::remote::WatchChangeAggregator; @@ -101,15 +107,14 @@ @interface FSTLocalStoreTests () -@property(nonatomic, strong, readwrite) id localStorePersistence; -@property(nonatomic, strong, readwrite) FSTLocalStore *localStore; - -@property(nonatomic, strong, readonly) NSMutableArray *batches; @property(nonatomic, assign, readwrite) TargetId lastTargetID; @end @implementation FSTLocalStoreTests { + std::unique_ptr _localStore; + std::unique_ptr _localStorePersistence; + std::vector _batches; MaybeDocumentMap _lastChanges; } @@ -120,23 +125,24 @@ - (void)setUp { return; } - id persistence = [self persistence]; - self.localStorePersistence = persistence; - self.localStore = [[FSTLocalStore alloc] initWithPersistence:persistence - initialUser:User::Unauthenticated()]; - [self.localStore start]; + std::unique_ptr persistence = [self persistence]; + _localStorePersistence = std::move(persistence); + _localStore = + absl::make_unique(_localStorePersistence.get(), User::Unauthenticated()); + _localStore->Start(); - _batches = [NSMutableArray array]; _lastTargetID = 0; } - (void)tearDown { - [self.localStorePersistence shutdown]; + if (_localStorePersistence) { + _localStorePersistence->Shutdown(); + } [super tearDown]; } -- (id)persistence { +- (std::unique_ptr)persistence { @throw FSTAbstractMethodException(); // NOLINT } @@ -154,32 +160,33 @@ - (BOOL)isTestBaseClass { } - (void)writeMutation:(Mutation)mutation { - [self writeMutations:{mutation}]; + [self writeMutations:{std::move(mutation)}]; } - (void)writeMutations:(std::vector &&)mutations { auto mutationsCopy = mutations; - LocalWriteResult result = [self.localStore locallyWriteMutations:std::move(mutationsCopy)]; - [self.batches addObject:[[FSTMutationBatch alloc] initWithBatchID:result.batch_id() - localWriteTime:Timestamp::Now() - baseMutations:{} - mutations:std::move(mutations)]]; + LocalWriteResult result = _localStore->WriteLocally(std::move(mutationsCopy)); + _batches.emplace_back(result.batch_id(), Timestamp::Now(), std::vector{}, + std::move(mutations)); _lastChanges = result.changes(); } - (void)applyRemoteEvent:(const RemoteEvent &)event { - _lastChanges = [self.localStore applyRemoteEvent:event]; + _lastChanges = _localStore->ApplyRemoteEvent(event); } - (void)notifyLocalViewChanges:(LocalViewChanges)changes { - [self.localStore notifyLocalViewChanges:std::vector{std::move(changes)}]; + _localStore->NotifyLocalViewChanges(std::vector{std::move(changes)}); } - (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion transformResult:(id _Nullable)transformResult { - FSTMutationBatch *batch = [self.batches firstObject]; - [self.batches removeObjectAtIndex:0]; - XCTAssertEqual(batch.mutations.size(), 1, @"Acknowledging more than one mutation not supported."); + XCTAssertGreaterThan(_batches.size(), 0, @"Missing batch to acknowledge."); + MutationBatch batch = _batches.front(); + _batches.erase(_batches.begin()); + + XCTAssertEqual(batch.mutations().size(), 1, + @"Acknowledging more than one mutation not supported."); SnapshotVersion version = testutil::Version(documentVersion); absl::optional> mutationTransformResult; @@ -188,11 +195,8 @@ - (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion } MutationResult mutationResult(version, mutationTransformResult); - FSTMutationBatchResult *result = [FSTMutationBatchResult resultWithBatch:batch - commitVersion:version - mutationResults:{mutationResult} - streamToken:nil]; - _lastChanges = [self.localStore acknowledgeBatchWithResult:result]; + MutationBatchResult result(batch, version, {mutationResult}, {}); + _lastChanges = _localStore->AcknowledgeBatch(result); } - (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { @@ -200,15 +204,15 @@ - (void)acknowledgeMutationWithVersion:(FSTTestSnapshotVersion)documentVersion { } - (void)rejectMutation { - FSTMutationBatch *batch = [self.batches firstObject]; - [self.batches removeObjectAtIndex:0]; - _lastChanges = [self.localStore rejectBatchID:batch.batchID]; + MutationBatch batch = _batches.front(); + _batches.erase(_batches.begin()); + _lastChanges = _localStore->RejectBatch(batch.batch_id()); } - (TargetId)allocateQuery:(core::Query)query { - FSTQueryData *queryData = [self.localStore allocateQuery:std::move(query)]; - self.lastTargetID = queryData.targetID; - return queryData.targetID; + QueryData queryData = _localStore->AllocateQuery(std::move(query)); + self.lastTargetID = queryData.target_id(); + return queryData.target_id(); } /** Asserts that the last target ID is the given number. */ @@ -245,19 +249,19 @@ - (TargetId)allocateQuery:(core::Query)query { } while (0) /** Asserts that the given local store contains the given document. */ -#define FSTAssertContains(document) \ - do { \ - MaybeDocument expected = (document); \ - absl::optional actual = [self.localStore readDocument:expected.key()]; \ - XCTAssertEqual(actual, expected); \ +#define FSTAssertContains(document) \ + do { \ + MaybeDocument expected = (document); \ + absl::optional actual = _localStore->ReadDocument(expected.key()); \ + XCTAssertEqual(actual, expected); \ } while (0) /** Asserts that the given local store does not contain the given document. */ -#define FSTAssertNotContains(keyPathString) \ - do { \ - DocumentKey key = Key(keyPathString); \ - absl::optional actual = [self.localStore readDocument:key]; \ - XCTAssertEqual(actual, absl::nullopt); \ +#define FSTAssertNotContains(keyPathString) \ + do { \ + DocumentKey key = Key(keyPathString); \ + absl::optional actual = _localStore->ReadDocument(key); \ + XCTAssertEqual(actual, absl::nullopt); \ } while (0) - (void)testMutationBatchKeys { @@ -266,11 +270,8 @@ - (void)testMutationBatchKeys { Mutation base = FSTTestSetMutation(@"foo/ignore", @{@"foo" : @"bar"}); Mutation set1 = FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}); Mutation set2 = FSTTestSetMutation(@"bar/baz", @{@"bar" : @"baz"}); - FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:1 - localWriteTime:Timestamp::Now() - baseMutations:{base} - mutations:{set1, set2}]; - DocumentKeySet keys = [batch keys]; + MutationBatch batch = MutationBatch(1, Timestamp::Now(), {base}, {set1, set2}); + DocumentKeySet keys = batch.keys(); XCTAssertEqual(keys.size(), 2u); } @@ -364,7 +365,7 @@ - (void)testHandlesDeletedDocumentThenSetMutationThenAck { FSTAssertChanged(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations)); FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations)); // Can now remove the target, since we have a mutation pinning the document - [self.localStore releaseQuery:query]; + _localStore->ReleaseQuery(query); // Verify we didn't lose anything FSTAssertContains(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations)); @@ -520,7 +521,7 @@ - (void)testHandlesDocumentThenDeleteMutationThenAck { FSTAssertContains(DeletedDoc("foo/bar")); // Remove the target so only the mutation is pinning the document - [self.localStore releaseQuery:query]; + _localStore->ReleaseQuery(query); [self acknowledgeMutationWithVersion:2]; FSTAssertRemoved("foo/bar"); @@ -547,7 +548,7 @@ - (void)testHandlesDeleteMutationThenDocumentThenAck { FSTAssertContains(DeletedDoc("foo/bar")); // Don't need to keep it pinned anymore - [self.localStore releaseQuery:query]; + _localStore->ReleaseQuery(query); [self acknowledgeMutationWithVersion:2]; FSTAssertRemoved("foo/bar"); @@ -600,7 +601,7 @@ - (void)testHandlesSetMutationThenPatchMutationThenDocumentThenAckThenAck { FSTAssertChanged(Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations)); FSTAssertContains(Doc("foo/bar", 1, Map("foo", "bar"), DocumentState::kLocalMutations)); - [self.localStore releaseQuery:query]; + _localStore->ReleaseQuery(query); [self acknowledgeMutationWithVersion:2]; // delete mutation FSTAssertChanged(Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations)); FSTAssertContains(Doc("foo/bar", 2, Map("foo", "bar"), DocumentState::kLocalMutations)); @@ -724,7 +725,7 @@ - (void)testCollectsGarbageAfterAcknowledgedMutation { {})]; [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})]; // Release the query so that our target count goes back to 0 and we are considered up-to-date. - [self.localStore releaseQuery:query]; + _localStore->ReleaseQuery(query); [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; @@ -759,7 +760,7 @@ - (void)testCollectsGarbageAfterRejectedMutation { {})]; [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"foo" : @"bar"}, {})]; // Release the query so that our target count goes back to 0 and we are considered up-to-date. - [self.localStore releaseQuery:query]; + _localStore->ReleaseQuery(query); [self writeMutation:FSTTestSetMutation(@"foo/bah", @{@"foo" : @"bah"})]; [self writeMutation:FSTTestDeleteMutation(@"foo/baz")]; @@ -808,10 +809,10 @@ - (void)testPinsDocumentsInTheLocalView { FSTAssertContains(Doc("foo/baz", 2, Map("foo", "baz"))); [self notifyLocalViewChanges:TestViewChanges(targetID, @[], @[ @"foo/bar", @"foo/baz" ])]; - [self.localStore releaseQuery:query]; - FSTAssertNotContains("foo/bar"); FSTAssertNotContains("foo/baz"); + + _localStore->ReleaseQuery(query); } - (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately { @@ -828,13 +829,13 @@ - (void)testThrowsAwayDocumentsWithUnknownTargetIDsImmediately { - (void)testCanExecuteDocumentQueries { if ([self isTestBaseClass]) return; - [self.localStore locallyWriteMutations:{ + _localStore->WriteLocally({ FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}) - }]; + }); core::Query query = Query("foo/bar"); - DocumentMap docs = [self.localStore executeQuery:query]; + DocumentMap docs = _localStore->ExecuteQuery(query); XCTAssertEqual(DocMapToArray(docs), Vector(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations))); } @@ -842,15 +843,15 @@ - (void)testCanExecuteDocumentQueries { - (void)testCanExecuteCollectionQueries { if ([self isTestBaseClass]) return; - [self.localStore locallyWriteMutations:{ + _localStore->WriteLocally({ FSTTestSetMutation(@"fo/bar", @{@"fo" : @"bar"}), FSTTestSetMutation(@"foo/bar", @{@"foo" : @"bar"}), FSTTestSetMutation(@"foo/baz", @{@"foo" : @"baz"}), FSTTestSetMutation(@"foo/bar/Foo/Bar", @{@"Foo" : @"Bar"}), FSTTestSetMutation(@"fooo/blah", @{@"fooo" : @"blah"}) - }]; + }); core::Query query = Query("foo"); - DocumentMap docs = [self.localStore executeQuery:query]; + DocumentMap docs = _localStore->ExecuteQuery(query); XCTAssertEqual(DocMapToArray(docs), Vector(Doc("foo/bar", 0, Map("foo", "bar"), DocumentState::kLocalMutations), Doc("foo/baz", 0, Map("foo", "baz"), DocumentState::kLocalMutations))); @@ -866,9 +867,9 @@ - (void)testCanExecuteMixedCollectionQueries { [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/baz", 10, Map("a", "b")), {2}, {})]; [self applyRemoteEvent:FSTTestUpdateRemoteEvent(Doc("foo/bar", 20, Map("a", "b")), {2}, {})]; - [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) }]; + _localStore->WriteLocally({ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) }); - DocumentMap docs = [self.localStore executeQuery:query]; + DocumentMap docs = _localStore->ExecuteQuery(query); XCTAssertEqual(DocMapToArray(docs), Vector(Doc("foo/bar", 20, Map("a", "b")), Doc("foo/baz", 10, Map("a", "b")), Doc("foo/bonk", 0, Map("a", "b"), DocumentState::kLocalMutations))); @@ -880,10 +881,10 @@ - (void)testPersistsResumeTokens { if ([self gcIsEager]) return; core::Query query = Query("foo/bar"); - FSTQueryData *queryData = [self.localStore allocateQuery:query]; - ListenSequenceNumber initialSequenceNumber = queryData.sequenceNumber; - TargetId targetID = queryData.targetID; - NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(1000); + QueryData queryData = _localStore->AllocateQuery(query); + ListenSequenceNumber initialSequenceNumber = queryData.sequence_number(); + TargetId targetID = queryData.target_id(); + ByteString resumeToken = testutil::ResumeToken(1000); WatchTargetChange watchChange{WatchTargetChangeState::Current, {targetID}, resumeToken}; auto metadataProvider = TestTargetMetadataProvider::CreateSingleResultProvider( @@ -894,14 +895,14 @@ - (void)testPersistsResumeTokens { [self applyRemoteEvent:remoteEvent]; // Stop listening so that the query should become inactive (but persistent) - [self.localStore releaseQuery:query]; + _localStore->ReleaseQuery(query); // Should come back with the same resume token - FSTQueryData *queryData2 = [self.localStore allocateQuery:query]; - XCTAssertEqualObjects(queryData2.resumeToken, resumeToken); + QueryData queryData2 = _localStore->AllocateQuery(query); + XCTAssertEqual(queryData2.resume_token(), resumeToken); // The sequence number should have been bumped when we saved the new resume token. - ListenSequenceNumber newSequenceNumber = queryData2.sequenceNumber; + ListenSequenceNumber newSequenceNumber = queryData2.sequence_number(); XCTAssertGreaterThan(newSequenceNumber, initialSequenceNumber); } @@ -915,13 +916,13 @@ - (void)testRemoteDocumentKeysForTarget { [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/baz", 10, Map("a", "b")), {2})]; [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 20, Map("a", "b")), {2})]; - [self.localStore locallyWriteMutations:{ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) }]; + _localStore->WriteLocally({ FSTTestSetMutation(@"foo/bonk", @{@"a" : @"b"}) }); - DocumentKeySet keys = [self.localStore remoteDocumentKeysForTarget:2]; + DocumentKeySet keys = _localStore->GetRemoteDocumentKeys(2); DocumentKeySet expected{testutil::Key("foo/bar"), testutil::Key("foo/baz")}; XCTAssertEqual(keys, expected); - keys = [self.localStore remoteDocumentKeysForTarget:2]; + keys = _localStore->GetRemoteDocumentKeys(2); XCTAssertEqual(keys, (DocumentKeySet{testutil::Key("foo/bar"), testutil::Key("foo/baz")})); } @@ -1052,9 +1053,9 @@ - (void)testHoldsBackOnlyNonIdempotentTransforms { // The sum transform is not idempotent and the backend's updated value is ignored. The // ArrayUnion transform is recomputed and includes the backend value. [self applyRemoteEvent:FSTTestUpdateRemoteEvent( - Doc("foo/bar", 1, Map("sum", 1337, "array_union", Array("bar"))), {2}, + Doc("foo/bar", 2, Map("sum", 1337, "array_union", Array("bar"))), {2}, {})]; - FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 1, "array_union", Array("bar", "foo")), + FSTAssertChanged(Doc("foo/bar", 2, Map("sum", 1, "array_union", Array("bar", "foo")), DocumentState::kLocalMutations)); } @@ -1104,6 +1105,44 @@ - (void)testHandlesPatchMutationWithTransformThenRemoteEvent { FSTAssertChanged(Doc("foo/bar", 1, Map("sum", 1), DocumentState::kLocalMutations)); } +- (void)testGetHighestUnacknowledgeBatchId { + if ([self isTestBaseClass]) return; + + XCTAssertEqual(-1, _localStore->GetHighestUnacknowledgedBatchId()); + + [self writeMutation:FSTTestSetMutation(@"foo/bar", @{@"abc" : @123})]; + XCTAssertEqual(1, _localStore->GetHighestUnacknowledgedBatchId()); + + [self writeMutation:FSTTestPatchMutation("foo/bar", @{@"abc" : @321}, {})]; + XCTAssertEqual(2, _localStore->GetHighestUnacknowledgedBatchId()); + + [self acknowledgeMutationWithVersion:1]; + XCTAssertEqual(2, _localStore->GetHighestUnacknowledgedBatchId()); + + [self rejectMutation]; + XCTAssertEqual(-1, _localStore->GetHighestUnacknowledgedBatchId()); +} + +- (void)testOnlyPersistsUpdatesForDocumentsWhenVersionChanges { + if ([self isTestBaseClass]) return; + + core::Query query = Query("foo"); + [self allocateQuery:query]; + FSTAssertTargetID(2); + + [self applyRemoteEvent:FSTTestAddedRemoteEvent(Doc("foo/bar", 1, Map("val", "old")), {2})]; + FSTAssertContains(Doc("foo/bar", 1, Map("val", "old"))); + FSTAssertChanged(Doc("foo/bar", 1, Map("val", "old"))); + + [self applyRemoteEvent:FSTTestAddedRemoteEvent({Doc("foo/bar", 1, Map("val", "new")), + Doc("foo/baz", 2, Map("val", "new"))}, + {2})]; + // The update to foo/bar is ignored. + FSTAssertContains(Doc("foo/bar", 1, Map("val", "old"))); + FSTAssertContains(Doc("foo/baz", 2, Map("val", "new"))); + FSTAssertChanged(Doc("foo/baz", 2, Map("val", "new"))); +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMemoryLRUGarbageCollectorTests.mm b/Firestore/Example/Tests/Local/FSTMemoryLRUGarbageCollectorTests.mm index f0902deebd0..12a75e47add 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryLRUGarbageCollectorTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryLRUGarbageCollectorTests.mm @@ -17,13 +17,15 @@ #import "Firestore/Example/Tests/Local/FSTLRUGarbageCollectorTests.h" #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" + +#include "Firestore/core/src/firebase/firestore/local/memory_lru_reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" using firebase::firestore::model::DocumentKey; - using firebase::firestore::local::LruParams; +using firebase::firestore::local::MemoryLruReferenceDelegate; +using firebase::firestore::local::Persistence; NS_ASSUME_NONNULL_BEGIN @@ -32,14 +34,13 @@ @interface FSTMemoryLRUGarbageCollectionTests : FSTLRUGarbageCollectorTests @implementation FSTMemoryLRUGarbageCollectionTests -- (id)newPersistenceWithLruParams:(LruParams)lruParams { +- (std::unique_ptr)newPersistenceWithLruParams:(LruParams)lruParams { return [FSTPersistenceTestHelpers lruMemoryPersistenceWithLruParams:lruParams]; } - (BOOL)sentinelExists:(const DocumentKey &)key { - FSTMemoryLRUReferenceDelegate *delegate = - (FSTMemoryLRUReferenceDelegate *)self.persistence.referenceDelegate; - return [delegate isPinnedAtSequenceNumber:0 document:key]; + auto delegate = static_cast(self.persistence->reference_delegate()); + return delegate->IsPinnedAtSequenceNumber(0, key); } @end diff --git a/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.mm b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.mm index c387b94233e..8c98a14c881 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryLocalStoreTests.mm @@ -16,15 +16,18 @@ #import "Firestore/Example/Tests/Local/FSTLocalStoreTests.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" + NS_ASSUME_NONNULL_BEGIN +using firebase::firestore::local::Persistence; + /** * This tests the FSTLocalStore with an FSTMemoryPersistence persistence implementation. The tests - * are in FSTLocalStoreTests and this class is merely responsible for creating a new FSTPersistence + * are in FSTLocalStoreTests and this class is merely responsible for creating a new Persistence * implementation on demand. */ @interface FSTMemoryLocalStoreTests : FSTLocalStoreTests @@ -32,7 +35,7 @@ @interface FSTMemoryLocalStoreTests : FSTLocalStoreTests @implementation FSTMemoryLocalStoreTests -- (id)persistence { +- (std::unique_ptr)persistence { return [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; } diff --git a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm index 0d54f4466ec..3ff9f60cbb6 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryMutationQueueTests.mm @@ -14,15 +14,16 @@ * limitations under the License. */ -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - #import "Firestore/Example/Tests/Local/FSTMutationQueueTests.h" #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" using firebase::firestore::auth::User; +using firebase::firestore::local::MemoryPersistence; using firebase::firestore::local::ReferenceSet; @interface FSTMemoryMutationQueueTests : FSTMutationQueueTests @@ -33,15 +34,17 @@ @interface FSTMemoryMutationQueueTests : FSTMutationQueueTests * FSTMutationQueueTests. This class is merely responsible for setting up the @a mutationQueue. */ @implementation FSTMemoryMutationQueueTests { + std::unique_ptr _db; ReferenceSet _additionalReferences; } - (void)setUp { [super setUp]; - self.persistence = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; - [self.persistence.referenceDelegate addInMemoryPins:&_additionalReferences]; - self.mutationQueue = [self.persistence mutationQueueForUser:User("user")]; + _db = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; + self.persistence = _db.get(); + self.persistence->reference_delegate()->AddInMemoryPins(&_additionalReferences); + self.mutationQueue = self.persistence->GetMutationQueueForUser(User("user")); } @end diff --git a/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.mm b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.mm index 028898aa7b7..4310f982870 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryQueryCacheTests.mm @@ -14,13 +14,15 @@ * limitations under the License. */ -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" #import "Firestore/Example/Tests/Local/FSTQueryCacheTests.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/memory_query_cache.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" +using firebase::firestore::local::MemoryPersistence; using firebase::firestore::local::ReferenceSet; NS_ASSUME_NONNULL_BEGIN @@ -34,15 +36,17 @@ @interface FSTMemoryQueryCacheTests : FSTQueryCacheTests * @a queryCache. */ @implementation FSTMemoryQueryCacheTests { + std::unique_ptr _db; ReferenceSet _additionalReferences; } - (void)setUp { [super setUp]; - self.persistence = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; - self.queryCache = self.persistence.queryCache; - [self.persistence.referenceDelegate addInMemoryPins:&_additionalReferences]; + _db = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; + self.persistence = _db.get(); + self.queryCache = self.persistence->query_cache(); + self.persistence->reference_delegate()->AddInMemoryPins(&_additionalReferences); } - (void)tearDown { diff --git a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm index 7784056705b..4c15a983e9a 100644 --- a/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTMemoryRemoteDocumentCacheTests.mm @@ -16,14 +16,16 @@ #include -#import "Firestore/Source/Local/FSTMemoryPersistence.h" +#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" +#import "Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h" + +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" #include "Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" #include "absl/memory/memory.h" -#import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h" - +using firebase::firestore::local::MemoryPersistence; using firebase::firestore::local::MemoryRemoteDocumentCache; using firebase::firestore::local::RemoteDocumentCache; @@ -36,23 +38,25 @@ @interface FSTMemoryRemoteDocumentCacheTests : FSTRemoteDocumentCacheTests * tearing down the @a remoteDocumentCache. */ @implementation FSTMemoryRemoteDocumentCacheTests { - std::unique_ptr _cache; + std::unique_ptr _db; + MemoryRemoteDocumentCache *_cache; } - (void)setUp { [super setUp]; - self.persistence = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; + _db = [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; + self.persistence = _db.get(); HARD_ASSERT(!_cache, "Previous cache not torn down"); - _cache = absl::make_unique(self.persistence); + _cache = _db->remote_document_cache(); } - (RemoteDocumentCache *)remoteDocumentCache { - return _cache.get(); + return _cache; } - (void)tearDown { - _cache.reset(); + _cache = nullptr; self.persistence = nil; [super tearDown]; diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.h b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h index 78e9f8b7c4f..30361196026 100644 --- a/Firestore/Example/Tests/Local/FSTMutationQueueTests.h +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.h @@ -18,7 +18,18 @@ #include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" -@protocol FSTPersistence; +namespace firebase { +namespace firestore { +namespace local { + +class MutationQueue; +class Persistence; + +} // namespace local +} // namespace firestore +} // namespace firebase + +namespace local = firebase::firestore::local; NS_ASSUME_NONNULL_BEGIN @@ -32,8 +43,11 @@ NS_ASSUME_NONNULL_BEGIN * + override -tearDown, cleaning up mutationQueue and persistence */ @interface FSTMutationQueueTests : XCTestCase -@property(nonatomic, nullable) firebase::firestore::local::MutationQueue *mutationQueue; -@property(nonatomic, strong, nullable) id persistence; + +@property(nonatomic, nullable) local::MutationQueue *mutationQueue; + +@property(nonatomic, nullable) local::Persistence *persistence; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm index 182a17b4a0e..801a6788f34 100644 --- a/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm +++ b/Firestore/Example/Tests/Local/FSTMutationQueueTests.mm @@ -22,12 +22,10 @@ #include #include -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - #import "Firestore/Example/Tests/Util/FSTHelpers.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.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/mutation.h" @@ -43,7 +41,9 @@ using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::kBatchIdUnknown; using firebase::firestore::model::Mutation; +using firebase::firestore::model::MutationBatch; using firebase::firestore::model::SetMutation; +using firebase::firestore::nanopb::ByteString; using firebase::firestore::testutil::Key; using firebase::firestore::testutil::Query; @@ -52,7 +52,9 @@ @implementation FSTMutationQueueTests - (void)tearDown { - [self.persistence shutdown]; + if (self.persistence) { + self.persistence->Shutdown(); + } [super tearDown]; } @@ -68,15 +70,15 @@ - (BOOL)isTestBaseClass { - (void)testCountBatches { if ([self isTestBaseClass]) return; - self.persistence.run("testCountBatches", [&]() { + self.persistence->Run("testCountBatches", [&]() { XCTAssertEqual(0, [self batchCount]); XCTAssertTrue(self.mutationQueue->IsEmpty()); - FSTMutationBatch *batch1 = [self addMutationBatch]; + MutationBatch batch1 = [self addMutationBatch]; XCTAssertEqual(1, [self batchCount]); XCTAssertFalse(self.mutationQueue->IsEmpty()); - FSTMutationBatch *batch2 = [self addMutationBatch]; + MutationBatch batch2 = [self addMutationBatch]; XCTAssertEqual(2, [self batchCount]); self.mutationQueue->RemoveMutationBatch(batch1); @@ -91,23 +93,23 @@ - (void)testCountBatches { - (void)testAcknowledgeBatchID { if ([self isTestBaseClass]) return; - self.persistence.run("testAcknowledgeBatchID", [&]() { + self.persistence->Run("testAcknowledgeBatchID", [&]() { XCTAssertEqual([self batchCount], 0); - FSTMutationBatch *batch1 = [self addMutationBatch]; - FSTMutationBatch *batch2 = [self addMutationBatch]; - FSTMutationBatch *batch3 = [self addMutationBatch]; - XCTAssertGreaterThan(batch1.batchID, kBatchIdUnknown); - XCTAssertGreaterThan(batch2.batchID, batch1.batchID); - XCTAssertGreaterThan(batch3.batchID, batch2.batchID); + MutationBatch batch1 = [self addMutationBatch]; + MutationBatch batch2 = [self addMutationBatch]; + MutationBatch batch3 = [self addMutationBatch]; + XCTAssertGreaterThan(batch1.batch_id(), kBatchIdUnknown); + XCTAssertGreaterThan(batch2.batch_id(), batch1.batch_id()); + XCTAssertGreaterThan(batch3.batch_id(), batch2.batch_id()); XCTAssertEqual([self batchCount], 3); - self.mutationQueue->AcknowledgeBatch(batch1, nil); + self.mutationQueue->AcknowledgeBatch(batch1, {}); self.mutationQueue->RemoveMutationBatch(batch1); XCTAssertEqual([self batchCount], 2); - self.mutationQueue->AcknowledgeBatch(batch2, nil); + self.mutationQueue->AcknowledgeBatch(batch2, {}); XCTAssertEqual([self batchCount], 2); self.mutationQueue->RemoveMutationBatch(batch2); @@ -121,10 +123,10 @@ - (void)testAcknowledgeBatchID { - (void)testAcknowledgeThenRemove { if ([self isTestBaseClass]) return; - self.persistence.run("testAcknowledgeThenRemove", [&]() { - FSTMutationBatch *batch1 = [self addMutationBatch]; + self.persistence->Run("testAcknowledgeThenRemove", [&]() { + MutationBatch batch1 = [self addMutationBatch]; - self.mutationQueue->AcknowledgeBatch(batch1, nil); + self.mutationQueue->AcknowledgeBatch(batch1, {}); self.mutationQueue->RemoveMutationBatch(batch1); XCTAssertEqual([self batchCount], 0); @@ -135,66 +137,71 @@ - (void)testLookupMutationBatch { if ([self isTestBaseClass]) return; // Searching on an empty queue should not find a non-existent batch - self.persistence.run("testLookupMutationBatch", [&]() { - FSTMutationBatch *notFound = self.mutationQueue->LookupMutationBatch(42); - XCTAssertNil(notFound); + self.persistence->Run("testLookupMutationBatch", [&]() { + absl::optional notFound = self.mutationQueue->LookupMutationBatch(42); + XCTAssertEqual(notFound, absl::nullopt); - std::vector batches = [self createBatches:10]; - std::vector removed = [self removeFirstBatches:3 inBatches:&batches]; + std::vector batches = [self createBatches:10]; + std::vector removed = [self removeFirstBatches:3 inBatches:&batches]; // After removing, a batch should not be found for (size_t i = 0; i < removed.size(); i++) { - notFound = self.mutationQueue->LookupMutationBatch(removed[i].batchID); - XCTAssertNil(notFound); + notFound = self.mutationQueue->LookupMutationBatch(removed[i].batch_id()); + XCTAssertEqual(notFound, absl::nullopt); } // Remaining entries should still be found - for (FSTMutationBatch *batch : batches) { - FSTMutationBatch *found = self.mutationQueue->LookupMutationBatch(batch.batchID); - XCTAssertEqual(found.batchID, batch.batchID); + for (const MutationBatch& batch : batches) { + absl::optional found = + self.mutationQueue->LookupMutationBatch(batch.batch_id()); + XCTAssertEqual(found->batch_id(), batch.batch_id()); } // Even on a nonempty queue searching should not find a non-existent batch notFound = self.mutationQueue->LookupMutationBatch(42); - XCTAssertNil(notFound); + XCTAssertEqual(notFound, absl::nullopt); }); } - (void)testNextMutationBatchAfterBatchID { if ([self isTestBaseClass]) return; - self.persistence.run("testNextMutationBatchAfterBatchID", [&]() { - std::vector batches = [self createBatches:10]; - std::vector removed = [self removeFirstBatches:3 inBatches:&batches]; + self.persistence->Run("testNextMutationBatchAfterBatchID", [&]() { + std::vector batches = [self createBatches:10]; + std::vector removed = [self removeFirstBatches:3 inBatches:&batches]; for (size_t i = 0; i < batches.size() - 1; i++) { - FSTMutationBatch *current = batches[i]; - FSTMutationBatch *next = batches[i + 1]; - FSTMutationBatch *found = self.mutationQueue->NextMutationBatchAfterBatchId(current.batchID); - XCTAssertEqual(found.batchID, next.batchID); + const MutationBatch& current = batches[i]; + const MutationBatch& next = batches[i + 1]; + absl::optional found = + self.mutationQueue->NextMutationBatchAfterBatchId(current.batch_id()); + XCTAssertEqual(found->batch_id(), next.batch_id()); } for (size_t i = 0; i < removed.size(); i++) { - FSTMutationBatch *current = removed[i]; - FSTMutationBatch *next = batches[0]; - FSTMutationBatch *found = self.mutationQueue->NextMutationBatchAfterBatchId(current.batchID); - XCTAssertEqual(found.batchID, next.batchID); + const MutationBatch& current = removed[i]; + const MutationBatch& next = batches[0]; + absl::optional found = + self.mutationQueue->NextMutationBatchAfterBatchId(current.batch_id()); + XCTAssertEqual(found->batch_id(), next.batch_id()); } - FSTMutationBatch *first = batches[0]; - FSTMutationBatch *found = self.mutationQueue->NextMutationBatchAfterBatchId(first.batchID - 42); - XCTAssertEqual(found.batchID, first.batchID); + const MutationBatch& first = batches[0]; + absl::optional found = + self.mutationQueue->NextMutationBatchAfterBatchId(first.batch_id() - 42); + XCTAssertEqual(found->batch_id(), first.batch_id()); - FSTMutationBatch *last = batches[batches.size() - 1]; - FSTMutationBatch *notFound = self.mutationQueue->NextMutationBatchAfterBatchId(last.batchID); - XCTAssertNil(notFound); + const MutationBatch& last = batches[batches.size() - 1]; + absl::optional notFound = + self.mutationQueue->NextMutationBatchAfterBatchId(last.batch_id()); + XCTAssertEqual(notFound, absl::nullopt); }); } - (void)testAllMutationBatchesAffectingDocumentKey { if ([self isTestBaseClass]) return; - self.persistence.run("testAllMutationBatchesAffectingDocumentKey", [&]() { + self.persistence->Run("testAllMutationBatchesAffectingDocumentKey", [&]() { std::vector mutations = { FSTTestSetMutation(@"foi/bar", @{@"a" : @1}), FSTTestSetMutation(@"foo/bar", @{@"a" : @1}), @@ -205,25 +212,24 @@ - (void)testAllMutationBatchesAffectingDocumentKey { }; // Store all the mutations. - std::vector batches; - for (const Mutation &mutation : mutations) { - FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, {mutation}); + std::vector batches; + for (const Mutation& mutation : mutations) { + MutationBatch batch = self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, {mutation}); batches.push_back(batch); } - std::vector expected{batches[1], batches[2]}; - std::vector matches = + std::vector expected{batches[1], batches[2]}; + std::vector matches = self.mutationQueue->AllMutationBatchesAffectingDocumentKey(testutil::Key("foo/bar")); - FSTAssertEqualVectors(matches, expected); + XCTAssertEqual(matches, expected); }); } - (void)testAllMutationBatchesAffectingDocumentKeys { if ([self isTestBaseClass]) return; - self.persistence.run("testAllMutationBatchesAffectingDocumentKey", [&]() { + self.persistence->Run("testAllMutationBatchesAffectingDocumentKey", [&]() { std::vector mutations = { FSTTestSetMutation(@"fob/bar", @{@"a" : @1}), FSTTestSetMutation(@"foo/bar", @{@"a" : @1}), @@ -234,10 +240,9 @@ - (void)testAllMutationBatchesAffectingDocumentKeys { }; // Store all the mutations. - std::vector batches; - for (const Mutation &mutation : mutations) { - FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, {mutation}); + std::vector batches; + for (const Mutation& mutation : mutations) { + MutationBatch batch = self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, {mutation}); batches.push_back(batch); } @@ -246,23 +251,23 @@ - (void)testAllMutationBatchesAffectingDocumentKeys { Key("foo/baz"), }; - std::vector expected{batches[1], batches[2], batches[4]}; - std::vector matches = + std::vector expected{batches[1], batches[2], batches[4]}; + std::vector matches = self.mutationQueue->AllMutationBatchesAffectingDocumentKeys(keys); - FSTAssertEqualVectors(matches, expected); + XCTAssertEqual(matches, expected); }); } - (void)testAllMutationBatchesAffectingDocumentKeys_handlesOverlap { if ([self isTestBaseClass]) return; - self.persistence.run("testAllMutationBatchesAffectingDocumentKeys_handlesOverlap", [&]() { + self.persistence->Run("testAllMutationBatchesAffectingDocumentKeys_handlesOverlap", [&]() { std::vector group1 = { FSTTestSetMutation(@"foo/bar", @{@"a" : @1}), FSTTestSetMutation(@"foo/baz", @{@"a" : @1}), }; - FSTMutationBatch *batch1 = + MutationBatch batch1 = self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, std::move(group1)); std::vector group2 = {FSTTestSetMutation(@"food/bar", @{@"a" : @1})}; @@ -271,7 +276,7 @@ - (void)testAllMutationBatchesAffectingDocumentKeys_handlesOverlap { std::vector group3 = { FSTTestSetMutation(@"foo/bar", @{@"b" : @1}), }; - FSTMutationBatch *batch3 = + MutationBatch batch3 = self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, std::move(group3)); DocumentKeySet keys{ @@ -279,18 +284,18 @@ - (void)testAllMutationBatchesAffectingDocumentKeys_handlesOverlap { Key("foo/baz"), }; - std::vector expected{batch1, batch3}; - std::vector matches = + std::vector expected{batch1, batch3}; + std::vector matches = self.mutationQueue->AllMutationBatchesAffectingDocumentKeys(keys); - FSTAssertEqualVectors(matches, expected); + XCTAssertEqual(matches, expected); }); } - (void)testAllMutationBatchesAffectingQuery { if ([self isTestBaseClass]) return; - self.persistence.run("testAllMutationBatchesAffectingQuery", [&]() { + self.persistence->Run("testAllMutationBatchesAffectingQuery", [&]() { std::vector mutations = { FSTTestSetMutation(@"fob/bar", @{@"a" : @1}), FSTTestSetMutation(@"foo/bar", @{@"a" : @1}), @@ -301,37 +306,36 @@ - (void)testAllMutationBatchesAffectingQuery { }; // Store all the mutations. - std::vector batches; - for (const Mutation &mutation : mutations) { - FSTMutationBatch *batch = - self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, {mutation}); + std::vector batches; + for (const Mutation& mutation : mutations) { + MutationBatch batch = self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, {mutation}); batches.push_back(batch); } - std::vector expected = {batches[1], batches[2], batches[4]}; + std::vector expected = {batches[1], batches[2], batches[4]}; core::Query query = Query("foo"); - std::vector matches = + std::vector matches = self.mutationQueue->AllMutationBatchesAffectingQuery(query); - FSTAssertEqualVectors(matches, expected); + XCTAssertEqual(matches, expected); }); } - (void)testRemoveMutationBatches { if ([self isTestBaseClass]) return; - self.persistence.run("testRemoveMutationBatches", [&]() { - std::vector batches = [self createBatches:10]; + self.persistence->Run("testRemoveMutationBatches", [&]() { + std::vector batches = [self createBatches:10]; self.mutationQueue->RemoveMutationBatch(batches[0]); batches.erase(batches.begin()); XCTAssertEqual([self batchCount], 9); - std::vector found; + std::vector found; found = self.mutationQueue->AllMutationBatches(); - FSTAssertEqualVectors(found, batches); + XCTAssertEqual(found, batches); XCTAssertEqual(found.size(), 9); self.mutationQueue->RemoveMutationBatch(batches[0]); @@ -341,7 +345,7 @@ - (void)testRemoveMutationBatches { XCTAssertEqual([self batchCount], 6); found = self.mutationQueue->AllMutationBatches(); - FSTAssertEqualVectors(found, batches); + XCTAssertEqual(found, batches); XCTAssertEqual(found.size(), 6); self.mutationQueue->RemoveMutationBatch(batches[0]); @@ -349,7 +353,7 @@ - (void)testRemoveMutationBatches { XCTAssertEqual([self batchCount], 5); found = self.mutationQueue->AllMutationBatches(); - FSTAssertEqualVectors(found, batches); + XCTAssertEqual(found, batches); XCTAssertEqual(found.size(), 5); self.mutationQueue->RemoveMutationBatch(batches[0]); @@ -361,11 +365,11 @@ - (void)testRemoveMutationBatches { XCTAssertEqual([self batchCount], 3); found = self.mutationQueue->AllMutationBatches(); - FSTAssertEqualVectors(found, batches); + XCTAssertEqual(found, batches); XCTAssertEqual(found.size(), 3); XCTAssertFalse(self.mutationQueue->IsEmpty()); - for (FSTMutationBatch *batch : batches) { + for (const MutationBatch& batch : batches) { self.mutationQueue->RemoveMutationBatch(batch); } found = self.mutationQueue->AllMutationBatches(); @@ -377,49 +381,49 @@ - (void)testRemoveMutationBatches { - (void)testStreamToken { if ([self isTestBaseClass]) return; - NSData *streamToken1 = [@"token1" dataUsingEncoding:NSUTF8StringEncoding]; - NSData *streamToken2 = [@"token2" dataUsingEncoding:NSUTF8StringEncoding]; + ByteString streamToken1("token1"); + ByteString streamToken2("token2"); - self.persistence.run("testStreamToken", [&]() { + self.persistence->Run("testStreamToken", [&]() { self.mutationQueue->SetLastStreamToken(streamToken1); - FSTMutationBatch *batch1 = [self addMutationBatch]; + MutationBatch batch1 = [self addMutationBatch]; [self addMutationBatch]; - XCTAssertEqualObjects(self.mutationQueue->GetLastStreamToken(), streamToken1); + XCTAssertEqual(self.mutationQueue->GetLastStreamToken(), streamToken1); self.mutationQueue->AcknowledgeBatch(batch1, streamToken2); - XCTAssertEqualObjects(self.mutationQueue->GetLastStreamToken(), streamToken2); + XCTAssertEqual(self.mutationQueue->GetLastStreamToken(), streamToken2); }); } #pragma mark - Helpers -/** Creates a new FSTMutationBatch with the next batch ID and a set of dummy mutations. */ -- (FSTMutationBatch *)addMutationBatch { +/** Creates a new MutationBatch with the next batch ID and a set of dummy mutations. */ +- (MutationBatch)addMutationBatch { return [self addMutationBatchWithKey:@"foo/bar"]; } /** - * Creates a new FSTMutationBatch with the given key, the next batch ID and a set of dummy + * Creates a new MutationBatch with the given key, the next batch ID and a set of dummy * mutations. */ -- (FSTMutationBatch *)addMutationBatchWithKey:(NSString *)key { +- (MutationBatch)addMutationBatchWithKey:(NSString*)key { SetMutation mutation = FSTTestSetMutation(key, @{@"a" : @1}); - FSTMutationBatch *batch = self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, {mutation}); + MutationBatch batch = self.mutationQueue->AddMutationBatch(Timestamp::Now(), {}, {mutation}); return batch; } /** - * Creates an array of batches containing @a number dummy FSTMutationBatches. Each has a different + * Creates an array of batches containing @a number dummy MutationBatches. Each has a different * batchID. */ -- (std::vector)createBatches:(int)number { - std::vector batches; +- (std::vector)createBatches:(int)number { + std::vector batches; for (int i = 0; i < number; i++) { - FSTMutationBatch *batch = [self addMutationBatch]; + MutationBatch batch = [self addMutationBatch]; batches.push_back(batch); } @@ -438,12 +442,12 @@ - (size_t)batchCount { * @param batches The array to mutate, removing entries from it. * @return A new array containing all the entries that were removed from @a batches. */ -- (std::vector)removeFirstBatches:(size_t)n - inBatches:(std::vector *)batches { - std::vector removed(batches->begin(), batches->begin() + n); +- (std::vector)removeFirstBatches:(size_t)n + inBatches:(std::vector*)batches { + std::vector removed(batches->begin(), batches->begin() + n); batches->erase(batches->begin(), batches->begin() + n); - for (FSTMutationBatch *batch : removed) { + for (const MutationBatch& batch : removed) { self.mutationQueue->RemoveMutationBatch(batch); } return removed; diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h index 8d5937afc67..504d5c4f099 100644 --- a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h +++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h @@ -16,11 +16,23 @@ #import -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" +#include + #include "Firestore/core/src/firebase/firestore/util/path.h" -@class FSTLevelDB; -@class FSTMemoryPersistence; +namespace firebase { +namespace firestore { +namespace local { + +class LevelDbPersistence; +class LruParams; +class MemoryPersistence; + +} // namespace local +} // namespace firestore +} // namespace firebase + +namespace local = firebase::firestore::local; NS_ASSUME_NONNULL_BEGIN @@ -40,14 +52,15 @@ NS_ASSUME_NONNULL_BEGIN * database is reused. This prevents concurrent running of tests using this database. We may * need to revisit this if we want to parallelize the tests. */ -+ (FSTLevelDB *)levelDBPersistence; ++ (std::unique_ptr)levelDBPersistence; /** * Creates and starts a new FSTLevelDB instance for testing. Does not delete any data * present in the given directory. As a consequence, the resulting databse is not guaranteed * to be empty. */ -+ (FSTLevelDB *)levelDBPersistenceWithDir:(firebase::firestore::util::Path)dir; ++ (std::unique_ptr)levelDBPersistenceWithDir: + (firebase::firestore::util::Path)dir; /** * Creates and starts a new FSTLevelDB instance for testing, destroying any previous contents @@ -55,14 +68,15 @@ NS_ASSUME_NONNULL_BEGIN * * Sets up the LRU garbage collection to use the provided params. */ -+ (FSTLevelDB *)levelDBPersistenceWithLruParams:(firebase::firestore::local::LruParams)lruParams; ++ (std::unique_ptr)levelDBPersistenceWithLruParams: + (firebase::firestore::local::LruParams)lruParams; -/** Creates and starts a new FSTMemoryPersistence instance for testing. */ -+ (FSTMemoryPersistence *)eagerGCMemoryPersistence; +/** Creates and starts a new MemoryPersistence instance for testing. */ ++ (std::unique_ptr)eagerGCMemoryPersistence; -+ (FSTMemoryPersistence *)lruMemoryPersistence; ++ (std::unique_ptr)lruMemoryPersistence; -+ (FSTMemoryPersistence *)lruMemoryPersistenceWithLruParams: ++ (std::unique_ptr)lruMemoryPersistenceWithLruParams: (firebase::firestore::local::LruParams)lruParams; @end diff --git a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm index af8eaf12c1b..5d6f99a5405 100644 --- a/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm +++ b/Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.mm @@ -18,12 +18,13 @@ #include -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTLevelDB.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/proto_sizer.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/util/filesystem.h" #include "Firestore/core/src/firebase/firestore/util/path.h" @@ -31,7 +32,10 @@ #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; +using firebase::firestore::local::LevelDbPersistence; using firebase::firestore::local::LruParams; +using firebase::firestore::local::MemoryPersistence; +using firebase::firestore::local::ProtoSizer; using firebase::firestore::model::DatabaseId; using firebase::firestore::util::Path; using firebase::firestore::util::Status; @@ -59,43 +63,42 @@ + (Path)levelDBDir { return dir; } -+ (FSTLevelDB *)levelDBPersistenceWithDir:(Path)dir { ++ (std::unique_ptr)levelDBPersistenceWithDir:(Path)dir { return [self levelDBPersistenceWithDir:dir lruParams:LruParams::Default()]; } -+ (FSTLevelDB *)levelDBPersistenceWithDir:(Path)dir lruParams:(LruParams)params { ++ (std::unique_ptr)levelDBPersistenceWithDir:(Path)dir + lruParams:(LruParams)params { FSTLocalSerializer *serializer = [self localSerializer]; - FSTLevelDB *ldb; - util::Status status = [FSTLevelDB dbWithDirectory:std::move(dir) - serializer:serializer - lruParams:params - ptr:&ldb]; - if (!status.ok()) { + auto created = LevelDbPersistence::Create(std::move(dir), serializer, params); + if (!created.ok()) { [NSException raise:NSInternalInconsistencyException - format:@"Failed to open DB: %s", status.ToString().c_str()]; + format:@"Failed to open DB: %s", created.status().ToString().c_str()]; } - return ldb; + return std::move(created).ValueOrDie(); } -+ (FSTLevelDB *)levelDBPersistenceWithLruParams:(LruParams)lruParams { ++ (std::unique_ptr)levelDBPersistenceWithLruParams:(LruParams)lruParams { return [self levelDBPersistenceWithDir:[self levelDBDir] lruParams:lruParams]; } -+ (FSTLevelDB *)levelDBPersistence { ++ (std::unique_ptr)levelDBPersistence { return [self levelDBPersistenceWithDir:[self levelDBDir]]; } -+ (FSTMemoryPersistence *)eagerGCMemoryPersistence { - return [FSTMemoryPersistence persistenceWithEagerGC]; ++ (std::unique_ptr)eagerGCMemoryPersistence { + return MemoryPersistence::WithEagerGarbageCollector(); } -+ (FSTMemoryPersistence *)lruMemoryPersistence { ++ (std::unique_ptr)lruMemoryPersistence { return [self lruMemoryPersistenceWithLruParams:LruParams::Default()]; } -+ (FSTMemoryPersistence *)lruMemoryPersistenceWithLruParams:(LruParams)lruParams { ++ (std::unique_ptr)lruMemoryPersistenceWithLruParams: + (LruParams)lruParams { FSTLocalSerializer *serializer = [self localSerializer]; - return [FSTMemoryPersistence persistenceWithLruParams:lruParams serializer:serializer]; + auto sizer = absl::make_unique(serializer); + return MemoryPersistence::WithLruGarbageCollector(lruParams, std::move(sizer)); } @end diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.h b/Firestore/Example/Tests/Local/FSTQueryCacheTests.h index 8f1d016ba97..fcfa19ac91a 100644 --- a/Firestore/Example/Tests/Local/FSTQueryCacheTests.h +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.h @@ -20,7 +20,18 @@ #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/types.h" -@protocol FSTPersistence; +namespace firebase { +namespace firestore { +namespace local { + +class Persistence; + +} // namespace local +} // namespace firestore +} // namespace firebase + +namespace local = firebase::firestore::local; +namespace model = firebase::firestore::model; NS_ASSUME_NONNULL_BEGIN @@ -36,17 +47,16 @@ NS_ASSUME_NONNULL_BEGIN @interface FSTQueryCacheTests : XCTestCase /** Helper method to add a single document key to target association */ -- (void)addMatchingKey:(const firebase::firestore::model::DocumentKey&)key - forTargetID:(firebase::firestore::model::TargetId)targetID; +- (void)addMatchingKey:(const model::DocumentKey&)key forTargetID:(model::TargetId)targetID; /** The implementation of the query cache to test. */ -@property(nonatomic, nullable) firebase::firestore::local::QueryCache* queryCache; +@property(nonatomic, nullable) local::QueryCache* queryCache; /** * The persistence implementation to use while testing the queryCache (e.g. for committing write * groups). */ -@property(nonatomic, strong, nullable) id persistence; +@property(nonatomic, nullable) local::Persistence* persistence; @end diff --git a/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm b/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm index e0c5dfc0296..d4e1787f53f 100644 --- a/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTQueryCacheTests.mm @@ -19,22 +19,27 @@ #include #include -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Util/FSTClasses.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/local/reference_set.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +namespace core = firebase::firestore::core; namespace testutil = firebase::firestore::testutil; + +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; using firebase::firestore::model::ListenSequenceNumber; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::nanopb::ByteString; using testutil::Filter; using testutil::Query; @@ -58,7 +63,9 @@ - (void)setUp { } - (void)tearDown { - [self.persistence shutdown]; + if (self.persistence) { + self.persistence->Shutdown(); + } } /** @@ -72,56 +79,58 @@ - (BOOL)isTestBaseClass { - (void)testReadQueryNotInCache { if ([self isTestBaseClass]) return; - self.persistence.run("testReadQueryNotInCache", - [&]() { XCTAssertNil(self.queryCache->GetTarget(_queryRooms)); }); + self.persistence->Run("testReadQueryNotInCache", [&]() { + XCTAssertEqual(self.queryCache->GetTarget(_queryRooms), absl::nullopt); + }); } - (void)testSetAndReadAQuery { if ([self isTestBaseClass]) return; - self.persistence.run("testSetAndReadAQuery", [&]() { - FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms]; + self.persistence->Run("testSetAndReadAQuery", [&]() { + QueryData queryData = [self queryDataWithQuery:_queryRooms]; self.queryCache->AddTarget(queryData); - FSTQueryData *result = self.queryCache->GetTarget(_queryRooms); - XCTAssertEqual(result.query, queryData.query); - XCTAssertEqual(result.targetID, queryData.targetID); - XCTAssertEqualObjects(result.resumeToken, queryData.resumeToken); + auto result = self.queryCache->GetTarget(_queryRooms); + XCTAssertNotEqual(result, absl::nullopt); + XCTAssertEqual(result->query(), queryData.query()); + XCTAssertEqual(result->target_id(), queryData.target_id()); + XCTAssertEqual(result->resume_token(), queryData.resume_token()); }); } - (void)testCanonicalIDCollision { if ([self isTestBaseClass]) return; - self.persistence.run("testCanonicalIDCollision", [&]() { + self.persistence->Run("testCanonicalIDCollision", [&]() { // Type information is currently lost in our canonicalID implementations so this currently an // easy way to force colliding canonicalIDs core::Query q1 = Query("a").AddingFilter(Filter("foo", "==", 1)); core::Query q2 = Query("a").AddingFilter(Filter("foo", "==", "1")); XCTAssertEqual(q1.CanonicalId(), q2.CanonicalId()); - FSTQueryData *data1 = [self queryDataWithQuery:q1]; + QueryData data1 = [self queryDataWithQuery:q1]; self.queryCache->AddTarget(data1); // Using the other query should not return the query cache entry despite equal canonicalIDs. - XCTAssertNil(self.queryCache->GetTarget(q2)); - XCTAssertEqualObjects(self.queryCache->GetTarget(q1), data1); + XCTAssertEqual(self.queryCache->GetTarget(q2), absl::nullopt); + XCTAssertEqual(self.queryCache->GetTarget(q1), data1); - FSTQueryData *data2 = [self queryDataWithQuery:q2]; + QueryData data2 = [self queryDataWithQuery:q2]; self.queryCache->AddTarget(data2); XCTAssertEqual(self.queryCache->size(), 2); - XCTAssertEqualObjects(self.queryCache->GetTarget(q1), data1); - XCTAssertEqualObjects(self.queryCache->GetTarget(q2), data2); + XCTAssertEqual(self.queryCache->GetTarget(q1), data1); + XCTAssertEqual(self.queryCache->GetTarget(q2), data2); self.queryCache->RemoveTarget(data1); - XCTAssertNil(self.queryCache->GetTarget(q1)); - XCTAssertEqualObjects(self.queryCache->GetTarget(q2), data2); + XCTAssertEqual(self.queryCache->GetTarget(q1), absl::nullopt); + XCTAssertEqual(self.queryCache->GetTarget(q2), data2); XCTAssertEqual(self.queryCache->size(), 1); self.queryCache->RemoveTarget(data2); - XCTAssertNil(self.queryCache->GetTarget(q1)); - XCTAssertNil(self.queryCache->GetTarget(q2)); + XCTAssertEqual(self.queryCache->GetTarget(q1), absl::nullopt); + XCTAssertEqual(self.queryCache->GetTarget(q2), absl::nullopt); XCTAssertEqual(self.queryCache->size(), 0); }); } @@ -129,46 +138,46 @@ - (void)testCanonicalIDCollision { - (void)testSetQueryToNewValue { if ([self isTestBaseClass]) return; - self.persistence.run("testSetQueryToNewValue", [&]() { - FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms - targetID:1 - listenSequenceNumber:10 - version:1]; + self.persistence->Run("testSetQueryToNewValue", [&]() { + QueryData queryData1 = [self queryDataWithQuery:_queryRooms + targetID:1 + listenSequenceNumber:10 + version:1]; self.queryCache->AddTarget(queryData1); - FSTQueryData *queryData2 = [self queryDataWithQuery:_queryRooms - targetID:1 - listenSequenceNumber:10 - version:2]; + QueryData queryData2 = [self queryDataWithQuery:_queryRooms + targetID:1 + listenSequenceNumber:10 + version:2]; self.queryCache->AddTarget(queryData2); - FSTQueryData *result = self.queryCache->GetTarget(_queryRooms); - XCTAssertNotEqualObjects(queryData2.resumeToken, queryData1.resumeToken); - XCTAssertNotEqual(queryData2.snapshotVersion, queryData1.snapshotVersion); - XCTAssertEqualObjects(result.resumeToken, queryData2.resumeToken); - XCTAssertEqual(result.snapshotVersion, queryData2.snapshotVersion); + auto result = self.queryCache->GetTarget(_queryRooms); + XCTAssertNotEqual(queryData2.resume_token(), queryData1.resume_token()); + XCTAssertNotEqual(queryData2.snapshot_version(), queryData1.snapshot_version()); + XCTAssertEqual(result->resume_token(), queryData2.resume_token()); + XCTAssertEqual(result->snapshot_version(), queryData2.snapshot_version()); }); } - (void)testRemoveQuery { if ([self isTestBaseClass]) return; - self.persistence.run("testRemoveQuery", [&]() { - FSTQueryData *queryData1 = [self queryDataWithQuery:_queryRooms]; + self.persistence->Run("testRemoveQuery", [&]() { + QueryData queryData1 = [self queryDataWithQuery:_queryRooms]; self.queryCache->AddTarget(queryData1); self.queryCache->RemoveTarget(queryData1); - FSTQueryData *result = self.queryCache->GetTarget(_queryRooms); - XCTAssertNil(result); + auto result = self.queryCache->GetTarget(_queryRooms); + XCTAssertEqual(result, absl::nullopt); }); } - (void)testRemoveNonExistentQuery { if ([self isTestBaseClass]) return; - self.persistence.run("testRemoveNonExistentQuery", [&]() { - FSTQueryData *queryData = [self queryDataWithQuery:_queryRooms]; + self.persistence->Run("testRemoveNonExistentQuery", [&]() { + QueryData queryData = [self queryDataWithQuery:_queryRooms]; // no-op, but make sure it doesn't throw. XCTAssertNoThrow(self.queryCache->RemoveTarget(queryData)); @@ -178,14 +187,14 @@ - (void)testRemoveNonExistentQuery { - (void)testRemoveQueryRemovesMatchingKeysToo { if ([self isTestBaseClass]) return; - self.persistence.run("testRemoveQueryRemovesMatchingKeysToo", [&]() { - FSTQueryData *rooms = [self queryDataWithQuery:_queryRooms]; + self.persistence->Run("testRemoveQueryRemovesMatchingKeysToo", [&]() { + QueryData rooms = [self queryDataWithQuery:_queryRooms]; self.queryCache->AddTarget(rooms); DocumentKey key1 = testutil::Key("rooms/foo"); DocumentKey key2 = testutil::Key("rooms/bar"); - [self addMatchingKey:key1 forTargetID:rooms.targetID]; - [self addMatchingKey:key2 forTargetID:rooms.targetID]; + [self addMatchingKey:key1 forTargetID:rooms.target_id()]; + [self addMatchingKey:key2 forTargetID:rooms.target_id()]; XCTAssertTrue(self.queryCache->Contains(key1)); XCTAssertTrue(self.queryCache->Contains(key2)); @@ -199,7 +208,7 @@ - (void)testRemoveQueryRemovesMatchingKeysToo { - (void)testAddOrRemoveMatchingKeys { if ([self isTestBaseClass]) return; - self.persistence.run("testAddOrRemoveMatchingKeys", [&]() { + self.persistence->Run("testAddOrRemoveMatchingKeys", [&]() { DocumentKey key = testutil::Key("foo/bar"); XCTAssertFalse(self.queryCache->Contains(key)); @@ -221,7 +230,7 @@ - (void)testAddOrRemoveMatchingKeys { - (void)testMatchingKeysForTargetID { if ([self isTestBaseClass]) return; - self.persistence.run("testMatchingKeysForTargetID", [&]() { + self.persistence->Run("testMatchingKeysForTargetID", [&]() { DocumentKey key1 = testutil::Key("foo/bar"); DocumentKey key2 = testutil::Key("foo/baz"); DocumentKey key3 = testutil::Key("foo/blah"); @@ -242,16 +251,10 @@ - (void)testMatchingKeysForTargetID { - (void)testHighestListenSequenceNumber { if ([self isTestBaseClass]) return; - self.persistence.run("testHighestListenSequenceNumber", [&]() { - FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:Query("rooms") - targetID:1 - listenSequenceNumber:10 - purpose:FSTQueryPurposeListen]; + self.persistence->Run("testHighestListenSequenceNumber", [&]() { + QueryData query1(Query("rooms"), 1, 10, QueryPurpose::Listen); self.queryCache->AddTarget(query1); - FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:Query("halls") - targetID:2 - listenSequenceNumber:20 - purpose:FSTQueryPurposeListen]; + QueryData query2(Query("halls"), 2, 20, QueryPurpose::Listen); self.queryCache->AddTarget(query2); XCTAssertEqual(self.queryCache->highest_listen_sequence_number(), 20); @@ -259,10 +262,7 @@ - (void)testHighestListenSequenceNumber { self.queryCache->RemoveTarget(query2); XCTAssertEqual(self.queryCache->highest_listen_sequence_number(), 20); - FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:Query("garages") - targetID:42 - listenSequenceNumber:100 - purpose:FSTQueryPurposeListen]; + QueryData query3(Query("garages"), 42, 100, QueryPurpose::Listen); self.queryCache->AddTarget(query3); XCTAssertEqual(self.queryCache->highest_listen_sequence_number(), 100); @@ -277,23 +277,17 @@ - (void)testHighestListenSequenceNumber { - (void)testHighestTargetID { if ([self isTestBaseClass]) return; - self.persistence.run("testHighestTargetID", [&]() { + self.persistence->Run("testHighestTargetID", [&]() { XCTAssertEqual(self.queryCache->highest_target_id(), 0); - FSTQueryData *query1 = [[FSTQueryData alloc] initWithQuery:Query("rooms") - targetID:1 - listenSequenceNumber:10 - purpose:FSTQueryPurposeListen]; + QueryData query1(Query("rooms"), 1, 10, QueryPurpose::Listen); DocumentKey key1 = testutil::Key("rooms/bar"); DocumentKey key2 = testutil::Key("rooms/foo"); self.queryCache->AddTarget(query1); [self addMatchingKey:key1 forTargetID:1]; [self addMatchingKey:key2 forTargetID:1]; - FSTQueryData *query2 = [[FSTQueryData alloc] initWithQuery:Query("halls") - targetID:2 - listenSequenceNumber:20 - purpose:FSTQueryPurposeListen]; + QueryData query2(Query("halls"), 2, 20, QueryPurpose::Listen); DocumentKey key3 = testutil::Key("halls/foo"); self.queryCache->AddTarget(query2); [self addMatchingKey:key3 forTargetID:2]; @@ -304,10 +298,7 @@ - (void)testHighestTargetID { XCTAssertEqual(self.queryCache->highest_target_id(), 2); // A query with an empty result set still counts. - FSTQueryData *query3 = [[FSTQueryData alloc] initWithQuery:Query("garages") - targetID:42 - listenSequenceNumber:100 - purpose:FSTQueryPurposeListen]; + QueryData query3(Query("garages"), 42, 100, QueryPurpose::Listen); self.queryCache->AddTarget(query3); XCTAssertEqual(self.queryCache->highest_target_id(), 42); @@ -322,7 +313,7 @@ - (void)testHighestTargetID { - (void)testLastRemoteSnapshotVersion { if ([self isTestBaseClass]) return; - self.persistence.run("testLastRemoteSnapshotVersion", [&]() { + self.persistence->Run("testLastRemoteSnapshotVersion", [&]() { XCTAssertEqual(self.queryCache->GetLastRemoteSnapshotVersion(), SnapshotVersion::None()); // Can set the snapshot version. @@ -334,27 +325,23 @@ - (void)testLastRemoteSnapshotVersion { #pragma mark - Helpers /** - * Creates a new FSTQueryData object from the given parameters, synthesizing a resume token from - * the snapshot version. + * Creates a new QueryData object from the given parameters, synthesizing a resume token from the + * snapshot version. */ -- (FSTQueryData *)queryDataWithQuery:(core::Query)query { +- (QueryData)queryDataWithQuery:(core::Query)query { return [self queryDataWithQuery:std::move(query) targetID:++_previousTargetID listenSequenceNumber:++_previousSequenceNumber version:++_previousSnapshotVersion]; } -- (FSTQueryData *)queryDataWithQuery:(core::Query)query - targetID:(TargetId)targetID - listenSequenceNumber:(ListenSequenceNumber)sequenceNumber - version:(FSTTestSnapshotVersion)version { - NSData *resumeToken = FSTTestResumeTokenFromSnapshotVersion(version); - return [[FSTQueryData alloc] initWithQuery:std::move(query) - targetID:targetID - listenSequenceNumber:sequenceNumber - purpose:FSTQueryPurposeListen - snapshotVersion:testutil::Version(version) - resumeToken:resumeToken]; +- (QueryData)queryDataWithQuery:(core::Query)query + targetID:(TargetId)targetID + listenSequenceNumber:(ListenSequenceNumber)sequenceNumber + version:(FSTTestSnapshotVersion)version { + ByteString resumeToken = testutil::ResumeToken(version); + return QueryData(std::move(query), targetID, sequenceNumber, QueryPurpose::Listen, + testutil::Version(version), resumeToken); } - (void)addMatchingKey:(const DocumentKey &)key forTargetID:(TargetId)targetID { diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h index 8b660eae0ef..64c663c104d 100644 --- a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.h @@ -16,9 +16,18 @@ #import -#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" +namespace firebase { +namespace firestore { +namespace local { -@protocol FSTPersistence; +class Persistence; +class RemoteDocumentCache; + +} // namespace local +} // namespace firestore +} // namespace firebase + +namespace local = firebase::firestore::local; NS_ASSUME_NONNULL_BEGIN @@ -32,8 +41,8 @@ NS_ASSUME_NONNULL_BEGIN * + override -tearDown, cleaning up remoteDocumentCache and persistence */ @interface FSTRemoteDocumentCacheTests : XCTestCase -@property(nonatomic, nullable) firebase::firestore::local::RemoteDocumentCache* remoteDocumentCache; -@property(nonatomic, strong, nullable) id persistence; +@property(nonatomic, nullable) local::RemoteDocumentCache* remoteDocumentCache; +@property(nonatomic, nullable) local::Persistence* persistence; @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm index 03911cee871..c78cc71d1a2 100644 --- a/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm +++ b/Firestore/Example/Tests/Local/FSTRemoteDocumentCacheTests.mm @@ -19,11 +19,10 @@ #include #include -#import "Firestore/Source/Local/FSTPersistence.h" - #import "Firestore/Example/Tests/Util/FSTHelpers.h" #include "Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" @@ -98,20 +97,22 @@ - (void)setUp { } - (void)tearDown { - [self.persistence shutdown]; + if (self.persistence) { + self.persistence->Shutdown(); + } } - (void)testReadDocumentNotInCache { if (!self.remoteDocumentCache) return; - self.persistence.run("testReadDocumentNotInCache", [&] { + self.persistence->Run("testReadDocumentNotInCache", [&] { XCTAssertEqual(absl::nullopt, self.remoteDocumentCache->Get(testutil::Key(kDocPath))); }); } // Helper for next two tests. - (void)setAndReadADocumentAtPath:(const absl::string_view)path { - self.persistence.run("setAndReadADocumentAtPath", [&] { + self.persistence->Run("setAndReadADocumentAtPath", [&] { Document written = [self setTestDocumentAtPath:path]; absl::optional read = self.remoteDocumentCache->Get(testutil::Key(path)); XCTAssertEqual(*read, written); @@ -127,7 +128,7 @@ - (void)testSetAndReadADocument { - (void)testSetAndReadSeveralDocuments { if (!self.remoteDocumentCache) return; - self.persistence.run("testSetAndReadSeveralDocuments", [=] { + self.persistence->Run("testSetAndReadSeveralDocuments", [=] { std::vector written = { [self setTestDocumentAtPath:kDocPath], [self setTestDocumentAtPath:kLongDocPath], @@ -141,7 +142,7 @@ - (void)testSetAndReadSeveralDocuments { - (void)testSetAndReadSeveralDocumentsIncludingMissingDocument { if (!self.remoteDocumentCache) return; - self.persistence.run("testSetAndReadSeveralDocumentsIncludingMissingDocument", [=] { + self.persistence->Run("testSetAndReadSeveralDocumentsIncludingMissingDocument", [=] { std::vector written = { [self setTestDocumentAtPath:kDocPath], [self setTestDocumentAtPath:kLongDocPath], @@ -167,7 +168,7 @@ - (void)testSetAndReadADocumentAtDeepPath { - (void)testSetAndReadDeletedDocument { if (!self.remoteDocumentCache) return; - self.persistence.run("testSetAndReadDeletedDocument", [&] { + self.persistence->Run("testSetAndReadDeletedDocument", [&] { absl::optional deletedDoc = DeletedDoc(kDocPath, kVersion); self.remoteDocumentCache->Add(*deletedDoc); @@ -178,7 +179,7 @@ - (void)testSetAndReadDeletedDocument { - (void)testSetDocumentToNewValue { if (!self.remoteDocumentCache) return; - self.persistence.run("testSetDocumentToNewValue", [&] { + self.persistence->Run("testSetDocumentToNewValue", [&] { [self setTestDocumentAtPath:kDocPath]; absl::optional newDoc = Doc(kDocPath, kVersion, Map("data", 2)); self.remoteDocumentCache->Add(*newDoc); @@ -189,7 +190,7 @@ - (void)testSetDocumentToNewValue { - (void)testRemoveDocument { if (!self.remoteDocumentCache) return; - self.persistence.run("testRemoveDocument", [&] { + self.persistence->Run("testRemoveDocument", [&] { [self setTestDocumentAtPath:kDocPath]; self.remoteDocumentCache->Remove(testutil::Key(kDocPath)); @@ -200,7 +201,7 @@ - (void)testRemoveDocument { - (void)testRemoveNonExistentDocument { if (!self.remoteDocumentCache) return; - self.persistence.run("testRemoveNonExistentDocument", [&] { + self.persistence->Run("testRemoveNonExistentDocument", [&] { // no-op, but make sure it doesn't throw. XCTAssertNoThrow(self.remoteDocumentCache->Remove(testutil::Key(kDocPath))); }); @@ -210,7 +211,7 @@ - (void)testRemoveNonExistentDocument { - (void)testDocumentsMatchingQuery { if (!self.remoteDocumentCache) return; - self.persistence.run("testDocumentsMatchingQuery", [&] { + self.persistence->Run("testDocumentsMatchingQuery", [&] { // TODO(rsgowman): This just verifies that we do a prefix scan against the // query path. We'll need more tests once we add index support. [self setTestDocumentAtPath:"a/1"]; diff --git a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm index b5de657712b..ff3b3321c92 100644 --- a/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm +++ b/Firestore/Example/Tests/Remote/FSTRemoteEventTests.mm @@ -21,8 +21,7 @@ #include #include -#import "Firestore/Source/Local/FSTQueryData.h" - +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/existence_filter.h" @@ -34,7 +33,11 @@ #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "absl/memory/memory.h" +namespace core = firebase::firestore::core; namespace testutil = firebase::firestore::testutil; + +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::model::Document; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; @@ -43,6 +46,7 @@ using firebase::firestore::model::NoDocument; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::nanopb::ByteString; using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilter; using firebase::firestore::remote::ExistenceFilterWatchChange; @@ -89,8 +93,8 @@ std::unique_ptr MakeTargetChange(WatchTargetChangeState state, std::vector target_ids, - NSData *token) { - return absl::make_unique(state, std::move(target_ids), token); + ByteString token) { + return absl::make_unique(state, std::move(target_ids), std::move(token)); } } // namespace @@ -99,28 +103,25 @@ @interface FSTRemoteEventTests : XCTestCase @end @implementation FSTRemoteEventTests { - NSData *_resumeToken1; + ByteString _resumeToken1; TestTargetMetadataProvider _targetMetadataProvider; std::unordered_map _noOutstandingResponses; } - (void)setUp { - _resumeToken1 = [@"resume1" dataUsingEncoding:NSUTF8StringEncoding]; + _resumeToken1 = testutil::ResumeToken(7); } /** * Creates a map with query data for the provided target IDs. All targets are considered active * and query a collection named "coll". */ -- (std::unordered_map)queryDataForTargets: +- (std::unordered_map)queryDataForTargets: (std::initializer_list)targetIDs { - std::unordered_map targets; + std::unordered_map targets; for (TargetId targetID : targetIDs) { core::Query query = Query("coll"); - targets[targetID] = [[FSTQueryData alloc] initWithQuery:std::move(query) - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen]; + targets[targetID] = QueryData(std::move(query), targetID, 0, QueryPurpose::Listen); } return targets; } @@ -129,15 +130,12 @@ - (void)setUp { * Creates a map with query data for the provided target IDs. All targets are marked as limbo * queries for the document at "coll/limbo". */ -- (std::unordered_map)queryDataForLimboTargets: +- (std::unordered_map)queryDataForLimboTargets: (std::initializer_list)targetIDs { - std::unordered_map targets; + std::unordered_map targets; for (TargetId targetID : targetIDs) { core::Query query = Query("coll/limbo"); - targets[targetID] = [[FSTQueryData alloc] initWithQuery:std::move(query) - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeLimboResolution]; + targets[targetID] = QueryData(std::move(query), targetID, 0, QueryPurpose::LimboResolution); } return targets; } @@ -157,7 +155,7 @@ - (void)setUp { * changes are `DocumentWatchChange` and `WatchTargetChange`. */ - (WatchChangeAggregator) - aggregatorWithTargetMap:(const std::unordered_map &)targetMap + aggregatorWithTargetMap:(const std::unordered_map &)targetMap outstandingResponses:(const std::unordered_map &)outstandingResponses existingKeys:(DocumentKeySet)existingKeys changes:(const std::vector> &)watchChanges { @@ -166,7 +164,7 @@ - (void)setUp { std::vector targetIDs; for (const auto &kv : targetMap) { TargetId targetID = kv.first; - FSTQueryData *queryData = kv.second; + const QueryData &queryData = kv.second; targetIDs.push_back(targetID); _targetMetadataProvider.SetSyncedKeys(existingKeys, queryData); @@ -217,7 +215,7 @@ - (void)setUp { */ - (RemoteEvent) remoteEventAtSnapshotVersion:(FSTTestSnapshotVersion)snapshotVersion - targetMap:(std::unordered_map)targetMap + targetMap:(std::unordered_map)targetMap outstandingResponses:(const std::unordered_map &)outstandingResponses existingKeys:(DocumentKeySet)existingKeys changes:(const std::vector> &)watchChanges { @@ -232,8 +230,7 @@ - (void)testWillAccumulateDocumentAddedAndRemovedEvents { // The target map that contains an entry for every target in this test. If a target ID is // omitted, the target is considered inactive and `TestTargetMetadataProvider` will fail on // access. - std::unordered_map targetMap{ - [self queryDataForTargets:{1, 2, 3, 4, 5, 6}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1, 2, 3, 4, 5, 6}]}; Document existingDoc = Doc("docs/1", 1, Map("value", 1)); auto change1 = MakeDocChange({1, 2, 3}, {4, 5, 6}, existingDoc.key(), existingDoc); @@ -285,7 +282,7 @@ - (void)testWillAccumulateDocumentAddedAndRemovedEvents { } - (void)testWillIgnoreEventsForPendingTargets { - std::unordered_map targetMap{[self queryDataForTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1}]}; Document doc1 = Doc("docs/1", 1, Map("value", 1)); auto change1 = MakeDocChange({1}, {}, doc1.key(), doc1); @@ -314,7 +311,7 @@ - (void)testWillIgnoreEventsForPendingTargets { } - (void)testWillIgnoreEventsForRemovedTargets { - std::unordered_map targetMap{[self queryDataForTargets:{}]}; + std::unordered_map targetMap{[self queryDataForTargets:{}]}; Document doc1 = Doc("docs/1", 1, Map("value", 1)); auto change1 = MakeDocChange({1}, {}, doc1.key(), doc1); @@ -338,7 +335,7 @@ - (void)testWillIgnoreEventsForRemovedTargets { } - (void)testWillKeepResetMappingEvenWithUpdates { - std::unordered_map targetMap{[self queryDataForTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1}]}; Document doc1 = Doc("docs/1", 1, Map("value", 1)); auto change1 = MakeDocChange({1}, {}, doc1.key(), doc1); @@ -379,7 +376,7 @@ - (void)testWillKeepResetMappingEvenWithUpdates { } - (void)testWillHandleSingleReset { - std::unordered_map targetMap{[self queryDataForTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1}]}; // Reset target WatchTargetChange change{WatchTargetChangeState::Reset, {1}}; @@ -397,13 +394,13 @@ - (void)testWillHandleSingleReset { XCTAssertEqual(event.target_changes().size(), 1); // Reset mapping is empty - TargetChange expectedChange{ - [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; + TargetChange expectedChange{ByteString(), false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; XCTAssertTrue(event.target_changes().at(1) == expectedChange); } - (void)testWillHandleTargetAddAndRemovalInSameBatch { - std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; Document doc1a = Doc("docs/1", 1, Map("value", 1)); auto change1 = MakeDocChange({1}, {2}, doc1a.key(), doc1a); @@ -433,7 +430,7 @@ - (void)testWillHandleTargetAddAndRemovalInSameBatch { } - (void)testTargetCurrentChangeWillMarkTheTargetCurrent { - std::unordered_map targetMap{[self queryDataForTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1}]}; auto change = MakeTargetChange(WatchTargetChangeState::Current, {1}, _resumeToken1); @@ -453,7 +450,7 @@ - (void)testTargetCurrentChangeWillMarkTheTargetCurrent { } - (void)testTargetAddedChangeWillResetPreviousState { - std::unordered_map targetMap{[self queryDataForTargets:{1, 3}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1, 3}]}; Document doc1 = Doc("docs/1", 1, Map("value", 1)); auto change1 = MakeDocChange({1, 3}, {2}, doc1.key(), doc1); @@ -497,7 +494,7 @@ - (void)testTargetAddedChangeWillResetPreviousState { } - (void)testNoChangeWillStillMarkTheAffectedTargets { - std::unordered_map targetMap{[self queryDataForTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1}]}; WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -519,7 +516,7 @@ - (void)testNoChangeWillStillMarkTheAffectedTargets { } - (void)testExistenceFilterMismatchClearsTarget { - std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; Document doc1 = Doc("docs/1", 1, Map("value", 1)); auto change1 = MakeDocChange({1}, {}, doc1.key(), doc1); @@ -557,10 +554,7 @@ - (void)testExistenceFilterMismatchClearsTarget { event = aggregator.CreateRemoteEvent(testutil::Version(4)); - TargetChange targetChange3{[NSData data], - false, - DocumentKeySet{}, - DocumentKeySet{}, + TargetChange targetChange3{ByteString(), false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{doc1.key(), doc2.key()}}; XCTAssertTrue(event.target_changes().at(1) == targetChange3); @@ -570,7 +564,7 @@ - (void)testExistenceFilterMismatchClearsTarget { } - (void)testExistenceFilterMismatchRemovesCurrentChanges { - std::unordered_map targetMap{[self queryDataForTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1}]}; WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -598,13 +592,13 @@ - (void)testExistenceFilterMismatchRemovesCurrentChanges { XCTAssertEqual(event.target_changes().size(), 1); - TargetChange targetChange1{ - [NSData data], false, DocumentKeySet{}, DocumentKeySet{}, DocumentKeySet{}}; + TargetChange targetChange1{ByteString(), false, DocumentKeySet{}, DocumentKeySet{}, + DocumentKeySet{}}; XCTAssertTrue(event.target_changes().at(1) == targetChange1); } - (void)testDocumentUpdate { - std::unordered_map targetMap{[self queryDataForTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1}]}; Document doc1 = Doc("docs/1", 1, Map("value", 1)); auto change1 = MakeDocChange({1}, {}, doc1.key(), doc1); @@ -658,7 +652,7 @@ - (void)testDocumentUpdate { } - (void)testResumeTokensHandledPerTarget { - std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -668,7 +662,7 @@ - (void)testResumeTokensHandledPerTarget { WatchTargetChange change1{WatchTargetChangeState::Current, {1}, _resumeToken1}; aggregator.HandleTargetChange(change1); - NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + ByteString resumeToken2 = testutil::ResumeToken(7); WatchTargetChange change2{WatchTargetChangeState::Current, {2}, resumeToken2}; aggregator.HandleTargetChange(change2); @@ -685,7 +679,7 @@ - (void)testResumeTokensHandledPerTarget { } - (void)testLastResumeTokenWins { - std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1, 2}]}; WatchChangeAggregator aggregator = [self aggregatorWithTargetMap:targetMap outstandingResponses:_noOutstandingResponses @@ -695,11 +689,11 @@ - (void)testLastResumeTokenWins { WatchTargetChange change1{WatchTargetChangeState::Current, {1}, _resumeToken1}; aggregator.HandleTargetChange(change1); - NSData *resumeToken2 = [@"resume2" dataUsingEncoding:NSUTF8StringEncoding]; + ByteString resumeToken2 = testutil::ResumeToken(2); WatchTargetChange change2{WatchTargetChangeState::NoChange, {1}, resumeToken2}; aggregator.HandleTargetChange(change2); - NSData *resumeToken3 = [@"resume3" dataUsingEncoding:NSUTF8StringEncoding]; + ByteString resumeToken3 = testutil::ResumeToken(3); WatchTargetChange change3{WatchTargetChangeState::NoChange, {2}, resumeToken3}; aggregator.HandleTargetChange(change3); @@ -716,7 +710,7 @@ - (void)testLastResumeTokenWins { } - (void)testSynthesizeDeletes { - std::unordered_map targetMap{[self queryDataForLimboTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForLimboTargets:{1}]}; DocumentKey limboKey = testutil::Key("coll/limbo"); auto resolveLimboTarget = MakeTargetChange(WatchTargetChangeState::Current, {1}); @@ -732,7 +726,7 @@ - (void)testSynthesizeDeletes { } - (void)testDoesntSynthesizeDeletesForWrongState { - std::unordered_map targetMap{[self queryDataForTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForTargets:{1}]}; auto wrongState = MakeTargetChange(WatchTargetChangeState::NoChange, {1}); @@ -747,7 +741,7 @@ - (void)testDoesntSynthesizeDeletesForWrongState { } - (void)testDoesntSynthesizeDeletesForExistingDoc { - std::unordered_map targetMap{[self queryDataForTargets:{3}]}; + std::unordered_map targetMap{[self queryDataForTargets:{3}]}; auto hasDocument = MakeTargetChange(WatchTargetChangeState::Current, {3}); @@ -763,7 +757,7 @@ - (void)testDoesntSynthesizeDeletesForExistingDoc { } - (void)testSeparatesDocumentUpdates { - std::unordered_map targetMap{[self queryDataForLimboTargets:{1}]}; + std::unordered_map targetMap{[self queryDataForLimboTargets:{1}]}; Document newDoc = Doc("docs/new", 1, Map("key", "value")); auto newDocChange = MakeDocChange({1}, {}, newDoc.key(), newDoc); @@ -793,7 +787,7 @@ - (void)testSeparatesDocumentUpdates { } - (void)testTracksLimboDocuments { - std::unordered_map targetMap = [self queryDataForTargets:{1}]; + std::unordered_map targetMap = [self queryDataForTargets:{1}]; auto additionalTargets = [self queryDataForLimboTargets:{2}]; targetMap.insert(additionalTargets.begin(), additionalTargets.end()); diff --git a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm index 6f74f3e022a..af5b4abd3df 100644 --- a/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm +++ b/Firestore/Example/Tests/Remote/FSTSerializerBetaTests.mm @@ -36,8 +36,6 @@ #import "Firestore/Protos/objc/google/rpc/Status.pbobjc.h" #import "Firestore/Protos/objc/google/type/Latlng.pbobjc.h" #import "Firestore/Source/API/FIRFieldValue+Internal.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Example/Tests/API/FSTAPIHelpers.h" #import "Firestore/Example/Tests/Util/FSTHelpers.h" @@ -47,6 +45,7 @@ #include "Firestore/core/src/firebase/firestore/core/field_filter.h" #include "Firestore/core/src/firebase/firestore/core/filter.h" #include "Firestore/core/src/firebase/firestore/core/order_by.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/delete_mutation.h" #include "Firestore/core/src/firebase/firestore/model/field_mask.h" @@ -57,6 +56,7 @@ #include "Firestore/core/src/firebase/firestore/model/precondition.h" #include "Firestore/core/src/firebase/firestore/model/set_mutation.h" #include "Firestore/core/src/firebase/firestore/model/transform_mutation.h" +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/status.h" @@ -70,6 +70,8 @@ using firebase::firestore::core::Direction; using firebase::firestore::core::FieldFilter; using firebase::firestore::core::OrderBy; +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DeleteMutation; using firebase::firestore::model::DocumentKey; @@ -86,6 +88,8 @@ using firebase::firestore::model::SetMutation; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TransformMutation; +using firebase::firestore::nanopb::ByteString; +using firebase::firestore::nanopb::MakeNSData; using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilterWatchChange; using firebase::firestore::remote::WatchChange; @@ -94,6 +98,7 @@ using firebase::firestore::testutil::Array; using firebase::firestore::util::Status; +using testutil::Bytes; using testutil::DeletedDoc; using testutil::Doc; using testutil::Filter; @@ -520,26 +525,17 @@ - (void)testRoundTripSpecialFieldNames { - (void)testEncodesListenRequestLabels { core::Query query = Query("collection/key"); - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:2 - listenSequenceNumber:3 - purpose:FSTQueryPurposeListen]; + QueryData queryData(query, 2, 3, QueryPurpose::Listen); NSDictionary *result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; XCTAssertNil(result); - queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:2 - listenSequenceNumber:3 - purpose:FSTQueryPurposeLimboResolution]; + queryData = QueryData(query, 2, 3, QueryPurpose::LimboResolution); result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"limbo-document"}); - queryData = [[FSTQueryData alloc] initWithQuery:query - targetID:2 - listenSequenceNumber:3 - purpose:FSTQueryPurposeExistenceFilterMismatch]; + queryData = QueryData(query, 2, 3, QueryPurpose::ExistenceFilterMismatch); result = [self.serializer encodedListenRequestLabelsForQueryData:queryData]; XCTAssertEqualObjects(result, @{@"goog-listen-tags" : @"existence-filter-mismatch"}); } @@ -637,7 +633,7 @@ - (void)testEncodesKeyFieldFilter { - (void)testEncodesFirstLevelKeyQueries { core::Query query = Query("docs/1"); - FSTQueryData *model = [self queryDataForQuery:std::move(query)]; + QueryData model = [self queryDataForQuery:std::move(query)]; GCFSTarget *expected = [GCFSTarget message]; [expected.documents.documentsArray addObject:@"projects/p/databases/d/documents/docs/1"]; @@ -648,7 +644,7 @@ - (void)testEncodesFirstLevelKeyQueries { - (void)testEncodesFirstLevelAncestorQueries { core::Query q = Query("messages"); - FSTQueryData *model = [self queryDataForQuery:std::move(q)]; + QueryData model = [self queryDataForQuery:std::move(q)]; GCFSTarget *expected = [GCFSTarget message]; expected.query.parent = @"projects/p/databases/d/documents"; @@ -664,7 +660,7 @@ - (void)testEncodesFirstLevelAncestorQueries { - (void)testEncodesNestedAncestorQueries { core::Query q = Query("rooms/1/messages/10/attachments"); - FSTQueryData *model = [self queryDataForQuery:std::move(q)]; + QueryData model = [self queryDataForQuery:std::move(q)]; GCFSTarget *expected = [GCFSTarget message]; expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; @@ -680,7 +676,7 @@ - (void)testEncodesNestedAncestorQueries { - (void)testEncodesSingleFiltersAtFirstLevelCollections { core::Query q = Query("docs").AddingFilter(Filter("prop", "<", 42)); - FSTQueryData *model = [self queryDataForQuery:std::move(q)]; + QueryData model = [self queryDataForQuery:std::move(q)]; GCFSTarget *expected = [GCFSTarget message]; expected.query.parent = @"projects/p/databases/d/documents"; @@ -706,7 +702,7 @@ - (void)testEncodesMultipleFiltersOnDeeperCollections { .AddingFilter(Filter("prop", ">=", 42)) .AddingFilter(Filter("author", "==", "dimond")) .AddingFilter(Filter("tags", "array_contains", "pending")); - FSTQueryData *model = [self queryDataForQuery:std::move(q)]; + QueryData model = [self queryDataForQuery:std::move(q)]; GCFSTarget *expected = [GCFSTarget message]; expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; @@ -761,7 +757,7 @@ - (void)testEncodesNanFilter { - (void)unaryFilterTestWithValue:(FieldValue)value expectedUnaryOperator:(GCFSStructuredQuery_UnaryFilter_Operator)op { core::Query q = Query("docs").AddingFilter(Filter("prop", "==", value)); - FSTQueryData *model = [self queryDataForQuery:std::move(q)]; + QueryData model = [self queryDataForQuery:std::move(q)]; GCFSTarget *expected = [GCFSTarget message]; expected.query.parent = @"projects/p/databases/d/documents"; @@ -781,7 +777,7 @@ - (void)unaryFilterTestWithValue:(FieldValue)value - (void)testEncodesSortOrders { core::Query query = Query("docs").AddingOrderBy(OrderBy("prop", "asc")); - FSTQueryData *model = [self queryDataForQuery:std::move(query)]; + QueryData model = [self queryDataForQuery:std::move(query)]; GCFSTarget *expected = [GCFSTarget message]; expected.query.parent = @"projects/p/databases/d/documents"; @@ -800,7 +796,7 @@ - (void)testEncodesSortOrders { - (void)testEncodesSortOrdersDescending { core::Query query = Query("rooms/1/messages/10/attachments").AddingOrderBy(OrderBy("prop", "desc")); - FSTQueryData *model = [self queryDataForQuery:std::move(query)]; + QueryData model = [self queryDataForQuery:std::move(query)]; GCFSTarget *expected = [GCFSTarget message]; expected.query.parent = @"projects/p/databases/d/documents/rooms/1/messages/10"; @@ -818,7 +814,7 @@ - (void)testEncodesSortOrdersDescending { - (void)testEncodesLimits { core::Query query = Query("docs").WithLimit(26); - FSTQueryData *model = [self queryDataForQuery:std::move(query)]; + QueryData model = [self queryDataForQuery:std::move(query)]; GCFSTarget *expected = [GCFSTarget message]; expected.query.parent = @"projects/p/databases/d/documents"; @@ -835,12 +831,8 @@ - (void)testEncodesLimits { - (void)testEncodesResumeTokens { core::Query q = Query("docs"); - FSTQueryData *model = [[FSTQueryData alloc] initWithQuery:std::move(q) - targetID:1 - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen - snapshotVersion:SnapshotVersion::None() - resumeToken:FSTTestData(1, 2, 3, -1)]; + QueryData model(std::move(q), 1, 0, QueryPurpose::Listen, SnapshotVersion::None(), + testutil::Bytes(1, 2, 3)); GCFSTarget *expected = [GCFSTarget message]; expected.query.parent = @"projects/p/databases/d/documents"; @@ -850,22 +842,17 @@ - (void)testEncodesResumeTokens { [expected.query.structuredQuery.orderByArray addObject:[GCFSStructuredQuery_Order messageWithProperty:kDocumentKeyPath ascending:YES]]; expected.targetId = 1; - expected.resumeToken = FSTTestData(1, 2, 3, -1); + expected.resumeToken = MakeNSData(testutil::Bytes(1, 2, 3)); [self assertRoundTripForQueryData:model proto:expected]; } -- (FSTQueryData *)queryDataForQuery:(core::Query)query { - return [[FSTQueryData alloc] initWithQuery:std::move(query) - targetID:1 - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen - snapshotVersion:SnapshotVersion::None() - resumeToken:[NSData data]]; +- (QueryData)queryDataForQuery:(core::Query)query { + return QueryData(std::move(query), 1, 0, QueryPurpose::Listen); } -- (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget *)proto { - // Verify that the encoded FSTQueryData matches the target. +- (void)assertRoundTripForQueryData:(const QueryData &)queryData proto:(GCFSTarget *)proto { + // Verify that the encoded QueryData matches the target. GCFSTarget *actualProto = [self.serializer encodedTarget:queryData]; XCTAssertEqualObjects(actualProto, proto); @@ -877,7 +864,7 @@ - (void)assertRoundTripForQueryData:(FSTQueryData *)queryData proto:(GCFSTarget } else { actualModel = [self.serializer decodedQueryFromDocumentsTarget:proto.documents]; } - XCTAssertEqual(actualModel, queryData.query); + XCTAssertEqual(actualModel, queryData.query()); } - (void)testConvertsTargetChangeWithAdded { @@ -894,14 +881,14 @@ - (void)testConvertsTargetChangeWithAdded { - (void)testConvertsTargetChangeWithRemoved { WatchTargetChange expected{WatchTargetChangeState::Removed, {1, 4}, - FSTTestData(0, 1, 2, -1), + Bytes(0, 1, 2), Status{Error::PermissionDenied, "Error message"}}; GCFSListenResponse *listenResponse = [GCFSListenResponse message]; listenResponse.targetChange.targetChangeType = GCFSTargetChange_TargetChangeType_Remove; listenResponse.targetChange.cause.code = FIRFirestoreErrorCodePermissionDenied; listenResponse.targetChange.cause.message = @"Error message"; - listenResponse.targetChange.resumeToken = FSTTestData(0, 1, 2, -1); + listenResponse.targetChange.resumeToken = MakeNSData(Bytes(0, 1, 2)); [listenResponse.targetChange.targetIdsArray addValue:1]; [listenResponse.targetChange.targetIdsArray addValue:4]; diff --git a/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.mm index 7944f5a5ba1..eacef05aa33 100644 --- a/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTLevelDBSpecTests.mm @@ -16,12 +16,13 @@ #import "Firestore/Example/Tests/SpecTests/FSTSpecTests.h" -#import "Firestore/Source/Local/FSTLevelDB.h" -#include "Firestore/core/src/firebase/firestore/util/path.h" - #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" #import "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" +#include "Firestore/core/src/firebase/firestore/util/path.h" + +using firebase::firestore::local::Persistence; using firebase::firestore::util::Path; NS_ASSUME_NONNULL_BEGIN @@ -45,7 +46,7 @@ - (void)setUpForSpecWithConfig:(NSDictionary *)config { } /** Overrides -[FSTSpecTests persistence] */ -- (id)persistenceWithGCEnabled:(__unused BOOL)GCEnabled { +- (std::unique_ptr)persistenceWithGCEnabled:(__unused BOOL)GCEnabled { return [FSTPersistenceTestHelpers levelDBPersistenceWithDir:_levelDbDir]; } diff --git a/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.mm index 840ae992c2f..b0084baa9f8 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTMemorySpecTests.mm @@ -16,13 +16,16 @@ #import "Firestore/Example/Tests/SpecTests/FSTSpecTests.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" #import "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" + NS_ASSUME_NONNULL_BEGIN +using firebase::firestore::local::Persistence; + /** * An implementation of FSTSpecTests that uses the memory-only implementation of local storage. * @@ -34,7 +37,7 @@ @interface FSTMemorySpecTests : FSTSpecTests @implementation FSTMemorySpecTests /** Overrides -[FSTSpecTests persistence] */ -- (id)persistenceWithGCEnabled:(BOOL)GCEnabled { +- (std::unique_ptr)persistenceWithGCEnabled:(BOOL)GCEnabled { if (GCEnabled) { return [FSTPersistenceTestHelpers eagerGCMemoryPersistence]; } else { diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h index 3486f014917..988e7f37c0f 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.h @@ -23,7 +23,7 @@ #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/datastore.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" NS_ASSUME_NONNULL_BEGIN @@ -71,7 +71,7 @@ class MockDatastore : public Datastore { void FailWatchStream(const util::Status& error); /** Returns the set of active targets on the watch stream. */ - const std::unordered_map& ActiveTargets() const; + const std::unordered_map& ActiveTargets() const; /** Helper method to expose watch stream state to verify in tests. */ bool IsWatchStreamOpen() const; diff --git a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm index 390bfcd830a..50831e97c64 100644 --- a/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm +++ b/Firestore/Example/Tests/SpecTests/FSTMockDatastore.mm @@ -21,12 +21,12 @@ #include #include -#import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #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/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/mutation.h" #include "Firestore/core/src/firebase/firestore/remote/connectivity_monitor.h" @@ -45,6 +45,7 @@ using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::EmptyCredentialsProvider; using firebase::firestore::core::DatabaseInfo; +using firebase::firestore::local::QueryData; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::Mutation; using firebase::firestore::model::MutationResult; @@ -77,7 +78,7 @@ callback_{callback} { } - const std::unordered_map& ActiveTargets() const { + const std::unordered_map& ActiveTargets() const { return active_targets_; } @@ -100,15 +101,15 @@ bool IsOpen() const override { return open_; } - void WatchQuery(FSTQueryData* query) override { - LOG_DEBUG("WatchQuery: %s: %s, %s", query.targetID, query.query.ToString(), query.resumeToken); + void WatchQuery(const QueryData& query) override { + LOG_DEBUG("WatchQuery: %s: %s, %s", query.target_id(), query.query().ToString(), + query.resume_token().ToString()); // Snapshot version is ignored on the wire - FSTQueryData* sentQueryData = [query queryDataByReplacingSnapshotVersion:SnapshotVersion::None() - resumeToken:query.resumeToken - sequenceNumber:query.sequenceNumber]; + QueryData sentQueryData = + query.Copy(SnapshotVersion::None(), query.resume_token(), query.sequence_number()); datastore_->IncrementWatchStreamRequests(); - active_targets_[query.targetID] = sentQueryData; + active_targets_[query.target_id()] = sentQueryData; } void UnwatchTargetId(model::TargetId target_id) override { @@ -150,7 +151,7 @@ void WriteWatchChange(const WatchChange& change, SnapshotVersion snap) { private: bool open_ = false; - std::unordered_map active_targets_; + std::unordered_map active_targets_; MockDatastore* datastore_ = nullptr; WatchStreamCallback* callback_ = nullptr; }; @@ -274,7 +275,7 @@ int sent_mutations_count() const { watch_stream_->FailStream(error); } -const std::unordered_map& MockDatastore::ActiveTargets() const { +const std::unordered_map& MockDatastore::ActiveTargets() const { return watch_stream_->ActiveTargets(); } diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.h b/Firestore/Example/Tests/SpecTests/FSTSpecTests.h index 4dd028171eb..afd3895a0d6 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.h +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.h @@ -17,8 +17,6 @@ #import #import -@protocol FSTPersistence; - NS_ASSUME_NONNULL_BEGIN extern NSString *const kEagerGC; @@ -37,7 +35,7 @@ extern NSString *const kDurablePersistence; * store implementation. To create a new variant of FSTSpecTests: * * + Subclass FSTSpecTests - * + override -persistence to create and return an appropriate id implementation. + * + override -persistence to create and return an appropriate Persistence implementation. */ @interface FSTSpecTests : XCTestCase diff --git a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm index 737bbcb26cf..51e50a330fa 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSpecTests.mm @@ -26,8 +26,6 @@ #include #import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Util/FSTClasses.h" #import "Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h" @@ -35,6 +33,8 @@ #include "Firestore/core/include/firebase/firestore/firestore_errors.h" #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.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/document_key_set.h" @@ -44,6 +44,7 @@ #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/model/types.h" +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/objc/objc_compatibility.h" #include "Firestore/core/src/firebase/firestore/remote/existence_filter.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" @@ -63,6 +64,9 @@ using firebase::firestore::auth::User; using firebase::firestore::core::DocumentViewChange; using firebase::firestore::core::Query; +using firebase::firestore::local::Persistence; +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::model::Document; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; @@ -75,6 +79,8 @@ using firebase::firestore::model::ResourcePath; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; +using firebase::firestore::nanopb::ByteString; +using firebase::firestore::nanopb::MakeByteString; using firebase::firestore::remote::ExistenceFilter; using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::ExistenceFilterWatchChange; @@ -116,10 +122,6 @@ namespace { -NSString *Describe(NSData *data) { - return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; -} - std::vector ConvertTargetsArray(NSArray *from) { std::vector result; for (NSNumber *targetID in from) { @@ -128,6 +130,10 @@ return result; } +ByteString MakeResumeToken(NSString *specString) { + return MakeByteString([specString dataUsingEncoding:NSUTF8StringEncoding]); +} + } // namespace @interface FSTSpecTests () @@ -141,7 +147,7 @@ @implementation FSTSpecTests { FSTUserDataConverter *_converter; } -- (id)persistenceWithGCEnabled:(BOOL)GCEnabled { +- (std::unique_ptr)persistenceWithGCEnabled:(BOOL)GCEnabled { @throw FSTAbstractMethodException(); // NOLINT } @@ -166,8 +172,8 @@ - (void)setUpForSpecWithConfig:(NSDictionary *)config { if (numClients) { XCTAssertEqualObjects(numClients, @1, @"The iOS client does not support multi-client tests"); } - id persistence = [self persistenceWithGCEnabled:_gcEnabled]; - self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:persistence]; + std::unique_ptr persistence = [self persistenceWithGCEnabled:_gcEnabled]; + self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:std::move(persistence)]; [self.driver start]; } @@ -272,6 +278,14 @@ - (void)doDelete:(NSString *)key { [self.driver writeUserMutation:FSTTestDeleteMutation(key)]; } +- (void)doAddSnapshotsInSyncListener { + [self.driver addSnapshotsInSyncListener]; +} + +- (void)doRemoveSnapshotsInSyncListener { + [self.driver removeSnapshotsInSyncListener]; +} + - (void)doWatchAck:(NSArray *)ackedTargets { WatchTargetChange change{WatchTargetChangeState::Added, ConvertTargetsArray(ackedTargets)}; [self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()]; @@ -279,7 +293,7 @@ - (void)doWatchAck:(NSArray *)ackedTargets { - (void)doWatchCurrent:(NSArray *)currentSpec { NSArray *currentTargets = currentSpec[0]; - NSData *resumeToken = [currentSpec[1] dataUsingEncoding:NSUTF8StringEncoding]; + ByteString resumeToken = MakeResumeToken(currentSpec[1]); WatchTargetChange change{WatchTargetChangeState::Current, ConvertTargetsArray(currentTargets), resumeToken}; [self.driver receiveWatchChange:change snapshotVersion:SnapshotVersion::None()]; @@ -365,7 +379,7 @@ - (void)doWatchSnapshot:(NSDictionary *)watchSnapshot { // set of target IDs. NSArray *targetIDs = watchSnapshot[@"targetIds"] ? watchSnapshot[@"targetIds"] : [NSArray array]; - NSData *resumeToken = [watchSnapshot[@"resumeToken"] dataUsingEncoding:NSUTF8StringEncoding]; + ByteString resumeToken = MakeResumeToken(watchSnapshot[@"resumeToken"]); WatchTargetChange change{WatchTargetChangeState::NoChange, ConvertTargetsArray(targetIDs), resumeToken}; [self.driver receiveWatchChange:change @@ -452,8 +466,8 @@ - (void)doRestart { [self.driver shutdown]; - id persistence = [self persistenceWithGCEnabled:_gcEnabled]; - self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:persistence + std::unique_ptr persistence = [self persistenceWithGCEnabled:_gcEnabled]; + self.driver = [[FSTSyncEngineTestDriver alloc] initWithPersistence:std::move(persistence) initialUser:currentUser outstandingWrites:outstandingWrites]; [self.driver start]; @@ -473,6 +487,10 @@ - (void)doStep:(NSDictionary *)step { [self doPatch:step[@"userPatch"]]; } else if (step[@"userDelete"]) { [self doDelete:step[@"userDelete"]]; + } else if (step[@"addSnapshotsInSyncListener"]) { + [self doAddSnapshotsInSyncListener]; + } else if (step[@"removeSnapshotsInSyncListener"]) { + [self doRemoveSnapshotsInSyncListener]; } else if (step[@"drainQueue"]) { [self doDrainQueue]; } else if (step[@"watchAck"]) { @@ -529,22 +547,22 @@ - (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected { NSMutableArray *removed = expected[@"removed"]; for (NSDictionary *changeSpec in removed) { expectedChanges.push_back([self parseChange:changeSpec - ofType:DocumentViewChange::Type::kRemoved]); + ofType:DocumentViewChange::Type::Removed]); } NSMutableArray *added = expected[@"added"]; for (NSDictionary *changeSpec in added) { expectedChanges.push_back([self parseChange:changeSpec - ofType:DocumentViewChange::Type::kAdded]); + ofType:DocumentViewChange::Type::Added]); } NSMutableArray *modified = expected[@"modified"]; for (NSDictionary *changeSpec in modified) { expectedChanges.push_back([self parseChange:changeSpec - ofType:DocumentViewChange::Type::kModified]); + ofType:DocumentViewChange::Type::Modified]); } NSMutableArray *metadata = expected[@"metadata"]; for (NSDictionary *changeSpec in metadata) { expectedChanges.push_back([self parseChange:changeSpec - ofType:DocumentViewChange::Type::kMetadata]); + ofType:DocumentViewChange::Type::Metadata]); } XCTAssertEqual(actual.viewSnapshot.value().document_changes().size(), expectedChanges.size()); @@ -561,10 +579,10 @@ - (void)validateEvent:(FSTQueryEvent *)actual matches:(NSDictionary *)expected { } } -- (void)validateStepExpectations:(NSArray *_Nullable)stepExpectations { +- (void)validateExpectedSnapshotEvents:(NSArray *_Nullable)expectedEvents { NSArray *events = self.driver.capturedEventsSinceLastCall; - if (!stepExpectations) { + if (!expectedEvents) { XCTAssertEqual(events.count, 0); for (FSTQueryEvent *event in events) { XCTFail(@"Unexpected event: %@", event); @@ -572,12 +590,12 @@ - (void)validateStepExpectations:(NSArray *_Nullable)stepExpectations { return; } - XCTAssertEqual(events.count, stepExpectations.count); + XCTAssertEqual(events.count, expectedEvents.count); events = [events sortedArrayUsingComparator:^NSComparisonResult(FSTQueryEvent *q1, FSTQueryEvent *q2) { return util::WrapCompare(q1.query.CanonicalId(), q2.query.CanonicalId()); }]; - stepExpectations = [stepExpectations + expectedEvents = [expectedEvents sortedArrayUsingComparator:^NSComparisonResult(NSDictionary *left, NSDictionary *right) { Query leftQuery = [self parseQuery:left[@"query"]]; Query rightQuery = [self parseQuery:right[@"query"]]; @@ -585,70 +603,72 @@ - (void)validateStepExpectations:(NSArray *_Nullable)stepExpectations { }]; NSUInteger i = 0; - for (; i < stepExpectations.count && i < events.count; ++i) { - [self validateEvent:events[i] matches:stepExpectations[i]]; + for (; i < expectedEvents.count && i < events.count; ++i) { + [self validateEvent:events[i] matches:expectedEvents[i]]; } - for (; i < stepExpectations.count; ++i) { - XCTFail(@"Missing event: %@", stepExpectations[i]); + for (; i < expectedEvents.count; ++i) { + XCTFail(@"Missing event: %@", expectedEvents[i]); } for (; i < events.count; ++i) { XCTFail(@"Unexpected event: %@", events[i]); } } -- (void)validateStateExpectations:(nullable NSDictionary *)expected { - if (expected) { - if (expected[@"numOutstandingWrites"]) { - XCTAssertEqual([self.driver sentWritesCount], [expected[@"numOutstandingWrites"] intValue]); +- (void)validateExpectedState:(nullable NSDictionary *)expectedState { + if (expectedState) { + if (expectedState[@"numOutstandingWrites"]) { + XCTAssertEqual([self.driver sentWritesCount], + [expectedState[@"numOutstandingWrites"] intValue]); } - if (expected[@"writeStreamRequestCount"]) { + if (expectedState[@"writeStreamRequestCount"]) { XCTAssertEqual([self.driver writeStreamRequestCount], - [expected[@"writeStreamRequestCount"] intValue]); + [expectedState[@"writeStreamRequestCount"] intValue]); } - if (expected[@"watchStreamRequestCount"]) { + if (expectedState[@"watchStreamRequestCount"]) { XCTAssertEqual([self.driver watchStreamRequestCount], - [expected[@"watchStreamRequestCount"] intValue]); + [expectedState[@"watchStreamRequestCount"] intValue]); } - if (expected[@"limboDocs"]) { + if (expectedState[@"limboDocs"]) { DocumentKeySet expectedLimboDocuments; - NSArray *docNames = expected[@"limboDocs"]; + NSArray *docNames = expectedState[@"limboDocs"]; for (NSString *name in docNames) { expectedLimboDocuments = expectedLimboDocuments.insert(FSTTestDocKey(name)); } // Update the expected limbo documents [self.driver setExpectedLimboDocuments:std::move(expectedLimboDocuments)]; } - if (expected[@"activeTargets"]) { - __block std::unordered_map expectedActiveTargets; - [expected[@"activeTargets"] enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString, - NSDictionary *queryData, - BOOL *stop) { - TargetId targetID = [targetIDString intValue]; - Query query = [self parseQuery:queryData[@"query"]]; - NSData *resumeToken = [queryData[@"resumeToken"] dataUsingEncoding:NSUTF8StringEncoding]; - // TODO(mcg): populate the purpose of the target once it's possible to encode that in the - // spec tests. For now, hard-code that it's a listen despite the fact that it's not always - // the right value. - expectedActiveTargets[targetID] = - [[FSTQueryData alloc] initWithQuery:std::move(query) - targetID:targetID - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen - snapshotVersion:SnapshotVersion::None() - resumeToken:resumeToken]; - }]; + if (expectedState[@"activeTargets"]) { + __block std::unordered_map expectedActiveTargets; + [expectedState[@"activeTargets"] + enumerateKeysAndObjectsUsingBlock:^(NSString *targetIDString, NSDictionary *queryData, + BOOL *stop) { + TargetId targetID = [targetIDString intValue]; + Query query = [self parseQuery:queryData[@"query"]]; + ByteString resumeToken = MakeResumeToken(queryData[@"resumeToken"]); + // TODO(mcg): populate the purpose of the target once it's possible to encode that in + // the spec tests. For now, hard-code that it's a listen despite the fact that it's not + // always the right value. + expectedActiveTargets[targetID] = + QueryData(std::move(query), targetID, 0, QueryPurpose::Listen, + SnapshotVersion::None(), std::move(resumeToken)); + }]; [self.driver setExpectedActiveTargets:expectedActiveTargets]; } } // Always validate the we received the expected number of callbacks. - [self validateUserCallbacks:expected]; + [self validateUserCallbacks:expectedState]; // Always validate that the expected limbo docs match the actual limbo docs. [self validateLimboDocuments]; // Always validate that the expected active targets match the actual active targets. [self validateActiveTargets]; } +- (void)validateSnapshotsInSyncEvents:(int)expectedSnapshotInSyncEvents { + XCTAssertEqual(expectedSnapshotInSyncEvents, [self.driver snapshotsInSyncEvents]); + [self.driver resetSnapshotsInSyncEvents]; +} + - (void)validateUserCallbacks:(nullable NSDictionary *)expected { NSDictionary *expectedCallbacks = expected[@"userCallbacks"]; NSArray *actualAcknowledgedDocs = @@ -690,26 +710,26 @@ - (void)validateActiveTargets { return; } - // Create a copy so we can modify it in tests - std::unordered_map actualTargets = [self.driver activeTargets]; + // Create a copy so we can modify it below + std::unordered_map actualTargets = [self.driver activeTargets]; for (const auto &kv : [self.driver activeTargets]) { TargetId targetID = kv.first; - FSTQueryData *queryData = kv.second; - XCTAssertNotNil(actualTargets[targetID], @"Expected active target not found: %@", queryData); + const QueryData &queryData = kv.second; + + auto found = actualTargets.find(targetID); + XCTAssertNotEqual(found, actualTargets.end(), @"Expected active target not found: %s", + queryData.ToString().c_str()); // TODO(mcg): validate the purpose of the target once it's possible to encode that in the // spec tests. For now, only validate properties that can be validated. // XCTAssertEqualObjects(actualTargets[targetID], queryData); - FSTQueryData *actual = actualTargets[targetID]; - XCTAssertNotNil(actual); - if (actual) { - XCTAssertEqual(actual.query, queryData.query); - XCTAssertEqual(actual.targetID, queryData.targetID); - XCTAssertEqual(actual.snapshotVersion, queryData.snapshotVersion); - XCTAssertEqualObjects(Describe(actual.resumeToken), Describe(queryData.resumeToken)); - } + const QueryData &actual = found->second; + XCTAssertEqual(actual.query(), queryData.query()); + XCTAssertEqual(actual.target_id(), queryData.target_id()); + XCTAssertEqual(actual.snapshot_version(), queryData.snapshot_version()); + XCTAssertEqual(actual.resume_token(), queryData.resume_token()); actualTargets.erase(targetID); } @@ -724,8 +744,10 @@ - (void)runSpecTestSteps:(NSArray *)steps config:(NSDictionary *)config { for (NSDictionary *step in steps) { LOG_DEBUG("Doing step %s", step); [self doStep:step]; - [self validateStepExpectations:step[@"expect"]]; - [self validateStateExpectations:step[@"stateExpect"]]; + [self validateExpectedSnapshotEvents:step[@"expectedSnapshotEvents"]]; + [self validateExpectedState:step[@"expectedState"]]; + int expectedSnapshotsInSyncEvents = [step[@"expectedSnapshotsInSyncEvents"] intValue]; + [self validateSnapshotsInSyncEvents:expectedSnapshotsInSyncEvents]; } [self.driver validateUsage]; } @finally { diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h index 81a874557dc..eeb881b6ccb 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.h @@ -17,12 +17,15 @@ #import #include +#include #include #include #include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/core/event_listener.h" #include "Firestore/core/src/firebase/firestore/core/query.h" #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.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/mutation.h" @@ -30,11 +33,20 @@ #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/empty.h" -@class FSTQueryData; -@protocol FSTPersistence; +namespace firebase { +namespace firestore { +namespace local { + +class Persistence; + +} // namespace local +} // namespace firestore +} // namespace firebase namespace core = firebase::firestore::core; +namespace local = firebase::firestore::local; namespace model = firebase::firestore::model; NS_ASSUME_NONNULL_BEGIN @@ -100,14 +112,14 @@ typedef std::unordered_map)persistence; +- (instancetype)initWithPersistence:(std::unique_ptr)persistence; /** * Initializes the underlying FSTSyncEngine with the given local persistence implementation and - * a set of existing outstandingWrites (useful when your FSTPersistence object has - * persisted mutation queues). + * a set of existing outstandingWrites (useful when your Persistence object has persisted + * mutation queues). */ -- (instancetype)initWithPersistence:(id)persistence +- (instancetype)initWithPersistence:(std::unique_ptr)persistence initialUser:(const firebase::firestore::auth::User &)initialUser outstandingWrites:(const FSTOutstandingWriteQueues &)outstandingWrites NS_DESIGNATED_INITIALIZER; @@ -289,7 +301,7 @@ typedef std::unordered_map &)activeTargets; +- (const std::unordered_map &)activeTargets; /** The expected set of active targets, keyed by target ID. */ -- (const std::unordered_map &) +- (const std::unordered_map &) expectedActiveTargets; - (void)setExpectedActiveTargets: - (const std::unordered_map &)targets; + (const std::unordered_map &)targets; @end diff --git a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm index bfc8c39c163..c62556987d3 100644 --- a/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm +++ b/Firestore/Example/Tests/SpecTests/FSTSyncEngineTestDriver.mm @@ -25,11 +25,6 @@ #include #include -#import "Firestore/Source/Core/FSTSyncEngine.h" -#import "Firestore/Source/Local/FSTLocalStore.h" -#import "Firestore/Source/Local/FSTPersistence.h" - -#import "Firestore/Example/Tests/Core/FSTSyncEngine+Testing.h" #import "Firestore/Example/Tests/SpecTests/FSTMockDatastore.h" #include "Firestore/core/include/firebase/firestore/firestore_errors.h" @@ -37,6 +32,9 @@ #include "Firestore/core/src/firebase/firestore/auth/user.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/core/event_manager.h" +#include "Firestore/core/src/firebase/firestore/core/sync_engine.h" +#include "Firestore/core/src/firebase/firestore/local/local_store.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.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/objc/objc_compatibility.h" @@ -44,25 +42,33 @@ #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" -#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "Firestore/core/src/firebase/firestore/util/string_format.h" #include "Firestore/core/src/firebase/firestore/util/to_string.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" #include "absl/memory/memory.h" +namespace testutil = firebase::firestore::testutil; + using firebase::firestore::Error; using firebase::firestore::auth::EmptyCredentialsProvider; using firebase::firestore::auth::HashUser; using firebase::firestore::auth::User; using firebase::firestore::core::DatabaseInfo; +using firebase::firestore::core::EventListener; using firebase::firestore::core::EventManager; using firebase::firestore::core::ListenOptions; using firebase::firestore::core::Query; using firebase::firestore::core::QueryListener; +using firebase::firestore::core::SyncEngine; using firebase::firestore::core::ViewSnapshot; +using firebase::firestore::local::LocalStore; +using firebase::firestore::local::Persistence; +using firebase::firestore::local::QueryData; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentKeySet; @@ -76,8 +82,10 @@ using firebase::firestore::remote::WatchChange; using firebase::firestore::util::AsyncQueue; using firebase::firestore::util::DelayedConstructor; -using firebase::firestore::util::ExecutorLibdispatch; +using firebase::firestore::util::Empty; +using firebase::firestore::util::Executor; using firebase::firestore::util::MakeNSError; +using firebase::firestore::util::MakeNSString; using firebase::firestore::util::MakeString; using firebase::firestore::util::Status; using firebase::firestore::util::StatusOr; @@ -103,7 +111,7 @@ - (NSString *)description { // The Query is also included in the view, so we skip it. std::string str = StringFormat("", ToString(_maybeViewSnapshot), self.error); - return util::MakeNSString(str); + return MakeNSString(str); } @end @@ -126,10 +134,6 @@ @interface FSTSyncEngineTestDriver () #pragma mark - Parts of the Firestore system that the spec tests need to control. -@property(nonatomic, strong, readonly) FSTLocalStore *localStore; -@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; -@property(nonatomic, strong, readonly) id persistence; - #pragma mark - Data structures for holding events sent by the watch stream. /** A block for the FSTEventAggregator to use to report events to the test. */ @@ -147,13 +151,19 @@ @interface FSTSyncEngineTestDriver () @end @implementation FSTSyncEngineTestDriver { + std::unique_ptr _persistence; + + std::unique_ptr _localStore; + + std::unique_ptr _syncEngine; + std::shared_ptr _workerQueue; std::unique_ptr _remoteStore; DelayedConstructor _eventManager; - std::unordered_map _expectedActiveTargets; + std::unordered_map _expectedActiveTargets; // ivar is declared as mutable. std::unordered_map *, HashUser> _outstandingWrites; @@ -165,16 +175,19 @@ @implementation FSTSyncEngineTestDriver { DatabaseInfo _databaseInfo; User _currentUser; + std::vector>> _snapshotsInSyncListeners; std::shared_ptr _datastore; + + int _snapshotsInSyncEvents; } -- (instancetype)initWithPersistence:(id)persistence { - return [self initWithPersistence:persistence +- (instancetype)initWithPersistence:(std::unique_ptr)persistence { + return [self initWithPersistence:std::move(persistence) initialUser:User::Unauthenticated() outstandingWrites:{}]; } -- (instancetype)initWithPersistence:(id)persistence +- (instancetype)initWithPersistence:(std::unique_ptr)persistence initialUser:(const User &)initialUser outstandingWrites:(const FSTOutstandingWriteQueues &)outstandingWrites { if (self = [super init]) { @@ -188,26 +201,20 @@ - (instancetype)initWithPersistence:(id)persistence _databaseInfo = {DatabaseId{"project", "database"}, "persistence", "host", false}; // Set up the sync engine and various stores. - dispatch_queue_t queue = - dispatch_queue_create("sync_engine_test_driver", DISPATCH_QUEUE_SERIAL); - _workerQueue = std::make_shared(absl::make_unique(queue)); - _persistence = persistence; - _localStore = [[FSTLocalStore alloc] initWithPersistence:persistence initialUser:initialUser]; + _workerQueue = testutil::AsyncQueueForTesting(); + _persistence = std::move(persistence); + _localStore = absl::make_unique(_persistence.get(), initialUser); _datastore = std::make_shared(_databaseInfo, _workerQueue, std::make_shared()); _remoteStore = absl::make_unique( - _localStore, _datastore, _workerQueue, [self](OnlineState onlineState) { - [self.syncEngine applyChangedOnlineState:onlineState]; - _eventManager->HandleOnlineStateChange(onlineState); - }); + _localStore.get(), _datastore, _workerQueue, + [self](OnlineState onlineState) { _syncEngine->HandleOnlineStateChange(onlineState); }); ; - _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore - remoteStore:_remoteStore.get() - initialUser:initialUser]; - _remoteStore->set_sync_engine(_syncEngine); - _eventManager.Init(_syncEngine); + _syncEngine = absl::make_unique(_localStore.get(), _remoteStore.get(), initialUser); + _remoteStore->set_sync_engine(_syncEngine.get()); + _eventManager.Init(_syncEngine.get()); // Set up internal event tracking for the spec tests. NSMutableArray *events = [NSMutableArray array]; @@ -245,9 +252,37 @@ - (void)drainQueue { return _currentUser; } +- (void)incrementSnapshotsInSyncEvents { + _snapshotsInSyncEvents += 1; +} + +- (void)resetSnapshotsInSyncEvents { + _snapshotsInSyncEvents = 0; +} + +- (void)addSnapshotsInSyncListener { + std::shared_ptr> eventListener = EventListener::Create( + [self](const StatusOr &) { [self incrementSnapshotsInSyncEvents]; }); + _snapshotsInSyncListeners.push_back(eventListener); + _eventManager->AddSnapshotsInSyncListener(eventListener); +} + +- (void)removeSnapshotsInSyncListener { + if (_snapshotsInSyncListeners.empty()) { + HARD_FAIL("There must be a listener to unlisten to"); + } else { + _eventManager->RemoveSnapshotsInSyncListener(_snapshotsInSyncListeners.back()); + _snapshotsInSyncListeners.pop_back(); + } +} + +- (int)snapshotsInSyncEvents { + return _snapshotsInSyncEvents; +} + - (void)start { _workerQueue->EnqueueBlocking([&] { - [self.localStore start]; + _localStore->Start(); _remoteStore->Start(); }); } @@ -261,7 +296,7 @@ - (void)validateUsage { - (void)shutdown { _workerQueue->EnqueueBlocking([&] { _remoteStore->Shutdown(); - [self.persistence shutdown]; + _persistence->Shutdown(); }); } @@ -307,7 +342,7 @@ - (void)runTimer:(TimerId)timerID { - (void)changeUser:(const User &)user { _currentUser = user; - _workerQueue->EnqueueBlocking([&] { [self.syncEngine credentialDidChangeWithUser:user]; }); + _workerQueue->EnqueueBlocking([&] { _syncEngine->HandleCredentialChange(user); }); } - (FSTOutstandingWrite *)receiveWriteAckWithVersion:(const SnapshotVersion &)commitVersion @@ -398,19 +433,18 @@ - (void)writeUserMutation:(Mutation)mutation { [[self currentOutstandingWrites] addObject:write]; LOG_DEBUG("sending a user write."); _workerQueue->EnqueueBlocking([=] { - [self.syncEngine writeMutations:{mutation} - completion:^(NSError *_Nullable error) { - LOG_DEBUG("A callback was called with error: %s", error); - write.done = YES; - write.error = error; - - NSString *mutationKey = util::MakeNSString(mutation.key().ToString()); - if (error) { - [self.rejectedDocs addObject:mutationKey]; - } else { - [self.acknowledgedDocs addObject:mutationKey]; - } - }]; + _syncEngine->WriteMutations({mutation}, [self, write, mutation](Status error) { + LOG_DEBUG("A callback was called with error: %s", error.error_message()); + write.done = YES; + write.error = error.ToNSError(); + + NSString *mutationKey = MakeNSString(mutation.key().ToString()); + if (!error.ok()) { + [self.rejectedDocs addObject:mutationKey]; + } else { + [self.acknowledgedDocs addObject:mutationKey]; + } + }); }); } @@ -432,18 +466,18 @@ - (void)receiveWatchStreamError:(int)errorCode userInfo:(NSDictionary)currentLimboDocuments { - return [self.syncEngine currentLimboDocuments]; + return _syncEngine->GetCurrentLimboDocuments(); } -- (const std::unordered_map &)activeTargets { +- (const std::unordered_map &)activeTargets { return _datastore->ActiveTargets(); } -- (const std::unordered_map &)expectedActiveTargets { +- (const std::unordered_map &)expectedActiveTargets { return _expectedActiveTargets; } -- (void)setExpectedActiveTargets:(const std::unordered_map &)targets { +- (void)setExpectedActiveTargets:(const std::unordered_map &)targets { _expectedActiveTargets = targets; } diff --git a/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json index 3f7caafd004..dd34d97cac0 100644 --- a/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/collection_spec_test.json @@ -17,7 +17,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -68,7 +68,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -114,7 +114,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -134,7 +134,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", diff --git a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json index 9eaf185ca4a..d1c691351e1 100644 --- a/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/existence_filter_spec_test.json @@ -17,7 +17,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -68,7 +68,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -128,7 +128,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -159,7 +159,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -205,7 +205,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -251,7 +251,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -282,7 +282,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -308,7 +308,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -341,7 +341,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -392,7 +392,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -427,7 +427,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -440,7 +440,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -452,7 +452,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -510,7 +510,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -543,7 +543,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -605,7 +605,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -655,7 +655,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -667,7 +667,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -725,7 +725,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/2" ], @@ -767,7 +767,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -780,7 +780,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -826,7 +826,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -888,7 +888,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -933,7 +933,7 @@ }, "runBackoffTimer": true }, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -964,7 +964,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -976,7 +976,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1034,7 +1034,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/2" ], @@ -1076,7 +1076,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -1089,7 +1089,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1135,7 +1135,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1186,7 +1186,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1246,7 +1246,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1271,7 +1271,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1351,7 +1351,7 @@ "version": 3000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1397,7 +1397,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1448,7 +1448,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/a", @@ -1486,7 +1486,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/a", @@ -1532,7 +1532,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1594,7 +1594,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1644,7 +1644,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1656,7 +1656,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1714,7 +1714,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/2" ], @@ -1747,7 +1747,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1760,7 +1760,7 @@ }, "limboDocs": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", diff --git a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json index 5bd75e4cdb3..4f84bc4d2d4 100644 --- a/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/limbo_spec_test.json @@ -17,7 +17,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -68,7 +68,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -112,7 +112,7 @@ "version": 1001, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/a" ], @@ -135,7 +135,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -166,7 +166,7 @@ "version": 1002, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -179,7 +179,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -225,7 +225,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -276,7 +276,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -320,7 +320,7 @@ "version": 1001, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/a" ], @@ -343,7 +343,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -381,7 +381,7 @@ "version": 1002, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -394,7 +394,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -446,7 +446,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -503,7 +503,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -553,7 +553,7 @@ "version": 1001, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/a" ], @@ -582,7 +582,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -611,7 +611,7 @@ "docs": [ { "key": "collection/a", - "version": 1000, + "version": 1002, "value": { "key": "b" }, @@ -639,7 +639,7 @@ "version": 1002, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -658,7 +658,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -716,7 +716,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -773,7 +773,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -820,7 +820,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -871,7 +871,7 @@ "version": 1001, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/a" ], @@ -914,7 +914,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -948,7 +948,7 @@ "docs": [ { "key": "collection/a", - "version": 1000, + "version": 1002, "value": { "key": "b" }, @@ -977,7 +977,7 @@ "version": 1002, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -1010,7 +1010,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1055,7 +1055,7 @@ "added": [ { "key": "collection/a", - "version": 1000, + "version": 1002, "value": { "key": "b" }, @@ -1105,7 +1105,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1167,7 +1167,7 @@ "version": 1002, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1217,7 +1217,7 @@ "version": 1003, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b" ], @@ -1240,7 +1240,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1271,7 +1271,7 @@ "version": 1004, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -1284,7 +1284,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1330,7 +1330,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1394,7 +1394,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1442,7 +1442,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1462,7 +1462,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -1481,7 +1481,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1554,7 +1554,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1581,7 +1581,7 @@ "include": false } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1628,7 +1628,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b" ], @@ -1752,7 +1752,7 @@ "version": 4000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1771,7 +1771,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "4": { @@ -1812,7 +1812,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1876,7 +1876,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1924,7 +1924,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1944,7 +1944,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -1963,7 +1963,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2036,7 +2036,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2063,7 +2063,7 @@ "include": false } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2110,7 +2110,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b" ], @@ -2165,7 +2165,7 @@ "version": 3000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "4": { @@ -2185,7 +2185,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2261,7 +2261,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -2279,7 +2279,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2295,7 +2295,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2365,7 +2365,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2421,7 +2421,7 @@ "version": 1003, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b" ], @@ -2448,7 +2448,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2486,7 +2486,7 @@ "version": 1004, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -2503,7 +2503,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2545,7 +2545,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -2563,7 +2563,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2579,7 +2579,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2649,7 +2649,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2705,7 +2705,7 @@ "version": 2000000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b" ], @@ -2732,7 +2732,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -2740,7 +2740,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2756,7 +2756,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -2800,7 +2800,7 @@ "version": 3000000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b" ], @@ -2845,7 +2845,7 @@ "version": 4000000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -2858,7 +2858,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2900,7 +2900,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -2914,7 +2914,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2991,7 +2991,7 @@ "version": 1000000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3063,7 +3063,7 @@ "version": 2000000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3075,7 +3075,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b", "collection/c" @@ -3117,7 +3117,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 1 @@ -3128,7 +3128,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": false, "limboDocs": [], "activeTargets": { @@ -3146,7 +3146,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3189,7 +3189,7 @@ "version": 3000000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b", "collection/c" @@ -3243,7 +3243,7 @@ "version": 3000000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/c" ], @@ -3270,7 +3270,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3301,7 +3301,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -3345,7 +3345,7 @@ "version": 5000000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/c" ], @@ -3390,7 +3390,7 @@ "version": 6000000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -3403,7 +3403,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3461,7 +3461,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -3474,7 +3474,7 @@ "writeAck": { "version": 1001 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/b" @@ -3492,7 +3492,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3504,7 +3504,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3579,7 +3579,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b" ], @@ -3618,7 +3618,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "1": { "query": { @@ -3652,7 +3652,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3704,7 +3704,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -3733,7 +3733,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3759,7 +3759,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", diff --git a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json index 79e7c1d3f02..eea16ff9ac6 100644 --- a/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/limit_spec_test.json @@ -18,7 +18,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -81,7 +81,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -164,7 +164,7 @@ "version": 1002, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -225,7 +225,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -288,7 +288,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -336,7 +336,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -358,7 +358,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -371,7 +371,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -430,7 +430,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -493,7 +493,7 @@ "version": 1002, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -580,7 +580,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/a" ], @@ -604,7 +604,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -636,7 +636,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -650,7 +650,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -711,7 +711,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -774,7 +774,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -845,7 +845,7 @@ "version": 1002, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -859,7 +859,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -920,7 +920,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -944,7 +944,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1060,7 +1060,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1167,7 +1167,7 @@ "version": 1002, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -1190,7 +1190,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1280,7 +1280,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1359,7 +1359,7 @@ "version": 1002, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1428,7 +1428,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1448,7 +1448,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -1467,7 +1467,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1518,7 +1518,7 @@ "matches": false } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1590,7 +1590,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1669,7 +1669,7 @@ "version": 1003, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1738,7 +1738,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1758,7 +1758,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -1777,7 +1777,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1870,7 +1870,7 @@ "version": 1004, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1906,7 +1906,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1941,7 +1941,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -1960,7 +1960,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2024,7 +2024,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2097,7 +2097,7 @@ "version": 1003, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2154,7 +2154,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2180,7 +2180,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -2198,7 +2198,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2290,7 +2290,7 @@ "version": 1004, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2324,7 +2324,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2358,7 +2358,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -2376,7 +2376,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2439,7 +2439,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2512,7 +2512,7 @@ "version": 1003, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2569,7 +2569,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2595,7 +2595,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -2613,7 +2613,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2705,7 +2705,7 @@ "version": 1004, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2739,7 +2739,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2759,7 +2759,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2771,7 +2771,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2857,7 +2857,7 @@ "version": 1005, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2898,7 +2898,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2924,7 +2924,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2995,7 +2995,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3052,7 +3052,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3099,7 +3099,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -3124,7 +3124,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -3141,7 +3141,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3221,7 +3221,7 @@ "version": 1002, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3265,7 +3265,7 @@ "version": 1003, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3282,7 +3282,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/a" ], @@ -3325,7 +3325,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] } @@ -3339,7 +3339,7 @@ }, { "restart": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] } @@ -3359,7 +3359,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3377,7 +3377,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3447,7 +3447,7 @@ "version": 1004, "targetIds": [] }, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3511,7 +3511,7 @@ "version": 1005, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/a" ], @@ -3559,7 +3559,7 @@ "version": 1006, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -3578,7 +3578,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3649,7 +3649,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3706,7 +3706,7 @@ "version": 2001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3753,7 +3753,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -3778,7 +3778,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -3795,7 +3795,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3875,7 +3875,7 @@ "version": 2003, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3933,7 +3933,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -3959,7 +3959,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3977,7 +3977,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4030,7 +4030,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4093,7 +4093,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4140,7 +4140,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4161,7 +4161,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4291,7 +4291,7 @@ "version": 1005, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4399,7 +4399,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/a", "collection/b" @@ -4440,7 +4440,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4472,7 +4472,7 @@ "version": 2000, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/b", "collection/c" @@ -4513,7 +4513,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4601,7 +4601,7 @@ "version": 2001, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/c", "collection/d" @@ -4642,7 +4642,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4730,7 +4730,7 @@ "version": 2002, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/d" ], @@ -4762,7 +4762,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4850,7 +4850,7 @@ "version": 2003, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -4872,7 +4872,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4979,7 +4979,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4996,7 +4996,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5067,7 +5067,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5161,7 +5161,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5239,7 +5239,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5256,7 +5256,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5327,7 +5327,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5381,7 +5381,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5483,7 +5483,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5513,7 +5513,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" diff --git a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json index 512bd9b2fa1..a4e63ea226a 100644 --- a/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/listen_spec_test.json @@ -20,7 +20,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -71,7 +71,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -106,7 +106,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -119,7 +119,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -152,7 +152,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -203,7 +203,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -254,7 +254,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -300,7 +300,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -322,7 +322,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -352,7 +352,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -429,7 +429,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection1", @@ -501,7 +501,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -513,7 +513,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -559,7 +559,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -610,7 +610,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -638,7 +638,7 @@ }, { "userDelete": "collection/a", - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -668,7 +668,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -720,7 +720,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -782,7 +782,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -818,7 +818,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -839,7 +839,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -886,7 +886,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -937,7 +937,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -970,7 +970,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -997,10 +997,10 @@ "code": 8 } }, - "stateExpect": { + "expectedState": { "activeTargets": {} }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1033,7 +1033,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1060,10 +1060,10 @@ "code": 8 } }, - "stateExpect": { + "expectedState": { "activeTargets": {} }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1085,7 +1085,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1124,7 +1124,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1157,7 +1157,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1179,7 +1179,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1259,7 +1259,7 @@ "code": 8 } }, - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -1271,7 +1271,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection1", @@ -1297,7 +1297,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection2", @@ -1343,7 +1343,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1394,7 +1394,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1445,7 +1445,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1480,7 +1480,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1500,7 +1500,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1512,7 +1512,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1602,7 +1602,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1637,7 +1637,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1688,7 +1688,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1739,7 +1739,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1767,7 +1767,7 @@ }, { "restart": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] } @@ -1781,7 +1781,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1793,7 +1793,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1883,7 +1883,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1922,7 +1922,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1980,7 +1980,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2028,7 +2028,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2048,7 +2048,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -2060,7 +2060,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2126,7 +2126,7 @@ "version": 4000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2162,7 +2162,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2188,7 +2188,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2246,7 +2246,7 @@ "version": 5000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2280,7 +2280,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2300,7 +2300,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -2312,7 +2312,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2365,7 +2365,7 @@ "version": 6000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2404,7 +2404,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2462,7 +2462,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2510,7 +2510,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2530,7 +2530,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -2542,7 +2542,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2601,7 +2601,7 @@ "version": 4000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2637,7 +2637,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2663,7 +2663,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2721,7 +2721,7 @@ "version": 5000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2755,7 +2755,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2775,7 +2775,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -2814,7 +2814,7 @@ "version": 6000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2847,7 +2847,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2898,7 +2898,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2926,7 +2926,7 @@ }, { "userDelete": "collection/a", - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2982,7 +2982,7 @@ "writeAck": { "version": 4000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -3042,7 +3042,7 @@ "version": 5000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3070,12 +3070,12 @@ } ] }, - "Deleted documents in cache are fixed": { + "Listens are reestablished after network disconnect": { "describeName": "Listens:", - "itName": "Deleted documents in cache are fixed", + "itName": "Listens are reestablished after network disconnect", "tags": [], "config": { - "useGarbageCollection": false, + "useGarbageCollection": true, "numClients": 1 }, "steps": [ @@ -3084,33 +3084,22 @@ 2, { "path": "collection", - "filters": [ - [ - "key", - "==", - "a" - ] - ], + "filters": [], "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { "path": "collection", - "filters": [ - [ - "key", - "==", - "a" - ] - ], + "filters": [], "orderBys": [] }, "resumeToken": "" } - } + }, + "watchStreamRequestCount": 1 } }, { @@ -3151,17 +3140,11 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", - "filters": [ - [ - "key", - "==", - "a" - ] - ], + "filters": [], "orderBys": [] }, "added": [ @@ -3184,126 +3167,53 @@ ] }, { - "watchEntity": { - "docs": [ - { - "key": "collection/a", - "version": 2000, - "value": null - } - ], - "removedTargets": [ - 2 - ] - } - }, - { - "watchSnapshot": { - "version": 2000, - "targetIds": [ - 2 - ], - "resumeToken": "resume-token-2000" - } - }, - { - "watchSnapshot": { - "version": 2000, - "targetIds": [] + "enableNetwork": false, + "expectedState": { + "activeTargets": {}, + "limboDocs": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", - "filters": [ - [ - "key", - "==", - "a" - ] - ], + "filters": [], "orderBys": [] }, - "removed": [ - { - "key": "collection/a", - "version": 1000, - "value": { - "key": "a" - }, - "options": { - "hasLocalMutations": false, - "hasCommittedMutations": false - } - } - ], "errorCode": 0, - "fromCache": false, + "fromCache": true, "hasPendingWrites": false } ] }, { - "userUnlisten": [ - 2, - { - "path": "collection", - "filters": [ - [ - "key", - "==", - "a" - ] - ], - "orderBys": [] - } - ], - "stateExpect": { - "activeTargets": {} - } - }, - { - "watchRemove": { - "targetIds": [ - 2 - ] - } - }, - { - "userListen": [ - 4, - { - "path": "collection", - "filters": [], - "orderBys": [] - } - ], - "stateExpect": { + "enableNetwork": true, + "expectedState": { "activeTargets": { - "4": { + "2": { "query": { "path": "collection", "filters": [], "orderBys": [] }, - "resumeToken": "" + "resumeToken": "resume-token-1000" } - } + }, + "watchStreamRequestCount": 2 } }, { "watchAck": [ - 4 + 2 ] }, { "watchEntity": { "docs": [ { - "key": "collection/a", - "version": 1000, + "key": "collection/b", + "version": 2000, "value": { - "key": "a" + "key": "b" }, "options": { "hasLocalMutations": false, @@ -3312,24 +3222,24 @@ } ], "targets": [ - 4 + 2 ] } }, { "watchCurrent": [ [ - 4 + 2 ], - "resume-token-3000" + "resume-token-2000" ] }, { "watchSnapshot": { - "version": 3000, + "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3338,10 +3248,10 @@ }, "added": [ { - "key": "collection/a", - "version": 1000, + "key": "collection/b", + "version": 2000, "value": { - "key": "a" + "key": "b" }, "options": { "hasLocalMutations": false, @@ -3357,12 +3267,12 @@ } ] }, - "Listens are reestablished after network disconnect": { + "Synthesizes deletes for missing document": { "describeName": "Listens:", - "itName": "Listens are reestablished after network disconnect", + "itName": "Synthesizes deletes for missing document", "tags": [], "config": { - "useGarbageCollection": true, + "useGarbageCollection": false, "numClients": 1 }, "steps": [ @@ -3375,7 +3285,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3385,8 +3295,7 @@ }, "resumeToken": "" } - }, - "watchStreamRequestCount": 1 + } } }, { @@ -3407,6 +3316,17 @@ "hasLocalMutations": false, "hasCommittedMutations": false } + }, + { + "key": "collection/b", + "version": 1000, + "value": { + "key": "a" + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } } ], "targets": [ @@ -3427,7 +3347,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3445,6 +3365,17 @@ "hasLocalMutations": false, "hasCommittedMutations": false } + }, + { + "key": "collection/b", + "version": 1000, + "value": { + "key": "a" + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } } ], "errorCode": 0, @@ -3454,27 +3385,35 @@ ] }, { - "enableNetwork": false, - "stateExpect": { - "activeTargets": {}, - "limboDocs": [] - }, - "expect": [ + "userUnlisten": [ + 2, { - "query": { - "path": "collection", - "filters": [], - "orderBys": [] - }, - "errorCode": 0, - "fromCache": true, - "hasPendingWrites": false + "path": "collection", + "filters": [], + "orderBys": [] } - ] + ], + "expectedState": { + "activeTargets": {} + } }, { - "enableNetwork": true, - "stateExpect": { + "watchRemove": { + "targetIds": [ + 2 + ] + } + }, + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "expectedState": { "activeTargets": { "2": { "query": { @@ -3484,49 +3423,9 @@ }, "resumeToken": "resume-token-1000" } - }, - "watchStreamRequestCount": 2 - } - }, - { - "watchAck": [ - 2 - ] - }, - { - "watchEntity": { - "docs": [ - { - "key": "collection/b", - "version": 2000, - "value": { - "key": "b" - }, - "options": { - "hasLocalMutations": false, - "hasCommittedMutations": false - } - } - ], - "targets": [ - 2 - ] - } - }, - { - "watchCurrent": [ - [ - 2 - ], - "resume-token-2000" - ] - }, - { - "watchSnapshot": { - "version": 2000, - "targetIds": [] + } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3535,196 +3434,10 @@ }, "added": [ { - "key": "collection/b", - "version": 2000, + "key": "collection/a", + "version": 1000, "value": { - "key": "b" - }, - "options": { - "hasLocalMutations": false, - "hasCommittedMutations": false - } - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false - } - ] - } - ] - }, - "Synthesizes deletes for missing document": { - "describeName": "Listens:", - "itName": "Synthesizes deletes for missing document", - "tags": [], - "config": { - "useGarbageCollection": false, - "numClients": 1 - }, - "steps": [ - { - "userListen": [ - 2, - { - "path": "collection", - "filters": [], - "orderBys": [] - } - ], - "stateExpect": { - "activeTargets": { - "2": { - "query": { - "path": "collection", - "filters": [], - "orderBys": [] - }, - "resumeToken": "" - } - } - } - }, - { - "watchAck": [ - 2 - ] - }, - { - "watchEntity": { - "docs": [ - { - "key": "collection/a", - "version": 1000, - "value": { - "key": "a" - }, - "options": { - "hasLocalMutations": false, - "hasCommittedMutations": false - } - }, - { - "key": "collection/b", - "version": 1000, - "value": { - "key": "a" - }, - "options": { - "hasLocalMutations": false, - "hasCommittedMutations": false - } - } - ], - "targets": [ - 2 - ] - } - }, - { - "watchCurrent": [ - [ - 2 - ], - "resume-token-1000" - ] - }, - { - "watchSnapshot": { - "version": 1000, - "targetIds": [] - }, - "expect": [ - { - "query": { - "path": "collection", - "filters": [], - "orderBys": [] - }, - "added": [ - { - "key": "collection/a", - "version": 1000, - "value": { - "key": "a" - }, - "options": { - "hasLocalMutations": false, - "hasCommittedMutations": false - } - }, - { - "key": "collection/b", - "version": 1000, - "value": { - "key": "a" - }, - "options": { - "hasLocalMutations": false, - "hasCommittedMutations": false - } - } - ], - "errorCode": 0, - "fromCache": false, - "hasPendingWrites": false - } - ] - }, - { - "userUnlisten": [ - 2, - { - "path": "collection", - "filters": [], - "orderBys": [] - } - ], - "stateExpect": { - "activeTargets": {} - } - }, - { - "watchRemove": { - "targetIds": [ - 2 - ] - } - }, - { - "userListen": [ - 2, - { - "path": "collection", - "filters": [], - "orderBys": [] - } - ], - "stateExpect": { - "activeTargets": { - "2": { - "query": { - "path": "collection", - "filters": [], - "orderBys": [] - }, - "resumeToken": "resume-token-1000" - } - } - }, - "expect": [ - { - "query": { - "path": "collection", - "filters": [], - "orderBys": [] - }, - "added": [ - { - "key": "collection/a", - "version": 1000, - "value": { - "key": "a" + "key": "a" }, "options": { "hasLocalMutations": false, @@ -3758,7 +3471,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -3771,7 +3484,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -3783,7 +3496,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/a", @@ -3827,7 +3540,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/a", @@ -3862,7 +3575,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -3882,7 +3595,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3894,7 +3607,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3940,7 +3653,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3991,7 +3704,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4026,7 +3739,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -4046,7 +3759,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4058,7 +3771,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4116,7 +3829,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4162,7 +3875,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4213,7 +3926,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4248,7 +3961,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -4294,7 +4007,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4306,7 +4019,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4352,7 +4065,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4395,7 +4108,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/a", @@ -4434,7 +4147,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/a", @@ -4456,7 +4169,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -4469,7 +4182,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4481,7 +4194,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/a", @@ -4527,7 +4240,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4549,10 +4262,10 @@ "code": 8 } }, - "stateExpect": { + "expectedState": { "activeTargets": {} }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4574,7 +4287,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4613,7 +4326,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4646,7 +4359,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4685,7 +4398,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4732,7 +4445,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4767,7 +4480,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -4787,7 +4500,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4799,7 +4512,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4851,7 +4564,7 @@ "version": 3000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4890,7 +4603,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4935,7 +4648,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4993,7 +4706,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5045,7 +4758,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -5071,7 +4784,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5089,7 +4802,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5152,7 +4865,7 @@ "version": 3000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5191,7 +4904,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5242,7 +4955,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5284,7 +4997,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -5304,7 +5017,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5316,7 +5029,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5360,7 +5073,7 @@ "version": 3000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5395,7 +5108,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5446,7 +5159,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5481,7 +5194,7 @@ }, { "restart": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] } @@ -5495,7 +5208,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5507,7 +5220,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5551,7 +5264,7 @@ "version": 3000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5586,7 +5299,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5637,7 +5350,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5672,7 +5385,7 @@ }, { "restart": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] } @@ -5686,7 +5399,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5698,7 +5411,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5742,7 +5455,7 @@ "version": 300002000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5791,7 +5504,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5807,7 +5520,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5857,7 +5570,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5906,7 +5619,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5952,7 +5665,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6007,7 +5720,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6047,7 +5760,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6059,7 +5772,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6099,7 +5812,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6111,7 +5824,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6168,7 +5881,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6197,7 +5910,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6226,7 +5939,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6268,7 +5981,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -6286,7 +5999,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6302,7 +6015,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6361,7 +6074,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6429,7 +6142,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6441,7 +6154,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6505,7 +6218,7 @@ "version": 3000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6534,7 +6247,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6624,7 +6337,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6651,7 +6364,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6667,7 +6380,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6730,7 +6443,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6759,7 +6472,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6818,7 +6531,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6873,7 +6586,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6913,7 +6626,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6925,7 +6638,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6965,7 +6678,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7009,7 +6722,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7045,14 +6758,14 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} }, "clientIndex": 1 }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": {} }, "clientIndex": 0 @@ -7093,7 +6806,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7109,7 +6822,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7168,7 +6881,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7204,14 +6917,14 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} }, "clientIndex": 1 }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": {} }, "clientIndex": 0 @@ -7237,7 +6950,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7249,7 +6962,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7278,7 +6991,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7337,7 +7050,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7400,7 +7113,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7416,7 +7129,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7439,14 +7152,14 @@ "code": 8 } }, - "stateExpect": { + "expectedState": { "activeTargets": {} }, "clientIndex": 0 }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7496,7 +7209,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7512,7 +7225,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7535,14 +7248,14 @@ "code": 8 } }, - "stateExpect": { + "expectedState": { "activeTargets": {} }, "clientIndex": 0 }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7565,7 +7278,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7581,7 +7294,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7628,7 +7341,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7678,7 +7391,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7694,7 +7407,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7741,7 +7454,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7761,7 +7474,7 @@ }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -7769,7 +7482,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7789,7 +7502,7 @@ }, { "enableNetwork": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7836,7 +7549,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7876,7 +7589,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7919,7 +7632,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7939,7 +7652,7 @@ }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -7954,7 +7667,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7966,7 +7679,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7986,11 +7699,11 @@ }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8017,7 +7730,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8029,7 +7742,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8052,7 +7765,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8064,7 +7777,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8104,7 +7817,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8147,7 +7860,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8174,7 +7887,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8186,7 +7899,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8206,11 +7919,11 @@ }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8226,7 +7939,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8242,7 +7955,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8290,7 +8003,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8306,7 +8019,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8356,7 +8069,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8372,7 +8085,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8419,7 +8132,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8435,7 +8148,7 @@ }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -8475,7 +8188,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8508,7 +8221,7 @@ }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [], "isPrimary": true @@ -8517,7 +8230,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8529,7 +8242,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 1 @@ -8560,7 +8273,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8576,11 +8289,11 @@ }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8596,7 +8309,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -8615,7 +8328,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8644,7 +8357,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -8662,7 +8375,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8678,7 +8391,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8737,7 +8450,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8777,7 +8490,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8789,7 +8502,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8822,7 +8535,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -8834,7 +8547,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -8890,7 +8603,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8919,7 +8632,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8961,7 +8674,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -8975,7 +8688,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9030,7 +8743,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9074,7 +8787,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9086,7 +8799,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9119,7 +8832,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -9131,7 +8844,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -9191,7 +8904,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9233,7 +8946,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -9247,7 +8960,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9302,7 +9015,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9342,7 +9055,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9354,7 +9067,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9385,7 +9098,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -9441,7 +9154,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9502,7 +9215,7 @@ "version": 3000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9535,10 +9248,10 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": false }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9591,7 +9304,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 1 @@ -9609,7 +9322,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9625,7 +9338,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9684,7 +9397,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9719,7 +9432,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -9740,7 +9453,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 1 @@ -9794,7 +9507,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9829,7 +9542,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -9889,7 +9602,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9931,7 +9644,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -9949,7 +9662,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9965,7 +9678,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9987,7 +9700,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 1 @@ -10041,7 +9754,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10096,7 +9809,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10138,7 +9851,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -10156,7 +9869,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10172,7 +9885,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10231,7 +9944,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10264,7 +9977,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -10272,7 +9985,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -10332,7 +10045,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10385,7 +10098,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10428,7 +10141,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10450,7 +10163,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -10510,7 +10223,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -10518,14 +10231,14 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -10538,7 +10251,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10566,5 +10279,814 @@ "clientIndex": 0 } ] + }, + "onSnapshotsInSync should not fire for doc changes if there are no listeners": { + "describeName": "Listens:", + "itName": "onSnapshotsInSync should not fire for doc changes if there are no listeners", + "tags": [], + "config": { + "useGarbageCollection": true, + "numClients": 1 + }, + "steps": [ + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ] + } + ] + }, + "onSnapshotsInSync fires when called even if there are no local listeners": { + "describeName": "Listens:", + "itName": "onSnapshotsInSync fires when called even if there are no local listeners", + "tags": [], + "config": { + "useGarbageCollection": true, + "numClients": 1 + }, + "steps": [ + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + } + ] + }, + "onSnapshotsInSync fires for metadata changes": { + "describeName": "Listens:", + "itName": "onSnapshotsInSync fires for metadata changes", + "tags": [], + "config": { + "useGarbageCollection": true, + "numClients": 1 + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "expectedState": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ], + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 2 + }, + "options": { + "hasLocalMutations": true, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ], + "expectedSnapshotsInSyncEvents": 1 + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "version": 2000, + "value": { + "v": 2 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchSnapshot": { + "version": 2000, + "targetIds": [] + } + }, + { + "writeAck": { + "version": 2000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [] + } + }, + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + { + "key": "collection/a", + "version": 2000, + "value": { + "v": 2 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ], + "expectedSnapshotsInSyncEvents": 1 + } + ] + }, + "onSnapshotsInSync fires once for multiple event snapshots": { + "describeName": "Listens:", + "itName": "onSnapshotsInSync fires once for multiple event snapshots", + "tags": [], + "config": { + "useGarbageCollection": true, + "numClients": 1 + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "expectedState": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "userListen": [ + 4, + { + "path": "collection/a", + "filters": [], + "orderBys": [] + } + ], + "expectedState": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + }, + "4": { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + }, + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": true, + "hasPendingWrites": false + } + ] + }, + { + "watchAck": [ + 4 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 4 + ] + } + }, + { + "watchCurrent": [ + [ + 4 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ], + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 2 + }, + "options": { + "hasLocalMutations": true, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + }, + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "modified": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 2 + }, + "options": { + "hasLocalMutations": true, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ], + "expectedSnapshotsInSyncEvents": 1 + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "version": 2000, + "value": { + "v": 2 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 2, + 4 + ] + } + }, + { + "watchSnapshot": { + "version": 2000, + "targetIds": [] + } + }, + { + "writeAck": { + "version": 2000 + }, + "expectedState": { + "userCallbacks": { + "acknowledgedDocs": [ + "collection/a" + ], + "rejectedDocs": [] + } + }, + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "metadata": [ + { + "key": "collection/a", + "version": 2000, + "value": { + "v": 2 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + }, + { + "query": { + "path": "collection/a", + "filters": [], + "orderBys": [] + }, + "metadata": [ + { + "key": "collection/a", + "version": 2000, + "value": { + "v": 2 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ], + "expectedSnapshotsInSyncEvents": 1 + } + ] + }, + "onSnapshotsInSync fires for multiple listeners": { + "describeName": "Listens:", + "itName": "onSnapshotsInSync fires for multiple listeners", + "tags": [], + "config": { + "useGarbageCollection": true, + "numClients": 1 + }, + "steps": [ + { + "userListen": [ + 2, + { + "path": "collection", + "filters": [], + "orderBys": [] + } + ], + "expectedState": { + "activeTargets": { + "2": { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "resumeToken": "" + } + } + } + }, + { + "watchAck": [ + 2 + ] + }, + { + "watchEntity": { + "docs": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "targets": [ + 2 + ] + } + }, + { + "watchCurrent": [ + [ + 2 + ], + "resume-token-1000" + ] + }, + { + "watchSnapshot": { + "version": 1000, + "targetIds": [] + }, + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "added": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 1 + }, + "options": { + "hasLocalMutations": false, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": false + } + ] + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "userSet": [ + "collection/a", + { + "v": 2 + } + ], + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 2 + }, + "options": { + "hasLocalMutations": true, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ], + "expectedSnapshotsInSyncEvents": 1 + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "addSnapshotsInSyncListener": true, + "expectedSnapshotsInSyncEvents": 1 + }, + { + "userSet": [ + "collection/a", + { + "v": 3 + } + ], + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 3 + }, + "options": { + "hasLocalMutations": true, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ], + "expectedSnapshotsInSyncEvents": 3 + }, + { + "removeSnapshotsInSyncListener": true + }, + { + "userSet": [ + "collection/a", + { + "v": 4 + } + ], + "expectedSnapshotEvents": [ + { + "query": { + "path": "collection", + "filters": [], + "orderBys": [] + }, + "modified": [ + { + "key": "collection/a", + "version": 1000, + "value": { + "v": 4 + }, + "options": { + "hasLocalMutations": true, + "hasCommittedMutations": false + } + } + ], + "errorCode": 0, + "fromCache": false, + "hasPendingWrites": true + } + ], + "expectedSnapshotsInSyncEvents": 2 + } + ] } } diff --git a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json index 6dc71cfeada..83902862e76 100644 --- a/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/offline_spec_test.json @@ -17,7 +17,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -38,7 +38,7 @@ }, "runBackoffTimer": true }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -89,7 +89,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -124,7 +124,7 @@ }, "runBackoffTimer": true }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -178,7 +178,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -199,7 +199,7 @@ }, "runBackoffTimer": true }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -221,7 +221,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -243,7 +243,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -264,7 +264,7 @@ }, "runBackoffTimer": true }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -297,7 +297,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -348,7 +348,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -382,7 +382,7 @@ }, "runBackoffTimer": true }, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -403,7 +403,7 @@ }, "runBackoffTimer": true }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -442,7 +442,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -475,7 +475,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -526,7 +526,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -570,7 +570,7 @@ "version": 1001, "targetIds": [] }, - "stateExpect": { + "expectedState": { "limboDocs": [ "collection/a" ], @@ -593,7 +593,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -614,7 +614,7 @@ }, "runBackoffTimer": true }, - "stateExpect": { + "expectedState": { "activeTargets": { "1": { "query": { @@ -697,7 +697,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -722,7 +722,7 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "limboDocs": [], "activeTargets": { "2": { @@ -756,7 +756,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -771,7 +771,7 @@ }, { "runTimer": "online_state_timeout", - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -840,7 +840,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -877,7 +877,7 @@ }, "runBackoffTimer": true }, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -892,7 +892,7 @@ }, { "runTimer": "online_state_timeout", - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -925,7 +925,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -946,7 +946,7 @@ }, "runBackoffTimer": true }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -968,7 +968,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -988,7 +988,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection2", @@ -1021,7 +1021,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1036,7 +1036,7 @@ }, { "runTimer": "online_state_timeout", - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1058,7 +1058,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1078,7 +1078,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection2", @@ -1106,7 +1106,7 @@ "steps": [ { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] } @@ -1120,7 +1120,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1132,7 +1132,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1154,7 +1154,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1167,7 +1167,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -1179,7 +1179,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1201,7 +1201,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } } diff --git a/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json index 90246bb6b4f..2ca80ced550 100644 --- a/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/orderby_spec_test.json @@ -47,7 +47,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -64,7 +64,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -134,7 +134,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -191,7 +191,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -260,7 +260,7 @@ "version": 1002, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -318,7 +318,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -343,7 +343,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -360,7 +360,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -430,7 +430,7 @@ "version": 1002, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", diff --git a/Firestore/Example/Tests/SpecTests/json/perf_spec_test.json b/Firestore/Example/Tests/SpecTests/json/perf_spec_test.json index e821e3c7145..2c770fc9f00 100644 --- a/Firestore/Example/Tests/SpecTests/json/perf_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/perf_spec_test.json @@ -22,7 +22,7 @@ "writeAck": { "version": 0 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -43,7 +43,7 @@ "writeAck": { "version": 1 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -64,7 +64,7 @@ "writeAck": { "version": 2 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -85,7 +85,7 @@ "writeAck": { "version": 3 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -106,7 +106,7 @@ "writeAck": { "version": 4 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -127,7 +127,7 @@ "writeAck": { "version": 5 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -148,7 +148,7 @@ "writeAck": { "version": 6 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -169,7 +169,7 @@ "writeAck": { "version": 7 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -190,7 +190,7 @@ "writeAck": { "version": 8 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -211,7 +211,7 @@ "writeAck": { "version": 9 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/{i}" @@ -242,7 +242,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -262,7 +262,7 @@ "doc": 0 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/0", @@ -292,7 +292,7 @@ "writeAck": { "version": 2 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/0" @@ -339,7 +339,7 @@ "version": 3, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/0", @@ -374,7 +374,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -394,7 +394,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -414,7 +414,7 @@ "doc": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/1", @@ -444,7 +444,7 @@ "writeAck": { "version": 4 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/1" @@ -491,7 +491,7 @@ "version": 5, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/1", @@ -526,7 +526,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -546,7 +546,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -566,7 +566,7 @@ "doc": 2 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/2", @@ -596,7 +596,7 @@ "writeAck": { "version": 6 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/2" @@ -643,7 +643,7 @@ "version": 7, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/2", @@ -678,7 +678,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -698,7 +698,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -718,7 +718,7 @@ "doc": 3 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/3", @@ -748,7 +748,7 @@ "writeAck": { "version": 8 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/3" @@ -795,7 +795,7 @@ "version": 9, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/3", @@ -830,7 +830,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -850,7 +850,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -870,7 +870,7 @@ "doc": 4 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/4", @@ -900,7 +900,7 @@ "writeAck": { "version": 10 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/4" @@ -947,7 +947,7 @@ "version": 11, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/4", @@ -982,7 +982,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1002,7 +1002,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -1022,7 +1022,7 @@ "doc": 5 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/5", @@ -1052,7 +1052,7 @@ "writeAck": { "version": 12 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/5" @@ -1099,7 +1099,7 @@ "version": 13, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/5", @@ -1134,7 +1134,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1154,7 +1154,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -1174,7 +1174,7 @@ "doc": 6 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/6", @@ -1204,7 +1204,7 @@ "writeAck": { "version": 14 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/6" @@ -1251,7 +1251,7 @@ "version": 15, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/6", @@ -1286,7 +1286,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1306,7 +1306,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -1326,7 +1326,7 @@ "doc": 7 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/7", @@ -1356,7 +1356,7 @@ "writeAck": { "version": 16 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/7" @@ -1403,7 +1403,7 @@ "version": 17, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/7", @@ -1438,7 +1438,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1458,7 +1458,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -1478,7 +1478,7 @@ "doc": 8 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/8", @@ -1508,7 +1508,7 @@ "writeAck": { "version": 18 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/8" @@ -1555,7 +1555,7 @@ "version": 19, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/8", @@ -1590,7 +1590,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -1610,7 +1610,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -1630,7 +1630,7 @@ "doc": 9 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/9", @@ -1660,7 +1660,7 @@ "writeAck": { "version": 20 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/9" @@ -1707,7 +1707,7 @@ "version": 21, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/9", @@ -1742,7 +1742,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -2580,7 +2580,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -2597,7 +2597,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3731,7 +3731,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -3749,7 +3749,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3766,7 +3766,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4900,7 +4900,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -4918,7 +4918,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4935,7 +4935,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -6069,7 +6069,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -6087,7 +6087,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6104,7 +6104,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7238,7 +7238,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -7256,7 +7256,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7273,7 +7273,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8407,7 +8407,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -8425,7 +8425,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8442,7 +8442,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9576,7 +9576,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -9594,7 +9594,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9611,7 +9611,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10745,7 +10745,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -10763,7 +10763,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10780,7 +10780,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -11914,7 +11914,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -11932,7 +11932,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -11949,7 +11949,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -13083,7 +13083,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -13101,7 +13101,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -13118,7 +13118,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -14252,7 +14252,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } } @@ -14289,7 +14289,7 @@ "writeAck": { "version": 1 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14310,7 +14310,7 @@ "writeAck": { "version": 2 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14331,7 +14331,7 @@ "writeAck": { "version": 3 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14352,7 +14352,7 @@ "writeAck": { "version": 4 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14373,7 +14373,7 @@ "writeAck": { "version": 5 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14394,7 +14394,7 @@ "writeAck": { "version": 6 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14415,7 +14415,7 @@ "writeAck": { "version": 7 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14436,7 +14436,7 @@ "writeAck": { "version": 8 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14457,7 +14457,7 @@ "writeAck": { "version": 9 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14478,7 +14478,7 @@ "writeAck": { "version": 10 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14509,7 +14509,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -14529,7 +14529,7 @@ "v": 0 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -14559,7 +14559,7 @@ "writeAck": { "version": 2 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14606,7 +14606,7 @@ "version": 3, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -14639,7 +14639,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -14669,7 +14669,7 @@ "writeAck": { "version": 4 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14703,7 +14703,7 @@ "version": 5, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -14736,7 +14736,7 @@ "v": 2 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -14766,7 +14766,7 @@ "writeAck": { "version": 6 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14800,7 +14800,7 @@ "version": 7, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -14833,7 +14833,7 @@ "v": 3 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -14863,7 +14863,7 @@ "writeAck": { "version": 8 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14897,7 +14897,7 @@ "version": 9, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -14930,7 +14930,7 @@ "v": 4 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -14960,7 +14960,7 @@ "writeAck": { "version": 10 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -14994,7 +14994,7 @@ "version": 11, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15027,7 +15027,7 @@ "v": 5 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15057,7 +15057,7 @@ "writeAck": { "version": 12 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -15091,7 +15091,7 @@ "version": 13, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15124,7 +15124,7 @@ "v": 6 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15154,7 +15154,7 @@ "writeAck": { "version": 14 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -15188,7 +15188,7 @@ "version": 15, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15221,7 +15221,7 @@ "v": 7 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15251,7 +15251,7 @@ "writeAck": { "version": 16 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -15285,7 +15285,7 @@ "version": 17, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15318,7 +15318,7 @@ "v": 8 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15348,7 +15348,7 @@ "writeAck": { "version": 18 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -15382,7 +15382,7 @@ "version": 19, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15415,7 +15415,7 @@ "v": 9 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15445,7 +15445,7 @@ "writeAck": { "version": 20 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -15479,7 +15479,7 @@ "version": 21, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15512,7 +15512,7 @@ "v": 10 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15542,7 +15542,7 @@ "writeAck": { "version": 22 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -15576,7 +15576,7 @@ "version": 23, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -15629,7 +15629,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -15673,7 +15673,7 @@ "version": 1, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -16805,7 +16805,7 @@ "version": 102, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -19039,7 +19039,7 @@ "version": 203, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -21273,7 +21273,7 @@ "version": 304, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -23507,7 +23507,7 @@ "version": 405, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -25741,7 +25741,7 @@ "version": 506, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -27975,7 +27975,7 @@ "version": 607, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -30209,7 +30209,7 @@ "version": 708, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -32443,7 +32443,7 @@ "version": 809, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -34677,7 +34677,7 @@ "version": 910, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -36911,7 +36911,7 @@ "version": 1011, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -38058,7 +38058,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -39203,7 +39203,7 @@ "version": 102, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/1/coll", @@ -40337,7 +40337,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -40362,7 +40362,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -40379,7 +40379,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/1/coll", @@ -41525,7 +41525,7 @@ "version": 103, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/1/coll", @@ -41557,7 +41557,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -41582,7 +41582,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -42727,7 +42727,7 @@ "version": 204, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/2/coll", @@ -43861,7 +43861,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -43886,7 +43886,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -43903,7 +43903,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/2/coll", @@ -45049,7 +45049,7 @@ "version": 205, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/2/coll", @@ -45081,7 +45081,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -45106,7 +45106,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -46251,7 +46251,7 @@ "version": 306, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/3/coll", @@ -47385,7 +47385,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -47410,7 +47410,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -47427,7 +47427,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/3/coll", @@ -48573,7 +48573,7 @@ "version": 307, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/3/coll", @@ -48605,7 +48605,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -48630,7 +48630,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -49775,7 +49775,7 @@ "version": 408, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/4/coll", @@ -50909,7 +50909,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -50934,7 +50934,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -50951,7 +50951,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/4/coll", @@ -52097,7 +52097,7 @@ "version": 409, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/4/coll", @@ -52129,7 +52129,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -52154,7 +52154,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -53299,7 +53299,7 @@ "version": 510, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/5/coll", @@ -54433,7 +54433,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -54458,7 +54458,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -54475,7 +54475,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/5/coll", @@ -55621,7 +55621,7 @@ "version": 511, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/5/coll", @@ -55653,7 +55653,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -55678,7 +55678,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -56823,7 +56823,7 @@ "version": 612, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/6/coll", @@ -57957,7 +57957,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -57982,7 +57982,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -57999,7 +57999,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/6/coll", @@ -59145,7 +59145,7 @@ "version": 613, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/6/coll", @@ -59177,7 +59177,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -59202,7 +59202,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -60347,7 +60347,7 @@ "version": 714, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/7/coll", @@ -61481,7 +61481,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -61506,7 +61506,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -61523,7 +61523,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/7/coll", @@ -62669,7 +62669,7 @@ "version": 715, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/7/coll", @@ -62701,7 +62701,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -62726,7 +62726,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -63871,7 +63871,7 @@ "version": 816, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/8/coll", @@ -65005,7 +65005,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -65030,7 +65030,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -65047,7 +65047,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/8/coll", @@ -66193,7 +66193,7 @@ "version": 817, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/8/coll", @@ -66225,7 +66225,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -66250,7 +66250,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -67395,7 +67395,7 @@ "version": 918, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/9/coll", @@ -68529,7 +68529,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -68554,7 +68554,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -68571,7 +68571,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/9/coll", @@ -69717,7 +69717,7 @@ "version": 919, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/9/coll", @@ -69749,7 +69749,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -69774,7 +69774,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -70919,7 +70919,7 @@ "version": 1020, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/10/coll", @@ -72053,7 +72053,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -72078,7 +72078,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -72095,7 +72095,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/10/coll", @@ -73241,7 +73241,7 @@ "version": 1021, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/10/coll", @@ -73273,7 +73273,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -73312,7 +73312,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -73351,7 +73351,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -73404,7 +73404,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -73471,7 +73471,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -73552,7 +73552,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -73647,7 +73647,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -73756,7 +73756,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -73879,7 +73879,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -74016,7 +74016,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -74167,7 +74167,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -74332,7 +74332,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -74511,7 +74511,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -74704,7 +74704,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -74911,7 +74911,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -75132,7 +75132,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -75367,7 +75367,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -75616,7 +75616,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -75879,7 +75879,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -76156,7 +76156,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -76447,7 +76447,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -76752,7 +76752,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -77071,7 +77071,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -77404,7 +77404,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -77751,7 +77751,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -78112,7 +78112,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -78521,7 +78521,7 @@ "version": 3, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/1/coll", @@ -79264,7 +79264,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -79627,7 +79627,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -79976,7 +79976,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -80311,7 +80311,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -80632,7 +80632,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -80939,7 +80939,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -81232,7 +81232,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -81511,7 +81511,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -81776,7 +81776,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -82027,7 +82027,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -82264,7 +82264,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "24": { "query": { @@ -82487,7 +82487,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "26": { "query": { @@ -82696,7 +82696,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "28": { "query": { @@ -82891,7 +82891,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "30": { "query": { @@ -83072,7 +83072,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "32": { "query": { @@ -83239,7 +83239,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "34": { "query": { @@ -83392,7 +83392,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "36": { "query": { @@ -83531,7 +83531,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "38": { "query": { @@ -83656,7 +83656,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "40": { "query": { @@ -83767,7 +83767,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "42": { "query": { @@ -83864,7 +83864,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "44": { "query": { @@ -83947,7 +83947,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "46": { "query": { @@ -84016,7 +84016,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "48": { "query": { @@ -84071,7 +84071,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "50": { "query": { @@ -84112,7 +84112,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -84138,7 +84138,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -84177,7 +84177,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -84230,7 +84230,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -84297,7 +84297,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -84378,7 +84378,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -84473,7 +84473,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -84582,7 +84582,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -84705,7 +84705,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -84842,7 +84842,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -84993,7 +84993,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -85158,7 +85158,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -85337,7 +85337,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -85530,7 +85530,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -85737,7 +85737,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -85958,7 +85958,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -86193,7 +86193,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -86442,7 +86442,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -86705,7 +86705,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -86982,7 +86982,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -87273,7 +87273,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -87578,7 +87578,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -87897,7 +87897,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -88230,7 +88230,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -88577,7 +88577,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -88938,7 +88938,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "52": { "query": { @@ -89347,7 +89347,7 @@ "version": 5, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/2/coll", @@ -90090,7 +90090,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "54": { "query": { @@ -90453,7 +90453,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "56": { "query": { @@ -90802,7 +90802,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "58": { "query": { @@ -91137,7 +91137,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "60": { "query": { @@ -91458,7 +91458,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "62": { "query": { @@ -91765,7 +91765,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "64": { "query": { @@ -92058,7 +92058,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "66": { "query": { @@ -92337,7 +92337,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "68": { "query": { @@ -92602,7 +92602,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "70": { "query": { @@ -92853,7 +92853,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "72": { "query": { @@ -93090,7 +93090,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "74": { "query": { @@ -93313,7 +93313,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "76": { "query": { @@ -93522,7 +93522,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "78": { "query": { @@ -93717,7 +93717,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "80": { "query": { @@ -93898,7 +93898,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "82": { "query": { @@ -94065,7 +94065,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "84": { "query": { @@ -94218,7 +94218,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "86": { "query": { @@ -94357,7 +94357,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "88": { "query": { @@ -94482,7 +94482,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "90": { "query": { @@ -94593,7 +94593,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "92": { "query": { @@ -94690,7 +94690,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "94": { "query": { @@ -94773,7 +94773,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "96": { "query": { @@ -94842,7 +94842,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "98": { "query": { @@ -94897,7 +94897,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "100": { "query": { @@ -94938,7 +94938,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -94964,7 +94964,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95003,7 +95003,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95056,7 +95056,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95123,7 +95123,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95204,7 +95204,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95299,7 +95299,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95408,7 +95408,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95531,7 +95531,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95668,7 +95668,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95819,7 +95819,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -95984,7 +95984,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -96163,7 +96163,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -96356,7 +96356,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -96563,7 +96563,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -96784,7 +96784,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -97019,7 +97019,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -97268,7 +97268,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -97531,7 +97531,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -97808,7 +97808,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -98099,7 +98099,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -98404,7 +98404,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -98723,7 +98723,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -99056,7 +99056,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -99403,7 +99403,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -99764,7 +99764,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "102": { "query": { @@ -100173,7 +100173,7 @@ "version": 7, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/3/coll", @@ -100916,7 +100916,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "104": { "query": { @@ -101279,7 +101279,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "106": { "query": { @@ -101628,7 +101628,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "108": { "query": { @@ -101963,7 +101963,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "110": { "query": { @@ -102284,7 +102284,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "112": { "query": { @@ -102591,7 +102591,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "114": { "query": { @@ -102884,7 +102884,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "116": { "query": { @@ -103163,7 +103163,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "118": { "query": { @@ -103428,7 +103428,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "120": { "query": { @@ -103679,7 +103679,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "122": { "query": { @@ -103916,7 +103916,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "124": { "query": { @@ -104139,7 +104139,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "126": { "query": { @@ -104348,7 +104348,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "128": { "query": { @@ -104543,7 +104543,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "130": { "query": { @@ -104724,7 +104724,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "132": { "query": { @@ -104891,7 +104891,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "134": { "query": { @@ -105044,7 +105044,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "136": { "query": { @@ -105183,7 +105183,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "138": { "query": { @@ -105308,7 +105308,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "140": { "query": { @@ -105419,7 +105419,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "142": { "query": { @@ -105516,7 +105516,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "144": { "query": { @@ -105599,7 +105599,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "146": { "query": { @@ -105668,7 +105668,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "148": { "query": { @@ -105723,7 +105723,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "150": { "query": { @@ -105764,7 +105764,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -105790,7 +105790,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -105829,7 +105829,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -105882,7 +105882,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -105949,7 +105949,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -106030,7 +106030,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -106125,7 +106125,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -106234,7 +106234,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -106357,7 +106357,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -106494,7 +106494,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -106645,7 +106645,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -106810,7 +106810,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -106989,7 +106989,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -107182,7 +107182,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -107389,7 +107389,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -107610,7 +107610,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -107845,7 +107845,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -108094,7 +108094,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -108357,7 +108357,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -108634,7 +108634,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -108925,7 +108925,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -109230,7 +109230,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -109549,7 +109549,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -109882,7 +109882,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -110229,7 +110229,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -110590,7 +110590,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "152": { "query": { @@ -110999,7 +110999,7 @@ "version": 9, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/4/coll", @@ -111742,7 +111742,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "154": { "query": { @@ -112105,7 +112105,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "156": { "query": { @@ -112454,7 +112454,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "158": { "query": { @@ -112789,7 +112789,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "160": { "query": { @@ -113110,7 +113110,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "162": { "query": { @@ -113417,7 +113417,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "164": { "query": { @@ -113710,7 +113710,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "166": { "query": { @@ -113989,7 +113989,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "168": { "query": { @@ -114254,7 +114254,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "170": { "query": { @@ -114505,7 +114505,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "172": { "query": { @@ -114742,7 +114742,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "174": { "query": { @@ -114965,7 +114965,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "176": { "query": { @@ -115174,7 +115174,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "178": { "query": { @@ -115369,7 +115369,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "180": { "query": { @@ -115550,7 +115550,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "182": { "query": { @@ -115717,7 +115717,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "184": { "query": { @@ -115870,7 +115870,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "186": { "query": { @@ -116009,7 +116009,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "188": { "query": { @@ -116134,7 +116134,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "190": { "query": { @@ -116245,7 +116245,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "192": { "query": { @@ -116342,7 +116342,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "194": { "query": { @@ -116425,7 +116425,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "196": { "query": { @@ -116494,7 +116494,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "198": { "query": { @@ -116549,7 +116549,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "200": { "query": { @@ -116590,7 +116590,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -116616,7 +116616,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -116655,7 +116655,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -116708,7 +116708,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -116775,7 +116775,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -116856,7 +116856,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -116951,7 +116951,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -117060,7 +117060,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -117183,7 +117183,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -117320,7 +117320,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -117471,7 +117471,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -117636,7 +117636,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -117815,7 +117815,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -118008,7 +118008,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -118215,7 +118215,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -118436,7 +118436,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -118671,7 +118671,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -118920,7 +118920,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -119183,7 +119183,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -119460,7 +119460,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -119751,7 +119751,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -120056,7 +120056,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -120375,7 +120375,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -120708,7 +120708,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -121055,7 +121055,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -121416,7 +121416,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "202": { "query": { @@ -121825,7 +121825,7 @@ "version": 11, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/5/coll", @@ -122568,7 +122568,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "204": { "query": { @@ -122931,7 +122931,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "206": { "query": { @@ -123280,7 +123280,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "208": { "query": { @@ -123615,7 +123615,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "210": { "query": { @@ -123936,7 +123936,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "212": { "query": { @@ -124243,7 +124243,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "214": { "query": { @@ -124536,7 +124536,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "216": { "query": { @@ -124815,7 +124815,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "218": { "query": { @@ -125080,7 +125080,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "220": { "query": { @@ -125331,7 +125331,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "222": { "query": { @@ -125568,7 +125568,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "224": { "query": { @@ -125791,7 +125791,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "226": { "query": { @@ -126000,7 +126000,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "228": { "query": { @@ -126195,7 +126195,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "230": { "query": { @@ -126376,7 +126376,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "232": { "query": { @@ -126543,7 +126543,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "234": { "query": { @@ -126696,7 +126696,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "236": { "query": { @@ -126835,7 +126835,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "238": { "query": { @@ -126960,7 +126960,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "240": { "query": { @@ -127071,7 +127071,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "242": { "query": { @@ -127168,7 +127168,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "244": { "query": { @@ -127251,7 +127251,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "246": { "query": { @@ -127320,7 +127320,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "248": { "query": { @@ -127375,7 +127375,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "250": { "query": { @@ -127416,7 +127416,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -127442,7 +127442,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -127481,7 +127481,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -127534,7 +127534,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -127601,7 +127601,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -127682,7 +127682,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -127777,7 +127777,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -127886,7 +127886,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -128009,7 +128009,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -128146,7 +128146,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -128297,7 +128297,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -128462,7 +128462,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -128641,7 +128641,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -128834,7 +128834,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -129041,7 +129041,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -129262,7 +129262,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -129497,7 +129497,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -129746,7 +129746,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -130009,7 +130009,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -130286,7 +130286,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -130577,7 +130577,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -130882,7 +130882,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -131201,7 +131201,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -131534,7 +131534,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -131881,7 +131881,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -132242,7 +132242,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "252": { "query": { @@ -132651,7 +132651,7 @@ "version": 13, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/6/coll", @@ -133394,7 +133394,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "254": { "query": { @@ -133757,7 +133757,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "256": { "query": { @@ -134106,7 +134106,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "258": { "query": { @@ -134441,7 +134441,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "260": { "query": { @@ -134762,7 +134762,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "262": { "query": { @@ -135069,7 +135069,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "264": { "query": { @@ -135362,7 +135362,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "266": { "query": { @@ -135641,7 +135641,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "268": { "query": { @@ -135906,7 +135906,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "270": { "query": { @@ -136157,7 +136157,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "272": { "query": { @@ -136394,7 +136394,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "274": { "query": { @@ -136617,7 +136617,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "276": { "query": { @@ -136826,7 +136826,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "278": { "query": { @@ -137021,7 +137021,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "280": { "query": { @@ -137202,7 +137202,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "282": { "query": { @@ -137369,7 +137369,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "284": { "query": { @@ -137522,7 +137522,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "286": { "query": { @@ -137661,7 +137661,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "288": { "query": { @@ -137786,7 +137786,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "290": { "query": { @@ -137897,7 +137897,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "292": { "query": { @@ -137994,7 +137994,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "294": { "query": { @@ -138077,7 +138077,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "296": { "query": { @@ -138146,7 +138146,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "298": { "query": { @@ -138201,7 +138201,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "300": { "query": { @@ -138242,7 +138242,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -138268,7 +138268,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -138307,7 +138307,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -138360,7 +138360,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -138427,7 +138427,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -138508,7 +138508,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -138603,7 +138603,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -138712,7 +138712,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -138835,7 +138835,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -138972,7 +138972,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -139123,7 +139123,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -139288,7 +139288,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -139467,7 +139467,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -139660,7 +139660,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -139867,7 +139867,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -140088,7 +140088,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -140323,7 +140323,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -140572,7 +140572,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -140835,7 +140835,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -141112,7 +141112,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -141403,7 +141403,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -141708,7 +141708,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -142027,7 +142027,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -142360,7 +142360,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -142707,7 +142707,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -143068,7 +143068,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "302": { "query": { @@ -143477,7 +143477,7 @@ "version": 15, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/7/coll", @@ -144220,7 +144220,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "304": { "query": { @@ -144583,7 +144583,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "306": { "query": { @@ -144932,7 +144932,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "308": { "query": { @@ -145267,7 +145267,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "310": { "query": { @@ -145588,7 +145588,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "312": { "query": { @@ -145895,7 +145895,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "314": { "query": { @@ -146188,7 +146188,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "316": { "query": { @@ -146467,7 +146467,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "318": { "query": { @@ -146732,7 +146732,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "320": { "query": { @@ -146983,7 +146983,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "322": { "query": { @@ -147220,7 +147220,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "324": { "query": { @@ -147443,7 +147443,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "326": { "query": { @@ -147652,7 +147652,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "328": { "query": { @@ -147847,7 +147847,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "330": { "query": { @@ -148028,7 +148028,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "332": { "query": { @@ -148195,7 +148195,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "334": { "query": { @@ -148348,7 +148348,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "336": { "query": { @@ -148487,7 +148487,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "338": { "query": { @@ -148612,7 +148612,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "340": { "query": { @@ -148723,7 +148723,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "342": { "query": { @@ -148820,7 +148820,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "344": { "query": { @@ -148903,7 +148903,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "346": { "query": { @@ -148972,7 +148972,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "348": { "query": { @@ -149027,7 +149027,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "350": { "query": { @@ -149068,7 +149068,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -149094,7 +149094,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -149133,7 +149133,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -149186,7 +149186,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -149253,7 +149253,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -149334,7 +149334,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -149429,7 +149429,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -149538,7 +149538,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -149661,7 +149661,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -149798,7 +149798,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -149949,7 +149949,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -150114,7 +150114,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -150293,7 +150293,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -150486,7 +150486,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -150693,7 +150693,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -150914,7 +150914,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -151149,7 +151149,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -151398,7 +151398,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -151661,7 +151661,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -151938,7 +151938,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -152229,7 +152229,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -152534,7 +152534,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -152853,7 +152853,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -153186,7 +153186,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -153533,7 +153533,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -153894,7 +153894,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "352": { "query": { @@ -154303,7 +154303,7 @@ "version": 17, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/8/coll", @@ -155046,7 +155046,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "354": { "query": { @@ -155409,7 +155409,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "356": { "query": { @@ -155758,7 +155758,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "358": { "query": { @@ -156093,7 +156093,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "360": { "query": { @@ -156414,7 +156414,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "362": { "query": { @@ -156721,7 +156721,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "364": { "query": { @@ -157014,7 +157014,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "366": { "query": { @@ -157293,7 +157293,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "368": { "query": { @@ -157558,7 +157558,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "370": { "query": { @@ -157809,7 +157809,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "372": { "query": { @@ -158046,7 +158046,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "374": { "query": { @@ -158269,7 +158269,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "376": { "query": { @@ -158478,7 +158478,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "378": { "query": { @@ -158673,7 +158673,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "380": { "query": { @@ -158854,7 +158854,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "382": { "query": { @@ -159021,7 +159021,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "384": { "query": { @@ -159174,7 +159174,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "386": { "query": { @@ -159313,7 +159313,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "388": { "query": { @@ -159438,7 +159438,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "390": { "query": { @@ -159549,7 +159549,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "392": { "query": { @@ -159646,7 +159646,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "394": { "query": { @@ -159729,7 +159729,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "396": { "query": { @@ -159798,7 +159798,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "398": { "query": { @@ -159853,7 +159853,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "400": { "query": { @@ -159894,7 +159894,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -159920,7 +159920,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -159959,7 +159959,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -160012,7 +160012,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -160079,7 +160079,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -160160,7 +160160,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -160255,7 +160255,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -160364,7 +160364,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -160487,7 +160487,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -160624,7 +160624,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -160775,7 +160775,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -160940,7 +160940,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -161119,7 +161119,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -161312,7 +161312,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -161519,7 +161519,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -161740,7 +161740,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -161975,7 +161975,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -162224,7 +162224,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -162487,7 +162487,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -162764,7 +162764,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -163055,7 +163055,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -163360,7 +163360,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -163679,7 +163679,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -164012,7 +164012,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -164359,7 +164359,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -164720,7 +164720,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "402": { "query": { @@ -165129,7 +165129,7 @@ "version": 19, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/9/coll", @@ -165872,7 +165872,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "404": { "query": { @@ -166235,7 +166235,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "406": { "query": { @@ -166584,7 +166584,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "408": { "query": { @@ -166919,7 +166919,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "410": { "query": { @@ -167240,7 +167240,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "412": { "query": { @@ -167547,7 +167547,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "414": { "query": { @@ -167840,7 +167840,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "416": { "query": { @@ -168119,7 +168119,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "418": { "query": { @@ -168384,7 +168384,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "420": { "query": { @@ -168635,7 +168635,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "422": { "query": { @@ -168872,7 +168872,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "424": { "query": { @@ -169095,7 +169095,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "426": { "query": { @@ -169304,7 +169304,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "428": { "query": { @@ -169499,7 +169499,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "430": { "query": { @@ -169680,7 +169680,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "432": { "query": { @@ -169847,7 +169847,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "434": { "query": { @@ -170000,7 +170000,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "436": { "query": { @@ -170139,7 +170139,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "438": { "query": { @@ -170264,7 +170264,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "440": { "query": { @@ -170375,7 +170375,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "442": { "query": { @@ -170472,7 +170472,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "444": { "query": { @@ -170555,7 +170555,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "446": { "query": { @@ -170624,7 +170624,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "448": { "query": { @@ -170679,7 +170679,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "450": { "query": { @@ -170720,7 +170720,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -170746,7 +170746,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -170785,7 +170785,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -170838,7 +170838,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -170905,7 +170905,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -170986,7 +170986,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -171081,7 +171081,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -171190,7 +171190,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -171313,7 +171313,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -171450,7 +171450,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -171601,7 +171601,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -171766,7 +171766,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -171945,7 +171945,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -172138,7 +172138,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -172345,7 +172345,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -172566,7 +172566,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -172801,7 +172801,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -173050,7 +173050,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -173313,7 +173313,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -173590,7 +173590,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -173881,7 +173881,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -174186,7 +174186,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -174505,7 +174505,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -174838,7 +174838,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -175185,7 +175185,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -175546,7 +175546,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "452": { "query": { @@ -175955,7 +175955,7 @@ "version": 21, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/10/coll", @@ -176698,7 +176698,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "454": { "query": { @@ -177061,7 +177061,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "456": { "query": { @@ -177410,7 +177410,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "458": { "query": { @@ -177745,7 +177745,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "460": { "query": { @@ -178066,7 +178066,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "462": { "query": { @@ -178373,7 +178373,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "464": { "query": { @@ -178666,7 +178666,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "466": { "query": { @@ -178945,7 +178945,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "468": { "query": { @@ -179210,7 +179210,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "470": { "query": { @@ -179461,7 +179461,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "472": { "query": { @@ -179698,7 +179698,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "474": { "query": { @@ -179921,7 +179921,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "476": { "query": { @@ -180130,7 +180130,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "478": { "query": { @@ -180325,7 +180325,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "480": { "query": { @@ -180506,7 +180506,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "482": { "query": { @@ -180673,7 +180673,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "484": { "query": { @@ -180826,7 +180826,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "486": { "query": { @@ -180965,7 +180965,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "488": { "query": { @@ -181090,7 +181090,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "490": { "query": { @@ -181201,7 +181201,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "492": { "query": { @@ -181298,7 +181298,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "494": { "query": { @@ -181381,7 +181381,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "496": { "query": { @@ -181450,7 +181450,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "498": { "query": { @@ -181505,7 +181505,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "500": { "query": { @@ -181546,7 +181546,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -181584,7 +181584,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -191620,7 +191620,7 @@ "version": 502, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -197154,7 +197154,7 @@ ] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -197185,7 +197185,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -197208,7 +197208,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -197364,7 +197364,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -197388,7 +197388,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -197411,7 +197411,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -197567,7 +197567,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -197591,7 +197591,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -197614,7 +197614,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -197770,7 +197770,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -197794,7 +197794,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -197817,7 +197817,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -197973,7 +197973,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -197997,7 +197997,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -198020,7 +198020,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -198176,7 +198176,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -198200,7 +198200,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -198223,7 +198223,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -198379,7 +198379,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -198403,7 +198403,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -198426,7 +198426,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -198582,7 +198582,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -198606,7 +198606,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -198629,7 +198629,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -198785,7 +198785,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -198809,7 +198809,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -198832,7 +198832,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -198988,7 +198988,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -199012,7 +199012,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -199035,7 +199035,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -199191,7 +199191,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -199215,7 +199215,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -199238,7 +199238,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -199394,7 +199394,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -199418,7 +199418,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -199441,7 +199441,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -199597,7 +199597,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -199621,7 +199621,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -199644,7 +199644,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -199800,7 +199800,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -199824,7 +199824,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -199847,7 +199847,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -200003,7 +200003,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -200027,7 +200027,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -200050,7 +200050,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -200206,7 +200206,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -200230,7 +200230,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -200253,7 +200253,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -200409,7 +200409,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -200433,7 +200433,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -200456,7 +200456,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -200612,7 +200612,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -200636,7 +200636,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -200659,7 +200659,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -200815,7 +200815,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -200839,7 +200839,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -200862,7 +200862,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -201018,7 +201018,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -201042,7 +201042,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -201065,7 +201065,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -201221,7 +201221,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -201245,7 +201245,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -201268,7 +201268,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -201424,7 +201424,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -201448,7 +201448,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -201471,7 +201471,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -201627,7 +201627,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -201651,7 +201651,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -201674,7 +201674,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -201830,7 +201830,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -201854,7 +201854,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -201877,7 +201877,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -202033,7 +202033,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -202057,7 +202057,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -202080,7 +202080,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -202236,7 +202236,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -202260,7 +202260,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -202283,7 +202283,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -202439,7 +202439,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -202463,7 +202463,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -202486,7 +202486,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -202642,7 +202642,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -202666,7 +202666,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -202689,7 +202689,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -202845,7 +202845,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -202869,7 +202869,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -202892,7 +202892,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -203048,7 +203048,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -203072,7 +203072,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -203095,7 +203095,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -203251,7 +203251,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -203275,7 +203275,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -203298,7 +203298,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -203454,7 +203454,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -203478,7 +203478,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -203501,7 +203501,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -203657,7 +203657,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -203681,7 +203681,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -203704,7 +203704,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -203860,7 +203860,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -203884,7 +203884,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -203907,7 +203907,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -204063,7 +204063,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -204087,7 +204087,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -204110,7 +204110,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -204266,7 +204266,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -204290,7 +204290,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -204313,7 +204313,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -204469,7 +204469,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -204493,7 +204493,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -204516,7 +204516,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -204672,7 +204672,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -204696,7 +204696,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -204719,7 +204719,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -204875,7 +204875,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -204899,7 +204899,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -204922,7 +204922,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -205078,7 +205078,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -205102,7 +205102,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -205125,7 +205125,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -205281,7 +205281,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -205305,7 +205305,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -205328,7 +205328,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -205484,7 +205484,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -205508,7 +205508,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -205531,7 +205531,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -205687,7 +205687,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -205711,7 +205711,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -205734,7 +205734,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -205890,7 +205890,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -205914,7 +205914,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -205937,7 +205937,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -206093,7 +206093,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -206117,7 +206117,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -206140,7 +206140,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -206296,7 +206296,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -206320,7 +206320,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -206343,7 +206343,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -206499,7 +206499,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -206523,7 +206523,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -206546,7 +206546,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -206702,7 +206702,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -206726,7 +206726,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -206749,7 +206749,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -206905,7 +206905,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -206929,7 +206929,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -206952,7 +206952,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -207108,7 +207108,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -207132,7 +207132,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -207155,7 +207155,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -207311,7 +207311,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -207335,7 +207335,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -207358,7 +207358,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -207514,7 +207514,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -207538,7 +207538,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -207561,7 +207561,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -207717,7 +207717,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -207741,7 +207741,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -207764,7 +207764,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -207920,7 +207920,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -207944,7 +207944,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -207967,7 +207967,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -208123,7 +208123,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -208147,7 +208147,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -208170,7 +208170,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -208326,7 +208326,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -208350,7 +208350,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -208373,7 +208373,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -208529,7 +208529,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -208553,7 +208553,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -208576,7 +208576,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -208732,7 +208732,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -208756,7 +208756,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -208779,7 +208779,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -208935,7 +208935,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -208959,7 +208959,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -208982,7 +208982,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -209138,7 +209138,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -209162,7 +209162,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -209185,7 +209185,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -209341,7 +209341,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -209365,7 +209365,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -209388,7 +209388,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -209544,7 +209544,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -209568,7 +209568,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -209591,7 +209591,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -209747,7 +209747,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -209771,7 +209771,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -209794,7 +209794,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -209950,7 +209950,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -209974,7 +209974,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -209997,7 +209997,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -210153,7 +210153,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -210177,7 +210177,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -210200,7 +210200,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -210356,7 +210356,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -210380,7 +210380,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -210403,7 +210403,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -210559,7 +210559,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -210583,7 +210583,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -210606,7 +210606,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -210762,7 +210762,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -210786,7 +210786,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -210809,7 +210809,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -210965,7 +210965,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -210989,7 +210989,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -211012,7 +211012,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -211168,7 +211168,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -211192,7 +211192,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -211215,7 +211215,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -211371,7 +211371,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -211395,7 +211395,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -211418,7 +211418,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -211574,7 +211574,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -211598,7 +211598,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -211621,7 +211621,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -211777,7 +211777,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -211801,7 +211801,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -211824,7 +211824,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -211980,7 +211980,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -212004,7 +212004,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -212027,7 +212027,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -212183,7 +212183,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -212207,7 +212207,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -212230,7 +212230,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -212386,7 +212386,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -212410,7 +212410,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -212433,7 +212433,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -212589,7 +212589,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -212613,7 +212613,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -212636,7 +212636,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -212792,7 +212792,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -212816,7 +212816,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -212839,7 +212839,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -212995,7 +212995,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -213019,7 +213019,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -213042,7 +213042,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -213198,7 +213198,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -213222,7 +213222,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -213245,7 +213245,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -213401,7 +213401,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -213425,7 +213425,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -213448,7 +213448,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -213604,7 +213604,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -213628,7 +213628,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -213651,7 +213651,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -213807,7 +213807,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -213831,7 +213831,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -213854,7 +213854,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -214010,7 +214010,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -214034,7 +214034,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -214057,7 +214057,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -214213,7 +214213,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -214237,7 +214237,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -214260,7 +214260,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -214416,7 +214416,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -214440,7 +214440,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -214463,7 +214463,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -214619,7 +214619,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -214643,7 +214643,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -214666,7 +214666,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -214822,7 +214822,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -214846,7 +214846,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -214869,7 +214869,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -215025,7 +215025,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -215049,7 +215049,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -215072,7 +215072,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -215228,7 +215228,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -215252,7 +215252,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -215275,7 +215275,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -215431,7 +215431,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -215455,7 +215455,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -215478,7 +215478,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -215634,7 +215634,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -215658,7 +215658,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "6": { "query": { @@ -215681,7 +215681,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -215837,7 +215837,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -215861,7 +215861,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "8": { "query": { @@ -215884,7 +215884,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -216040,7 +216040,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -216064,7 +216064,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "10": { "query": { @@ -216087,7 +216087,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -216243,7 +216243,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -216267,7 +216267,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "12": { "query": { @@ -216290,7 +216290,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -216446,7 +216446,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -216470,7 +216470,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "14": { "query": { @@ -216493,7 +216493,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -216649,7 +216649,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -216673,7 +216673,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "16": { "query": { @@ -216696,7 +216696,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -216852,7 +216852,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -216876,7 +216876,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "18": { "query": { @@ -216899,7 +216899,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -217055,7 +217055,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -217079,7 +217079,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "20": { "query": { @@ -217102,7 +217102,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -217258,7 +217258,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -217282,7 +217282,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "22": { "query": { @@ -217305,7 +217305,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -217461,7 +217461,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } } diff --git a/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json index 9d2289eba5e..40efba6cbc0 100644 --- a/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/persistence_spec_test.json @@ -28,7 +28,7 @@ }, { "restart": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [], "numOutstandingWrites": 2 @@ -43,7 +43,7 @@ "writeAck": { "version": 2 }, - "stateExpect": { + "expectedState": { "numOutstandingWrites": 0 } } @@ -78,7 +78,7 @@ }, { "restart": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] } @@ -92,7 +92,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -104,7 +104,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -163,7 +163,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -214,7 +214,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -242,7 +242,7 @@ }, { "restart": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] } @@ -256,7 +256,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -268,7 +268,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -314,7 +314,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -365,7 +365,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -400,7 +400,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -413,7 +413,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -425,7 +425,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -474,7 +474,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -492,7 +492,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -504,7 +504,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -551,7 +551,7 @@ }, { "changeUser": "user1", - "stateExpect": { + "expectedState": { "numOutstandingWrites": 0 } }, @@ -574,7 +574,7 @@ }, { "changeUser": null, - "stateExpect": { + "expectedState": { "numOutstandingWrites": 1 } }, @@ -582,7 +582,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "users/anon" @@ -593,7 +593,7 @@ }, { "changeUser": "user1", - "stateExpect": { + "expectedState": { "numOutstandingWrites": 2 } }, @@ -601,7 +601,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "users/user1" @@ -614,7 +614,7 @@ "writeAck": { "version": 3000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "users/user1" @@ -646,7 +646,7 @@ }, { "changeUser": "user1", - "stateExpect": { + "expectedState": { "numOutstandingWrites": 0 } }, @@ -672,7 +672,7 @@ }, { "restart": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [], "numOutstandingWrites": 1 @@ -688,7 +688,7 @@ }, { "restart": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [], "numOutstandingWrites": 2 @@ -724,7 +724,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -775,7 +775,7 @@ "version": 500, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "users", @@ -808,7 +808,7 @@ "uid": "anon" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "users", @@ -836,7 +836,7 @@ }, { "changeUser": "user1", - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -848,7 +848,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "users", @@ -881,7 +881,7 @@ "uid": "user1" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "users", @@ -909,7 +909,7 @@ }, { "changeUser": null, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "users", @@ -969,7 +969,7 @@ "applyClientState": { "visibility": "hidden" }, - "stateExpect": { + "expectedState": { "numActiveClients": 1 }, "clientIndex": 0 @@ -982,7 +982,7 @@ "applyClientState": { "visibility": "visible" }, - "stateExpect": { + "expectedState": { "numActiveClients": 2 }, "clientIndex": 1 @@ -1008,7 +1008,7 @@ "applyClientState": { "visibility": "hidden" }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -1021,7 +1021,7 @@ "applyClientState": { "visibility": "hidden" }, - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 1 @@ -1032,7 +1032,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -1044,7 +1044,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 1 @@ -1070,7 +1070,7 @@ "applyClientState": { "visibility": "hidden" }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -1083,7 +1083,7 @@ "applyClientState": { "visibility": "hidden" }, - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 1 @@ -1096,7 +1096,7 @@ "applyClientState": { "visibility": "visible" }, - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 2 @@ -1107,7 +1107,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -1119,7 +1119,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 1 @@ -1130,7 +1130,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 2 @@ -1150,14 +1150,14 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [], "isPrimary": true @@ -1166,7 +1166,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 1 @@ -1177,7 +1177,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 0 @@ -1188,14 +1188,14 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 1 }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [], "isPrimary": true @@ -1208,7 +1208,7 @@ }, { "enableNetwork": true, - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 0 @@ -1219,7 +1219,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 1 @@ -1230,7 +1230,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -1272,7 +1272,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -1284,18 +1284,16 @@ }, { "drainQueue": true, - "clientIndex": 1 - }, - { - "expectIsShutdown": true, + "expectedState": { + "isShutdown": true + }, "clientIndex": 1 }, { "drainQueue": true, - "clientIndex": 2 - }, - { - "expectIsShutdown": true, + "expectedState": { + "isShutdown": true + }, "clientIndex": 2 } ] diff --git a/Firestore/Example/Tests/SpecTests/json/query_spec_test.json b/Firestore/Example/Tests/SpecTests/json/query_spec_test.json index 2501ede3346..a13c3303043 100644 --- a/Firestore/Example/Tests/SpecTests/json/query_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/query_spec_test.json @@ -17,7 +17,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -68,7 +68,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "cg/1", @@ -103,7 +103,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -162,7 +162,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "cg/2", @@ -197,7 +197,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -264,7 +264,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "not-cg/nope/cg/3", @@ -299,7 +299,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -374,7 +374,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "not-cg/nope", @@ -409,7 +409,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -492,7 +492,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "cg/1/not-cg/nope", @@ -528,7 +528,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -581,7 +581,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "", @@ -646,7 +646,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -714,7 +714,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "", @@ -778,7 +778,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -829,7 +829,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "cg/1", @@ -864,7 +864,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -923,7 +923,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "not-cg/nope", @@ -983,7 +983,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1012,7 +1012,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "", @@ -1077,7 +1077,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1121,7 +1121,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "", @@ -1185,7 +1185,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1236,7 +1236,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1269,7 +1269,7 @@ "match": true } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1310,7 +1310,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1336,7 +1336,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", diff --git a/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json index 57e9a2cd6f1..3bd4f592ba1 100644 --- a/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/remote_store_spec_test.json @@ -17,7 +17,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -44,7 +44,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -57,7 +57,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -149,7 +149,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -195,7 +195,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -222,7 +222,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -235,7 +235,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -257,7 +257,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -270,7 +270,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -292,7 +292,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -305,7 +305,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -489,7 +489,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -535,7 +535,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -556,7 +556,7 @@ }, "runBackoffTimer": true }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -607,7 +607,7 @@ "version": 1001, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -656,7 +656,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -677,7 +677,7 @@ }, "runBackoffTimer": false }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -689,13 +689,13 @@ "hasPendingWrites": false } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, { "changeUser": "abc", - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { diff --git a/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json index b78b9e16187..c7e126c3193 100644 --- a/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/resume_token_spec_test.json @@ -17,7 +17,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -68,7 +68,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -102,7 +102,7 @@ }, "runBackoffTimer": true }, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -135,7 +135,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -186,7 +186,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -221,7 +221,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -234,7 +234,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -246,7 +246,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", diff --git a/Firestore/Example/Tests/SpecTests/json/write_spec_test.json b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json index 1f8a024b58e..93c2f90209e 100644 --- a/Firestore/Example/Tests/SpecTests/json/write_spec_test.json +++ b/Firestore/Example/Tests/SpecTests/json/write_spec_test.json @@ -17,7 +17,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -79,7 +79,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -123,7 +123,7 @@ "v": 2 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -179,7 +179,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -187,7 +187,7 @@ "rejectedDocs": [] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -220,7 +220,7 @@ "v": 2 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -276,7 +276,7 @@ "writeAck": { "version": 2500 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/b" @@ -284,7 +284,7 @@ "rejectedDocs": [] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -330,7 +330,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -381,7 +381,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -414,7 +414,7 @@ "v": 2 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -470,7 +470,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -478,7 +478,7 @@ "rejectedDocs": [] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -532,7 +532,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -544,7 +544,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -593,7 +593,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -611,7 +611,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -623,7 +623,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -669,7 +669,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -708,7 +708,7 @@ "version": 250, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -733,7 +733,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -793,7 +793,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -839,7 +839,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -890,7 +890,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -923,7 +923,7 @@ "v": 2 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -979,7 +979,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -987,7 +987,7 @@ "rejectedDocs": [] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -1033,7 +1033,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1084,7 +1084,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -1117,7 +1117,7 @@ "v": 2 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -1147,7 +1147,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -1181,7 +1181,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -1227,7 +1227,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1278,7 +1278,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1311,7 +1311,7 @@ "v": 3 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1341,7 +1341,7 @@ "writeAck": { "version": 3000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -1412,7 +1412,7 @@ "version": 3000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1479,7 +1479,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1491,7 +1491,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1521,7 +1521,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -1569,7 +1569,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1603,7 +1603,7 @@ "local": 5 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1634,7 +1634,7 @@ "writeAck": { "version": 5000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -1723,7 +1723,7 @@ "version": 5000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1770,7 +1770,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -1803,7 +1803,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1836,7 +1836,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1869,7 +1869,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1902,7 +1902,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1935,7 +1935,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -1968,7 +1968,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2001,7 +2001,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2034,7 +2034,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2067,7 +2067,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2100,7 +2100,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2133,7 +2133,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2166,7 +2166,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2199,7 +2199,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2232,7 +2232,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2265,7 +2265,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2290,7 +2290,7 @@ "hasPendingWrites": true } ], - "stateExpect": { + "expectedState": { "numOutstandingWrites": 10 } }, @@ -2298,7 +2298,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a0" @@ -2332,7 +2332,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2362,7 +2362,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a1" @@ -2396,7 +2396,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2426,7 +2426,7 @@ "writeAck": { "version": 3000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a2" @@ -2460,7 +2460,7 @@ "version": 3000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2490,7 +2490,7 @@ "writeAck": { "version": 4000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a3" @@ -2524,7 +2524,7 @@ "version": 4000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2554,7 +2554,7 @@ "writeAck": { "version": 5000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a4" @@ -2588,7 +2588,7 @@ "version": 5000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2618,7 +2618,7 @@ "writeAck": { "version": 6000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a5" @@ -2652,7 +2652,7 @@ "version": 6000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2682,7 +2682,7 @@ "writeAck": { "version": 7000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a6" @@ -2716,7 +2716,7 @@ "version": 7000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2746,7 +2746,7 @@ "writeAck": { "version": 8000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a7" @@ -2780,7 +2780,7 @@ "version": 8000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2810,7 +2810,7 @@ "writeAck": { "version": 9000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a8" @@ -2844,7 +2844,7 @@ "version": 9000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2874,7 +2874,7 @@ "writeAck": { "version": 10000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a9" @@ -2908,7 +2908,7 @@ "version": 10000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -2938,7 +2938,7 @@ "writeAck": { "version": 11000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a10" @@ -2972,7 +2972,7 @@ "version": 11000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3002,7 +3002,7 @@ "writeAck": { "version": 12000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a11" @@ -3036,7 +3036,7 @@ "version": 12000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3066,7 +3066,7 @@ "writeAck": { "version": 13000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a12" @@ -3100,7 +3100,7 @@ "version": 13000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3130,7 +3130,7 @@ "writeAck": { "version": 14000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a13" @@ -3164,7 +3164,7 @@ "version": 14000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3194,7 +3194,7 @@ "writeAck": { "version": 15000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a14" @@ -3228,7 +3228,7 @@ "version": 15000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3274,7 +3274,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -3294,7 +3294,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3327,7 +3327,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3360,7 +3360,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3393,7 +3393,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3426,7 +3426,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3459,7 +3459,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3492,7 +3492,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3525,7 +3525,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3558,7 +3558,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3591,7 +3591,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3624,7 +3624,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3657,7 +3657,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3690,7 +3690,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3723,7 +3723,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3756,7 +3756,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3781,7 +3781,7 @@ "hasPendingWrites": true } ], - "stateExpect": { + "expectedState": { "numOutstandingWrites": 10 } }, @@ -3791,7 +3791,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -3799,7 +3799,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3831,7 +3831,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -3839,7 +3839,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3871,7 +3871,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -3879,7 +3879,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3911,7 +3911,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -3919,7 +3919,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3951,7 +3951,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -3959,7 +3959,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -3991,7 +3991,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -3999,7 +3999,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4031,7 +4031,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4039,7 +4039,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4071,7 +4071,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4079,7 +4079,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4111,7 +4111,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4119,7 +4119,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4151,7 +4151,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4159,7 +4159,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4191,7 +4191,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4199,7 +4199,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4231,7 +4231,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4239,7 +4239,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4271,7 +4271,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4279,7 +4279,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4311,7 +4311,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4319,7 +4319,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4351,7 +4351,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4360,7 +4360,7 @@ }, "numOutstandingWrites": 0 }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4406,7 +4406,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4457,7 +4457,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4490,7 +4490,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4520,7 +4520,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/b" @@ -4536,7 +4536,7 @@ "v": 2 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4568,7 +4568,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -4576,7 +4576,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4627,7 +4627,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4673,7 +4673,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4712,7 +4712,7 @@ "version": 500, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4732,7 +4732,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4762,7 +4762,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -4778,7 +4778,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4808,7 +4808,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/b" @@ -4853,7 +4853,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4910,7 +4910,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -4949,7 +4949,7 @@ "version": 500, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4969,7 +4969,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -4999,7 +4999,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -5011,12 +5011,12 @@ }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [], "writeStreamRequestCount": 3 }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5031,7 +5031,7 @@ }, { "enableNetwork": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5083,7 +5083,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5132,7 +5132,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5171,7 +5171,7 @@ "version": 500, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5191,7 +5191,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -5221,7 +5221,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -5239,7 +5239,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": {} } }, @@ -5252,7 +5252,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "4": { "query": { @@ -5285,7 +5285,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5305,7 +5305,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5337,7 +5337,7 @@ "code": 3 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -5345,7 +5345,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5391,7 +5391,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5411,7 +5411,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5443,7 +5443,7 @@ "code": 5 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -5451,7 +5451,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5497,7 +5497,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5517,7 +5517,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5549,7 +5549,7 @@ "code": 6 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -5557,7 +5557,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5603,7 +5603,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5623,7 +5623,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5655,7 +5655,7 @@ "code": 7 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -5663,7 +5663,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5709,7 +5709,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5729,7 +5729,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5761,7 +5761,7 @@ "code": 9 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -5769,7 +5769,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5815,7 +5815,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5835,7 +5835,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5867,7 +5867,7 @@ "code": 11 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -5875,7 +5875,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5921,7 +5921,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -5941,7 +5941,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -5973,7 +5973,7 @@ "code": 12 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -5981,7 +5981,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6027,7 +6027,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6047,7 +6047,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6079,7 +6079,7 @@ "code": 15 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -6087,7 +6087,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6133,7 +6133,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6153,7 +6153,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6207,7 +6207,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6227,7 +6227,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6265,7 +6265,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -6312,7 +6312,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6358,7 +6358,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6378,7 +6378,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6416,7 +6416,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -6463,7 +6463,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6509,7 +6509,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6529,7 +6529,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6567,7 +6567,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -6614,7 +6614,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6660,7 +6660,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6680,7 +6680,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6718,7 +6718,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -6765,7 +6765,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6811,7 +6811,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6831,7 +6831,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6869,7 +6869,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -6916,7 +6916,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -6962,7 +6962,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -6982,7 +6982,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -7020,7 +7020,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -7067,7 +7067,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -7113,7 +7113,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7133,7 +7133,7 @@ "foo": "bar" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -7171,7 +7171,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -7218,7 +7218,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/key", @@ -7264,7 +7264,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7318,7 +7318,7 @@ "version": 500, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -7355,7 +7355,7 @@ "a.c": "" } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -7417,7 +7417,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -7425,7 +7425,7 @@ "rejectedDocs": [] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -7472,13 +7472,13 @@ "foo": "bar" } ], - "stateExpect": { + "expectedState": { "numOutstandingWrites": 1 } }, { "enableNetwork": false, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [], "writeStreamRequestCount": 3 @@ -7486,7 +7486,7 @@ }, { "enableNetwork": true, - "stateExpect": { + "expectedState": { "writeStreamRequestCount": 5, "numOutstandingWrites": 1 } @@ -7495,7 +7495,7 @@ "writeAck": { "version": 1 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/key" @@ -7530,7 +7530,7 @@ "code": 9 } }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -7551,7 +7551,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/b" @@ -7608,7 +7608,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -7668,7 +7668,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -7704,7 +7704,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7747,7 +7747,7 @@ "version": 500, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7768,7 +7768,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7808,7 +7808,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -7820,7 +7820,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7858,7 +7858,7 @@ "v": 2 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7887,7 +7887,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7921,7 +7921,7 @@ "v": 3 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -7950,7 +7950,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8009,7 +8009,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8052,7 +8052,7 @@ "version": 500, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8079,7 +8079,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8091,7 +8091,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8112,7 +8112,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8141,7 +8141,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8200,7 +8200,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8229,7 +8229,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -8237,7 +8237,7 @@ "rejectedDocs": [] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8290,7 +8290,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8333,7 +8333,7 @@ "version": 500, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8360,7 +8360,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8372,7 +8372,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8393,7 +8393,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8422,7 +8422,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8455,7 +8455,7 @@ "code": 9 } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8484,7 +8484,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -8492,7 +8492,7 @@ ] } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8545,7 +8545,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8588,7 +8588,7 @@ "version": 500, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8617,7 +8617,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8652,7 +8652,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -8692,7 +8692,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8745,7 +8745,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8766,7 +8766,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8822,7 +8822,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -8840,7 +8840,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/doc" @@ -8858,7 +8858,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true, "activeTargets": { "2": { @@ -8882,7 +8882,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -8902,7 +8902,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -8986,7 +8986,7 @@ "version": 2000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection/doc", @@ -9002,7 +9002,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9026,10 +9026,10 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": false }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9129,7 +9129,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -9161,7 +9161,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/b" @@ -9173,7 +9173,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/c" @@ -9185,7 +9185,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -9255,7 +9255,7 @@ "writeAck": { "version": 4000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/f" @@ -9287,7 +9287,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/g" @@ -9299,7 +9299,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/h" @@ -9311,7 +9311,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -9378,7 +9378,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/j", @@ -9412,14 +9412,14 @@ "applyClientState": { "visibility": "visible" }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 1 @@ -9454,7 +9454,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -9462,7 +9462,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -9474,7 +9474,7 @@ }, { "runTimer": "client_metadata_refresh", - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 1 @@ -9483,7 +9483,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/b" @@ -9519,7 +9519,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9544,7 +9544,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9588,7 +9588,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -9600,7 +9600,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9634,7 +9634,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9663,7 +9663,7 @@ }, { "drainQueue": true, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9692,7 +9692,7 @@ }, { "changeUser": "user2", - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9737,7 +9737,7 @@ "v": 1 } ], - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9770,7 +9770,7 @@ }, { "changeUser": "user2", - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9823,7 +9823,7 @@ }, { "changeUser": "user1", - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9880,7 +9880,7 @@ }, { "changeUser": "user1", - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -9991,7 +9991,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10003,7 +10003,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10043,7 +10043,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10055,7 +10055,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10097,7 +10097,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -10124,7 +10124,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10136,7 +10136,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10165,7 +10165,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10187,7 +10187,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 1 @@ -10239,7 +10239,7 @@ "version": 1000, "targetIds": [] }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", @@ -10268,7 +10268,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -10293,14 +10293,14 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": false }, "clientIndex": 1 @@ -10320,7 +10320,7 @@ }, { "shutdown": true, - "stateExpect": { + "expectedState": { "activeTargets": {}, "limboDocs": [] }, @@ -10328,7 +10328,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true, "numOutstandingWrites": 1 }, @@ -10342,7 +10342,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -10367,7 +10367,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -10385,7 +10385,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -10416,7 +10416,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 2 @@ -10444,7 +10444,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/b" @@ -10469,7 +10469,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -10487,7 +10487,7 @@ "writeAck": { "version": 1000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -10518,7 +10518,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 2 @@ -10550,7 +10550,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [], "rejectedDocs": [ @@ -10575,7 +10575,7 @@ "steps": [ { "drainQueue": true, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -10606,7 +10606,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 1 @@ -10619,7 +10619,7 @@ }, { "drainQueue": true, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/a" @@ -10633,7 +10633,7 @@ "applyClientState": { "primary": true }, - "stateExpect": { + "expectedState": { "isPrimary": true }, "clientIndex": 0 @@ -10642,7 +10642,7 @@ "writeAck": { "version": 2000 }, - "stateExpect": { + "expectedState": { "userCallbacks": { "acknowledgedDocs": [ "collection/b" @@ -10661,7 +10661,7 @@ "orderBys": [] } ], - "stateExpect": { + "expectedState": { "activeTargets": { "2": { "query": { @@ -10673,7 +10673,7 @@ } } }, - "expect": [ + "expectedSnapshotEvents": [ { "query": { "path": "collection", diff --git a/Firestore/Example/Tests/Util/FSTHelpers.h b/Firestore/Example/Tests/Util/FSTHelpers.h index c3f566e6936..fe022d81c19 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.h +++ b/Firestore/Example/Tests/Util/FSTHelpers.h @@ -21,8 +21,10 @@ #include #include "Firestore/core/src/firebase/firestore/core/filter.h" +#include "Firestore/core/src/firebase/firestore/core/view.h" #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/local/local_view_changes.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/delete_mutation.h" #include "Firestore/core/src/firebase/firestore/model/document.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" @@ -45,9 +47,7 @@ @class FIRGeoPoint; @class FIRTimestamp; @class FSTDocumentKeyReference; -@class FSTLocalViewChanges; @class FSTUserDataConverter; -@class FSTView; namespace firebase { namespace firestore { @@ -59,6 +59,7 @@ class RemoteEvent; } // namespace firestore } // namespace firebase +namespace core = firebase::firestore::core; namespace local = firebase::firestore::local; namespace model = firebase::firestore::model; @@ -180,7 +181,7 @@ class TestTargetMetadataProvider : public TargetMetadataProvider { const std::vector &limbo_targets); /** - * Creates an `TestTargetMetadataProvider` that behaves as if there's an established listen for + * Creates a `TestTargetMetadataProvider` that behaves as if there's an established listen for * each of the given targets, where each target has not seen any previous document. * * Internally this means that the `GetRemoteKeysForTarget` callback for these targets will return @@ -188,17 +189,17 @@ class TestTargetMetadataProvider : public TargetMetadataProvider { * `GetQueryDataForTarget` target. */ static TestTargetMetadataProvider CreateEmptyResultProvider( - const model::DocumentKey &document_key, const std::vector &targets); + const model::ResourcePath &path, const std::vector &targets); /** Sets or replaces the local state for the provided query data. */ - void SetSyncedKeys(model::DocumentKeySet keys, FSTQueryData *query_data); + void SetSyncedKeys(model::DocumentKeySet keys, local::QueryData query_data); model::DocumentKeySet GetRemoteKeysForTarget(model::TargetId target_id) const override; - FSTQueryData *GetQueryDataForTarget(model::TargetId target_id) const override; + absl::optional GetQueryDataForTarget(model::TargetId target_id) const override; private: std::unordered_map synced_keys_; - std::unordered_map query_data_; + std::unordered_map query_data_; }; } // namespace remote @@ -249,8 +250,8 @@ typedef int64_t FSTTestSnapshotVersion; FSTDocumentKeyReference *FSTTestRef(std::string projectID, std::string databaseID, NSString *path); /** Computes changes to the view with the docs and then applies them and returns the snapshot. */ -absl::optional FSTTestApplyChanges( - FSTView *view, +absl::optional FSTTestApplyChanges( + core::View *view, const std::vector &docs, const absl::optional &targetChange); @@ -283,6 +284,11 @@ firebase::firestore::remote::RemoteEvent FSTTestAddedRemoteEvent( const model::MaybeDocument &doc, const std::vector &addedToTargets); +/** Creates a remote event that inserts a list of documents. */ +firebase::firestore::remote::RemoteEvent FSTTestAddedRemoteEvent( + const std::vector &doc, + const std::vector &addedToTargets); + /** Creates a remote event with changes to a document. */ firebase::firestore::remote::RemoteEvent FSTTestUpdateRemoteEvent( const model::MaybeDocument &doc, diff --git a/Firestore/Example/Tests/Util/FSTHelpers.mm b/Firestore/Example/Tests/Util/FSTHelpers.mm index 7d2ef590710..0d24efdb90b 100644 --- a/Firestore/Example/Tests/Util/FSTHelpers.mm +++ b/Firestore/Example/Tests/Util/FSTHelpers.mm @@ -27,12 +27,12 @@ #import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTView.h" -#import "Firestore/Source/Local/FSTQueryData.h" #include "Firestore/core/src/firebase/firestore/core/filter.h" +#include "Firestore/core/src/firebase/firestore/core/view.h" #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/local/local_view_changes.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/delete_mutation.h" #include "Firestore/core/src/firebase/firestore/model/document.h" @@ -60,8 +60,12 @@ using firebase::firestore::core::Filter; using firebase::firestore::core::ParsedUpdateData; using firebase::firestore::core::Query; +using firebase::firestore::core::View; +using firebase::firestore::core::ViewChange; using firebase::firestore::core::ViewSnapshot; using firebase::firestore::local::LocalViewChanges; +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DeleteMutation; using firebase::firestore::model::Document; @@ -88,6 +92,7 @@ using firebase::firestore::model::TransformMutation; using firebase::firestore::model::TransformOperation; using firebase::firestore::model::UnknownDocument; +using firebase::firestore::nanopb::ByteString; using firebase::firestore::remote::DocumentWatchChange; using firebase::firestore::remote::RemoteEvent; using firebase::firestore::remote::TargetChange; @@ -225,13 +230,12 @@ MaybeDocumentMap FSTTestDocUpdates(const std::vector &docs) { return updates; } -absl::optional FSTTestApplyChanges(FSTView *view, +absl::optional FSTTestApplyChanges(View *view, const std::vector &docs, const absl::optional &targetChange) { - FSTViewChange *change = - [view applyChangesToDocuments:[view computeChangesWithDocuments:FSTTestDocUpdates(docs)] - targetChange:targetChange]; - return std::move(change.snapshot); + ViewChange change = + view->ApplyChanges(view->ComputeDocumentChanges(FSTTestDocUpdates(docs)), targetChange); + return change.snapshot(); } namespace firebase { @@ -246,17 +250,11 @@ MaybeDocumentMap FSTTestDocUpdates(const std::vector &docs) { core::Query query(document_key.path()); for (TargetId target_id : listen_targets) { - FSTQueryData *query_data = [[FSTQueryData alloc] initWithQuery:query - targetID:target_id - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen]; + QueryData query_data(query, target_id, 0, QueryPurpose::Listen); metadata_provider.SetSyncedKeys(DocumentKeySet{document_key}, query_data); } for (TargetId target_id : limbo_targets) { - FSTQueryData *query_data = [[FSTQueryData alloc] initWithQuery:query - targetID:target_id - listenSequenceNumber:0 - purpose:FSTQueryPurposeLimboResolution]; + QueryData query_data(query, target_id, 0, QueryPurpose::LimboResolution); metadata_provider.SetSyncedKeys(DocumentKeySet{document_key}, query_data); } @@ -269,24 +267,21 @@ MaybeDocumentMap FSTTestDocUpdates(const std::vector &docs) { } TestTargetMetadataProvider TestTargetMetadataProvider::CreateEmptyResultProvider( - const DocumentKey &document_key, const std::vector &targets) { + const ResourcePath &path, const std::vector &targets) { TestTargetMetadataProvider metadata_provider; - core::Query query(document_key.path()); + core::Query query(path); for (TargetId target_id : targets) { - FSTQueryData *query_data = [[FSTQueryData alloc] initWithQuery:query - targetID:target_id - listenSequenceNumber:0 - purpose:FSTQueryPurposeListen]; + QueryData query_data(query, target_id, 0, QueryPurpose::Listen); metadata_provider.SetSyncedKeys(DocumentKeySet{}, query_data); } return metadata_provider; } -void TestTargetMetadataProvider::SetSyncedKeys(DocumentKeySet keys, FSTQueryData *query_data) { - synced_keys_[query_data.targetID] = keys; - query_data_[query_data.targetID] = query_data; +void TestTargetMetadataProvider::SetSyncedKeys(DocumentKeySet keys, QueryData query_data) { + synced_keys_[query_data.target_id()] = keys; + query_data_[query_data.target_id()] = std::move(query_data); } DocumentKeySet TestTargetMetadataProvider::GetRemoteKeysForTarget(TargetId target_id) const { @@ -295,7 +290,8 @@ MaybeDocumentMap FSTTestDocUpdates(const std::vector &docs) { return it->second; } -FSTQueryData *TestTargetMetadataProvider::GetQueryDataForTarget(TargetId target_id) const { +absl::optional TestTargetMetadataProvider::GetQueryDataForTarget( + TargetId target_id) const { auto it = query_data_.find(target_id); HARD_ASSERT(it != query_data_.end(), "Cannot process unknown target %s", target_id); return it->second; @@ -309,18 +305,29 @@ MaybeDocumentMap FSTTestDocUpdates(const std::vector &docs) { RemoteEvent FSTTestAddedRemoteEvent(const MaybeDocument &doc, const std::vector &addedToTargets) { - HARD_ASSERT(!doc.is_document() || !Document(doc).has_local_mutations(), - "Docs from remote updates shouldn't have local changes."); - DocumentWatchChange change{addedToTargets, {}, doc.key(), doc}; + std::vector docs{doc}; + return FSTTestAddedRemoteEvent(docs, addedToTargets); +} + +RemoteEvent FSTTestAddedRemoteEvent(const std::vector &docs, + const std::vector &addedToTargets) { + HARD_ASSERT(!docs.empty(), "Cannot pass empty docs array"); + + const ResourcePath &collectionPath = docs[0].key().path().PopLast(); auto metadataProvider = - TestTargetMetadataProvider::CreateEmptyResultProvider(doc.key(), addedToTargets); + TestTargetMetadataProvider::CreateEmptyResultProvider(collectionPath, addedToTargets); WatchChangeAggregator aggregator{&metadataProvider}; - aggregator.HandleDocumentChange(change); - return aggregator.CreateRemoteEvent(doc.version()); + for (const MaybeDocument &doc : docs) { + HARD_ASSERT(!doc.is_document() || !Document(doc).has_local_mutations(), + "Docs from remote updates shouldn't have local changes."); + DocumentWatchChange change{addedToTargets, {}, doc.key(), doc}; + aggregator.HandleDocumentChange(change); + } + return aggregator.CreateRemoteEvent(docs[0].version()); } TargetChange FSTTestTargetChangeMarkCurrent() { - return {[NSData data], + return {ByteString(), /*current=*/true, /*added_documents=*/DocumentKeySet{}, /*modified_documents=*/DocumentKeySet{}, @@ -328,7 +335,7 @@ TargetChange FSTTestTargetChangeMarkCurrent() { } TargetChange FSTTestTargetChangeAckDocuments(DocumentKeySet docs) { - return {[NSData data], + return {ByteString(), /*current=*/true, /*added_documents*/ std::move(docs), /*modified_documents*/ DocumentKeySet{}, diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h index c6d85c02eb3..2f5d69c4532 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.h @@ -55,11 +55,16 @@ extern "C" { /** Returns a new Firestore connected to the project with the given projectID. */ - (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID; -/** Returns a new Firestore connected to the project with the given app. */ +/** Triggers a user change with given user id. */ +- (void)triggerUserChangeWithUid:(NSString *)uid; + +/** + * Returns a new Firestore connected to the project with the given app. + */ - (FIRFirestore *)firestoreWithApp:(FIRApp *)app; -/** Synchronously shuts down the given firestore. */ -- (void)shutdownFirestore:(FIRFirestore *)firestore; +/** Synchronously terminates the given firestore. */ +- (void)terminateFirestore:(FIRFirestore *)firestore; /** Synchronously deletes the given FIRapp. */ - (void)deleteApp:(FIRApp *)app; diff --git a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm index 7d099b2bae5..d3bdb5fa6ed 100644 --- a/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm +++ b/Firestore/Example/Tests/Util/FSTIntegrationTestCase.mm @@ -33,38 +33,42 @@ #include #include +#import "Firestore/Example/Tests/Util/FIRFirestore+Testing.h" +#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" +#import "Firestore/Source/API/FIRFirestore+Internal.h" + +#include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/auth/empty_credentials_provider.h" +#include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/autoid.h" #include "Firestore/core/src/firebase/firestore/util/filesystem.h" #include "Firestore/core/src/firebase/firestore/util/path.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" #include "Firestore/core/test/firebase/firestore/testutil/app_testing.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" #include "Firestore/core/test/firebase/firestore/util/status_testing.h" - #include "absl/memory/memory.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/Local/FSTLevelDB.h" - -#import "Firestore/Example/Tests/Util/FIRFirestore+Testing.h" -#import "Firestore/Example/Tests/Util/FSTEventAccumulator.h" - -#include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" - +namespace testutil = firebase::firestore::testutil; namespace util = firebase::firestore::util; + +using firebase::firestore::auth::CredentialChangeListener; using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::EmptyCredentialsProvider; +using firebase::firestore::auth::User; +using firebase::firestore::local::LevelDbPersistence; using firebase::firestore::model::DatabaseId; using firebase::firestore::testutil::AppForUnitTesting; +using firebase::firestore::testutil::AsyncQueueForTesting; using firebase::firestore::remote::GrpcConnection; using firebase::firestore::util::AsyncQueue; using firebase::firestore::util::CreateAutoId; using firebase::firestore::util::Path; using firebase::firestore::util::Status; -using firebase::firestore::util::ExecutorLibdispatch; NS_ASSUME_NONNULL_BEGIN @@ -79,13 +83,37 @@ static bool runningAgainstEmulator = false; +// Behaves the same as `EmptyCredentialsProvider` except it can also trigger a user +// change. +class FakeCredentialsProvider : public EmptyCredentialsProvider { + public: + void SetCredentialChangeListener(CredentialChangeListener changeListener) override { + if (changeListener) { + listener_ = std::move(changeListener); + listener_(User::Unauthenticated()); + } + } + + void ChangeUser(NSString *new_id) { + if (listener_) { + listener_(firebase::firestore::auth::User::FromUid(new_id)); + } + } + + private: + CredentialChangeListener listener_; +}; + @implementation FSTIntegrationTestCase { NSMutableArray *_firestores; + std::shared_ptr _fakeCredentialsProvider; } - (void)setUp { [super setUp]; + _fakeCredentialsProvider = std::make_shared(); + [self clearPersistenceOnce]; [self primeBackend]; @@ -97,7 +125,7 @@ - (void)setUp { - (void)tearDown { @try { for (FIRFirestore *firestore in _firestores) { - [self shutdownFirestore:firestore]; + [self terminateFirestore:firestore]; } } @finally { _firestores = nil; @@ -116,7 +144,7 @@ - (void)clearPersistenceOnce { @synchronized([FSTIntegrationTestCase class]) { if (clearedPersistence) return; - Path levelDBDir = [FSTLevelDB documentsDirectory]; + Path levelDBDir = LevelDbPersistence::AppDataDirectory(); Status status = util::RecursivelyDelete(levelDBDir); ASSERT_OK(status); @@ -232,21 +260,14 @@ - (FIRFirestore *)firestoreWithProjectID:(NSString *)projectID { - (FIRFirestore *)firestoreWithApp:(FIRApp *)app { NSString *persistenceKey = [NSString stringWithFormat:@"db%lu", (unsigned long)_firestores.count]; - dispatch_queue_t queue = - dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL); - std::unique_ptr workerQueue = - absl::make_unique(absl::make_unique(queue)); - FIRSetLoggerLevel(FIRLoggerLevelDebug); - std::unique_ptr credentials_provider = - absl::make_unique(); std::string projectID = util::MakeString(app.options.projectID); FIRFirestore *firestore = [[FIRFirestore alloc] initWithDatabaseID:DatabaseId(projectID) persistenceKey:util::MakeString(persistenceKey) - credentialsProvider:std::move(credentials_provider) - workerQueue:std::move(workerQueue) + credentialsProvider:_fakeCredentialsProvider + workerQueue:AsyncQueueForTesting() firebaseApp:app instanceRegistry:nil]; @@ -255,6 +276,10 @@ - (FIRFirestore *)firestoreWithApp:(FIRApp *)app { return firestore; } +- (void)triggerUserChangeWithUid:(NSString *)uid { + _fakeCredentialsProvider->ChangeUser(uid); +} + - (void)primeBackend { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -297,12 +322,12 @@ - (void)primeBackend { [listenerRegistration remove]; - [self shutdownFirestore:db]; + [self terminateFirestore:db]; }); } -- (void)shutdownFirestore:(FIRFirestore *)firestore { - [firestore shutdownWithCompletion:[self completionForExpectationWithName:@"shutdown"]]; +- (void)terminateFirestore:(FIRFirestore *)firestore { + [firestore terminateWithCompletion:[self completionForExpectationWithName:@"shutdown"]]; [self awaitExpectations]; } diff --git a/Firestore/Protos/CMakeLists.txt b/Firestore/Protos/CMakeLists.txt index 2cb37b92dbb..58b1f5d40c8 100644 --- a/Firestore/Protos/CMakeLists.txt +++ b/Firestore/Protos/CMakeLists.txt @@ -113,8 +113,6 @@ endfunction() # omitted here. set( PROTOBUF_OBJC_GENERATED_SOURCES - ${OUTPUT_DIR}/objc/google/firestore/v1/Firestore.pbrpc.h - ${OUTPUT_DIR}/objc/google/firestore/v1/Firestore.pbrpc.m ) foreach(root ${PROTO_FILE_ROOTS}) get_filename_component(dir ${root} DIRECTORY) @@ -169,6 +167,26 @@ target_include_directories( ${FIREBASE_SOURCE_DIR}/Firestore/Protos/cpp ) +if(APPLE) + cc_library( + firebase_firestore_protos_objc + SOURCES + ${PROTOBUF_OBJC_GENERATED_SOURCES} + DEPENDS + libprotobuf_objc + EXCLUDE_FROM_ALL + ) + + target_include_directories( + firebase_firestore_protos_objc PRIVATE + ${FIREBASE_SOURCE_DIR}/Firestore/Protos/objc/firestore/local + ${FIREBASE_SOURCE_DIR}/Firestore/Protos/objc/google/firestore/v1 + ${FIREBASE_SOURCE_DIR}/Firestore/Protos/objc/google/api + ${FIREBASE_SOURCE_DIR}/Firestore/Protos/objc/google/rpc + ${FIREBASE_SOURCE_DIR}/Firestore/Protos/objc/google/type + ) +endif() + # Generate the python representation of descriptor.proto. set(PROTOBUF_DIR ${FIREBASE_BINARY_DIR}/external/src/grpc/third_party/protobuf) @@ -308,3 +326,95 @@ add_custom_target( generate_cpp_protos generate_objc_protos ) + +if(APPLE) + set( + protobuf_objc_source_dir + ${FIREBASE_EXTERNAL_SOURCE_DIR}/grpc/third_party/protobuf/objectivec + ) + objc_framework( + Protobuf + HEADERS + ${protobuf_objc_source_dir}/GPBArray.h + ${protobuf_objc_source_dir}/GPBArray_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBBootstrap.h + ${protobuf_objc_source_dir}/GPBCodedInputStream.h + ${protobuf_objc_source_dir}/GPBCodedInputStream_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBCodedOutputStream.h + ${protobuf_objc_source_dir}/GPBCodedOutputStream_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBDescriptor.h + ${protobuf_objc_source_dir}/GPBDescriptor_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBDictionary.h + ${protobuf_objc_source_dir}/GPBDictionary_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBExtensionInternals.h + ${protobuf_objc_source_dir}/GPBExtensionRegistry.h + ${protobuf_objc_source_dir}/GPBMessage.h + ${protobuf_objc_source_dir}/GPBMessage_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBProtocolBuffers.h + ${protobuf_objc_source_dir}/GPBProtocolBuffers_RuntimeSupport.h + ${protobuf_objc_source_dir}/GPBRootObject.h + ${protobuf_objc_source_dir}/GPBRootObject_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBRuntimeTypes.h + ${protobuf_objc_source_dir}/GPBUnknownField.h + ${protobuf_objc_source_dir}/GPBUnknownFieldSet.h + ${protobuf_objc_source_dir}/GPBUnknownFieldSet_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBUnknownField_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBUtilities.h + ${protobuf_objc_source_dir}/GPBUtilities_PackagePrivate.h + ${protobuf_objc_source_dir}/GPBWellKnownTypes.h + ${protobuf_objc_source_dir}/GPBWireFormat.h + ${protobuf_objc_source_dir}/google/protobuf/Any.pbobjc.h + ${protobuf_objc_source_dir}/google/protobuf/Api.pbobjc.h + ${protobuf_objc_source_dir}/google/protobuf/Duration.pbobjc.h + ${protobuf_objc_source_dir}/google/protobuf/Empty.pbobjc.h + ${protobuf_objc_source_dir}/google/protobuf/FieldMask.pbobjc.h + ${protobuf_objc_source_dir}/google/protobuf/SourceContext.pbobjc.h + ${protobuf_objc_source_dir}/google/protobuf/Struct.pbobjc.h + ${protobuf_objc_source_dir}/google/protobuf/Timestamp.pbobjc.h + ${protobuf_objc_source_dir}/google/protobuf/Type.pbobjc.h + ${protobuf_objc_source_dir}/google/protobuf/Wrappers.pbobjc.h + + SOURCES + ${protobuf_objc_source_dir}/GPBArray.m + ${protobuf_objc_source_dir}/GPBCodedInputStream.m + ${protobuf_objc_source_dir}/GPBCodedOutputStream.m + ${protobuf_objc_source_dir}/GPBDescriptor.m + ${protobuf_objc_source_dir}/GPBDictionary.m + ${protobuf_objc_source_dir}/GPBExtensionInternals.m + ${protobuf_objc_source_dir}/GPBExtensionRegistry.m + ${protobuf_objc_source_dir}/GPBMessage.m + ${protobuf_objc_source_dir}/GPBProtocolBuffers.m + ${protobuf_objc_source_dir}/GPBRootObject.m + ${protobuf_objc_source_dir}/GPBUnknownField.m + ${protobuf_objc_source_dir}/GPBUnknownFieldSet.m + ${protobuf_objc_source_dir}/GPBUtilities.m + ${protobuf_objc_source_dir}/GPBWellKnownTypes.m + ${protobuf_objc_source_dir}/GPBWireFormat.m + ${protobuf_objc_source_dir}/google/protobuf/Any.pbobjc.m + ${protobuf_objc_source_dir}/google/protobuf/Api.pbobjc.m + ${protobuf_objc_source_dir}/google/protobuf/Duration.pbobjc.m + ${protobuf_objc_source_dir}/google/protobuf/Empty.pbobjc.m + ${protobuf_objc_source_dir}/google/protobuf/FieldMask.pbobjc.m + ${protobuf_objc_source_dir}/google/protobuf/SourceContext.pbobjc.m + ${protobuf_objc_source_dir}/google/protobuf/Struct.pbobjc.m + ${protobuf_objc_source_dir}/google/protobuf/Timestamp.pbobjc.m + ${protobuf_objc_source_dir}/google/protobuf/Type.pbobjc.m + ${protobuf_objc_source_dir}/google/protobuf/Wrappers.pbobjc.m + ) + target_include_directories( + Protobuf + PRIVATE + ${protobuf_objc_source_dir}/google/protobuf + INTERFACE + ${PROJECT_BINARY_DIR}/Headers/Protobuf + PUBLIC + ${protobuf_objc_source_dir} + ) + target_compile_options( + Protobuf PRIVATE + -fno-objc-arc + -Wno-missing-noescape + ) + add_alias(libprotobuf_objc Protobuf) + +endif() diff --git a/Firestore/Source/API/FIRDocumentReference.mm b/Firestore/Source/API/FIRDocumentReference.mm index b76ebd853e0..4f90fa98cf9 100644 --- a/Firestore/Source/API/FIRDocumentReference.mm +++ b/Firestore/Source/API/FIRDocumentReference.mm @@ -39,7 +39,6 @@ #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" -#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; @@ -209,7 +208,7 @@ - (void)getDocumentWithSource:(FIRFirestoreSource)source - (id)addSnapshotListenerInternalWithOptions:(ListenOptions)internalOptions listener:(FIRDocumentSnapshotBlock) listener { - ListenerRegistration result = _documentReference.AddSnapshotListener( + std::unique_ptr result = _documentReference.AddSnapshotListener( std::move(internalOptions), [self wrapDocumentSnapshotBlock:listener]); return [[FSTListenerRegistration alloc] initWithRegistration:std::move(result)]; } diff --git a/Firestore/Source/API/FIRFirestore+Internal.h b/Firestore/Source/API/FIRFirestore+Internal.h index fb0719c9e6c..0a41b22d3a6 100644 --- a/Firestore/Source/API/FIRFirestore+Internal.h +++ b/Firestore/Source/API/FIRFirestore+Internal.h @@ -15,6 +15,7 @@ */ #import "FIRFirestore.h" +#import "FIRListenerRegistration.h" #include #include @@ -64,33 +65,29 @@ NS_ASSUME_NONNULL_BEGIN + (FIRFirestore *)recoverFromFirestore:(std::shared_ptr)firestore; +- (void)terminateInternalWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; + +- (const std::shared_ptr &)workerQueue; + /** - * Shuts down this `FIRFirestore` instance. - * - * After shutdown only the `clearPersistence` method may be used. Any other method - * will throw an error. - * - * To restart after shutdown, simply create a new instance of FIRFirestore with - * `firestore` or `firestoreForApp` methods. + * Attaches a listener for a snapshots-in-sync event. Server-generated + * updates and local changes can affect multiple snapshot listeners. + * The snapshots-in-sync event indicates that all listeners affected by + * a given change have fired. * - * Shutdown does not cancel any pending writes and any tasks that are awaiting a response from - * the server will not be resolved. The next time you start this instance, it will resume - * attempting to send these writes to the server. + * NOTE: The snapshots-in-sync event only indicates that listeners are + * in sync with each other, but does not relate to whether those + * snapshots are in sync with the server. Use SnapshotMetadata in the + * individual listeners to determine if a snapshot is from the cache or + * the server. * - * Note: Under normal circumstances, calling this method is not required. This - * method is useful only when you want to force this instance to release all of its resources or - * in combination with `clearPersistence` to ensure that all local state is destroyed - * between test runs. - * - * @param completion A block to execute once everything has shut down. + * @param listener A callback to be called every time all snapshot + * listeners are in sync with each other. + * @return A FIRListenerRegistration object that can be used to remove the + * listener. */ -// TODO(b/135755126): Make this public. -- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion - NS_SWIFT_NAME(shutdown(completion:)); - -- (void)shutdownInternalWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; - -- (const std::shared_ptr &)workerQueue; +- (id)addSnapshotsInSyncListener:(void (^)(void))listener + NS_SWIFT_NAME(addSnapshotsInSyncListener(_:)); @property(nonatomic, assign, readonly) std::shared_ptr wrapped; diff --git a/Firestore/Source/API/FIRFirestore.mm b/Firestore/Source/API/FIRFirestore.mm index 742483cc343..e85c423f1e9 100644 --- a/Firestore/Source/API/FIRFirestore.mm +++ b/Firestore/Source/API/FIRFirestore.mm @@ -30,6 +30,8 @@ #import "Firestore/Source/API/FIRCollectionReference+Internal.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" +#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" +#import "Firestore/Source/API/FIRQuery+Internal.h" #import "Firestore/Source/API/FIRTransaction+Internal.h" #import "Firestore/Source/API/FIRWriteBatch+Internal.h" #import "Firestore/Source/API/FSTFirestoreComponent.h" @@ -44,20 +46,30 @@ #include "Firestore/core/src/firebase/firestore/core/transaction.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/empty.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert_apple.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; using firebase::firestore::api::DocumentReference; using firebase::firestore::api::Firestore; +using firebase::firestore::api::ListenerRegistration; using firebase::firestore::api::ThrowIllegalState; using firebase::firestore::api::ThrowInvalidArgument; using firebase::firestore::auth::CredentialsProvider; +using firebase::firestore::core::EventListener; using firebase::firestore::model::DatabaseId; +using firebase::firestore::util::ObjcFailureHandler; using firebase::firestore::util::AsyncQueue; +using firebase::firestore::util::SetFailureHandler; +using firebase::firestore::util::StatusOr; +using firebase::firestore::util::Empty; NS_ASSUME_NONNULL_BEGIN @@ -75,6 +87,12 @@ @implementation FIRFirestore { __weak id _registry; } ++ (void)initialize { + if (self == [FIRFirestore class]) { + SetFailureHandler(ObjcFailureHandler); + } +} + + (instancetype)firestore { FIRApp *app = [FIRApp defaultApp]; if (!app) { @@ -186,7 +204,8 @@ - (FIRQuery *)collectionGroupWithID:(NSString *)collectionID { collectionID); } - return _firestore->GetCollectionGroup(util::MakeString(collectionID)); + auto query = _firestore->GetCollectionGroup(util::MakeString(collectionID)); + return [[FIRQuery alloc] initWithQuery:std::move(query) firestore:_firestore]; } - (FIRWriteBatch *)batch { @@ -277,6 +296,19 @@ - (void)clearPersistenceWithCompletion:(nullable void (^)(NSError *_Nullable err _firestore->ClearPersistence(util::MakeCallback(completion)); } +- (void)waitForPendingWritesWithCompletion:(void (^)(NSError *_Nullable error))completion { + _firestore->WaitForPendingWrites(util::MakeCallback(completion)); +} + +- (void)terminateWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + id strongRegistry = _registry; + if (strongRegistry) { + [strongRegistry + removeInstanceWithDatabase:util::MakeNSString(_firestore->database_id().database_id())]; + } + [self terminateInternalWithCompletion:completion]; +} + @end @implementation FIRFirestore (Internal) @@ -301,17 +333,16 @@ + (FIRFirestore *)recoverFromFirestore:(std::shared_ptr)firestore { return (__bridge FIRFirestore *)firestore->extension(); } -- (void)shutdownInternalWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - _firestore->Shutdown(util::MakeCallback(completion)); +- (void)terminateInternalWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + _firestore->Terminate(util::MakeCallback(completion)); } -- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - id strongRegistry = _registry; - if (strongRegistry) { - [strongRegistry - removeInstanceWithDatabase:util::MakeNSString(_firestore->database_id().database_id())]; - } - [self shutdownInternalWithCompletion:completion]; +- (id)addSnapshotsInSyncListener:(void (^)(void))listener { + std::unique_ptr> eventListener = + core::EventListener::Create([listener](const StatusOr &v) { listener(); }); + std::unique_ptr result = + _firestore->AddSnapshotsInSyncListener(std::move(eventListener)); + return [[FSTListenerRegistration alloc] initWithRegistration:std::move(result)]; } @end diff --git a/Firestore/Source/API/FIRListenerRegistration+Internal.h b/Firestore/Source/API/FIRListenerRegistration+Internal.h index 072bf559f5d..9630da89692 100644 --- a/Firestore/Source/API/FIRListenerRegistration+Internal.h +++ b/Firestore/Source/API/FIRListenerRegistration+Internal.h @@ -14,6 +14,8 @@ * limitations under the License. */ +#include + #import "FIRListenerRegistration.h" #include "Firestore/core/src/firebase/firestore/api/listener_registration.h" @@ -25,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN /** Private implementation of the FIRListenerRegistration protocol. */ @interface FSTListenerRegistration : NSObject -- (instancetype)initWithRegistration:(api::ListenerRegistration &&)registration; +- (instancetype)initWithRegistration:(std::unique_ptr)registration; @end diff --git a/Firestore/Source/API/FIRListenerRegistration.mm b/Firestore/Source/API/FIRListenerRegistration.mm index eef8b7b87dc..82b09d25c5b 100644 --- a/Firestore/Source/API/FIRListenerRegistration.mm +++ b/Firestore/Source/API/FIRListenerRegistration.mm @@ -14,23 +14,17 @@ * limitations under the License. */ -#include - #import "Firestore/Source/API/FIRListenerRegistration+Internal.h" -#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" - NS_ASSUME_NONNULL_BEGIN -using firebase::firestore::util::DelayedConstructor; - @implementation FSTListenerRegistration { - DelayedConstructor _registration; + std::unique_ptr _registration; } -- (instancetype)initWithRegistration:(api::ListenerRegistration &&)registration { +- (instancetype)initWithRegistration:(std::unique_ptr)registration { if (self = [super init]) { - _registration.Init(std::move(registration)); + _registration = std::move(registration); } return self; } diff --git a/Firestore/Source/API/FIRQuery.mm b/Firestore/Source/API/FIRQuery.mm index df6fee6f38e..24fcba61f67 100644 --- a/Firestore/Source/API/FIRQuery.mm +++ b/Firestore/Source/API/FIRQuery.mm @@ -33,13 +33,14 @@ #import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" #import "Firestore/Source/API/FSTUserDataConverter.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" #include "Firestore/core/src/firebase/firestore/api/input_validation.h" #include "Firestore/core/src/firebase/firestore/api/query_core.h" +#include "Firestore/core/src/firebase/firestore/api/query_listener_registration.h" #include "Firestore/core/src/firebase/firestore/core/bound.h" #include "Firestore/core/src/firebase/firestore/core/direction.h" #include "Firestore/core/src/firebase/firestore/core/filter.h" +#include "Firestore/core/src/firebase/firestore/core/firestore_client.h" #include "Firestore/core/src/firebase/firestore/core/order_by.h" #include "Firestore/core/src/firebase/firestore/core/query.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" @@ -56,6 +57,7 @@ using firebase::firestore::api::Firestore; using firebase::firestore::api::ListenerRegistration; using firebase::firestore::api::Query; +using firebase::firestore::api::QueryListenerRegistration; using firebase::firestore::api::QuerySnapshot; using firebase::firestore::api::SnapshotMetadata; using firebase::firestore::api::Source; @@ -178,15 +180,16 @@ - (void)getDocumentsWithSource:(FIRFirestoreSource)publicSource }); // Call the view_listener on the user Executor. - auto async_listener = AsyncEventListener::Create(firestore->client().userExecutor, - std::move(view_listener)); + auto async_listener = AsyncEventListener::Create( + firestore->client()->user_executor(), std::move(view_listener)); std::shared_ptr query_listener = - [firestore->client() listenToQuery:query options:internalOptions listener:async_listener]; + firestore->client()->ListenToQuery(query, internalOptions, async_listener); return [[FSTListenerRegistration alloc] - initWithRegistration:ListenerRegistration(firestore->client(), std::move(async_listener), - std::move(query_listener))]; + initWithRegistration:absl::make_unique(firestore->client(), + std::move(async_listener), + std::move(query_listener))]; } - (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value { diff --git a/Firestore/Source/API/FSTFirestoreComponent.mm b/Firestore/Source/API/FSTFirestoreComponent.mm index b162fa54d73..8ce2b1285c4 100644 --- a/Firestore/Source/API/FSTFirestoreComponent.mm +++ b/Firestore/Source/API/FSTFirestoreComponent.mm @@ -134,7 +134,7 @@ - (void)appWillBeDeleted:(FIRApp *)app { [_instances removeAllObjects]; } for (NSString *key in instances) { - [instances[key] shutdownInternalWithCompletion:nil]; + [instances[key] terminateInternalWithCompletion:nil]; } } diff --git a/Firestore/Source/CMakeLists.txt b/Firestore/Source/CMakeLists.txt new file mode 100644 index 00000000000..ef3f6a724d4 --- /dev/null +++ b/Firestore/Source/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright 2019 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. + +add_subdirectory(Public) +add_subdirectory(Remote) diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h deleted file mode 100644 index 34675a83f49..00000000000 --- a/Firestore/Source/Core/FSTFirestoreClient.h +++ /dev/null @@ -1,135 +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 - -#include -#include - -#import "Firestore/Source/Core/FSTTypes.h" - -#include "Firestore/core/src/firebase/firestore/api/document_reference.h" -#include "Firestore/core/src/firebase/firestore/api/document_snapshot.h" -#include "Firestore/core/src/firebase/firestore/api/query_core.h" -#include "Firestore/core/src/firebase/firestore/api/settings.h" -#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/core/listen_options.h" -#include "Firestore/core/src/firebase/firestore/core/query.h" -#include "Firestore/core/src/firebase/firestore/core/query_listener.h" -#include "Firestore/core/src/firebase/firestore/core/transaction.h" -#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" -#include "Firestore/core/src/firebase/firestore/model/database_id.h" -#include "Firestore/core/src/firebase/firestore/model/mutation.h" -#include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor.h" -#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" - -@class FIRDocumentReference; -@class FIRDocumentSnapshot; -@class FIRQuery; -@class FIRQuerySnapshot; -@class FSTDatabaseID; -@class FSTDatabaseInfo; -@class FSTTransaction; - -namespace api = firebase::firestore::api; -namespace auth = firebase::firestore::auth; -namespace core = firebase::firestore::core; -namespace model = firebase::firestore::model; -namespace util = firebase::firestore::util; - -NS_ASSUME_NONNULL_BEGIN - -/** - * FirestoreClient is a top-level class that constructs and owns all of the pieces of the client - * SDK architecture. It is responsible for creating the worker queue that is shared by all of the - * other components in the system. - */ -@interface FSTFirestoreClient : NSObject - -/** - * Creates and returns a FSTFirestoreClient with the given parameters. - * - * All callbacks and events will be triggered on the provided userExecutor. - */ -+ (instancetype)clientWithDatabaseInfo:(const core::DatabaseInfo &)databaseInfo - settings:(const api::Settings &)settings - credentialsProvider: - (std::shared_ptr)credentialsProvider - userExecutor:(std::shared_ptr)userExecutor - workerQueue:(std::shared_ptr)workerQueue; - -- (instancetype)init NS_UNAVAILABLE; - -/** Shuts down this client, cancels all writes / listeners, and releases all resources. */ -- (void)shutdownWithCallback:(util::StatusCallback)callback; - -/** Disables the network connection. Pending operations will not complete. */ -- (void)disableNetworkWithCallback:(util::StatusCallback)callback; - -/** Enables the network connection and requeues all pending operations. */ -- (void)enableNetworkWithCallback:(util::StatusCallback)callback; - -/** Starts listening to a query. */ -- (std::shared_ptr)listenToQuery:(core::Query)query - options:(core::ListenOptions)options - listener: - (core::ViewSnapshot::SharedListener &&)listener; - -/** Stops listening to a query previously listened to. */ -- (void)removeListener:(const std::shared_ptr &)listener; - -/** - * Retrieves a document from the cache via the indicated callback. If the doc - * doesn't exist, an error will be sent to the callback. - */ -- (void)getDocumentFromLocalCache:(const api::DocumentReference &)doc - callback:(api::DocumentSnapshot::Listener &&)callback; - -/** - * Retrieves a (possibly empty) set of documents from the cache via the - * indicated completion. - */ -- (void)getDocumentsFromLocalCache:(const api::Query &)query - callback:(api::QuerySnapshot::Listener &&)callback; - -/** Write mutations. callback will be notified when it's written to the backend. */ -- (void)writeMutations:(std::vector &&)mutations - callback:(util::StatusCallback)callback; - -/** Tries to execute the transaction in updateCallback up to retries times. */ -- (void)transactionWithRetries:(int)retries - updateCallback:(core::TransactionUpdateCallback)updateCallback - resultCallback:(core::TransactionResultCallback)resultCallback; - -/** The database ID of the databaseInfo this client was initialized with. */ -@property(nonatomic, assign, readonly) const model::DatabaseId &databaseID; - -/** - * 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. - */ -- (const std::shared_ptr &)userExecutor; - -/** For testing only. */ -- (const std::shared_ptr &)workerQueue; - -- (bool)isShutdown; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTFirestoreClient.mm b/Firestore/Source/Core/FSTFirestoreClient.mm deleted file mode 100644 index 6ba2125310b..00000000000 --- a/Firestore/Source/Core/FSTFirestoreClient.mm +++ /dev/null @@ -1,474 +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/FSTFirestoreClient.h" - -#include // NOLINT(build/c++11) -#include // NOLINT(build/c++11) -#include -#include - -#import "FIRFirestoreErrors.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRQuery+Internal.h" -#import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" -#import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" -#import "Firestore/Source/Core/FSTSyncEngine.h" -#import "Firestore/Source/Core/FSTView.h" -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTLevelDB.h" -#import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Local/FSTLocalStore.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" -#import "Firestore/Source/Remote/FSTSerializerBeta.h" -#import "Firestore/Source/Util/FSTClasses.h" - -#include "Firestore/core/src/firebase/firestore/api/settings.h" -#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/core/event_manager.h" -#include "Firestore/core/src/firebase/firestore/model/database_id.h" -#include "Firestore/core/src/firebase/firestore/model/document_set.h" -#include "Firestore/core/src/firebase/firestore/model/mutation.h" -#include "Firestore/core/src/firebase/firestore/remote/datastore.h" -#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" -#include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" -#include "Firestore/core/src/firebase/firestore/util/string_apple.h" -#include "absl/memory/memory.h" - -namespace util = firebase::firestore::util; -using firebase::firestore::Error; -using firebase::firestore::api::DocumentReference; -using firebase::firestore::api::DocumentSnapshot; -using firebase::firestore::api::Settings; -using firebase::firestore::api::SnapshotMetadata; -using firebase::firestore::api::ThrowIllegalState; -using firebase::firestore::auth::CredentialsProvider; -using firebase::firestore::auth::User; -using firebase::firestore::core::DatabaseInfo; -using firebase::firestore::core::ListenOptions; -using firebase::firestore::core::EventManager; -using firebase::firestore::core::Query; -using firebase::firestore::core::QueryListener; -using firebase::firestore::core::ViewSnapshot; -using firebase::firestore::local::LruParams; -using firebase::firestore::model::DatabaseId; -using firebase::firestore::model::Document; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::DocumentMap; -using firebase::firestore::model::MaybeDocument; -using firebase::firestore::model::MaybeDocumentMap; -using firebase::firestore::model::Mutation; -using firebase::firestore::model::OnlineState; -using firebase::firestore::remote::Datastore; -using firebase::firestore::remote::RemoteStore; -using firebase::firestore::util::Path; -using firebase::firestore::util::AsyncQueue; -using firebase::firestore::util::DelayedConstructor; -using firebase::firestore::util::DelayedOperation; -using firebase::firestore::util::Executor; -using firebase::firestore::util::Status; -using firebase::firestore::util::StatusOr; -using firebase::firestore::util::StatusOrCallback; -using firebase::firestore::util::TimerId; - -NS_ASSUME_NONNULL_BEGIN - -/** How long we wait to try running LRU GC after SDK initialization. */ -static const std::chrono::milliseconds FSTLruGcInitialDelay = std::chrono::minutes(1); -/** Minimum amount of time between GC checks, after the first one. */ -static const std::chrono::milliseconds FSTLruGcRegularDelay = std::chrono::minutes(5); - -@interface FSTFirestoreClient () { - DatabaseInfo _databaseInfo; -} - -- (instancetype)initWithDatabaseInfo:(const DatabaseInfo &)databaseInfo - settings:(const Settings &)settings - credentialsProvider:(std::shared_ptr)credentialsProvider - userExecutor:(std::shared_ptr)userExecutor - workerQueue:(std::shared_ptr)queue NS_DESIGNATED_INITIALIZER; - -@property(nonatomic, assign, readonly) const DatabaseInfo *databaseInfo; -@property(nonatomic, strong, readonly) id persistence; -@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; -@property(nonatomic, strong, readonly) FSTLocalStore *localStore; - -@end - -@implementation FSTFirestoreClient { - /** - * Async queue responsible for all of our internal processing. When we get incoming work from - * the user (via public API) or the network (incoming gRPC messages), we should always dispatch - * onto this queue. This ensures our internal data structures are never accessed from multiple - * threads simultaneously. - */ - std::shared_ptr _workerQueue; - - std::unique_ptr _remoteStore; - DelayedConstructor _eventManager; - - std::shared_ptr _userExecutor; - std::shared_ptr _credentialsProvider; - std::chrono::milliseconds _initialGcDelay; - std::chrono::milliseconds _regularGcDelay; - bool _gcHasRun; - _Nullable id _lruDelegate; - DelayedOperation _lruCallback; -} - -- (const std::shared_ptr &)userExecutor { - return _userExecutor; -} - -- (const std::shared_ptr &)workerQueue { - return _workerQueue; -} - -- (bool)isShutdown { - // Technically, the asyncQueue is still running, but only accepting tasks related to shutdown - // or supposed to be run after shutdown. It is effectively shut down to the eyes of users. - return _workerQueue->is_shutting_down(); -} - -+ (instancetype)clientWithDatabaseInfo:(const DatabaseInfo &)databaseInfo - settings:(const Settings &)settings - credentialsProvider:(std::shared_ptr)credentialsProvider - userExecutor:(std::shared_ptr)userExecutor - workerQueue:(std::shared_ptr)workerQueue { - return [[FSTFirestoreClient alloc] initWithDatabaseInfo:databaseInfo - settings:settings - credentialsProvider:std::move(credentialsProvider) - userExecutor:std::move(userExecutor) - workerQueue:std::move(workerQueue)]; -} - -- (instancetype)initWithDatabaseInfo:(const DatabaseInfo &)databaseInfo - settings:(const Settings &)settings - credentialsProvider:(std::shared_ptr)credentialsProvider - userExecutor:(std::shared_ptr)userExecutor - workerQueue:(std::shared_ptr)workerQueue { - if (self = [super init]) { - _databaseInfo = databaseInfo; - _credentialsProvider = std::move(credentialsProvider); - _userExecutor = std::move(userExecutor); - _workerQueue = std::move(workerQueue); - _gcHasRun = false; - _initialGcDelay = FSTLruGcInitialDelay; - _regularGcDelay = FSTLruGcRegularDelay; - - auto userPromise = std::make_shared>(); - bool initialized = false; - - __weak __typeof__(self) weakSelf = self; - auto credentialChangeListener = [initialized, userPromise, weakSelf](User user) mutable { - __typeof__(self) strongSelf = weakSelf; - if (!strongSelf) return; - - if (!initialized) { - initialized = true; - userPromise->set_value(user); - } else { - strongSelf->_workerQueue->Enqueue( - [strongSelf, user] { [strongSelf credentialDidChangeWithUser:user]; }); - } - }; - - _credentialsProvider->SetCredentialChangeListener(credentialChangeListener); - - // Defer initialization until we get the current user from the credentialChangeListener. This is - // guaranteed to be synchronously dispatched onto our worker queue, so we will be initialized - // before any subsequently queued work runs. - _workerQueue->Enqueue([self, userPromise, settings] { - User user = userPromise->get_future().get(); - [self initializeWithUser:user settings:settings]; - }); - } - return self; -} - -- (void)initializeWithUser:(const User &)user settings:(const Settings &)settings { - // Do all of our initialization on our own dispatch queue. - _workerQueue->VerifyIsCurrentQueue(); - LOG_DEBUG("Initializing. Current user: %s", user.uid()); - - // Note: The initialization work must all be synchronous (we can't dispatch more work) since - // external write/listen operations could get queued to run before that subsequent work - // completes. - if (settings.persistence_enabled()) { - Path dir = [FSTLevelDB storageDirectoryForDatabaseInfo:*self.databaseInfo - documentsDirectory:[FSTLevelDB documentsDirectory]]; - - FSTSerializerBeta *remoteSerializer = - [[FSTSerializerBeta alloc] initWithDatabaseID:self.databaseInfo->database_id()]; - FSTLocalSerializer *serializer = - [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; - FSTLevelDB *ldb; - Status levelDbStatus = - [FSTLevelDB dbWithDirectory:std::move(dir) - serializer:serializer - lruParams:LruParams::WithCacheSize(settings.cache_size_bytes()) - ptr:&ldb]; - if (!levelDbStatus.ok()) { - // If leveldb fails to start then just throw up our hands: the error is unrecoverable. - // There's nothing an end-user can do and nearly all failures indicate the developer is doing - // something grossly wrong so we should stop them cold in their tracks with a failure they - // can't ignore. - [NSException raise:NSInternalInconsistencyException - format:@"Failed to open DB: %s", levelDbStatus.ToString().c_str()]; - } - _lruDelegate = ldb.referenceDelegate; - _persistence = ldb; - if (settings.gc_enabled()) { - [self scheduleLruGarbageCollection]; - } - } else { - _persistence = [FSTMemoryPersistence persistenceWithEagerGC]; - } - - _localStore = [[FSTLocalStore alloc] initWithPersistence:_persistence initialUser:user]; - - auto datastore = - std::make_shared(*self.databaseInfo, _workerQueue, _credentialsProvider); - - _remoteStore = absl::make_unique( - _localStore, std::move(datastore), _workerQueue, - [self](OnlineState onlineState) { [self.syncEngine applyChangedOnlineState:onlineState]; }); - - _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore - remoteStore:_remoteStore.get() - initialUser:user]; - - _eventManager.Init(_syncEngine); - - // Setup wiring for remote store. - _remoteStore->set_sync_engine(_syncEngine); - - // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation - // queue, etc.) so must be started after LocalStore. - [_localStore start]; - _remoteStore->Start(); -} - -/** - * Schedules a callback to try running LRU garbage collection. Reschedules itself after the GC has - * run. - */ -- (void)scheduleLruGarbageCollection { - std::chrono::milliseconds delay = _gcHasRun ? _regularGcDelay : _initialGcDelay; - _lruCallback = _workerQueue->EnqueueAfterDelay(delay, TimerId::GarbageCollectionDelay, [self]() { - [self->_localStore collectGarbage:self->_lruDelegate.gc]; - self->_gcHasRun = true; - [self scheduleLruGarbageCollection]; - }); -} - -- (void)credentialDidChangeWithUser:(const User &)user { - _workerQueue->VerifyIsCurrentQueue(); - - LOG_DEBUG("Credential Changed. Current user: %s", user.uid()); - [self.syncEngine credentialDidChangeWithUser:user]; -} - -- (void)disableNetworkWithCallback:(util::StatusCallback)callback { - [self verifyNotShutdown]; - _workerQueue->Enqueue([self, callback] { - _remoteStore->DisableNetwork(); - if (callback) { - self->_userExecutor->Execute([=] { callback(Status::OK()); }); - } - }); -} - -- (void)enableNetworkWithCallback:(util::StatusCallback)callback { - [self verifyNotShutdown]; - _workerQueue->Enqueue([self, callback] { - _remoteStore->EnableNetwork(); - if (callback) { - self->_userExecutor->Execute([=] { callback(Status::OK()); }); - } - }); -} - -- (void)shutdownWithCallback:(util::StatusCallback)callback { - _workerQueue->EnqueueAndInitiateShutdown([self, callback] { - self->_credentialsProvider->SetCredentialChangeListener(nullptr); - - // If we've scheduled LRU garbage collection, cancel it. - if (self->_lruCallback) { - self->_lruCallback.Cancel(); - } - _remoteStore->Shutdown(); - [self.persistence shutdown]; - }); - - // This separate enqueue ensures if shutdown is called multiple times - // every time the callback is triggered. If it is in the above - // enqueue, it might not get executed because after first shutdown - // all operations are not executed. - _workerQueue->EnqueueEvenAfterShutdown([self, callback] { - if (callback) { - self->_userExecutor->Execute([=] { callback(Status::OK()); }); - } - }); -} - -- (void)verifyNotShutdown { - if (self.isShutdown) { - ThrowIllegalState("The client has already been shutdown."); - } -} - -- (std::shared_ptr)listenToQuery:(Query)query - options:(core::ListenOptions)options - listener:(ViewSnapshot::SharedListener &&)listener { - auto query_listener = - QueryListener::Create(std::move(query), std::move(options), std::move(listener)); - - _workerQueue->Enqueue( - [self, query_listener] { _eventManager->AddQueryListener(std::move(query_listener)); }); - - return query_listener; -} - -- (void)removeListener:(const std::shared_ptr &)listener { - // Checks for shutdown but does not throw error, allowing it to be an no-op if client is - // already shutdown. - if (self.isShutdown) { - return; - } - _workerQueue->Enqueue([self, listener] { _eventManager->RemoveQueryListener(listener); }); -} - -- (void)getDocumentFromLocalCache:(const DocumentReference &)doc - callback:(DocumentSnapshot::Listener &&)callback { - [self verifyNotShutdown]; - - // TODO(c++14): move `callback` into lambda. - auto shared_callback = absl::ShareUniquePtr(std::move(callback)); - _workerQueue->Enqueue([self, doc, shared_callback] { - absl::optional maybeDoc = [self.localStore readDocument:doc.key()]; - StatusOr maybe_snapshot; - - if (maybeDoc && maybeDoc->is_document()) { - Document document(*maybeDoc); - maybe_snapshot = DocumentSnapshot{doc.firestore(), doc.key(), document, - /*from_cache=*/true, - /*has_pending_writes=*/document.has_local_mutations()}; - } else if (maybeDoc && maybeDoc->is_no_document()) { - maybe_snapshot = DocumentSnapshot{doc.firestore(), doc.key(), absl::nullopt, - /*from_cache=*/true, - /*has_pending_writes=*/false}; - } else { - maybe_snapshot = - Status{Error::Unavailable, "Failed to get document from cache. (However, this document " - "may exist on the server. Run again without setting source to " - "FirestoreSourceCache to attempt to retrieve the document "}; - } - - if (shared_callback) { - self->_userExecutor->Execute([=] { shared_callback->OnEvent(std::move(maybe_snapshot)); }); - } - }); -} - -- (void)getDocumentsFromLocalCache:(const api::Query &)query - callback:(api::QuerySnapshot::Listener &&)callback { - [self verifyNotShutdown]; - - // TODO(c++14): move `callback` into lambda. - auto shared_callback = absl::ShareUniquePtr(std::move(callback)); - _workerQueue->Enqueue([self, query, shared_callback] { - DocumentMap docs = [self.localStore executeQuery:query.query()]; - - FSTView *view = [[FSTView alloc] initWithQuery:query.query() remoteDocuments:DocumentKeySet{}]; - FSTViewDocumentChanges *viewDocChanges = - [view computeChangesWithDocuments:docs.underlying_map()]; - FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; - HARD_ASSERT(viewChange.limboChanges.count == 0, - "View returned limbo documents during local-only query execution."); - HARD_ASSERT(viewChange.snapshot.has_value(), "Expected a snapshot"); - - ViewSnapshot snapshot = std::move(viewChange.snapshot).value(); - SnapshotMetadata metadata(snapshot.has_pending_writes(), snapshot.from_cache()); - - api::QuerySnapshot result(query.firestore(), query.query(), std::move(snapshot), - std::move(metadata)); - - if (shared_callback) { - self->_userExecutor->Execute([=] { shared_callback->OnEvent(std::move(result)); }); - } - }); -} - -- (void)writeMutations:(std::vector &&)mutations callback:(util::StatusCallback)callback { - [self verifyNotShutdown]; - // TODO(c++14): move `mutations` into lambda (C++14). - _workerQueue->Enqueue([self, mutations, callback]() mutable { - if (mutations.empty()) { - if (callback) { - self->_userExecutor->Execute([=] { callback(Status::OK()); }); - } - } else { - [self.syncEngine - writeMutations:std::move(mutations) - completion:^(NSError *error) { - // Dispatch the result back onto the user dispatch queue. - if (callback) { - self->_userExecutor->Execute([=] { callback(Status::FromNSError(error)); }); - } - }]; - } - }); -}; - -- (void)transactionWithRetries:(int)retries - updateCallback:(core::TransactionUpdateCallback)update_callback - resultCallback:(core::TransactionResultCallback)resultCallback { - [self verifyNotShutdown]; - // Dispatch the result back onto the user dispatch queue. - auto async_callback = [self, resultCallback](util::StatusOr maybe_value) { - if (resultCallback) { - self->_userExecutor->Execute([=] { resultCallback(std::move(maybe_value)); }); - } - }; - - _workerQueue->Enqueue([self, retries, update_callback, async_callback] { - [self.syncEngine transactionWithRetries:retries - workerQueue:_workerQueue - updateCallback:std::move(update_callback) - resultCallback:std::move(async_callback)]; - }); -} - -- (const DatabaseInfo *)databaseInfo { - return &_databaseInfo; -} - -- (const DatabaseId &)databaseID { - return _databaseInfo.database_id(); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSyncEngine.h b/Firestore/Source/Core/FSTSyncEngine.h deleted file mode 100644 index a982207495e..00000000000 --- a/Firestore/Source/Core/FSTSyncEngine.h +++ /dev/null @@ -1,109 +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 - -#include -#include - -#import "Firestore/Source/Core/FSTTypes.h" - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/core/query.h" -#include "Firestore/core/src/firebase/firestore/core/sync_engine_callback.h" -#include "Firestore/core/src/firebase/firestore/core/transaction.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" -#include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" - -@class FSTLocalStore; - -namespace auth = firebase::firestore::auth; -namespace core = firebase::firestore::core; -namespace model = firebase::firestore::model; -namespace remote = firebase::firestore::remote; -namespace util = firebase::firestore::util; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - SyncEngineCallback - -/** - * SyncEngine is the central controller in the client SDK architecture. It is the glue code - * between the EventManager, LocalStore, and RemoteStore. Some of SyncEngine's responsibilities - * include: - * 1. Coordinating client requests and remote events between the EventManager and the local and - * remote data stores. - * 2. Managing a View object for each query, providing the unified view between the local and - * remote data stores. - * 3. Notifying the RemoteStore when the LocalStore has new mutations in its queue that need - * sending to the backend. - * - * The SyncEngine’s methods should only ever be called by methods running on our own worker - * queue. - */ -@interface FSTSyncEngine : NSObject - -- (instancetype)init NS_UNAVAILABLE; -- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore - remoteStore:(remote::RemoteStore *)remoteStore - initialUser:(const auth::User &)user NS_DESIGNATED_INITIALIZER; - -- (void)setCallback:(core::SyncEngineCallback *)callback; - -/** - * Initiates a new listen. The FSTLocalStore will be queried for initial data and the listen will - * be sent to the `RemoteStore` to get remote data. The registered FSTSyncEngineDelegate will be - * notified of resulting view snapshots and/or listen errors. - * - * @return the target ID assigned to the query. - */ -- (model::TargetId)listenToQuery:(core::Query)query; - -/** Stops listening to a query previously listened to via listenToQuery:. */ -- (void)stopListeningToQuery:(const core::Query &)query; - -/** - * Initiates the write of local mutation batch which involves adding the writes to the mutation - * queue, notifying the remote store about new mutations, and raising events for any changes this - * write caused. The provided completion block will be called once the write has been acked or - * rejected by the backend (or failed locally for any other reason). - */ -- (void)writeMutations:(std::vector &&)mutations - completion:(FSTVoidErrorBlock)completion; - -/** - * Runs the given transaction block up to retries times and then calls completion. - * - * @param retries The number of times to try before giving up. - * @param workerQueue The queue to dispatch sync engine calls to. - * @param updateCallback The callback to call to execute the user's transaction. - * @param resultCallback The callback to call when the transaction is finished or failed. - */ -- (void)transactionWithRetries:(int)retries - workerQueue:(const std::shared_ptr &)workerQueue - updateCallback:(core::TransactionUpdateCallback)updateCallback - resultCallback:(core::TransactionResultCallback)resultCallback; - -- (void)credentialDidChangeWithUser:(const auth::User &)user; - -/** Applies an OnlineState change to the sync engine and notifies any views of the change. */ -- (void)applyChangedOnlineState:(model::OnlineState)onlineState; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSyncEngine.mm b/Firestore/Source/Core/FSTSyncEngine.mm deleted file mode 100644 index 9290879b605..00000000000 --- a/Firestore/Source/Core/FSTSyncEngine.mm +++ /dev/null @@ -1,674 +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/FSTSyncEngine.h" - -#include -#include -#include -#include -#include -#include - -#import "FIRFirestoreErrors.h" -#import "Firestore/Source/Core/FSTView.h" -#import "Firestore/Source/Local/FSTLocalStore.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - -#include "Firestore/core/include/firebase/firestore/firestore_errors.h" -#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/core/transaction.h" -#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" -#include "Firestore/core/src/firebase/firestore/local/local_view_changes.h" -#include "Firestore/core/src/firebase/firestore/local/local_write_result.h" -#include "Firestore/core/src/firebase/firestore/local/reference_set.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/document_map.h" -#include "Firestore/core/src/firebase/firestore/model/document_set.h" -#include "Firestore/core/src/firebase/firestore/model/no_document.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" -#include "Firestore/core/src/firebase/firestore/util/error_apple.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "absl/types/optional.h" - -using firebase::firestore::Error; -using firebase::firestore::auth::HashUser; -using firebase::firestore::auth::User; -using firebase::firestore::core::Query; -using firebase::firestore::core::SyncEngineCallback; -using firebase::firestore::core::TargetIdGenerator; -using firebase::firestore::core::Transaction; -using firebase::firestore::core::ViewSnapshot; -using firebase::firestore::local::LocalViewChanges; -using firebase::firestore::local::LocalWriteResult; -using firebase::firestore::local::ReferenceSet; -using firebase::firestore::model::BatchId; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::DocumentMap; -using firebase::firestore::model::ListenSequenceNumber; -using firebase::firestore::model::MaybeDocumentMap; -using firebase::firestore::model::Mutation; -using firebase::firestore::model::NoDocument; -using firebase::firestore::model::OnlineState; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::TargetId; -using firebase::firestore::remote::Datastore; -using firebase::firestore::remote::RemoteEvent; -using firebase::firestore::remote::RemoteStore; -using firebase::firestore::remote::TargetChange; -using firebase::firestore::util::AsyncQueue; -using firebase::firestore::util::MakeNSError; -using firebase::firestore::util::Status; - -NS_ASSUME_NONNULL_BEGIN - -// Limbo documents don't use persistence, and are eagerly GC'd. So, listens for them don't need -// real sequence numbers. -static const ListenSequenceNumber kIrrelevantSequenceNumber = -1; - -#pragma mark - FSTQueryView - -/** - * FSTQueryView contains all of the info that FSTSyncEngine needs to track for a particular - * query and view. - */ -@interface FSTQueryView : NSObject - -- (instancetype)initWithQuery:(Query)query - targetID:(TargetId)targetID - resumeToken:(NSData *)resumeToken - view:(FSTView *)view NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** The query itself. */ -- (const Query &)query; - -/** The targetID created by the client that is used in the watch stream to identify this query. */ -@property(nonatomic, assign, readonly) TargetId targetID; - -/** - * An identifier from the datastore backend that indicates the last state of the results that - * was received. This can be used to indicate where to continue receiving new doc changes for the - * query. - */ -@property(nonatomic, copy, readonly) NSData *resumeToken; - -/** - * The view is responsible for computing the final merged truth of what docs are in the query. - * It gets notified of local and remote changes, and applies the query filters and limits to - * determine the most correct possible results. - */ -@property(nonatomic, strong, readonly) FSTView *view; - -@end - -@implementation FSTQueryView { - Query _query; -} - -- (instancetype)initWithQuery:(Query)query - targetID:(TargetId)targetID - resumeToken:(NSData *)resumeToken - view:(FSTView *)view { - if (self = [super init]) { - _query = std::move(query); - _targetID = targetID; - _resumeToken = resumeToken; - _view = view; - } - return self; -} - -- (const Query &)query { - return _query; -} - -@end - -#pragma mark - LimboResolution - -/** Tracks a limbo resolution. */ -class LimboResolution { - public: - LimboResolution() { - } - - explicit LimboResolution(const DocumentKey &key) : key{key} { - } - - DocumentKey key; - - /** - * Set to true once we've received a document. This is used in remoteKeysForTarget and - * ultimately used by `WatchChangeAggregator` to decide whether it needs to manufacture a delete - * event for the target once the target is CURRENT. - */ - bool document_received = false; -}; - -#pragma mark - FSTSyncEngine - -@interface FSTSyncEngine () - -/** The local store, used to persist mutations and cached documents. */ -@property(nonatomic, strong, readonly) FSTLocalStore *localStore; - -@end - -@implementation FSTSyncEngine { - /** The remote store for sending writes, watches, etc. to the backend. */ - RemoteStore *_remoteStore; - - /** - * A callback to be notified when queries being listened to produce new view snapshots or errors. - */ - SyncEngineCallback *_callback; - - /** Used for creating the TargetId for the listens used to resolve limbo documents. */ - TargetIdGenerator _targetIdGenerator; - - /** Stores user completion blocks, indexed by user and BatchId. */ - std::unordered_map *, HashUser> - _mutationCompletionBlocks; - - /** FSTQueryViews for all active queries, indexed by query. */ - std::unordered_map _queryViewsByQuery; - - /** FSTQueryViews for all active queries, indexed by target ID. */ - std::unordered_map _queryViewsByTarget; - - /** - * When a document is in limbo, we create a special listen to resolve it. This maps the - * DocumentKey of each limbo document to the TargetId of the listen resolving it. - */ - std::map _limboTargetsByKey; - - /** - * Basically the inverse of limboTargetsByKey, a map of target ID to a LimboResolution (which - * includes the DocumentKey as well as whether we've received a document for the target). - */ - std::map _limboResolutionsByTarget; - - User _currentUser; - - /** Used to track any documents that are currently in limbo. */ - ReferenceSet _limboDocumentRefs; -} - -- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore - remoteStore:(RemoteStore *)remoteStore - initialUser:(const User &)initialUser { - if (self = [super init]) { - _localStore = localStore; - _remoteStore = remoteStore; - - _targetIdGenerator = TargetIdGenerator::SyncEngineTargetIdGenerator(); - _currentUser = initialUser; - } - return self; -} - -- (void)setCallback:(SyncEngineCallback *)callback { - _callback = callback; -} - -- (TargetId)listenToQuery:(Query)query { - [self assertCallbackExistsForSelector:_cmd]; - HARD_ASSERT(_queryViewsByQuery.find(query) == _queryViewsByQuery.end(), - "We already listen to query: %s", query.ToString()); - - FSTQueryData *queryData = [self.localStore allocateQuery:query]; - ViewSnapshot viewSnapshot = [self initializeViewAndComputeSnapshotForQueryData:queryData]; - _callback->OnViewSnapshots({viewSnapshot}); - - _remoteStore->Listen(queryData); - return queryData.targetID; -} - -- (ViewSnapshot)initializeViewAndComputeSnapshotForQueryData:(FSTQueryData *)queryData { - DocumentMap docs = [self.localStore executeQuery:queryData.query]; - DocumentKeySet remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID]; - - FSTView *view = [[FSTView alloc] initWithQuery:queryData.query - remoteDocuments:std::move(remoteKeys)]; - FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs.underlying_map()]; - FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; - HARD_ASSERT(viewChange.limboChanges.count == 0, - "View returned limbo docs before target ack from the server."); - - FSTQueryView *queryView = [[FSTQueryView alloc] initWithQuery:queryData.query - targetID:queryData.targetID - resumeToken:queryData.resumeToken - view:view]; - _queryViewsByQuery[queryData.query] = queryView; - _queryViewsByTarget[queryData.targetID] = queryView; - - HARD_ASSERT(viewChange.snapshot.has_value(), - "applyChangesToDocuments for new view should always return a snapshot"); - return viewChange.snapshot.value(); -} - -- (void)stopListeningToQuery:(const Query &)query { - [self assertCallbackExistsForSelector:_cmd]; - - FSTQueryView *queryView = _queryViewsByQuery[query]; - HARD_ASSERT(queryView, "Trying to stop listening to a query not found"); - - [self.localStore releaseQuery:query]; - _remoteStore->StopListening(queryView.targetID); - [self removeAndCleanupQuery:queryView]; -} - -- (void)writeMutations:(std::vector &&)mutations - completion:(FSTVoidErrorBlock)completion { - [self assertCallbackExistsForSelector:_cmd]; - - LocalWriteResult result = [self.localStore locallyWriteMutations:std::move(mutations)]; - [self addMutationCompletionBlock:completion batchID:result.batch_id()]; - - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:result.changes() remoteEvent:absl::nullopt]; - _remoteStore->FillWritePipeline(); -} - -- (void)addMutationCompletionBlock:(FSTVoidErrorBlock)completion batchID:(BatchId)batchID { - NSMutableDictionary *completionBlocks = - _mutationCompletionBlocks[_currentUser]; - if (!completionBlocks) { - completionBlocks = [NSMutableDictionary dictionary]; - _mutationCompletionBlocks[_currentUser] = completionBlocks; - } - [completionBlocks setObject:completion forKey:@(batchID)]; -} - -/** - * Takes an updateCallback in which a set of reads and writes can be performed atomically. In the - * updateCallback, user code can read and write values using a transaction object. After the - * updateCallback, all changes will be committed. If someone else has changed any of the data - * referenced, then the updateCallback will be called again. If the updateCallback still fails after - * the given number of retries, then the transaction will be rejected. - * - * The transaction object passed to the updateCallback contains methods for accessing documents - * and collections. Unlike other firestore access, data accessed with the transaction will not - * reflect local changes that have not been committed. For this reason, it is required that all - * reads are performed before any writes. Transactions must be performed while online. - */ -- (void)transactionWithRetries:(int)retries - workerQueue:(const std::shared_ptr &)workerQueue - updateCallback:(core::TransactionUpdateCallback)updateCallback - resultCallback:(core::TransactionResultCallback)resultCallback { - workerQueue->VerifyIsCurrentQueue(); - HARD_ASSERT(retries >= 0, "Got negative number of retries for transaction"); - - std::shared_ptr transaction = _remoteStore->CreateTransaction(); - updateCallback(transaction, [=](util::StatusOr maybe_result) { - workerQueue->Enqueue( - [self, retries, workerQueue, updateCallback, resultCallback, transaction, maybe_result] { - if (!maybe_result.ok()) { - if (retries > 0 && [self isRetryableTransactionError:maybe_result.status()] && - !transaction->IsPermanentlyFailed()) { - return [self transactionWithRetries:(retries - 1) - workerQueue:workerQueue - updateCallback:updateCallback - resultCallback:resultCallback]; - } else { - resultCallback(std::move(maybe_result)); - } - } else { - transaction->Commit([self, retries, workerQueue, updateCallback, resultCallback, - maybe_result, transaction](Status status) { - if (status.ok()) { - resultCallback(std::move(maybe_result)); - return; - } - - if (retries > 0 && [self isRetryableTransactionError:status] && - !transaction->IsPermanentlyFailed()) { - workerQueue->VerifyIsCurrentQueue(); - return [self transactionWithRetries:(retries - 1) - workerQueue:workerQueue - updateCallback:updateCallback - resultCallback:resultCallback]; - } - resultCallback(std::move(status)); - }); - } - }); - }); -} - -- (void)applyRemoteEvent:(const RemoteEvent &)remoteEvent { - [self assertCallbackExistsForSelector:_cmd]; - - // Update `receivedDocument` as appropriate for any limbo targets. - for (const auto &entry : remoteEvent.target_changes()) { - TargetId targetID = entry.first; - const TargetChange &change = entry.second; - const auto iter = _limboResolutionsByTarget.find(targetID); - if (iter != _limboResolutionsByTarget.end()) { - LimboResolution &limboResolution = iter->second; - // Since this is a limbo resolution lookup, it's for a single document and it could be - // added, modified, or removed, but not a combination. - HARD_ASSERT(change.added_documents().size() + change.modified_documents().size() + - change.removed_documents().size() <= - 1, - "Limbo resolution for single document contains multiple changes."); - - if (change.added_documents().size() > 0) { - limboResolution.document_received = true; - } else if (change.modified_documents().size() > 0) { - HARD_ASSERT(limboResolution.document_received, - "Received change for limbo target document without add."); - } else if (change.removed_documents().size() > 0) { - HARD_ASSERT(limboResolution.document_received, - "Received remove for limbo target document without add."); - limboResolution.document_received = false; - } else { - // This was probably just a CURRENT targetChange or similar. - } - } - } - - MaybeDocumentMap changes = [self.localStore applyRemoteEvent:remoteEvent]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:remoteEvent]; -} - -- (void)applyChangedOnlineState:(OnlineState)onlineState { - [self assertCallbackExistsForSelector:_cmd]; - - std::vector newViewSnapshots; - for (const auto &entry : _queryViewsByQuery) { - FSTQueryView *queryView = entry.second; - FSTViewChange *viewChange = [queryView.view applyChangedOnlineState:onlineState]; - HARD_ASSERT(viewChange.limboChanges.count == 0, - "OnlineState should not affect limbo documents."); - if (viewChange.snapshot.has_value()) { - newViewSnapshots.push_back(std::move(viewChange.snapshot.value())); - } - } - - _callback->OnViewSnapshots(std::move(newViewSnapshots)); - _callback->HandleOnlineStateChange(onlineState); -} - -- (void)rejectListenWithTargetID:(const TargetId)targetID error:(NSError *)error { - [self assertCallbackExistsForSelector:_cmd]; - - const auto iter = _limboResolutionsByTarget.find(targetID); - if (iter != _limboResolutionsByTarget.end()) { - const DocumentKey limboKey = iter->second.key; - // Since this query failed, we won't want to manually unlisten to it. - // So go ahead and remove it from bookkeeping. - _limboTargetsByKey.erase(limboKey); - _limboResolutionsByTarget.erase(targetID); - - // TODO(dimond): Retry on transient errors? - - // It's a limbo doc. Create a synthetic event saying it was deleted. This is kind of a hack. - // Ideally, we would have a method in the local store to purge a document. However, it would - // be tricky to keep all of the local store's invariants with another method. - NoDocument doc(limboKey, SnapshotVersion::None(), /* has_committed_mutations= */ false); - DocumentKeySet limboDocuments = DocumentKeySet{limboKey}; - RemoteEvent event{SnapshotVersion::None(), /*target_changes=*/{}, /*target_mismatches=*/{}, - /*document_updates=*/{{limboKey, doc}}, std::move(limboDocuments)}; - [self applyRemoteEvent:event]; - } else { - auto found = _queryViewsByTarget.find(targetID); - HARD_ASSERT(found != _queryViewsByTarget.end(), "Unknown targetId: %s", targetID); - FSTQueryView *queryView = found->second; - const Query &query = queryView.query; - [self.localStore releaseQuery:query]; - [self removeAndCleanupQuery:queryView]; - if ([self errorIsInteresting:error]) { - LOG_WARN("Listen for query at %s failed: %s", query.path().CanonicalString(), - error.localizedDescription); - } - _callback->OnError(query, Status::FromNSError(error)); - } -} - -- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { - [self assertCallbackExistsForSelector:_cmd]; - - // The local store may or may not be able to apply the write result and raise events immediately - // (depending on whether the watcher is caught up), so we raise user callbacks first so that they - // consistently happen before listen events. - [self processUserCallbacksForBatchID:batchResult.batch.batchID error:nil]; - - MaybeDocumentMap changes = [self.localStore acknowledgeBatchWithResult:batchResult]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:absl::nullopt]; -} - -- (void)rejectFailedWriteWithBatchID:(BatchId)batchID error:(NSError *)error { - [self assertCallbackExistsForSelector:_cmd]; - MaybeDocumentMap changes = [self.localStore rejectBatchID:batchID]; - - if (!changes.empty() && [self errorIsInteresting:error]) { - const DocumentKey &minKey = changes.min()->first; - LOG_WARN("Write at %s failed: %s", minKey.ToString(), error.localizedDescription); - } - - // The local store may or may not be able to apply the write result and raise events immediately - // (depending on whether the watcher is caught up), so we raise user callbacks first so that they - // consistently happen before listen events. - [self processUserCallbacksForBatchID:batchID error:error]; - - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:absl::nullopt]; -} - -- (void)processUserCallbacksForBatchID:(BatchId)batchID error:(NSError *_Nullable)error { - NSMutableDictionary *completionBlocks = - _mutationCompletionBlocks[_currentUser]; - - // NOTE: Mutations restored from persistence won't have completion blocks, so it's okay for - // this (or the completion below) to be nil. - if (completionBlocks) { - NSNumber *boxedBatchID = @(batchID); - FSTVoidErrorBlock completion = completionBlocks[boxedBatchID]; - if (completion) { - completion(error); - [completionBlocks removeObjectForKey:boxedBatchID]; - } - } -} - -- (void)assertCallbackExistsForSelector:(SEL)methodSelector { - HARD_ASSERT(_callback, "Tried to call '%s' before callback was registered.", - NSStringFromSelector(methodSelector)); -} - -- (void)removeAndCleanupQuery:(FSTQueryView *)queryView { - _queryViewsByQuery.erase(queryView.query); - _queryViewsByTarget.erase(queryView.targetID); - - DocumentKeySet limboKeys = _limboDocumentRefs.ReferencedKeys(queryView.targetID); - _limboDocumentRefs.RemoveReferences(queryView.targetID); - for (const DocumentKey &key : limboKeys) { - if (!_limboDocumentRefs.ContainsKey(key)) { - // We removed the last reference for this key. - [self removeLimboTargetForKey:key]; - } - } -} - -/** - * Computes a new snapshot from the changes and calls the registered callback with the new snapshot. - */ -- (void)emitNewSnapshotsAndNotifyLocalStoreWithChanges:(const MaybeDocumentMap &)changes - remoteEvent:(const absl::optional &) - maybeRemoteEvent { - std::vector newSnapshots; - std::vector documentChangesInAllViews; - - for (const auto &entry : _queryViewsByQuery) { - FSTQueryView *queryView = entry.second; - FSTView *view = queryView.view; - FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:changes]; - if (viewDocChanges.needsRefill) { - // The query has a limit and some docs were removed/updated, so we need to re-run the - // query against the local store to make sure we didn't lose any good docs that had been - // past the limit. - DocumentMap docs = [self.localStore executeQuery:queryView.query]; - viewDocChanges = [view computeChangesWithDocuments:docs.underlying_map() - previousChanges:viewDocChanges]; - } - - absl::optional targetChange; - if (maybeRemoteEvent.has_value()) { - const RemoteEvent &remoteEvent = maybeRemoteEvent.value(); - auto it = remoteEvent.target_changes().find(queryView.targetID); - if (it != remoteEvent.target_changes().end()) { - targetChange = it->second; - } - } - FSTViewChange *viewChange = [queryView.view applyChangesToDocuments:viewDocChanges - targetChange:targetChange]; - - [self updateTrackedLimboDocumentsWithChanges:viewChange.limboChanges - targetID:queryView.targetID]; - - if (viewChange.snapshot.has_value()) { - newSnapshots.push_back(viewChange.snapshot.value()); - LocalViewChanges docChanges = - LocalViewChanges::FromViewSnapshot(viewChange.snapshot.value(), queryView.targetID); - documentChangesInAllViews.push_back(std::move(docChanges)); - } - } - - _callback->OnViewSnapshots(std::move(newSnapshots)); - [self.localStore notifyLocalViewChanges:documentChangesInAllViews]; -} - -/** Updates the limbo document state for the given targetID. */ -- (void)updateTrackedLimboDocumentsWithChanges:(NSArray *)limboChanges - targetID:(TargetId)targetID { - for (FSTLimboDocumentChange *limboChange in limboChanges) { - switch (limboChange.type) { - case FSTLimboDocumentChangeTypeAdded: - _limboDocumentRefs.AddReference(limboChange.key, targetID); - [self trackLimboChange:limboChange]; - break; - - case FSTLimboDocumentChangeTypeRemoved: - LOG_DEBUG("Document no longer in limbo: %s", limboChange.key.ToString()); - _limboDocumentRefs.RemoveReference(limboChange.key, targetID); - if (!_limboDocumentRefs.ContainsKey(limboChange.key)) { - // We removed the last reference for this key - [self removeLimboTargetForKey:limboChange.key]; - } - break; - - default: - HARD_FAIL("Unknown limbo change type: %s", limboChange.type); - } - } -} - -- (void)trackLimboChange:(FSTLimboDocumentChange *)limboChange { - DocumentKey key{limboChange.key}; - - if (_limboTargetsByKey.find(key) == _limboTargetsByKey.end()) { - LOG_DEBUG("New document in limbo: %s", key.ToString()); - TargetId limboTargetID = _targetIdGenerator.NextId(); - Query query(key.path()); - FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:std::move(query) - targetID:limboTargetID - listenSequenceNumber:kIrrelevantSequenceNumber - purpose:FSTQueryPurposeLimboResolution]; - _limboResolutionsByTarget.emplace(limboTargetID, LimboResolution{key}); - _remoteStore->Listen(queryData); - _limboTargetsByKey[key] = limboTargetID; - } -} - -- (void)removeLimboTargetForKey:(const DocumentKey &)key { - const auto iter = _limboTargetsByKey.find(key); - if (iter == _limboTargetsByKey.end()) { - // This target already got removed, because the query failed. - return; - } - TargetId limboTargetID = iter->second; - _remoteStore->StopListening(limboTargetID); - _limboTargetsByKey.erase(key); - _limboResolutionsByTarget.erase(limboTargetID); -} - -// Used for testing -- (std::map)currentLimboDocuments { - // Return defensive copy - return _limboTargetsByKey; -} - -- (void)credentialDidChangeWithUser:(const firebase::firestore::auth::User &)user { - BOOL userChanged = (_currentUser != user); - _currentUser = user; - - if (userChanged) { - // Notify local store and emit any resulting events from swapping out the mutation queue. - MaybeDocumentMap changes = [self.localStore userDidChange:user]; - [self emitNewSnapshotsAndNotifyLocalStoreWithChanges:changes remoteEvent:absl::nullopt]; - } - - // Notify remote store so it can restart its streams. - _remoteStore->HandleCredentialChange(); -} - -- (DocumentKeySet)remoteKeysForTarget:(TargetId)targetId { - const auto iter = _limboResolutionsByTarget.find(targetId); - if (iter != _limboResolutionsByTarget.end() && iter->second.document_received) { - return DocumentKeySet{iter->second.key}; - } else { - auto found = _queryViewsByTarget.find(targetId); - FSTQueryView *queryView = found != _queryViewsByTarget.end() ? found->second : nil; - return queryView ? queryView.view.syncedDocuments : DocumentKeySet{}; - } -} - -/** - * Decides if the error likely represents a developer mistake such as forgetting to create an index - * or permission denied. Used to decide whether an error is worth automatically logging as a - * warning. - */ -- (BOOL)errorIsInteresting:(NSError *)error { - if (error.domain == FIRFirestoreErrorDomain) { - if (error.code == FIRFirestoreErrorCodeFailedPrecondition && - [error.localizedDescription containsString:@"requires an index"]) { - return YES; - } else if (error.code == FIRFirestoreErrorCodePermissionDenied) { - return YES; - } - } - - return NO; -} - -- (BOOL)isRetryableTransactionError:(const Status &)error { - // In transactions, the backend will fail outdated reads with FAILED_PRECONDITION and - // non-matching document versions with ABORTED. These errors should be retried. - Error code = error.code(); - return code == Error::Aborted || code == Error::FailedPrecondition || - !Datastore::IsPermanentError(error); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h deleted file mode 100644 index 7f864643f08..00000000000 --- a/Firestore/Source/Core/FSTView.h +++ /dev/null @@ -1,167 +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 - -#include "Firestore/core/src/firebase/firestore/core/view_snapshot.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/document_map.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "absl/types/optional.h" - -namespace firebase { -namespace firestore { -namespace remote { - -class TargetChange; - -} // namespace remote -} // namespace firestore -} // namespace firebase - -namespace core = firebase::firestore::core; -namespace model = firebase::firestore::model; -namespace remote = firebase::firestore::remote; - -NS_ASSUME_NONNULL_BEGIN - -#pragma mark - FSTViewDocumentChanges - -/** The result of applying a set of doc changes to a view. */ -@interface FSTViewDocumentChanges : NSObject - -- (instancetype)init NS_UNAVAILABLE; - -- (const model::DocumentKeySet &)mutatedKeys; - -/** The new set of docs that should be in the view. */ -- (const model::DocumentSet &)documentSet; - -/** The diff of this these docs with the previous set of docs. */ -- (const core::DocumentViewChangeSet &)changeSet; - -/** - * Whether the set of documents passed in was not sufficient to calculate the new state of the view - * and there needs to be another pass based on the local cache. - */ -@property(nonatomic, assign, readonly) BOOL needsRefill; - -@end - -#pragma mark - FSTLimboDocumentChange - -typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { - FSTLimboDocumentChangeTypeAdded = 0, - FSTLimboDocumentChangeTypeRemoved, -}; - -// A change to a particular document wrt to whether it is in "limbo". -@interface FSTLimboDocumentChange : NSObject - -+ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(model::DocumentKey)key; - -- (id)init __attribute__((unavailable("Use a static constructor method."))); - -- (const model::DocumentKey &)key; - -@property(nonatomic, assign, readonly) FSTLimboDocumentChangeType type; -@end - -#pragma mark - FSTViewChange - -// A set of changes to a view. -@interface FSTViewChange : NSObject - -- (id)init __attribute__((unavailable("Use a static constructor method."))); - -- (absl::optional &)snapshot; -@property(nonatomic, strong, readonly) NSArray *limboChanges; -@end - -#pragma mark - FSTView - -/** - * View is responsible for computing the final merged truth of what docs are in a query. It gets - * notified of local and remote changes to docs, and applies the query filters and limits to - * determine the most correct possible results. - */ -@interface FSTView : NSObject - -- (instancetype)init NS_UNAVAILABLE; - -- (instancetype)initWithQuery:(core::Query)query - remoteDocuments:(model::DocumentKeySet)remoteDocuments NS_DESIGNATED_INITIALIZER; - -/** - * Iterates over a set of doc changes, applies the query limit, and computes what the new results - * should be, what the changes were, and whether we may need to go back to the local cache for - * more results. Does not make any changes to the view. - * - * @param docChanges The doc changes to apply to this view. - * @return a new set of docs, changes, and refill flag. - */ -- (FSTViewDocumentChanges *)computeChangesWithDocuments:(const model::MaybeDocumentMap &)docChanges; - -/** - * Iterates over a set of doc changes, applies the query limit, and computes what the new results - * should be, what the changes were, and whether we may need to go back to the local cache for - * more results. Does not make any changes to the view. - * - * @param docChanges The doc changes to apply to this view. - * @param previousChanges If this is being called with a refill, then start with this set of docs - * and changes instead of the current view. - * @return a new set of docs, changes, and refill flag. - */ -- (FSTViewDocumentChanges *)computeChangesWithDocuments:(const model::MaybeDocumentMap &)docChanges - previousChanges: - (nullable FSTViewDocumentChanges *)previousChanges; - -/** - * Updates the view with the given ViewDocumentChanges. - * - * @param docChanges The set of changes to make to the view's docs. - * @return A new FSTViewChange with the given docs, changes, and sync state. - */ -- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges; - -/** - * Updates the view with the given FSTViewDocumentChanges and updates limbo docs and sync state from - * the given (optional) target change. - * - * @param docChanges The set of changes to make to the view's docs. - * @param targetChange A target change to apply for computing limbo docs and sync state. - * @return A new FSTViewChange with the given docs, changes, and sync state. - */ -- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges - targetChange: - (const absl::optional &)targetChange; - -/** - * Applies an OnlineState change to the view, potentially generating an FSTViewChange if the - * view's syncState changes as a result. - */ -- (FSTViewChange *)applyChangedOnlineState:(model::OnlineState)onlineState; - -/** - * The set of remote documents that the server has told us belongs to the target associated with - * this view. - */ -- (const model::DocumentKeySet &)syncedDocuments; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.mm b/Firestore/Source/Core/FSTView.mm deleted file mode 100644 index e3710fb4547..00000000000 --- a/Firestore/Source/Core/FSTView.mm +++ /dev/null @@ -1,536 +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/FSTView.h" - -#include -#include -#include - -#include "Firestore/core/src/firebase/firestore/core/query.h" -#include "Firestore/core/src/firebase/firestore/core/view_snapshot.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/document_set.h" -#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" -#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" - -namespace util = firebase::firestore::util; -using firebase::firestore::core::DocumentViewChange; -using firebase::firestore::core::DocumentViewChangeSet; -using firebase::firestore::core::Query; -using firebase::firestore::core::SyncState; -using firebase::firestore::core::ViewSnapshot; -using firebase::firestore::model::Document; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::DocumentSet; -using firebase::firestore::model::MaybeDocument; -using firebase::firestore::model::MaybeDocumentMap; -using firebase::firestore::model::OnlineState; -using firebase::firestore::remote::TargetChange; -using firebase::firestore::util::ComparisonResult; -using firebase::firestore::util::DelayedConstructor; - -NS_ASSUME_NONNULL_BEGIN - -namespace { - -int GetDocumentViewChangeTypePosition(DocumentViewChange::Type changeType) { - switch (changeType) { - case DocumentViewChange::Type::kRemoved: - return 0; - case DocumentViewChange::Type::kAdded: - return 1; - case DocumentViewChange::Type::kModified: - return 2; - case DocumentViewChange::Type::kMetadata: - // A metadata change is converted to a modified change at the public API layer. Since we sort - // by document key and then change type, metadata and modified changes must be sorted - // equivalently. - return 2; - } - HARD_FAIL("Unknown DocumentViewChange::Type %s", changeType); -} - -} // namespace - -#pragma mark - FSTViewDocumentChanges - -/** The result of applying a set of doc changes to a view. */ -@interface FSTViewDocumentChanges () - -- (instancetype)initWithDocumentSet:(DocumentSet)documentSet - changeSet:(DocumentViewChangeSet &&)changeSet - needsRefill:(BOOL)needsRefill - mutatedKeys:(DocumentKeySet)mutatedKeys NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTViewDocumentChanges { - DelayedConstructor _documentSet; - DocumentKeySet _mutatedKeys; - DocumentViewChangeSet _changeSet; -} - -- (instancetype)initWithDocumentSet:(DocumentSet)documentSet - changeSet:(DocumentViewChangeSet &&)changeSet - needsRefill:(BOOL)needsRefill - mutatedKeys:(DocumentKeySet)mutatedKeys { - self = [super init]; - if (self) { - _documentSet.Init(std::move(documentSet)); - _changeSet = std::move(changeSet); - _needsRefill = needsRefill; - _mutatedKeys = std::move(mutatedKeys); - } - return self; -} - -- (const DocumentKeySet &)mutatedKeys { - return _mutatedKeys; -} - -- (const firebase::firestore::model::DocumentSet &)documentSet { - return *_documentSet; -} - -- (const firebase::firestore::core::DocumentViewChangeSet &)changeSet { - return _changeSet; -} - -@end - -#pragma mark - FSTLimboDocumentChange - -@interface FSTLimboDocumentChange () - -+ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(DocumentKey)key; - -- (instancetype)initWithType:(FSTLimboDocumentChangeType)type - key:(DocumentKey)key NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTLimboDocumentChange { - DocumentKey _key; -} - -+ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(DocumentKey)key { - return [[FSTLimboDocumentChange alloc] initWithType:type key:std::move(key)]; -} - -- (instancetype)initWithType:(FSTLimboDocumentChangeType)type key:(DocumentKey)key { - self = [super init]; - if (self) { - _type = type; - _key = std::move(key); - } - return self; -} - -- (const DocumentKey &)key { - return _key; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } - if (![other isKindOfClass:[FSTLimboDocumentChange class]]) { - return NO; - } - FSTLimboDocumentChange *otherChange = (FSTLimboDocumentChange *)other; - return self.type == otherChange.type && self.key == otherChange.key; -} - -- (NSUInteger)hash { - NSUInteger hash = self.type; - hash = hash * 31u + self.key.Hash(); - return hash; -} - -@end - -#pragma mark - FSTViewChange - -@interface FSTViewChange () - -+ (FSTViewChange *)changeWithSnapshot:(absl::optional &&)snapshot - limboChanges:(NSArray *)limboChanges; - -- (instancetype)initWithSnapshot:(absl::optional &&)snapshot - limboChanges:(NSArray *)limboChanges - NS_DESIGNATED_INITIALIZER; - -@end - -@implementation FSTViewChange { - absl::optional _snapshot; -} - -+ (FSTViewChange *)changeWithSnapshot:(absl::optional &&)snapshot - limboChanges:(NSArray *)limboChanges { - return [[self alloc] initWithSnapshot:std::move(snapshot) limboChanges:limboChanges]; -} - -- (instancetype)initWithSnapshot:(absl::optional &&)snapshot - limboChanges:(NSArray *)limboChanges { - self = [super init]; - if (self) { - _snapshot = std::move(snapshot); - _limboChanges = limboChanges; - } - return self; -} - -- (absl::optional &)snapshot { - return _snapshot; -} - -@end - -#pragma mark - FSTView - -@interface FSTView () - -@property(nonatomic, assign) firebase::firestore::core::SyncState syncState; - -/** - * A flag whether the view is current with the backend. A view is considered current after it - * has seen the current flag from the backend and did not lose consistency within the watch stream - * (e.g. because of an existence filter mismatch). - */ -@property(nonatomic, assign, getter=isCurrent) BOOL current; - -@end - -@implementation FSTView { - Query _query; - - DelayedConstructor _documentSet; - - /** Documents included in the remote target. */ - DocumentKeySet _syncedDocuments; - - /** Documents in the view but not in the remote target */ - DocumentKeySet _limboDocuments; - - /** Document Keys that have local changes. */ - DocumentKeySet _mutatedKeys; -} - -- (instancetype)initWithQuery:(Query)query remoteDocuments:(DocumentKeySet)remoteDocuments { - self = [super init]; - if (self) { - _query = std::move(query); - _documentSet.Init(_query.Comparator()); - _syncedDocuments = std::move(remoteDocuments); - } - return self; -} - -- (ComparisonResult)compare:(const Document &)document with:(const Document &)otherDocument { - return _documentSet->comparator().Compare(document, otherDocument); -} - -- (const DocumentKeySet &)syncedDocuments { - return _syncedDocuments; -} - -- (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap &)docChanges { - return [self computeChangesWithDocuments:docChanges previousChanges:nil]; -} - -- (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap &)docChanges - previousChanges: - (nullable FSTViewDocumentChanges *)previousChanges { - DocumentViewChangeSet changeSet; - if (previousChanges) { - changeSet = previousChanges.changeSet; - } - DocumentSet oldDocumentSet = previousChanges ? previousChanges.documentSet : *_documentSet; - - DocumentKeySet newMutatedKeys = previousChanges ? previousChanges.mutatedKeys : _mutatedKeys; - DocumentKeySet oldMutatedKeys = _mutatedKeys; - DocumentSet newDocumentSet = oldDocumentSet; - BOOL needsRefill = NO; - - // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an - // update moving a doc past the old limit) might mean there is some other document in the local - // cache that either should come (1) between the old last limit doc and the new last document, - // in the case of updates, or (2) after the new last document, in the case of deletes. So we - // keep this doc at the old limit to compare the updates to. - // - // Note that this should never get used in a refill (when previousChanges is set), because there - // will only be adds -- no deletes or updates. - absl::optional lastDocInLimit; - if (_query.limit() != Query::kNoLimit && oldDocumentSet.size() == _query.limit()) { - lastDocInLimit = oldDocumentSet.GetLastDocument(); - } - - for (const auto &kv : docChanges) { - const DocumentKey &key = kv.first; - const MaybeDocument &maybeNewDoc = kv.second; - - absl::optional oldDoc = oldDocumentSet.GetDocument(key); - absl::optional newDoc; - if (maybeNewDoc.is_document()) { - newDoc = Document(maybeNewDoc); - } - if (newDoc) { - HARD_ASSERT(key == newDoc->key(), "Mismatching key in document changes: %s != %s", - key.ToString(), newDoc->key().ToString()); - if (!_query.Matches(*newDoc)) { - newDoc = absl::nullopt; - } - } - - bool oldDocHadPendingMutations = oldDoc && oldMutatedKeys.contains(key); - - // We only consider committed mutations for documents that were mutated during the lifetime of - // the view. - bool newDocHasPendingMutations = - newDoc && (newDoc->has_local_mutations() || - (oldMutatedKeys.contains(key) && newDoc->has_committed_mutations())); - - bool changeApplied = false; - // Calculate change - if (oldDoc && newDoc) { - bool docsEqual = oldDoc->data() == newDoc->data(); - if (!docsEqual) { - if (![self shouldWaitForSyncedDocument:*newDoc oldDocument:*oldDoc]) { - changeSet.AddChange(DocumentViewChange{*newDoc, DocumentViewChange::Type::kModified}); - changeApplied = true; - - if (lastDocInLimit && util::Descending([self compare:*newDoc with:*lastDocInLimit])) { - // This doc moved from inside the limit to after the limit. That means there may be - // some doc in the local cache that's actually less than this one. - needsRefill = true; - } - } - } else if (oldDocHadPendingMutations != newDocHasPendingMutations) { - changeSet.AddChange(DocumentViewChange{*newDoc, DocumentViewChange::Type::kMetadata}); - changeApplied = true; - } - - } else if (!oldDoc && newDoc) { - changeSet.AddChange(DocumentViewChange{*newDoc, DocumentViewChange::Type::kAdded}); - changeApplied = true; - } else if (oldDoc && !newDoc) { - changeSet.AddChange(DocumentViewChange{*oldDoc, DocumentViewChange::Type::kRemoved}); - changeApplied = true; - - if (lastDocInLimit) { - // A doc was removed from a full limit query. We'll need to re-query from the local cache - // to see if we know about some other doc that should be in the results. - needsRefill = true; - } - } - - if (changeApplied) { - if (newDoc) { - newDocumentSet = newDocumentSet.insert(newDoc); - if (newDoc->has_local_mutations()) { - newMutatedKeys = newMutatedKeys.insert(key); - } else { - newMutatedKeys = newMutatedKeys.erase(key); - } - } else { - newDocumentSet = newDocumentSet.erase(key); - newMutatedKeys = newMutatedKeys.erase(key); - } - } - } - - int32_t limit = _query.limit(); - if (limit != Query::kNoLimit && newDocumentSet.size() > limit) { - for (size_t i = newDocumentSet.size() - limit; i > 0; --i) { - const Document &oldDoc = *newDocumentSet.GetLastDocument(); - newDocumentSet = newDocumentSet.erase(oldDoc.key()); - newMutatedKeys = newMutatedKeys.erase(oldDoc.key()); - changeSet.AddChange(DocumentViewChange{oldDoc, DocumentViewChange::Type::kRemoved}); - } - } - - HARD_ASSERT(!needsRefill || !previousChanges, - "View was refilled using docs that themselves needed refilling."); - - return [[FSTViewDocumentChanges alloc] initWithDocumentSet:std::move(newDocumentSet) - changeSet:std::move(changeSet) - needsRefill:needsRefill - mutatedKeys:newMutatedKeys]; -} - -- (BOOL)shouldWaitForSyncedDocument:(const Document &)newDoc oldDocument:(const Document &)oldDoc { - // We suppress the initial change event for documents that were modified as part of a write - // acknowledgment (e.g. when the value of a server transform is applied) as Watch will send us - // the same document again. By suppressing the event, we only raise two user visible events (one - // with `hasPendingWrites` and the final state of the document) instead of three (one with - // `hasPendingWrites`, the modified document with `hasPendingWrites` and the final state of the - // document). - return (oldDoc.has_local_mutations() && newDoc.has_committed_mutations() && - !newDoc.has_local_mutations()); -} - -- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges { - return [self applyChangesToDocuments:docChanges targetChange:{}]; -} - -- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges - targetChange:(const absl::optional &)targetChange { - HARD_ASSERT(!docChanges.needsRefill, "Cannot apply changes that need a refill"); - - DocumentSet oldDocuments = *_documentSet; - *_documentSet = docChanges.documentSet; - _mutatedKeys = docChanges.mutatedKeys; - - // Sort changes based on type and query comparator. - std::vector changes = docChanges.changeSet.GetChanges(); - std::sort(changes.begin(), changes.end(), - [self](const DocumentViewChange &lhs, const DocumentViewChange &rhs) { - int pos1 = GetDocumentViewChangeTypePosition(lhs.type()); - int pos2 = GetDocumentViewChangeTypePosition(rhs.type()); - if (pos1 != pos2) { - return pos1 < pos2; - } - return util::Ascending([self compare:lhs.document() with:rhs.document()]); - }); - - [self applyTargetChange:targetChange]; - NSArray *limboChanges = [self updateLimboDocuments]; - BOOL synced = _limboDocuments.empty() && self.isCurrent; - SyncState newSyncState = synced ? SyncState::Synced : SyncState::Local; - bool syncStateChanged = newSyncState != self.syncState; - self.syncState = newSyncState; - - if (changes.empty() && !syncStateChanged) { - // No changes. - return [FSTViewChange changeWithSnapshot:absl::nullopt limboChanges:limboChanges]; - } else { - ViewSnapshot snapshot{_query, - docChanges.documentSet, - oldDocuments, - std::move(changes), - docChanges.mutatedKeys, - /*from_cache=*/newSyncState == SyncState::Local, - syncStateChanged, - /*excludes_metadata_changes=*/false}; - - return [FSTViewChange changeWithSnapshot:std::move(snapshot) limboChanges:limboChanges]; - } -} - -- (FSTViewChange *)applyChangedOnlineState:(OnlineState)onlineState { - if (self.isCurrent && onlineState == OnlineState::Offline) { - // If we're offline, set `current` to NO and then call applyChanges to refresh our syncState - // and generate an FSTViewChange as appropriate. We are guaranteed to get a new `TargetChange` - // that sets `current` back to YES once the client is back online. - self.current = NO; - return [self applyChangesToDocuments:[[FSTViewDocumentChanges alloc] - initWithDocumentSet:*_documentSet - changeSet:DocumentViewChangeSet {} - needsRefill:NO - mutatedKeys:_mutatedKeys]]; - } else { - // No effect, just return a no-op FSTViewChange. - return [[FSTViewChange alloc] initWithSnapshot:absl::nullopt limboChanges:@[]]; - } -} - -#pragma mark - Private methods - -/** 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 (_syncedDocuments.contains(key)) { - return NO; - } - // The local store doesn't think it's a result, so it shouldn't be in limbo. - if (!_documentSet->ContainsKey(key)) { - return NO; - } - // If there are local changes to the doc, they might explain why the server doesn't know that it's - // part of the query. So don't put it in limbo. - // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific - // query. - if (_documentSet->GetDocument(key)->has_local_mutations()) { - return NO; - } - // Everything else is in limbo. - return YES; -} - -/** - * Updates syncedDocuments and current based on the given change. - */ -- (void)applyTargetChange:(const absl::optional &)maybeTargetChange { - if (maybeTargetChange.has_value()) { - const TargetChange &target_change = maybeTargetChange.value(); - - for (const DocumentKey &key : target_change.added_documents()) { - _syncedDocuments = _syncedDocuments.insert(key); - } - for (const DocumentKey &key : target_change.modified_documents()) { - HARD_ASSERT(_syncedDocuments.find(key) != _syncedDocuments.end(), - "Modified document %s not found in view.", key.ToString()); - } - for (const DocumentKey &key : target_change.removed_documents()) { - _syncedDocuments = _syncedDocuments.erase(key); - } - - self.current = target_change.current(); - } -} - -/** Updates limboDocuments and returns any changes as FSTLimboDocumentChanges. */ -- (NSArray *)updateLimboDocuments { - // We can only determine limbo documents when we're in-sync with the server. - if (!self.isCurrent) { - return @[]; - } - - // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents. - DocumentKeySet oldLimboDocuments = std::move(_limboDocuments); - _limboDocuments = DocumentKeySet{}; - for (const Document &doc : *_documentSet) { - if ([self shouldBeLimboDocumentKey:doc.key()]) { - _limboDocuments = _limboDocuments.insert(doc.key()); - } - } - - // Diff the new limbo docs with the old limbo docs. - NSMutableArray *changes = - [NSMutableArray arrayWithCapacity:(oldLimboDocuments.size() + _limboDocuments.size())]; - for (const DocumentKey &key : oldLimboDocuments) { - if (!_limboDocuments.contains(key)) { - [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved - key:key]]; - } - } - for (const DocumentKey &key : _limboDocuments) { - if (!oldLimboDocuments.contains(key)) { - [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded - key:key]]; - } - } - return changes; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLRUGarbageCollector.h b/Firestore/Source/Local/FSTLRUGarbageCollector.h deleted file mode 100644 index 42c91c1d50e..00000000000 --- a/Firestore/Source/Local/FSTLRUGarbageCollector.h +++ /dev/null @@ -1,161 +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. - */ - -#import - -#include -#include - -#import "Firestore/Source/Local/FSTQueryData.h" - -#include "Firestore/core/src/firebase/firestore/api/settings.h" -#include "Firestore/core/src/firebase/firestore/local/query_cache.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" - -@class FSTLRUGarbageCollector; - -namespace model = firebase::firestore::model; - -extern const model::ListenSequenceNumber kFSTListenSequenceNumberInvalid; - -namespace firebase { -namespace firestore { -namespace local { - -struct LruParams { - static LruParams Default() { - return LruParams{100 * 1024 * 1024, 10, 1000}; - } - - static LruParams Disabled() { - return LruParams{api::Settings::CacheSizeUnlimited, 0, 0}; - } - - static LruParams WithCacheSize(int64_t cacheSize) { - LruParams params = Default(); - params.minBytesThreshold = cacheSize; - return params; - } - - int64_t minBytesThreshold; - int percentileToCollect; - int maximumSequenceNumbersToCollect; -}; - -struct LruResults { - static LruResults DidNotRun() { - return LruResults{/* didRun= */ false, 0, 0, 0}; - } - - bool didRun; - int sequenceNumbersCollected; - int targetsRemoved; - int documentsRemoved; -}; - -} // namespace local -} // namespace firestore -} // namespace firebase - -namespace local = firebase::firestore::local; - -/** - * Persistence layers intending to use LRU Garbage collection should implement this protocol. This - * protocol defines the operations that the LRU garbage collector needs from the persistence layer. - */ -@protocol FSTLRUDelegate - -/** - * Enumerates all the targets that the delegate is aware of. This is typically all of the targets in - * an FSTQueryCache. - */ -- (void)enumerateTargetsUsingCallback:(const local::TargetCallback &)callback; - -/** - * Enumerates all of the outstanding mutations. - */ -- (void)enumerateMutationsUsingCallback:(const local::OrphanedDocumentCallback &)callback; - -/** - * Removes all unreferenced documents from the cache that have a sequence number less than or equal - * to the given sequence number. Returns the number of documents removed. - */ -- (int)removeOrphanedDocumentsThroughSequenceNumber:(model::ListenSequenceNumber)sequenceNumber; - -/** - * Removes all targets that are not currently being listened to and have a sequence number less than - * or equal to the given sequence number. Returns the number of targets removed. - */ -- (int)removeTargetsThroughSequenceNumber:(model::ListenSequenceNumber)sequenceNumber - liveQueries: - (const std::unordered_map &) - liveQueries; - -- (size_t)byteSize; - -/** Returns the number of targets and orphaned documents cached. */ -- (size_t)sequenceNumberCount; - -/** Access to the underlying LRU Garbage collector instance. */ -@property(strong, nonatomic, readonly) FSTLRUGarbageCollector *gc; - -@end - -/** - * FSTLRUGarbageCollector defines the LRU algorithm used to clean up old documents and targets. It - * is persistence-agnostic, as long as proper delegate is provided. - */ -@interface FSTLRUGarbageCollector : NSObject - -- (instancetype)initWithDelegate:(id)delegate - params:(local::LruParams)params NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** - * Given a target percentile, return the number of queries that make up that percentage of the - * queries that are cached. For instance, if 20 queries are cached, and the percentile is 40, the - * result will be 8. - */ -- (int)queryCountForPercentile:(NSUInteger)percentile; - -/** - * Given a number of queries n, return the nth sequence number in the cache. - */ -- (model::ListenSequenceNumber)sequenceNumberForQueryCount:(NSUInteger)queryCount; - -/** - * Removes queries that are not currently live (as indicated by presence in the liveQueries map) and - * have a sequence number less than or equal to the given sequence number. - */ -- (int)removeQueriesUpThroughSequenceNumber:(model::ListenSequenceNumber)sequenceNumber - liveQueries: - (const std::unordered_map &) - liveQueries; - -/** - * Removes all unreferenced documents from the cache that have a sequence number less than or equal - * to the given sequence number. Returns the number of documents removed. - */ -- (int)removeOrphanedDocumentsThroughSequenceNumber:(model::ListenSequenceNumber)sequenceNumber; - -- (size_t)byteSize; - -- (local::LruResults)collectWithLiveTargets: - (const std::unordered_map &)liveTargets; - -@end diff --git a/Firestore/Source/Local/FSTLRUGarbageCollector.mm b/Firestore/Source/Local/FSTLRUGarbageCollector.mm deleted file mode 100644 index b86ea1e132f..00000000000 --- a/Firestore/Source/Local/FSTLRUGarbageCollector.mm +++ /dev/null @@ -1,192 +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. - */ - -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" - -#include //NOLINT(build/c++11) -#include -#include - -#import "Firestore/Source/Local/FSTPersistence.h" -#include "Firestore/core/include/firebase/firestore/timestamp.h" -#include "Firestore/core/src/firebase/firestore/api/settings.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" - -namespace api = firebase::firestore::api; -using Millis = std::chrono::milliseconds; -using firebase::Timestamp; -using firebase::firestore::local::LruParams; -using firebase::firestore::local::LruResults; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::ListenSequenceNumber; -using firebase::firestore::model::TargetId; - -const ListenSequenceNumber kFSTListenSequenceNumberInvalid = -1; - -static Millis::rep millisecondsBetween(const Timestamp &start, const Timestamp &end) { - return std::chrono::duration_cast(end.ToTimePoint() - start.ToTimePoint()).count(); -} - -namespace { - -/** - * RollingSequenceNumberBuffer tracks the nth sequence number in a series. Sequence numbers may be - * added out of order. - */ -class RollingSequenceNumberBuffer { - public: - explicit RollingSequenceNumberBuffer(size_t max_elements) - : queue_(std::priority_queue()), max_elements_(max_elements) { - } - - RollingSequenceNumberBuffer(const RollingSequenceNumberBuffer &other) = delete; - - RollingSequenceNumberBuffer &operator=(const RollingSequenceNumberBuffer &other) = delete; - - void AddElement(ListenSequenceNumber sequence_number) { - if (queue_.size() < max_elements_) { - queue_.push(sequence_number); - } else { - ListenSequenceNumber highestValue = queue_.top(); - if (sequence_number < highestValue) { - queue_.pop(); - queue_.push(sequence_number); - } - } - } - - ListenSequenceNumber max_value() const { - return queue_.top(); - } - - size_t size() const { - return queue_.size(); - } - - private: - std::priority_queue queue_; - const size_t max_elements_; -}; - -} // namespace - -@implementation FSTLRUGarbageCollector { - __weak id _delegate; - LruParams _params; -} - -- (instancetype)initWithDelegate:(id)delegate params:(LruParams)params { - self = [super init]; - if (self) { - _delegate = delegate; - _params = std::move(params); - } - return self; -} - -- (LruResults)collectWithLiveTargets: - (const std::unordered_map &)liveTargets { - if (_params.minBytesThreshold == api::Settings::CacheSizeUnlimited) { - LOG_DEBUG("Garbage collection skipped; disabled"); - return LruResults::DidNotRun(); - } - - size_t currentSize = [self byteSize]; - if (currentSize < _params.minBytesThreshold) { - // Not enough on disk to warrant collection. Wait another timeout cycle. - LOG_DEBUG("Garbage collection skipped; Cache size %s is lower than threshold %s", currentSize, - _params.minBytesThreshold); - return LruResults::DidNotRun(); - } else { - LOG_DEBUG("Running garbage collection on cache of size: %s", currentSize); - return [self runGCWithLiveTargets:liveTargets]; - } -} - -- (LruResults)runGCWithLiveTargets: - (const std::unordered_map &)liveTargets { - Timestamp start = Timestamp::Now(); - int sequenceNumbers = [self queryCountForPercentile:_params.percentileToCollect]; - // Cap at the configured max - if (sequenceNumbers > _params.maximumSequenceNumbersToCollect) { - sequenceNumbers = _params.maximumSequenceNumbersToCollect; - } - Timestamp countedTargets = Timestamp::Now(); - - ListenSequenceNumber upperBound = [self sequenceNumberForQueryCount:sequenceNumbers]; - Timestamp foundUpperBound = Timestamp::Now(); - - int numTargetsRemoved = [self removeQueriesUpThroughSequenceNumber:upperBound - liveQueries:liveTargets]; - Timestamp removedTargets = Timestamp::Now(); - - int numDocumentsRemoved = [self removeOrphanedDocumentsThroughSequenceNumber:upperBound]; - Timestamp removedDocuments = Timestamp::Now(); - - std::string desc = "LRU Garbage Collection:\n"; - absl::StrAppend(&desc, "\tCounted targets in ", millisecondsBetween(start, countedTargets), - "ms\n"); - absl::StrAppend(&desc, "\tDetermined least recently used ", sequenceNumbers, - " sequence numbers in ", millisecondsBetween(countedTargets, foundUpperBound), - "ms\n"); - absl::StrAppend(&desc, "\tRemoved ", numTargetsRemoved, " targets in ", - millisecondsBetween(foundUpperBound, removedTargets), "ms\n"); - absl::StrAppend(&desc, "\tRemoved ", numDocumentsRemoved, " documents in ", - millisecondsBetween(removedTargets, removedDocuments), "ms\n"); - absl::StrAppend(&desc, "Total duration: ", millisecondsBetween(start, removedDocuments), "ms"); - LOG_DEBUG(desc.c_str()); - - return LruResults{/* didRun= */ true, sequenceNumbers, numTargetsRemoved, numDocumentsRemoved}; -} - -- (int)queryCountForPercentile:(NSUInteger)percentile { - size_t totalCount = [_delegate sequenceNumberCount]; - int setSize = (int)((percentile / 100.0f) * totalCount); - return setSize; -} - -- (ListenSequenceNumber)sequenceNumberForQueryCount:(NSUInteger)queryCount { - if (queryCount == 0) { - return kFSTListenSequenceNumberInvalid; - } - RollingSequenceNumberBuffer buffer(queryCount); - - [_delegate enumerateTargetsUsingCallback:[&buffer](FSTQueryData *queryData) { - buffer.AddElement(queryData.sequenceNumber); - }]; - [_delegate enumerateMutationsUsingCallback:[&buffer](const DocumentKey &docKey, - ListenSequenceNumber sequenceNumber) { - buffer.AddElement(sequenceNumber); - }]; - return buffer.max_value(); -} - -- (int)removeQueriesUpThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber - liveQueries:(const std::unordered_map &) - liveQueries { - return [_delegate removeTargetsThroughSequenceNumber:sequenceNumber liveQueries:liveQueries]; -} - -- (int)removeOrphanedDocumentsThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber { - return [_delegate removeOrphanedDocumentsThroughSequenceNumber:sequenceNumber]; -} - -- (size_t)byteSize { - return [_delegate byteSize]; -} - -@end diff --git a/Firestore/Source/Local/FSTLevelDB.h b/Firestore/Source/Local/FSTLevelDB.h deleted file mode 100644 index 97ce91cf7b9..00000000000 --- a/Firestore/Source/Local/FSTLevelDB.h +++ /dev/null @@ -1,94 +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 - -#include -#include -#include - -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#include "Firestore/core/src/firebase/firestore/core/database_info.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" -#include "Firestore/core/src/firebase/firestore/util/path.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" -#include "leveldb/db.h" - -@class FSTLocalSerializer; - -namespace core = firebase::firestore::core; -namespace local = firebase::firestore::local; -namespace util = firebase::firestore::util; - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTLevelDBLRUDelegate : NSObject -@end - -/** A LevelDB-backed instance of FSTPersistence. */ -// TODO(mikelehen): Rename to FSTLevelDBPersistence. -@interface FSTLevelDB : NSObject - -/** - * Creates a LevelDB in the given directory and sets `ptr` to point to it. Return value indicates - * success in creating the leveldb instance and must be checked before accessing `ptr`. C++ note: - * Once FSTLevelDB is ported to C++, this factory method should return StatusOr<>. It cannot - * currently do that because ObjC references are not allowed in StatusOr. - */ -+ (util::Status)dbWithDirectory:(util::Path)directory - serializer:(FSTLocalSerializer *)serializer - lruParams:(local::LruParams)lruParams - ptr:(FSTLevelDB *_Nullable *_Nonnull)ptr; - -- (instancetype)init NS_UNAVAILABLE; - -/** Finds a suitable directory to serve as the root of all Firestore local storage. */ -+ (util::Path)documentsDirectory; - -/** - * Computes a unique storage directory for the given identifying components of local storage. - * - * @param databaseInfo The identifying information for the local storage instance. - * @param documentsDirectory The root document directory relative to which the storage directory - * will be created. Usually just +[FSTLevelDB documentsDir]. - * @return A storage directory unique to the instance identified by databaseInfo. - */ -+ (util::Path)storageDirectoryForDatabaseInfo:(const core::DatabaseInfo &)databaseInfo - documentsDirectory:(const util::Path &)documentsDirectory; - -/** - * @return A standard set of read options - */ -+ (const leveldb::ReadOptions)standardReadOptions; - -+ (util::Status)clearPersistence:(const core::DatabaseInfo &)databaseInfo; - -/** The native db pointer, allocated during start. */ -@property(nonatomic, assign, readonly) leveldb::DB *ptr; - -@property(nonatomic, readonly) local::LevelDbTransaction *currentTransaction; - -@property(nonatomic, readonly) const std::set &users; - -@property(nonatomic, readonly, strong) FSTLevelDBLRUDelegate *referenceDelegate; - -@property(nonatomic, readonly, strong) FSTLocalSerializer *serializer; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm deleted file mode 100644 index 10134a516ac..00000000000 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ /dev/null @@ -1,531 +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/Local/FSTLevelDB.h" - -#include -#include -#include - -#import "FIRFirestoreErrors.h" -#import "Firestore/Source/Remote/FSTSerializerBeta.h" - -#include "Firestore/core/include/firebase/firestore/firestore_errors.h" -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/core/database_info.h" -#include "Firestore/core/src/firebase/firestore/local/index_manager.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_migrations.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" -#include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" -#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" -#include "Firestore/core/src/firebase/firestore/local/reference_set.h" -#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.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/resource_path.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/util/filesystem.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" -#include "Firestore/core/src/firebase/firestore/util/ordered_code.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" -#include "Firestore/core/src/firebase/firestore/util/string_apple.h" -#include "Firestore/core/src/firebase/firestore/util/string_util.h" -#include "absl/memory/memory.h" -#include "absl/strings/match.h" -#include "absl/strings/str_cat.h" -#include "leveldb/db.h" - -NS_ASSUME_NONNULL_BEGIN - -namespace util = firebase::firestore::util; -using firebase::firestore::Error; -using firebase::firestore::auth::User; -using firebase::firestore::core::DatabaseInfo; -using firebase::firestore::local::ConvertStatus; -using firebase::firestore::local::IndexManager; -using firebase::firestore::local::LevelDbDocumentMutationKey; -using firebase::firestore::local::LevelDbDocumentTargetKey; -using firebase::firestore::local::LevelDbIndexManager; -using firebase::firestore::local::LevelDbMigrations; -using firebase::firestore::local::LevelDbMutationKey; -using firebase::firestore::local::LevelDbMutationQueue; -using firebase::firestore::local::LevelDbQueryCache; -using firebase::firestore::local::LevelDbRemoteDocumentCache; -using firebase::firestore::local::LevelDbTransaction; -using firebase::firestore::local::ListenSequence; -using firebase::firestore::local::LruParams; -using firebase::firestore::local::OrphanedDocumentCallback; -using firebase::firestore::local::ReferenceSet; -using firebase::firestore::local::RemoteDocumentCache; -using firebase::firestore::local::TargetCallback; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::ListenSequenceNumber; -using firebase::firestore::model::ResourcePath; -using firebase::firestore::model::TargetId; -using firebase::firestore::util::OrderedCode; -using firebase::firestore::util::Path; -using firebase::firestore::util::Status; -using firebase::firestore::util::StatusOr; -using firebase::firestore::util::StringFormat; -using leveldb::DB; -using leveldb::Options; -using leveldb::ReadOptions; -using leveldb::WriteOptions; - -static const char *kReservedPathComponent = "firestore"; - -@interface FSTLevelDB () - -- (size_t)byteSize; - -@property(nonatomic, assign, getter=isStarted) BOOL started; - -- (LevelDbQueryCache *)queryCache; - -- (LevelDbMutationQueue *)mutationQueueForUser:(const User &)user; - -@end - -/** - * Provides LRU functionality for leveldb persistence. - * - * Although this could implement FSTTransactional, it doesn't because it is not directly tied to - * a transaction runner, it just happens to be called from FSTLevelDB, which is FSTTransactional. - */ -@interface FSTLevelDBLRUDelegate () - -- (void)transactionWillStart; - -- (void)transactionWillCommit; - -- (void)start; - -@end - -@implementation FSTLevelDBLRUDelegate { - FSTLRUGarbageCollector *_gc; - // This delegate should have the same lifetime as the persistence layer, but mark as - // weak to avoid retain cycle. - __weak FSTLevelDB *_db; - ReferenceSet *_additionalReferences; - ListenSequenceNumber _currentSequenceNumber; - // PORTING NOTE: doesn't need to be a pointer once this class is ported to C++. - std::unique_ptr _listenSequence; -} - -- (instancetype)initWithPersistence:(FSTLevelDB *)persistence lruParams:(LruParams)lruParams { - if (self = [super init]) { - _gc = [[FSTLRUGarbageCollector alloc] initWithDelegate:self params:lruParams]; - _db = persistence; - _currentSequenceNumber = kFSTListenSequenceNumberInvalid; - } - return self; -} - -- (void)start { - ListenSequenceNumber highestSequenceNumber = _db.queryCache->highest_listen_sequence_number(); - _listenSequence = absl::make_unique(highestSequenceNumber); -} - -- (void)transactionWillStart { - HARD_ASSERT(_currentSequenceNumber == kFSTListenSequenceNumberInvalid, - "Previous sequence number is still in effect"); - _currentSequenceNumber = _listenSequence->Next(); -} - -- (void)transactionWillCommit { - _currentSequenceNumber = kFSTListenSequenceNumberInvalid; -} - -- (ListenSequenceNumber)currentSequenceNumber { - HARD_ASSERT(_currentSequenceNumber != kFSTListenSequenceNumberInvalid, - "Asking for a sequence number outside of a transaction"); - return _currentSequenceNumber; -} - -- (void)addInMemoryPins:(ReferenceSet *)set { - // We should be able to assert that _additionalReferences is nil, but due to restarts in spec - // tests it would fail. - _additionalReferences = set; -} - -- (void)removeTarget:(FSTQueryData *)queryData { - FSTQueryData *updated = - [queryData queryDataByReplacingSnapshotVersion:queryData.snapshotVersion - resumeToken:queryData.resumeToken - sequenceNumber:[self currentSequenceNumber]]; - _db.queryCache->UpdateTarget(updated); -} - -- (void)addReference:(const DocumentKey &)key { - [self writeSentinelForKey:key]; -} - -- (void)removeReference:(const DocumentKey &)key { - [self writeSentinelForKey:key]; -} - -- (BOOL)mutationQueuesContainKey:(const DocumentKey &)docKey { - const std::set &users = _db.users; - const ResourcePath &path = docKey.path(); - std::string buffer; - auto it = _db.currentTransaction->NewIterator(); - // For each user, if there is any batch that contains this document in any batch, we know it's - // pinned. - for (const std::string &user : users) { - std::string mutationKey = LevelDbDocumentMutationKey::KeyPrefix(user, path); - it->Seek(mutationKey); - if (it->Valid() && absl::StartsWith(it->key(), mutationKey)) { - return YES; - } - } - return NO; -} - -- (BOOL)isPinned:(const DocumentKey &)docKey { - if (_additionalReferences->ContainsKey(docKey)) { - return YES; - } - if ([self mutationQueuesContainKey:docKey]) { - return YES; - } - return NO; -} - -- (void)enumerateTargetsUsingCallback:(const TargetCallback &)callback { - _db.queryCache->EnumerateTargets(callback); -} - -- (void)enumerateMutationsUsingCallback:(const OrphanedDocumentCallback &)callback { - _db.queryCache->EnumerateOrphanedDocuments(callback); -} - -- (int)removeOrphanedDocumentsThroughSequenceNumber:(ListenSequenceNumber)upperBound { - int count = 0; - _db.queryCache->EnumerateOrphanedDocuments( - [&count, self, upperBound](const DocumentKey &docKey, ListenSequenceNumber sequenceNumber) { - if (sequenceNumber <= upperBound) { - if (![self isPinned:docKey]) { - count++; - self->_db.remoteDocumentCache->Remove(docKey); - [self removeSentinel:docKey]; - } - } - }); - return count; -} - -- (void)removeSentinel:(const DocumentKey &)key { - _db.currentTransaction->Delete(LevelDbDocumentTargetKey::SentinelKey(key)); -} - -- (int)removeTargetsThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber - liveQueries:(const std::unordered_map &) - liveQueries { - return _db.queryCache->RemoveTargets(sequenceNumber, liveQueries); -} - -- (size_t)sequenceNumberCount { - size_t totalCount = _db.queryCache->size(); - [self enumerateMutationsUsingCallback:[&totalCount](const DocumentKey &key, - ListenSequenceNumber sequenceNumber) { - totalCount++; - }]; - return totalCount; -} - -- (FSTLRUGarbageCollector *)gc { - return _gc; -} - -- (void)writeSentinelForKey:(const DocumentKey &)key { - std::string sentinelKey = LevelDbDocumentTargetKey::SentinelKey(key); - std::string encodedSequenceNumber = - LevelDbDocumentTargetKey::EncodeSentinelValue([self currentSequenceNumber]); - _db.currentTransaction->Put(sentinelKey, encodedSequenceNumber); -} - -- (void)removeMutationReference:(const DocumentKey &)key { - [self writeSentinelForKey:key]; -} - -- (void)limboDocumentUpdated:(const DocumentKey &)key { - [self writeSentinelForKey:key]; -} - -- (size_t)byteSize { - return [_db byteSize]; -} - -@end - -@implementation FSTLevelDB { - Path _directory; - std::unique_ptr _transaction; - std::unique_ptr _ptr; - std::unique_ptr _documentCache; - std::unique_ptr _indexManager; - FSTTransactionRunner _transactionRunner; - FSTLevelDBLRUDelegate *_referenceDelegate; - std::unique_ptr _queryCache; - std::set _users; - std::unique_ptr _currentMutationQueue; -} - -/** - * For now this is paranoid, but perhaps disable that in production builds. - */ -+ (const ReadOptions)standardReadOptions { - ReadOptions options; - options.verify_checksums = true; - return options; -} - -+ (std::set)collectUserSet:(LevelDbTransaction *)transaction { - std::set users; - - std::string tablePrefix = LevelDbMutationKey::KeyPrefix(); - auto it = transaction->NewIterator(); - it->Seek(tablePrefix); - LevelDbMutationKey rowKey; - while (it->Valid() && absl::StartsWith(it->key(), tablePrefix) && rowKey.Decode(it->key())) { - users.insert(rowKey.user_id()); - - auto userEnd = LevelDbMutationKey::KeyPrefix(rowKey.user_id()); - userEnd = util::PrefixSuccessor(userEnd); - it->Seek(userEnd); - } - return users; -} - -+ (firebase::firestore::util::Status)dbWithDirectory:(firebase::firestore::util::Path)directory - serializer:(FSTLocalSerializer *)serializer - lruParams: - (firebase::firestore::local::LruParams)lruParams - ptr:(FSTLevelDB **)ptr { - Status status = [self ensureDirectory:directory]; - if (!status.ok()) return status; - - StatusOr> database = [self createDBWithDirectory:directory]; - if (!database.status().ok()) { - return database.status(); - } - - std::unique_ptr ldb = std::move(database.ValueOrDie()); - LevelDbMigrations::RunMigrations(ldb.get()); - LevelDbTransaction transaction(ldb.get(), "Start LevelDB"); - std::set users = [self collectUserSet:&transaction]; - transaction.Commit(); - FSTLevelDB *db = [[self alloc] initWithLevelDB:std::move(ldb) - users:users - directory:directory - serializer:serializer - lruParams:lruParams]; - *ptr = db; - return Status::OK(); -} - -+ (Status)clearPersistence:(const DatabaseInfo &)databaseInfo { - Path levelDBDir = [FSTLevelDB storageDirectoryForDatabaseInfo:databaseInfo - documentsDirectory:[FSTLevelDB documentsDirectory]]; - LOG_DEBUG("Clearing persistence for path: %s", levelDBDir.ToUtf8String()); - return util::RecursivelyDelete(levelDBDir); -} - -- (instancetype)initWithLevelDB:(std::unique_ptr)db - users:(std::set)users - directory:(firebase::firestore::util::Path)directory - serializer:(FSTLocalSerializer *)serializer - lruParams:(firebase::firestore::local::LruParams)lruParams { - if (self = [super init]) { - self.started = YES; - _ptr = std::move(db); - _directory = std::move(directory); - _serializer = serializer; - _queryCache = absl::make_unique(self, _serializer); - _documentCache = absl::make_unique(self, _serializer); - _indexManager = absl::make_unique(self); - _referenceDelegate = [[FSTLevelDBLRUDelegate alloc] initWithPersistence:self - lruParams:lruParams]; - _transactionRunner.SetBackingPersistence(self); - _users = std::move(users); - // TODO(gsoltis): set up a leveldb transaction for these operations. - _queryCache->Start(); - [_referenceDelegate start]; - } - return self; -} - -- (size_t)byteSize { - int64_t count = 0; - auto iter = util::DirectoryIterator::Create(_directory); - for (; iter->Valid(); iter->Next()) { - int64_t fileSize = util::FileSize(iter->file()).ValueOrDie(); - count += fileSize; - } - HARD_ASSERT(iter->status().ok(), "Failed to iterate leveldb directory: %s", - iter->status().error_message().c_str()); - HARD_ASSERT(count >= 0 && count <= SIZE_MAX, "Overflowed counting bytes cached"); - return static_cast(count); -} - -- (const std::set &)users { - return _users; -} - -- (leveldb::DB *)ptr { - return _ptr.get(); -} - -- (const FSTTransactionRunner &)run { - return _transactionRunner; -} - -+ (Path)documentsDirectory { -#if TARGET_OS_IOS - NSArray *directories = - NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - return Path::FromNSString(directories[0]).AppendUtf8(kReservedPathComponent); - -#elif TARGET_OS_TV - NSArray *directories = - NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); - return Path::FromNSString(directories[0]).AppendUtf8(kReservedPathComponent); - -#elif TARGET_OS_OSX - std::string dotPrefixed = absl::StrCat(".", kReservedPathComponent); - return Path::FromNSString(NSHomeDirectory()).AppendUtf8(dotPrefixed); - -#else -#error "Don't know where to store documents on this platform." - -#endif -} - -+ (Path)storageDirectoryForDatabaseInfo:(const DatabaseInfo &)databaseInfo - documentsDirectory:(const Path &)documentsDirectory { - // Use two different path formats: - // - // * persistenceKey / projectID . databaseID / name - // * persistenceKey / projectID / name - // - // projectIDs are DNS-compatible names and cannot contain dots so there's - // no danger of collisions. - std::string project_key = databaseInfo.database_id().project_id(); - if (!databaseInfo.database_id().IsDefaultDatabase()) { - absl::StrAppend(&project_key, ".", databaseInfo.database_id().database_id()); - } - - // Reserve one additional path component to allow multiple physical databases - return Path::JoinUtf8(documentsDirectory, databaseInfo.persistence_key(), project_key, "main"); -} - -#pragma mark - Startup - -/** Creates the directory at @a directory and marks it as excluded from iCloud backup. */ -+ (Status)ensureDirectory:(const Path &)directory { - Status status = util::RecursivelyCreateDir(directory); - if (!status.ok()) { - return Status{Error::Internal, "Failed to create persistence directory"}.CausedBy(status); - } - - NSURL *dirURL = [NSURL fileURLWithPath:directory.ToNSString()]; - NSError *localError = nil; - if (![dirURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&localError]) { - return Status{Error::Internal, "Failed to mark persistence directory as excluded from backups"} - .CausedBy(Status::FromNSError(localError)); - } - - return Status::OK(); -} - -/** Opens the database within the given directory. */ -+ (StatusOr>)createDBWithDirectory:(const Path &)directory { - Options options; - options.create_if_missing = true; - - DB *database = nullptr; - leveldb::Status status = DB::Open(options, directory.ToUtf8String(), &database); - if (!status.ok()) { - return Status{Error::Internal, - StringFormat("Failed to open LevelDB database at %s", directory.ToUtf8String())} - .CausedBy(ConvertStatus(status)); - } - - return std::unique_ptr(database); -} - -- (LevelDbTransaction *)currentTransaction { - HARD_ASSERT(_transaction != nullptr, "Attempting to access transaction before one has started"); - return _transaction.get(); -} - -#pragma mark - Persistence Factory methods - -- (LevelDbMutationQueue *)mutationQueueForUser:(const User &)user { - _users.insert(user.uid()); - _currentMutationQueue.reset(new LevelDbMutationQueue(user, self, self.serializer)); - return _currentMutationQueue.get(); -} - -- (LevelDbQueryCache *)queryCache { - return _queryCache.get(); -} - -- (RemoteDocumentCache *)remoteDocumentCache { - return _documentCache.get(); -} - -- (IndexManager *)indexManager { - return _indexManager.get(); -} - -- (void)startTransaction:(absl::string_view)label { - HARD_ASSERT(_transaction == nullptr, "Starting a transaction while one is already outstanding"); - _transaction = absl::make_unique(_ptr.get(), label); - [_referenceDelegate transactionWillStart]; -} - -- (void)commitTransaction { - HARD_ASSERT(_transaction != nullptr, "Committing a transaction before one is started"); - [_referenceDelegate transactionWillCommit]; - _transaction->Commit(); - _transaction.reset(); -} - -- (void)shutdown { - HARD_ASSERT(self.isStarted, "FSTLevelDB shutdown without start!"); - self.started = NO; - _ptr.reset(); -} - -- (id)referenceDelegate { - return _referenceDelegate; -} - -- (ListenSequenceNumber)currentSequenceNumber { - return [_referenceDelegate currentSequenceNumber]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalSerializer.h b/Firestore/Source/Local/FSTLocalSerializer.h index f779141c12d..4cb997f1e1e 100644 --- a/Firestore/Source/Local/FSTLocalSerializer.h +++ b/Firestore/Source/Local/FSTLocalSerializer.h @@ -16,11 +16,11 @@ #import +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/maybe_document.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -@class FSTMutationBatch; -@class FSTQueryData; @class FSTSerializerBeta; @class FSTPBMaybeDocument; @@ -29,6 +29,7 @@ @class GPBTimestamp; +namespace local = firebase::firestore::local; namespace model = firebase::firestore::model; NS_ASSUME_NONNULL_BEGIN @@ -52,17 +53,17 @@ NS_ASSUME_NONNULL_BEGIN /** Decodes an FSTPBMaybeDocument proto to the equivalent model. */ - (model::MaybeDocument)decodedMaybeDocument:(FSTPBMaybeDocument *)proto; -/** Encodes an FSTMutationBatch model for local storage in the mutation queue. */ -- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch; +/** Encodes an MutationBatch model for local storage in the mutation queue. */ +- (FSTPBWriteBatch *)encodedMutationBatch:(const model::MutationBatch &)batch; /** Decodes an FSTPBWriteBatch proto into a MutationBatch model. */ -- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch; +- (model::MutationBatch)decodedMutationBatch:(FSTPBWriteBatch *)batch; -/** Encodes an FSTQueryData model for local storage in the query cache. */ -- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData; +/** Encodes a QueryData model for local storage in the query cache. */ +- (FSTPBTarget *)encodedQueryData:(const local::QueryData &)queryData; -/** Decodes an FSTPBTarget proto from local storage into an FSTQueryData model. */ -- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target; +/** Decodes an FSTPBTarget proto from local storage into a QueryData model. */ +- (local::QueryData)decodedQueryData:(FSTPBTarget *)target; /** Encodes a SnapshotVersion model into a GPBTimestamp proto. */ - (GPBTimestamp *)encodedVersion:(const model::SnapshotVersion &)version; diff --git a/Firestore/Source/Local/FSTLocalSerializer.mm b/Firestore/Source/Local/FSTLocalSerializer.mm index 821ed988139..53b3a82235d 100644 --- a/Firestore/Source/Local/FSTLocalSerializer.mm +++ b/Firestore/Source/Local/FSTLocalSerializer.mm @@ -25,32 +25,39 @@ #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" #import "Firestore/Protos/objc/google/firestore/v1/Document.pbobjc.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/core/query.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.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/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/no_document.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/unknown_document.h" +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" using firebase::Timestamp; using firebase::firestore::core::Query; +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::model::Document; using firebase::firestore::model::DocumentKey; using firebase::firestore::model::DocumentState; using firebase::firestore::model::ListenSequenceNumber; using firebase::firestore::model::MaybeDocument; using firebase::firestore::model::Mutation; +using firebase::firestore::model::MutationBatch; using firebase::firestore::model::NoDocument; using firebase::firestore::model::ObjectValue; using firebase::firestore::model::SnapshotVersion; using firebase::firestore::model::TargetId; using firebase::firestore::model::UnknownDocument; +using firebase::firestore::nanopb::ByteString; +using firebase::firestore::nanopb::MakeByteString; +using firebase::firestore::nanopb::MakeNSData; @interface FSTLocalSerializer () @@ -181,25 +188,25 @@ - (UnknownDocument)decodedUnknownDocument:(FSTPBUnknownDocument *)proto { return UnknownDocument(std::move(key), version); } -- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch { +- (FSTPBWriteBatch *)encodedMutationBatch:(const MutationBatch &)batch { FSTSerializerBeta *remoteSerializer = self.remoteSerializer; FSTPBWriteBatch *proto = [FSTPBWriteBatch message]; - proto.batchId = batch.batchID; - proto.localWriteTime = [remoteSerializer encodedTimestamp:batch.localWriteTime]; + proto.batchId = batch.batch_id(); + proto.localWriteTime = [remoteSerializer encodedTimestamp:batch.local_write_time()]; NSMutableArray *baseWrites = proto.baseWritesArray; - for (const Mutation &baseMutation : [batch baseMutations]) { + for (const Mutation &baseMutation : batch.base_mutations()) { [baseWrites addObject:[remoteSerializer encodedMutation:baseMutation]]; } NSMutableArray *writes = proto.writesArray; - for (const Mutation &mutation : [batch mutations]) { + for (const Mutation &mutation : batch.mutations()) { [writes addObject:[remoteSerializer encodedMutation:mutation]]; } return proto; } -- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { +- (MutationBatch)decodedMutationBatch:(FSTPBWriteBatch *)batch { FSTSerializerBeta *remoteSerializer = self.remoteSerializer; int batchID = batch.batchId; @@ -215,26 +222,23 @@ - (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { Timestamp localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; - return [[FSTMutationBatch alloc] initWithBatchID:batchID - localWriteTime:localWriteTime - baseMutations:std::move(baseMutations) - mutations:std::move(mutations)]; + return MutationBatch(batchID, localWriteTime, std::move(baseMutations), std::move(mutations)); } -- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { +- (FSTPBTarget *)encodedQueryData:(const QueryData &)queryData { FSTSerializerBeta *remoteSerializer = self.remoteSerializer; - HARD_ASSERT(queryData.purpose == FSTQueryPurposeListen, - "only queries with purpose %s may be stored, got %s", FSTQueryPurposeListen, - queryData.purpose); + HARD_ASSERT(queryData.purpose() == QueryPurpose::Listen, + "only queries with purpose %s may be stored, got %s", QueryPurpose::Listen, + queryData.purpose()); FSTPBTarget *proto = [FSTPBTarget message]; - proto.targetId = queryData.targetID; - proto.lastListenSequenceNumber = queryData.sequenceNumber; - proto.snapshotVersion = [remoteSerializer encodedVersion:queryData.snapshotVersion]; - proto.resumeToken = queryData.resumeToken; + proto.targetId = queryData.target_id(); + proto.lastListenSequenceNumber = queryData.sequence_number(); + proto.snapshotVersion = [remoteSerializer encodedVersion:queryData.snapshot_version()]; + proto.resumeToken = MakeNullableNSData(queryData.resume_token()); - const Query &query = queryData.query; + const Query &query = queryData.query(); if (query.IsDocumentQuery()) { proto.documents = [remoteSerializer encodedDocumentsTarget:query]; } else { @@ -244,13 +248,13 @@ - (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { return proto; } -- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target { +- (QueryData)decodedQueryData:(FSTPBTarget *)target { FSTSerializerBeta *remoteSerializer = self.remoteSerializer; TargetId targetID = target.targetId; ListenSequenceNumber sequenceNumber = target.lastListenSequenceNumber; SnapshotVersion version = [remoteSerializer decodedVersion:target.snapshotVersion]; - NSData *resumeToken = target.resumeToken; + ByteString resumeToken = MakeByteString(target.resumeToken); Query query; switch (target.targetTypeOneOfCase) { @@ -266,12 +270,8 @@ - (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target { HARD_FAIL("Unknown Target.targetType %s", target.targetTypeOneOfCase); } - return [[FSTQueryData alloc] initWithQuery:std::move(query) - targetID:targetID - listenSequenceNumber:sequenceNumber - purpose:FSTQueryPurposeListen - snapshotVersion:version - resumeToken:resumeToken]; + return QueryData(std::move(query), targetID, sequenceNumber, QueryPurpose::Listen, version, + std::move(resumeToken)); } - (GPBTimestamp *)encodedVersion:(const SnapshotVersion &)version { diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h deleted file mode 100644 index 43ac5cb6d12..00000000000 --- a/Firestore/Source/Local/FSTLocalStore.h +++ /dev/null @@ -1,199 +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 - -#include - -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/local/local_view_changes.h" -#include "Firestore/core/src/firebase/firestore/local/local_write_result.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/document_map.h" -#include "Firestore/core/src/firebase/firestore/model/mutation.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" - -namespace firebase { -namespace firestore { -namespace remote { - -class RemoteEvent; - -} // namespace remote -} // namespace firestore -} // namespace firebase - -@class FSTLocalViewChanges; -@class FSTLocalWriteResult; -@class FSTMutationBatch; -@class FSTMutationBatchResult; -@class FSTQueryData; -@protocol FSTPersistence; - -namespace auth = firebase::firestore::auth; -namespace core = firebase::firestore::core; -namespace local = firebase::firestore::local; -namespace model = firebase::firestore::model; -namespace remote = firebase::firestore::remote; - -NS_ASSUME_NONNULL_BEGIN - -/** - * Local storage in the Firestore client. Coordinates persistence components like the mutation - * queue and remote document cache to present a latency compensated view of stored data. - * - * The LocalStore is responsible for accepting mutations from the Sync Engine. Writes from the - * client are put into a queue as provisional Mutations until they are processed by the RemoteStore - * and confirmed as having been written to the server. - * - * The local store provides the local version of documents that have been modified locally. It - * maintains the constraint: - * - * LocalDocument = RemoteDocument + Active(LocalMutations) - * - * (Active mutations are those that are enqueued and have not been previously acknowledged or - * rejected). - * - * The RemoteDocument ("ground truth") state is provided via the applyChangeBatch method. It will - * be some version of a server-provided document OR will be a server-provided document PLUS - * acknowledged mutations: - * - * RemoteDocument' = RemoteDocument + Acknowledged(LocalMutations) - * - * Note that this "dirty" version of a RemoteDocument will not be identical to a server base - * version, since it has LocalMutations added to it pending getting an authoritative copy from the - * server. - * - * Since LocalMutations can be rejected by the server, we have to be able to revert a LocalMutation - * that has already been applied to the LocalDocument (typically done by replaying all remaining - * LocalMutations to the RemoteDocument to re-apply). - * - * It also maintains the persistence of mapping queries to resume tokens and target ids. - * - * The LocalStore must be able to efficiently execute queries against its local cache of the - * documents, to provide the initial set of results before any remote changes have been received. - */ -@interface FSTLocalStore : NSObject - -/** Creates a new instance of the FSTLocalStore with its required dependencies as parameters. */ -- (instancetype)initWithPersistence:(id)persistence - initialUser:(const auth::User &)initialUser NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** Performs any initial startup actions required by the local store. */ -- (void)start; - -/** - * Tells the FSTLocalStore that the currently authenticated user has changed. - * - * In response the local store switches the mutation queue to the new user and returns any - * resulting document changes. - */ -- (model::MaybeDocumentMap)userDidChange:(const auth::User &)user; - -/** Accepts locally generated Mutations and commits them to storage. */ -- (local::LocalWriteResult)locallyWriteMutations:(std::vector &&)mutations; - -/** Returns the current value of a document with a given key, or nil if not found. */ -- (absl::optional)readDocument:(const model::DocumentKey &)key; - -/** - * Acknowledges the given batch. - * - * On the happy path when a batch is acknowledged, the local store will - * - * + remove the batch from the mutation queue; - * + apply the changes to the remote document cache; - * + recalculate the latency compensated view implied by those changes (there may be mutations in - * the queue that affect the documents but haven't been acknowledged yet); and - * + give the changed documents back the sync engine - * - * @return The resulting (modified) documents. - */ -- (model::MaybeDocumentMap)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult; - -/** - * Removes mutations from the MutationQueue for the specified batch. LocalDocuments will be - * recalculated. - * - * @return The resulting (modified) documents. - */ -- (model::MaybeDocumentMap)rejectBatchID:(model::BatchId)batchID; - -/** Returns the last recorded stream token for the current user. */ -- (nullable NSData *)lastStreamToken; - -/** - * Sets the stream token for the current user without acknowledging any mutation batch. This is - * usually only useful after a stream handshake or in response to an error that requires clearing - * the stream token. - */ -- (void)setLastStreamToken:(nullable NSData *)streamToken; - -/** - * Returns the last consistent snapshot processed (used by the RemoteStore to determine whether to - * buffer incoming snapshots from the backend). - */ -- (const model::SnapshotVersion &)lastRemoteSnapshotVersion; - -/** - * Updates the "ground-state" (remote) documents. We assume that the remote event reflects any - * write batches that have been acknowledged or rejected (i.e. we do not re-apply local mutations - * to updates from this event). - * - * LocalDocuments are re-calculated if there are remaining mutations in the queue. - */ -- (model::MaybeDocumentMap)applyRemoteEvent:(const remote::RemoteEvent &)remoteEvent; - -/** - * Returns the keys of the documents that are associated with the given targetID in the remote - * table. - */ -- (model::DocumentKeySet)remoteDocumentKeysForTarget:(model::TargetId)targetID; - -/** - * Assigns @a query an internal ID so that its results can be pinned so they don't get GC'd. - * A query must be allocated in the local store before the store can be used to manage its view. - */ -- (FSTQueryData *)allocateQuery:(core::Query)query; - -/** Unpin all the documents associated with @a query. */ -- (void)releaseQuery:(const core::Query &)query; - -/** Runs @a query against all the documents in the local store and returns the results. */ -- (model::DocumentMap)executeQuery:(const core::Query &)query; - -/** Notify the local store of the changed views to locally pin / unpin documents. */ -- (void)notifyLocalViewChanges:(const std::vector &)viewChanges; - -/** - * Gets the mutation batch after the passed in batchId in the mutation queue or nil if empty. - * - * @param batchID The batch to search after, or -1 for the first mutation in the queue. - * @return the next mutation or nil if there wasn't one. - */ -- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(model::BatchId)batchID; - -- (local::LruResults)collectGarbage:(FSTLRUGarbageCollector *)garbageCollector; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalStore.mm b/Firestore/Source/Local/FSTLocalStore.mm deleted file mode 100644 index 1be032e703f..00000000000 --- a/Firestore/Source/Local/FSTLocalStore.mm +++ /dev/null @@ -1,527 +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/Local/FSTLocalStore.h" - -#include -#include -#include -#include -#include - -#import "FIRTimestamp.h" -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTPersistence.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - -#include "Firestore/core/include/firebase/firestore/timestamp.h" -#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/immutable/sorted_set.h" -#include "Firestore/core/src/firebase/firestore/local/local_documents_view.h" -#include "Firestore/core/src/firebase/firestore/local/local_view_changes.h" -#include "Firestore/core/src/firebase/firestore/local/local_write_result.h" -#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/local/query_cache.h" -#include "Firestore/core/src/firebase/firestore/local/reference_set.h" -#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" -#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" -#include "Firestore/core/src/firebase/firestore/model/document_map.h" -#include "Firestore/core/src/firebase/firestore/model/patch_mutation.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/log.h" -#include "Firestore/core/src/firebase/firestore/util/to_string.h" -#include "absl/memory/memory.h" -#include "absl/types/optional.h" - -namespace util = firebase::firestore::util; -using firebase::Timestamp; -using firebase::firestore::auth::User; -using firebase::firestore::core::Query; -using firebase::firestore::core::TargetIdGenerator; -using firebase::firestore::local::LocalDocumentsView; -using firebase::firestore::local::LocalViewChanges; -using firebase::firestore::local::LocalWriteResult; -using firebase::firestore::local::LruResults; -using firebase::firestore::local::MutationQueue; -using firebase::firestore::local::QueryCache; -using firebase::firestore::local::ReferenceSet; -using firebase::firestore::local::RemoteDocumentCache; -using firebase::firestore::model::BatchId; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::DocumentMap; -using firebase::firestore::model::DocumentVersionMap; -using firebase::firestore::model::FieldMask; -using firebase::firestore::model::FieldPath; -using firebase::firestore::model::ListenSequenceNumber; -using firebase::firestore::model::MaybeDocument; -using firebase::firestore::model::MaybeDocumentMap; -using firebase::firestore::model::Mutation; -using firebase::firestore::model::ObjectValue; -using firebase::firestore::model::OptionalMaybeDocumentMap; -using firebase::firestore::model::PatchMutation; -using firebase::firestore::model::Precondition; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::TargetId; -using firebase::firestore::remote::RemoteEvent; -using firebase::firestore::remote::TargetChange; - -NS_ASSUME_NONNULL_BEGIN - -/** - * The maximum time to leave a resume token buffered without writing it out. This value is - * arbitrary: it's long enough to avoid several writes (possibly indefinitely if updates come more - * frequently than this) but short enough that restarting after crashing will still have a pretty - * recent resume token. - */ -static const int64_t kResumeTokenMaxAgeSeconds = 5 * 60; // 5 minutes - -@interface FSTLocalStore () - -/** Manages our in-memory or durable persistence. */ -@property(nonatomic, strong, readonly) id persistence; - -/** Maps a query to the data about that query. */ -@property(nonatomic) QueryCache *queryCache; - -@end - -@implementation FSTLocalStore { - /** Used to generate targetIDs for queries tracked locally. */ - TargetIdGenerator _targetIDGenerator; - /** The set of all cached remote documents. */ - RemoteDocumentCache *_remoteDocumentCache; - QueryCache *_queryCache; - /** The set of all mutations that have been sent but not yet been applied to the backend. */ - MutationQueue *_mutationQueue; - - /** The "local" view of all documents (layering mutationQueue on top of remoteDocumentCache). */ - std::unique_ptr _localDocuments; - - /** The set of document references maintained by any local views. */ - ReferenceSet _localViewReferences; - - /** Maps a targetID to data about its query. */ - std::unordered_map _targetIDs; -} - -- (instancetype)initWithPersistence:(id)persistence - initialUser:(const User &)initialUser { - if (self = [super init]) { - _persistence = persistence; - _mutationQueue = [persistence mutationQueueForUser:initialUser]; - _remoteDocumentCache = [persistence remoteDocumentCache]; - _queryCache = [persistence queryCache]; - _localDocuments = absl::make_unique(_remoteDocumentCache, _mutationQueue, - [_persistence indexManager]); - [_persistence.referenceDelegate addInMemoryPins:&_localViewReferences]; - - _targetIDGenerator = TargetIdGenerator::QueryCacheTargetIdGenerator(0); - } - return self; -} - -- (void)start { - [self startMutationQueue]; - TargetId targetID = _queryCache->highest_target_id(); - _targetIDGenerator = TargetIdGenerator::QueryCacheTargetIdGenerator(targetID); -} - -- (void)startMutationQueue { - self.persistence.run("Start MutationQueue", [&]() { _mutationQueue->Start(); }); -} - -- (MaybeDocumentMap)userDidChange:(const User &)user { - // Swap out the mutation queue, grabbing the pending mutation batches before and after. - std::vector oldBatches = self.persistence.run( - "OldBatches", - [&]() -> std::vector { return _mutationQueue->AllMutationBatches(); }); - - // The old one has a reference to the mutation queue, so nil it out first. - _localDocuments.reset(); - _mutationQueue = [self.persistence mutationQueueForUser:user]; - - [self startMutationQueue]; - - return self.persistence.run("NewBatches", [&] { - std::vector newBatches = _mutationQueue->AllMutationBatches(); - - // Recreate our LocalDocumentsView using the new MutationQueue. - _localDocuments = absl::make_unique(_remoteDocumentCache, _mutationQueue, - [_persistence indexManager]); - - // Union the old/new changed keys. - DocumentKeySet changedKeys; - for (const std::vector &batches : {oldBatches, newBatches}) { - for (FSTMutationBatch *batch : batches) { - for (const Mutation &mutation : [batch mutations]) { - changedKeys = changedKeys.insert(mutation.key()); - } - } - } - - // Return the set of all (potentially) changed documents as the result of the user change. - return _localDocuments->GetDocuments(changedKeys); - }); -} - -- (LocalWriteResult)locallyWriteMutations:(std::vector &&)mutations { - Timestamp localWriteTime = Timestamp::Now(); - DocumentKeySet keys; - for (const Mutation &mutation : mutations) { - keys = keys.insert(mutation.key()); - } - - return self.persistence.run("Locally write mutations", [&] { - // Load and apply all existing mutations. This lets us compute the current base state for - // all non-idempotent transforms before applying any additional user-provided writes. - MaybeDocumentMap existingDocuments = _localDocuments->GetDocuments(keys); - - // For non-idempotent mutations (such as `FieldValue.increment()`), we record the base - // state in a separate patch mutation. This is later used to guarantee consistent values - // and prevents flicker even if the backend sends us an update that already includes our - // transform. - std::vector baseMutations; - for (const Mutation &mutation : mutations) { - absl::optional base_document = existingDocuments.get(mutation.key()); - - absl::optional base_value = mutation.ExtractBaseValue(base_document); - if (base_value) { - // NOTE: The base state should only be applied if there's some existing document to - // override, so use a Precondition of exists=true - baseMutations.push_back(PatchMutation( - mutation.key(), *base_value, base_value->ToFieldMask(), Precondition::Exists(true))); - } - } - - FSTMutationBatch *batch = _mutationQueue->AddMutationBatch( - localWriteTime, std::move(baseMutations), std::move(mutations)); - MaybeDocumentMap changedDocuments = [batch applyToLocalDocumentSet:existingDocuments]; - return LocalWriteResult{batch.batchID, std::move(changedDocuments)}; - }); -} - -- (MaybeDocumentMap)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult { - return self.persistence.run("Acknowledge batch", [&] { - FSTMutationBatch *batch = batchResult.batch; - _mutationQueue->AcknowledgeBatch(batch, batchResult.streamToken); - [self applyBatchResult:batchResult]; - _mutationQueue->PerformConsistencyCheck(); - - return _localDocuments->GetDocuments(batch.keys); - }); -} - -- (MaybeDocumentMap)rejectBatchID:(BatchId)batchID { - return self.persistence.run("Reject batch", [&] { - FSTMutationBatch *toReject = _mutationQueue->LookupMutationBatch(batchID); - HARD_ASSERT(toReject, "Attempt to reject nonexistent batch!"); - - _mutationQueue->RemoveMutationBatch(toReject); - _mutationQueue->PerformConsistencyCheck(); - - return _localDocuments->GetDocuments(toReject.keys); - }); -} - -- (nullable NSData *)lastStreamToken { - return _mutationQueue->GetLastStreamToken(); -} - -- (void)setLastStreamToken:(nullable NSData *)streamToken { - self.persistence.run("Set stream token", - [&]() { _mutationQueue->SetLastStreamToken(streamToken); }); -} - -- (const SnapshotVersion &)lastRemoteSnapshotVersion { - return self.queryCache->GetLastRemoteSnapshotVersion(); -} - -- (MaybeDocumentMap)applyRemoteEvent:(const RemoteEvent &)remoteEvent { - return self.persistence.run("Apply remote event", [&] { - // TODO(gsoltis): move the sequence number into the reference delegate. - ListenSequenceNumber sequenceNumber = self.persistence.currentSequenceNumber; - - DocumentKeySet authoritativeUpdates; - for (const auto &entry : remoteEvent.target_changes()) { - TargetId targetID = entry.first; - const TargetChange &change = entry.second; - - // Do not ref/unref unassigned targetIDs - it may lead to leaks. - auto found = _targetIDs.find(targetID); - if (found == _targetIDs.end()) { - continue; - } - FSTQueryData *queryData = found->second; - - // When a global snapshot contains updates (either add or modify) we can completely trust - // these updates as authoritative and blindly apply them to our cache (as a defensive measure - // to promote self-healing in the unfortunate case that our cache is ever somehow corrupted / - // out-of-sync). - // - // If the document is only updated while removing it from a target then watch isn't obligated - // to send the absolute latest version: it can send the first version that caused the document - // not to match. - for (const DocumentKey &key : change.added_documents()) { - authoritativeUpdates = authoritativeUpdates.insert(key); - } - for (const DocumentKey &key : change.modified_documents()) { - authoritativeUpdates = authoritativeUpdates.insert(key); - } - - _queryCache->RemoveMatchingKeys(change.removed_documents(), targetID); - _queryCache->AddMatchingKeys(change.added_documents(), targetID); - - // 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.resume_token(); - if (resumeToken.length > 0) { - FSTQueryData *oldQueryData = queryData; - queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshot_version() - resumeToken:resumeToken - sequenceNumber:sequenceNumber]; - _targetIDs[targetID] = queryData; - - if ([self shouldPersistQueryData:queryData oldQueryData:oldQueryData change:change]) { - _queryCache->UpdateTarget(queryData); - } - } - } - - OptionalMaybeDocumentMap changedDocs; - const DocumentKeySet &limboDocuments = remoteEvent.limbo_document_changes(); - DocumentKeySet updatedKeys; - for (const auto &kv : remoteEvent.document_updates()) { - updatedKeys = updatedKeys.insert(kv.first); - } - // Each loop iteration only affects its "own" doc, so it's safe to get all the remote - // documents in advance in a single call. - OptionalMaybeDocumentMap existingDocs = _remoteDocumentCache->GetAll(updatedKeys); - - for (const auto &kv : remoteEvent.document_updates()) { - const DocumentKey &key = kv.first; - const MaybeDocument &doc = kv.second; - absl::optional existingDoc; - auto foundExisting = existingDocs.get(key); - if (foundExisting) { - existingDoc = *foundExisting; - } - - if (!existingDoc || - (authoritativeUpdates.contains(doc.key()) && !existingDoc->has_pending_writes()) || - doc.version() >= existingDoc->version()) { - // If a document update isn't authoritative, make sure we don't apply an old document - // version to the remote cache. - _remoteDocumentCache->Add(doc); - changedDocs = changedDocs.insert(key, doc); - } else if (doc.type() == MaybeDocument::Type::NoDocument && - doc.version() == SnapshotVersion::None()) { - // NoDocuments with SnapshotVersion.MIN are used in manufactured events (e.g. in the case - // of a limbo document resolution failing). We remove these documents from cache since we - // lost access. - _remoteDocumentCache->Remove(key); - changedDocs = changedDocs.insert(key, doc); - } else { - LOG_DEBUG("FSTLocalStore Ignoring outdated watch update for %s. " - "Current version: %s Watch version: %s", - key.ToString(), existingDoc->version().ToString(), doc.version().ToString()); - } - - // If this was a limbo resolution, make sure we mark when it was accessed. - 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. - const SnapshotVersion &lastRemoteVersion = _queryCache->GetLastRemoteSnapshotVersion(); - const SnapshotVersion &remoteVersion = remoteEvent.snapshot_version(); - if (remoteVersion != SnapshotVersion::None()) { - HARD_ASSERT(remoteVersion >= lastRemoteVersion, - "Watch stream reverted to previous snapshot?? (%s < %s)", - remoteVersion.ToString(), lastRemoteVersion.ToString()); - _queryCache->SetLastRemoteSnapshotVersion(remoteVersion); - } - - return _localDocuments->GetLocalViewOfDocuments(changedDocs); - }); -} - -/** - * Returns YES if the newQueryData should be persisted during an update of an active target. - * QueryData should always be persisted when a target is being released and should not call this - * function. - * - * While the target is active, QueryData updates can be omitted when nothing about the target has - * changed except metadata like the resume token or snapshot version. Occasionally it's worth the - * extra write to prevent these values from getting too stale after a crash, but this doesn't have - * to be too frequent. - */ -- (BOOL)shouldPersistQueryData:(FSTQueryData *)newQueryData - oldQueryData:(FSTQueryData *)oldQueryData - change:(const TargetChange &)change { - // Avoid clearing any existing value - if (newQueryData.resumeToken.length == 0) return NO; - - // Any resume token is interesting if there isn't one already. - if (oldQueryData.resumeToken.length == 0) return YES; - - // Don't allow resume token changes to be buffered indefinitely. This allows us to be reasonably - // up-to-date after a crash and avoids needing to loop over all active queries on shutdown. - // Especially in the browser we may not get time to do anything interesting while the current - // tab is closing. - int64_t newSeconds = newQueryData.snapshotVersion.timestamp().seconds(); - int64_t oldSeconds = oldQueryData.snapshotVersion.timestamp().seconds(); - int64_t timeDelta = newSeconds - oldSeconds; - if (timeDelta >= kResumeTokenMaxAgeSeconds) return YES; - - // Otherwise if the only thing that has changed about a target is its resume token then it's not - // worth persisting. Note that the RemoteStore keeps an in-memory view of the currently active - // targets which includes the current resume token, so stream failure or user changes will still - // use an up-to-date resume token regardless of what we do here. - size_t changes = change.added_documents().size() + change.modified_documents().size() + - change.removed_documents().size(); - return changes > 0; -} - -- (void)notifyLocalViewChanges:(const std::vector &)viewChanges { - self.persistence.run("NotifyLocalViewChanges", [&]() { - for (const LocalViewChanges &viewChange : viewChanges) { - for (const DocumentKey &key : viewChange.removed_keys()) { - [self->_persistence.referenceDelegate removeReference:key]; - } - _localViewReferences.AddReferences(viewChange.added_keys(), viewChange.target_id()); - _localViewReferences.AddReferences(viewChange.removed_keys(), viewChange.target_id()); - } - }); -} - -- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(BatchId)batchID { - FSTMutationBatch *result = self.persistence.run("NextMutationBatchAfterBatchID", [&] { - return _mutationQueue->NextMutationBatchAfterBatchId(batchID); - }); - return result; -} - -- (absl::optional)readDocument:(const DocumentKey &)key { - return self.persistence.run("ReadDocument", [&] { return _localDocuments->GetDocument(key); }); -} - -- (FSTQueryData *)allocateQuery:(Query)query { - FSTQueryData *queryData = self.persistence.run("Allocate query", [&] { - FSTQueryData *cached = _queryCache->GetTarget(query); - // TODO(mcg): freshen last accessed date if cached exists? - if (!cached) { - cached = [[FSTQueryData alloc] initWithQuery:query - targetID:_targetIDGenerator.NextId() - listenSequenceNumber:self.persistence.currentSequenceNumber - purpose:FSTQueryPurposeListen]; - _queryCache->AddTarget(cached); - } - return cached; - }); - // Sanity check to ensure that even when resuming a query it's not currently active. - TargetId targetID = queryData.targetID; - HARD_ASSERT(_targetIDs.find(targetID) == _targetIDs.end(), - "Tried to allocate an already allocated query: %s", query.ToString()); - _targetIDs[targetID] = queryData; - return queryData; -} - -- (void)releaseQuery:(const Query &)query { - self.persistence.run("Release query", [&]() { - FSTQueryData *queryData = _queryCache->GetTarget(query); - HARD_ASSERT(queryData, "Tried to release nonexistent query: %s", query.ToString()); - - TargetId targetID = queryData.targetID; - - auto found = _targetIDs.find(targetID); - if (found != _targetIDs.end()) { - FSTQueryData *cachedQueryData = found->second; - - if (cachedQueryData.snapshotVersion > queryData.snapshotVersion) { - // If we've been avoiding persisting the resumeToken (see shouldPersistQueryData for - // conditions and rationale) we need to persist the token now because there will no - // longer be an in-memory version to fall back on. - queryData = cachedQueryData; - _queryCache->UpdateTarget(queryData); - } - } - - // References for documents sent via Watch are automatically removed when we delete a - // query's target data from the reference delegate. Since this does not remove references - // for locally mutated documents, we have to remove the target associations for these - // documents manually. - DocumentKeySet removed = _localViewReferences.RemoveReferences(targetID); - for (const DocumentKey &key : removed) { - [self.persistence.referenceDelegate removeReference:key]; - } - _targetIDs.erase(targetID); - [self.persistence.referenceDelegate removeTarget:queryData]; - }); -} - -- (DocumentMap)executeQuery:(const Query &)query { - return self.persistence.run("ExecuteQuery", - [&] { return _localDocuments->GetDocumentsMatchingQuery(query); }); -} - -- (DocumentKeySet)remoteDocumentKeysForTarget:(TargetId)targetID { - return self.persistence.run("RemoteDocumentKeysForTarget", - [&] { return _queryCache->GetMatchingKeys(targetID); }); -} - -- (void)applyBatchResult:(FSTMutationBatchResult *)batchResult { - FSTMutationBatch *batch = batchResult.batch; - DocumentKeySet docKeys = batch.keys; - const DocumentVersionMap &versions = batchResult.docVersions; - for (const DocumentKey &docKey : docKeys) { - absl::optional remoteDoc = _remoteDocumentCache->Get(docKey); - absl::optional doc = remoteDoc; - - auto ackVersionIter = versions.find(docKey); - HARD_ASSERT(ackVersionIter != versions.end(), - "docVersions should contain every doc in the write."); - const SnapshotVersion &ackVersion = ackVersionIter->second; - if (!doc || doc->version() < ackVersion) { - doc = [batch applyToRemoteDocument:doc documentKey:docKey mutationBatchResult:batchResult]; - if (!doc) { - HARD_ASSERT(!remoteDoc, "Mutation batch %s applied to document %s resulted in nullopt.", - batch, util::ToString(remoteDoc)); - } else { - _remoteDocumentCache->Add(*doc); - } - } - } - - _mutationQueue->RemoveMutationBatch(batch); -} - -- (LruResults)collectGarbage:(FSTLRUGarbageCollector *)garbageCollector { - return self.persistence.run("Collect garbage", - [&] { return [garbageCollector collectWithLiveTargets:_targetIDs]; }); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.h b/Firestore/Source/Local/FSTMemoryPersistence.h deleted file mode 100644 index 7fd5fbb65c9..00000000000 --- a/Firestore/Source/Local/FSTMemoryPersistence.h +++ /dev/null @@ -1,68 +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 - -#import "Firestore/Source/Local/FSTLRUGarbageCollector.h" -#import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Local/FSTPersistence.h" - -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" - -namespace local = firebase::firestore::local; -namespace model = firebase::firestore::model; - -NS_ASSUME_NONNULL_BEGIN - -/** - * An in-memory implementation of the FSTPersistence protocol. Values are stored only in RAM and - * are never persisted to any durable storage. - */ -@interface FSTMemoryPersistence : NSObject - -+ (instancetype)persistenceWithEagerGC; - -+ (instancetype)persistenceWithLruParams:(local::LruParams)lruParams - serializer:(FSTLocalSerializer *)serializer; - -@end - -/** - * Provides the eager GC implementation for memory persistence. - */ -@interface FSTMemoryEagerReferenceDelegate : NSObject - -- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence; - -@end - -/** - * Provides the LRU GC implementation for memory persistence. - */ -@interface FSTMemoryLRUReferenceDelegate - : NSObject - -- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence - serializer:(FSTLocalSerializer *)serializer - lruParams:(local::LruParams)lruParams; - -- (BOOL)isPinnedAtSequenceNumber:(model::ListenSequenceNumber)upperBound - document:(const model::DocumentKey &)key; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.mm b/Firestore/Source/Local/FSTMemoryPersistence.mm deleted file mode 100644 index d60f7e37f69..00000000000 --- a/Firestore/Source/Local/FSTMemoryPersistence.mm +++ /dev/null @@ -1,425 +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/Local/FSTMemoryPersistence.h" - -#include -#include -#include -#include - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/local/index_manager.h" -#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" -#include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" -#include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/local/memory_query_cache.h" -#include "Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h" -#include "Firestore/core/src/firebase/firestore/local/reference_set.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "absl/memory/memory.h" - -using firebase::firestore::auth::HashUser; -using firebase::firestore::auth::User; -using firebase::firestore::local::ListenSequence; -using firebase::firestore::local::LruParams; -using firebase::firestore::local::MemoryIndexManager; -using firebase::firestore::local::MemoryMutationQueue; -using firebase::firestore::local::MemoryQueryCache; -using firebase::firestore::local::MemoryRemoteDocumentCache; -using firebase::firestore::local::ReferenceSet; -using firebase::firestore::local::TargetCallback; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeyHash; -using firebase::firestore::model::ListenSequenceNumber; -using firebase::firestore::model::TargetId; -using firebase::firestore::util::Status; - -using MutationQueues = std::unordered_map, HashUser>; - -NS_ASSUME_NONNULL_BEGIN - -@interface FSTMemoryPersistence () - -- (MemoryQueryCache *)queryCache; - -- (MemoryRemoteDocumentCache *)remoteDocumentCache; - -- (MemoryIndexManager *)indexManager; - -- (MemoryMutationQueue *)mutationQueueForUser:(const User &)user; - -@property(nonatomic, readonly) MutationQueues &mutationQueues; - -@property(nonatomic, assign, getter=isStarted) BOOL started; - -// Make this property writable so we can wire up a delegate. -@property(nonatomic, strong) id referenceDelegate; - -@end - -@implementation FSTMemoryPersistence { - /** - * The QueryCache representing the persisted cache of queries. - * - * Note that this is retained here to make it easier to write tests affecting both the in-memory - * and LevelDB-backed persistence layers. Tests can create a new FSTLocalStore wrapping this - * FSTPersistence instance and this will make the in-memory persistence layer behave as if it - * were actually persisting values. - */ - std::unique_ptr _queryCache; - - /** The RemoteDocumentCache representing the persisted cache of remote documents. */ - std::unique_ptr _remoteDocumentCache; - - MemoryIndexManager _indexManager; - - FSTTransactionRunner _transactionRunner; - - id _referenceDelegate; -} - -+ (instancetype)persistenceWithEagerGC { - FSTMemoryPersistence *persistence = [[FSTMemoryPersistence alloc] init]; - persistence.referenceDelegate = - [[FSTMemoryEagerReferenceDelegate alloc] initWithPersistence:persistence]; - return persistence; -} - -+ (instancetype)persistenceWithLruParams:(firebase::firestore::local::LruParams)lruParams - serializer:(FSTLocalSerializer *)serializer { - FSTMemoryPersistence *persistence = [[FSTMemoryPersistence alloc] init]; - persistence.referenceDelegate = - [[FSTMemoryLRUReferenceDelegate alloc] initWithPersistence:persistence - serializer:serializer - lruParams:lruParams]; - return persistence; -} - -- (instancetype)init { - if (self = [super init]) { - _queryCache = absl::make_unique(self); - _remoteDocumentCache = absl::make_unique(self); - self.started = YES; - } - return self; -} - -- (void)setReferenceDelegate:(id)referenceDelegate { - _referenceDelegate = referenceDelegate; - id delegate = _referenceDelegate; - if ([delegate conformsToProtocol:@protocol(FSTTransactional)]) { - _transactionRunner.SetBackingPersistence((id)_referenceDelegate); - } -} - -- (void)shutdown { - // No durable state to ensure is closed on shutdown. - HARD_ASSERT(self.isStarted, "FSTMemoryPersistence shutdown without start!"); - self.started = NO; -} - -- (id)referenceDelegate { - return _referenceDelegate; -} - -- (ListenSequenceNumber)currentSequenceNumber { - return [_referenceDelegate currentSequenceNumber]; -} - -- (const FSTTransactionRunner &)run { - return _transactionRunner; -} - -- (MemoryMutationQueue *)mutationQueueForUser:(const User &)user { - const std::unique_ptr &existing = _mutationQueues[user]; - if (!existing) { - _mutationQueues[user] = absl::make_unique(self); - return _mutationQueues[user].get(); - } else { - return existing.get(); - } -} - -- (MemoryQueryCache *)queryCache { - return _queryCache.get(); -} - -- (MemoryRemoteDocumentCache *)remoteDocumentCache { - return _remoteDocumentCache.get(); -} - -- (MemoryIndexManager *)indexManager { - return &_indexManager; -} - -@end - -@implementation FSTMemoryLRUReferenceDelegate { - // This delegate should have the same lifetime as the persistence layer, but mark as - // weak to avoid retain cycle. - __weak FSTMemoryPersistence *_persistence; - // Tracks sequence numbers of when documents are used. Equivalent to sentinel rows in - // the leveldb implementation. - std::unordered_map _sequenceNumbers; - ReferenceSet *_additionalReferences; - FSTLRUGarbageCollector *_gc; - // PORTING NOTE: when this class is ported to C++, this does not need to be a pointer - std::unique_ptr _listenSequence; - ListenSequenceNumber _currentSequenceNumber; - FSTLocalSerializer *_serializer; -} - -- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence - serializer:(FSTLocalSerializer *)serializer - lruParams:(firebase::firestore::local::LruParams)lruParams { - if (self = [super init]) { - _persistence = persistence; - _gc = [[FSTLRUGarbageCollector alloc] initWithDelegate:self params:lruParams]; - _currentSequenceNumber = kFSTListenSequenceNumberInvalid; - // Theoretically this is always 0, since this is all in-memory... - ListenSequenceNumber highestSequenceNumber = - _persistence.queryCache->highest_listen_sequence_number(); - _listenSequence = absl::make_unique(highestSequenceNumber); - _serializer = serializer; - } - return self; -} - -- (FSTLRUGarbageCollector *)gc { - return _gc; -} - -- (ListenSequenceNumber)currentSequenceNumber { - HARD_ASSERT(_currentSequenceNumber != kFSTListenSequenceNumberInvalid, - "Asking for a sequence number outside of a transaction"); - return _currentSequenceNumber; -} - -- (void)addInMemoryPins:(ReferenceSet *)set { - // Technically can't assert this, due to restartWithNoopGarbageCollector (for now...) - // FSTAssert(_additionalReferences == nil, @"Overwriting additional references"); - _additionalReferences = set; -} - -- (void)removeTarget:(FSTQueryData *)queryData { - FSTQueryData *updated = [queryData queryDataByReplacingSnapshotVersion:queryData.snapshotVersion - resumeToken:queryData.resumeToken - sequenceNumber:_currentSequenceNumber]; - _persistence.queryCache->UpdateTarget(updated); -} - -- (void)limboDocumentUpdated:(const DocumentKey &)key { - _sequenceNumbers[key] = self.currentSequenceNumber; -} - -- (void)startTransaction:(absl::string_view)label { - _currentSequenceNumber = _listenSequence->Next(); -} - -- (void)commitTransaction { - _currentSequenceNumber = kFSTListenSequenceNumberInvalid; -} - -- (void)enumerateTargetsUsingCallback:(const TargetCallback &)callback { - return _persistence.queryCache->EnumerateTargets(callback); -} - -- (void)enumerateMutationsUsingCallback: - (const firebase::firestore::local::OrphanedDocumentCallback &)callback { - for (const auto &entry : _sequenceNumbers) { - ListenSequenceNumber sequenceNumber = entry.second; - const DocumentKey &key = entry.first; - // Pass in the exact sequence number as the upper bound so we know it won't be pinned by being - // too recent. - if (![self isPinnedAtSequenceNumber:sequenceNumber document:key]) { - callback(key, sequenceNumber); - } - } -} - -- (int)removeTargetsThroughSequenceNumber:(ListenSequenceNumber)sequenceNumber - liveQueries:(const std::unordered_map &) - liveQueries { - return _persistence.queryCache->RemoveTargets(sequenceNumber, liveQueries); -} - -- (size_t)sequenceNumberCount { - size_t totalCount = _persistence.queryCache->size(); - [self enumerateMutationsUsingCallback:[&totalCount](const DocumentKey &key, - ListenSequenceNumber sequenceNumber) { - totalCount++; - }]; - return totalCount; -} - -- (int)removeOrphanedDocumentsThroughSequenceNumber:(ListenSequenceNumber)upperBound { - std::vector removed = - _persistence.remoteDocumentCache->RemoveOrphanedDocuments(self, upperBound); - for (const auto &key : removed) { - _sequenceNumbers.erase(key); - } - return static_cast(removed.size()); -} - -- (void)addReference:(const DocumentKey &)key { - _sequenceNumbers[key] = self.currentSequenceNumber; -} - -- (void)removeReference:(const DocumentKey &)key { - _sequenceNumbers[key] = self.currentSequenceNumber; -} - -- (BOOL)mutationQueuesContainKey:(const DocumentKey &)key { - const MutationQueues &queues = [_persistence mutationQueues]; - for (const auto &entry : queues) { - if (entry.second->ContainsKey(key)) { - return YES; - } - } - return NO; -} - -- (void)removeMutationReference:(const DocumentKey &)key { - _sequenceNumbers[key] = self.currentSequenceNumber; -} - -- (BOOL)isPinnedAtSequenceNumber:(ListenSequenceNumber)upperBound - document:(const DocumentKey &)key { - if ([self mutationQueuesContainKey:key]) { - return YES; - } - if (_additionalReferences->ContainsKey(key)) { - return YES; - } - if (_persistence.queryCache->Contains(key)) { - return YES; - } - auto it = _sequenceNumbers.find(key); - if (it != _sequenceNumbers.end() && it->second > upperBound) { - return YES; - } - return NO; -} - -- (size_t)byteSize { - // Note that this method is only used for testing because this delegate is only - // used for testing. The algorithm here (loop through everything, serialize it - // and count bytes) is inefficient and inexact, but won't run in production. - size_t count = 0; - count += _persistence.queryCache->CalculateByteSize(_serializer); - count += _persistence.remoteDocumentCache->CalculateByteSize(_serializer); - const MutationQueues &queues = [_persistence mutationQueues]; - for (const auto &entry : queues) { - count += entry.second->CalculateByteSize(_serializer); - } - return count; -} - -@end - -@implementation FSTMemoryEagerReferenceDelegate { - std::unique_ptr> _orphaned; - // This delegate should have the same lifetime as the persistence layer, but mark as - // weak to avoid retain cycle. - __weak FSTMemoryPersistence *_persistence; - ReferenceSet *_additionalReferences; -} - -- (instancetype)initWithPersistence:(FSTMemoryPersistence *)persistence { - if (self = [super init]) { - _persistence = persistence; - } - return self; -} - -- (ListenSequenceNumber)currentSequenceNumber { - return kFSTListenSequenceNumberInvalid; -} - -- (void)addInMemoryPins:(ReferenceSet *)set { - // We should be able to assert that _additionalReferences is nil, but due to restarts in spec - // tests it would fail. - _additionalReferences = set; -} - -- (void)removeTarget:(FSTQueryData *)queryData { - for (const DocumentKey &docKey : _persistence.queryCache->GetMatchingKeys(queryData.targetID)) { - _orphaned->insert(docKey); - } - _persistence.queryCache->RemoveTarget(queryData); -} - -- (void)addReference:(const DocumentKey &)key { - _orphaned->erase(key); -} - -- (void)removeReference:(const DocumentKey &)key { - _orphaned->insert(key); -} - -- (void)removeMutationReference:(const DocumentKey &)key { - _orphaned->insert(key); -} - -- (BOOL)isReferenced:(const DocumentKey &)key { - if (_persistence.queryCache->Contains(key)) { - return YES; - } - if ([self mutationQueuesContainKey:key]) { - return YES; - } - if (_additionalReferences->ContainsKey(key)) { - return YES; - } - return NO; -} - -- (void)limboDocumentUpdated:(const DocumentKey &)key { - if ([self isReferenced:key]) { - _orphaned->erase(key); - } else { - _orphaned->insert(key); - } -} - -- (void)startTransaction:(__unused absl::string_view)label { - _orphaned = absl::make_unique>(); -} - -- (BOOL)mutationQueuesContainKey:(const DocumentKey &)key { - const MutationQueues &queues = [_persistence mutationQueues]; - for (const auto &entry : queues) { - if (entry.second->ContainsKey(key)) { - return YES; - } - } - return NO; -} - -- (void)commitTransaction { - for (const auto &key : *_orphaned) { - if (![self isReferenced:key]) { - _persistence.remoteDocumentCache->Remove(key); - } - } - _orphaned.reset(); -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTPersistence.h b/Firestore/Source/Local/FSTPersistence.h deleted file mode 100644 index 8dcc22a8d3e..00000000000 --- a/Firestore/Source/Local/FSTPersistence.h +++ /dev/null @@ -1,229 +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 - -#include "Firestore/core/src/firebase/firestore/auth/user.h" -#include "Firestore/core/src/firebase/firestore/local/index_manager.h" -#include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" -#include "Firestore/core/src/firebase/firestore/local/query_cache.h" -#include "Firestore/core/src/firebase/firestore/local/reference_set.h" -#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" -#include "Firestore/core/src/firebase/firestore/model/document_key.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" - -@class FSTQueryData; -@protocol FSTReferenceDelegate; - -namespace auth = firebase::firestore::auth; -namespace local = firebase::firestore::local; -namespace model = firebase::firestore::model; - -struct FSTTransactionRunner; - -NS_ASSUME_NONNULL_BEGIN - -/** - * FSTPersistence is the lowest-level shared interface to persistent storage in Firestore. - * - * FSTPersistence is used to create FSTMutationQueue and FSTRemoteDocumentCache instances backed - * by persistence (which might be in-memory or LevelDB). - * - * FSTPersistence also exposes an API to create and commit FSTWriteGroup instances. - * Implementations of FSTWriteGroup/FSTPersistence only need to guarantee that writes made - * against the FSTWriteGroup are not made to durable storage until commitGroup:action: is called - * here. Since memory-only storage components do not alter durable storage, they are free to ignore - * the group. - * - * This contract is enough to allow the FSTLocalStore be be written independently of whether or not - * the stored state actually is durably persisted. If persistent storage is enabled, writes are - * grouped together to avoid inconsistent state that could cause crashes. - * - * Concretely, when persistent storage is enabled, the persistent versions of FSTMutationQueue, - * FSTRemoteDocumentCache, and others (the mutators) will defer their writes into an FSTWriteGroup. - * Once the local store has completed one logical operation, it commits the write group using - * [FSTPersistence commitGroup:action:]. - * - * When persistent storage is disabled, the non-persistent versions of the mutators ignore the - * FSTWriteGroup and [FSTPersistence commitGroup:action:] is a no-op. This short-cut is allowed - * because memory-only storage leaves no state so it cannot be inconsistent. - * - * This simplifies the implementations of the mutators and allows memory-only implementations to - * supplement the persistent ones without requiring any special dual-store implementation of - * FSTPersistence. The cost is that the FSTLocalStore needs to be slightly careful about the order - * of its reads and writes in order to avoid relying on being able to read back uncommitted writes. - */ -@protocol FSTPersistence - -/** Releases any resources held during eager shutdown. */ -- (void)shutdown; - -/** - * Returns a MutationQueue representing the persisted mutations for the given user. - * - *

Note: The implementation is free to return the same instance every time this is called for a - * given user. In particular, the memory-backed implementation does this to emulate the persisted - * implementation to the extent possible (e.g. in the case of uid switching from - * sally=>jack=>sally, sally's mutation queue will be preserved). - */ -- (local::MutationQueue *)mutationQueueForUser:(const auth::User &)user; - -/** Creates an FSTQueryCache representing the persisted cache of queries. */ -- (local::QueryCache *)queryCache; - -/** Creates a RemoteDocumentCache representing the persisted cache of remote documents. */ -- (local::RemoteDocumentCache *)remoteDocumentCache; - -/** Creates an IndexManager that manages our persisted query indexes. */ -- (local::IndexManager *)indexManager; - -@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) id referenceDelegate; - -@property(nonatomic, readonly) model::ListenSequenceNumber currentSequenceNumber; - -@end - -@protocol FSTTransactional - -- (void)startTransaction:(absl::string_view)label; - -- (void)commitTransaction; - -@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:(local::ReferenceSet *)set; - -/** - * Notify the delegate that a target was removed. - */ -- (void)removeTarget:(FSTQueryData *)queryData; - -/** - * Notify the delegate that the given document was added to a target. - */ -- (void)addReference:(const model::DocumentKey &)key; - -/** - * Notify the delegate that the given document was removed from a target. - */ -- (void)removeReference:(const model::DocumentKey &)key; - -/** - * Notify the delegate that a document is no longer being mutated by the user. - */ -- (void)removeMutationReference:(const model::DocumentKey &)key; - -/** - * Notify the delegate that a limbo document was updated. - */ -- (void)limboDocumentUpdated:(const model::DocumentKey &)key; - -@property(nonatomic, readonly) model::ListenSequenceNumber currentSequenceNumber; - -@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 -// to annotate non-pointer types with a nullability annotation. -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wnullability-completeness" - - /** - * The following two functions handle accepting callables and optionally running them within a - * transaction. Persistence layers that conform to the FSTTransactional protocol can set - * themselves as the backing persistence for a transaction runner, in which case a transaction - * will be started before a block is run, and committed after the block has executed. If there is - * no backing instance of FSTTransactional, the block will be run directly. - * - * There are two instances of operator() to handle the case where the block returns void, rather - * than a type. - * - * The transaction runner keeps a weak reference to the backing persistence so as not to cause a - * retain cycle. The reference is upgraded to strong (with a fatal error if it has disappeared) - * for the duration of running a transaction. - */ - - template - auto operator()(absl::string_view label, F block) const -> - typename std::enable_if::value, void>::type { - __strong id strongDb = _db; - if (!strongDb && _expect_db) { - HARD_FAIL("Transaction runner accessed without underlying db when it expected one"); - } - if (strongDb) { - [strongDb startTransaction:label]; - } - block(); - if (strongDb) { - [strongDb commitTransaction]; - } - } - - template - auto operator()(absl::string_view label, F block) const -> - typename std::enable_if::value, decltype(block())>::type { - using ReturnT = decltype(block()); - __strong id strongDb = _db; - if (!strongDb && _expect_db) { - HARD_FAIL("Transaction runner accessed without underlying db when it expected one"); - } - if (strongDb) { - [strongDb startTransaction:label]; - } - ReturnT result = block(); - if (strongDb) { - [strongDb commitTransaction]; - } - return result; - } -#pragma clang diagnostic pop - void SetBackingPersistence(id db) { - _db = db; - _expect_db = true; - } - - private: - __weak id _db; - bool _expect_db = false; -}; - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryData.h b/Firestore/Source/Local/FSTQueryData.h deleted file mode 100644 index 96032d976e1..00000000000 --- a/Firestore/Source/Local/FSTQueryData.h +++ /dev/null @@ -1,92 +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 - -#include "Firestore/core/src/firebase/firestore/core/query.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" - -namespace core = firebase::firestore::core; -namespace model = firebase::firestore::model; - -NS_ASSUME_NONNULL_BEGIN - -/** An enumeration of the different purposes we have for queries. */ -typedef NS_ENUM(NSInteger, FSTQueryPurpose) { - /** A regular, normal query. */ - FSTQueryPurposeListen, - - /** The query was used to refill a query after an existence filter mismatch. */ - FSTQueryPurposeExistenceFilterMismatch, - - /** The query was used to resolve a limbo document. */ - FSTQueryPurposeLimboResolution, -}; - -/** An immutable set of metadata that the store will need to keep track of for each query. */ -@interface FSTQueryData : NSObject - -- (instancetype)initWithQuery:(core::Query)query - targetID:(model::TargetId)targetID - listenSequenceNumber:(model::ListenSequenceNumber)sequenceNumber - purpose:(FSTQueryPurpose)purpose - snapshotVersion:(model::SnapshotVersion)snapshotVersion - resumeToken:(NSData *)resumeToken NS_DESIGNATED_INITIALIZER; - -/** Convenience initializer for use when creating an FSTQueryData for the first time. */ -- (instancetype)initWithQuery:(core::Query)query - targetID:(model::TargetId)targetID - listenSequenceNumber:(model::ListenSequenceNumber)sequenceNumber - purpose:(FSTQueryPurpose)purpose; - -- (instancetype)init NS_UNAVAILABLE; - -/** - * Creates a new query data instance with an updated snapshot version, resume token, and sequence - * number. - */ -- (instancetype)queryDataByReplacingSnapshotVersion:(model::SnapshotVersion)snapshotVersion - resumeToken:(NSData *)resumeToken - sequenceNumber:(model::ListenSequenceNumber)sequenceNumber; - -/** The latest snapshot version seen for this target. */ -- (const model::SnapshotVersion &)snapshotVersion; - -/** The query being listened to. */ -- (const core::Query &)query; - -/** - * The targetID to which the query corresponds, assigned by the FSTLocalStore for user queries or - * the FSTSyncEngine for limbo queries. - */ -@property(nonatomic, assign, readonly) model::TargetId targetID; - -@property(nonatomic, assign, readonly) model::ListenSequenceNumber sequenceNumber; - -/** The purpose of the query. */ -@property(nonatomic, assign, readonly) FSTQueryPurpose purpose; - -/** - * 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. - */ -@property(nonatomic, copy, readonly) NSData *resumeToken; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryData.mm b/Firestore/Source/Local/FSTQueryData.mm deleted file mode 100644 index ebe460a644c..00000000000 --- a/Firestore/Source/Local/FSTQueryData.mm +++ /dev/null @@ -1,116 +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/Local/FSTQueryData.h" - -#include - -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/util/hashing.h" - -namespace util = firebase::firestore::util; -using firebase::firestore::core::Query; -using firebase::firestore::model::ListenSequenceNumber; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::TargetId; - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTQueryData { - Query _query; - SnapshotVersion _snapshotVersion; -} - -- (instancetype)initWithQuery:(Query)query - targetID:(TargetId)targetID - listenSequenceNumber:(ListenSequenceNumber)sequenceNumber - purpose:(FSTQueryPurpose)purpose - snapshotVersion:(SnapshotVersion)snapshotVersion - resumeToken:(NSData *)resumeToken { - self = [super init]; - if (self) { - _query = std::move(query); - _targetID = targetID; - _sequenceNumber = sequenceNumber; - _purpose = purpose; - _snapshotVersion = std::move(snapshotVersion); - _resumeToken = [resumeToken copy]; - } - return self; -} - -- (instancetype)initWithQuery:(Query)query - targetID:(TargetId)targetID - listenSequenceNumber:(ListenSequenceNumber)sequenceNumber - purpose:(FSTQueryPurpose)purpose { - return [self initWithQuery:std::move(query) - targetID:targetID - listenSequenceNumber:sequenceNumber - purpose:purpose - snapshotVersion:SnapshotVersion::None() - resumeToken:[NSData data]]; -} - -- (const Query &)query { - return _query; -} - -- (const firebase::firestore::model::SnapshotVersion &)snapshotVersion { - return _snapshotVersion; -} - -- (BOOL)isEqual:(id)object { - if (self == object) { - return YES; - } - if (![object isKindOfClass:[FSTQueryData class]]) { - return NO; - } - - FSTQueryData *other = (FSTQueryData *)object; - return self.query == other.query && self.targetID == other.targetID && - self.sequenceNumber == other.sequenceNumber && self.purpose == other.purpose && - self.snapshotVersion == other.snapshotVersion && - [self.resumeToken isEqual:other.resumeToken]; -} - -- (NSUInteger)hash { - return util::Hash(self.query, self.targetID, self.sequenceNumber, - static_cast(self.purpose), self.snapshotVersion.Hash(), - [self.resumeToken hash]); -} - -- (NSString *)description { - return [NSString - stringWithFormat:@"", - self.query.ToString().c_str(), self.targetID, (unsigned long)self.purpose, - self.snapshotVersion.ToString().c_str(), self.resumeToken]; -} - -- (instancetype)queryDataByReplacingSnapshotVersion:(SnapshotVersion)snapshotVersion - resumeToken:(NSData *)resumeToken - sequenceNumber:(ListenSequenceNumber)sequenceNumber { - return [[FSTQueryData alloc] initWithQuery:self.query - targetID:self.targetID - listenSequenceNumber:sequenceNumber - purpose:self.purpose - snapshotVersion:std::move(snapshotVersion) - resumeToken:resumeToken]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutationBatch.h b/Firestore/Source/Model/FSTMutationBatch.h deleted file mode 100644 index d9c9ec7e017..00000000000 --- a/Firestore/Source/Model/FSTMutationBatch.h +++ /dev/null @@ -1,144 +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 - -#include -#include - -#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/document_key_set.h" -#include "Firestore/core/src/firebase/firestore/model/document_map.h" -#include "Firestore/core/src/firebase/firestore/model/maybe_document.h" -#include "Firestore/core/src/firebase/firestore/model/mutation.h" -#include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" -#include "Firestore/core/src/firebase/firestore/model/types.h" - -@class FSTMutationBatchResult; - -namespace model = firebase::firestore::model; - -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; - -} // namespace model -} // namespace firestore -} // namespace firebase - -NS_ASSUME_NONNULL_BEGIN - -/** - * A batch of mutations that will be sent as one unit to the backend. Batches can be marked as a - * tombstone if the mutation queue does not remove them immediately. When a batch is a tombstone - * it has no mutations. - */ -@interface FSTMutationBatch : NSObject - -/** - * Initializes a mutation batch with the given batchID, localWriteTime, base mutations, and - * mutations. - */ -- (instancetype)initWithBatchID:(model::BatchId)batchID - localWriteTime:(const firebase::Timestamp &)localWriteTime - baseMutations:(std::vector &&)baseMutations - mutations:(std::vector &&)mutations - NS_DESIGNATED_INITIALIZER; - -- (id)init NS_UNAVAILABLE; - -/** - * Applies all the mutations in this FSTMutationBatch to the specified document to create a new - * remote document. - * - * @param maybeDoc The document to apply mutations to. - * @param documentKey The key of the document to apply mutations to. - * @param mutationBatchResult The result of applying the MutationBatch to the backend. If omitted - * it's assumed that this is a local (latency-compensated) application and documents will have - * their hasLocalMutations flag set. - */ -- (absl::optional) - applyToRemoteDocument:(absl::optional)maybeDoc - documentKey:(const model::DocumentKey &)documentKey - mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult; - -/** - * A helper version of applyTo for applying mutations locally (without a mutation batch result from - * the backend). - */ -- (absl::optional) - applyToLocalDocument:(absl::optional)maybeDoc - documentKey:(const model::DocumentKey &)documentKey; - -/** Computes the local view for all provided documents given the mutations in this batch. */ -- (model::MaybeDocumentMap)applyToLocalDocumentSet:(const model::MaybeDocumentMap &)documentSet; - -/** Returns the set of unique keys referenced by all mutations in the batch. */ -- (model::DocumentKeySet)keys; - -/** The unique ID of this mutation batch. */ -@property(nonatomic, assign, readonly) model::BatchId batchID; - -/** The original write time of this mutation. */ -@property(nonatomic, assign, readonly) const firebase::Timestamp &localWriteTime; - -/** - * Mutations that are used to populate the base values when this mutation is applied locally. This - * can be used to locally overwrite values that are persisted in the remote document cache. Base - * mutations are never sent to the backend. - */ -- (const std::vector &)baseMutations; - -/** - * The user-provided mutations in this mutation batch. User-provided mutations are applied both - * locally and remotely on the backend. - */ -- (const std::vector &)mutations; - -@end - -#pragma mark - FSTMutationBatchResult - -/** The result of applying a mutation batch to the backend. */ -@interface FSTMutationBatchResult : NSObject - -- (instancetype)init NS_UNAVAILABLE; - -/** - * Creates a new FSTMutationBatchResult for the given batch and results. There must be one result - * for each mutation in the batch. This static factory caches a document=>version mapping - * (as docVersions). - */ -+ (instancetype)resultWithBatch:(FSTMutationBatch *)batch - commitVersion:(model::SnapshotVersion)commitVersion - mutationResults:(std::vector)mutationResults - streamToken:(nullable NSData *)streamToken; - -- (const model::SnapshotVersion &)commitVersion; -- (const std::vector &)mutationResults; - -@property(nonatomic, strong, readonly) FSTMutationBatch *batch; -@property(nonatomic, strong, readonly, nullable) NSData *streamToken; - -- (const model::DocumentVersionMap &)docVersions; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutationBatch.mm b/Firestore/Source/Model/FSTMutationBatch.mm deleted file mode 100644 index 3725ec8c0ef..00000000000 --- a/Firestore/Source/Model/FSTMutationBatch.mm +++ /dev/null @@ -1,253 +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/FSTMutationBatch.h" - -#include -#include - -#import "FIRTimestamp.h" - -#include "Firestore/core/src/firebase/firestore/model/document_map.h" -#include "Firestore/core/src/firebase/firestore/objc/objc_compatibility.h" -#include "Firestore/core/src/firebase/firestore/timestamp_internal.h" -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/hashing.h" - -namespace objc = firebase::firestore::objc; -using firebase::Timestamp; -using firebase::TimestampInternal; -using firebase::firestore::model::BatchId; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeyHash; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::DocumentVersionMap; -using firebase::firestore::model::MaybeDocument; -using firebase::firestore::model::MaybeDocumentMap; -using firebase::firestore::model::Mutation; -using firebase::firestore::model::MutationResult; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::util::Hash; - -NS_ASSUME_NONNULL_BEGIN - -@implementation FSTMutationBatch { - Timestamp _localWriteTime; - std::vector _baseMutations; - std::vector _mutations; -} - -- (instancetype)initWithBatchID:(BatchId)batchID - localWriteTime:(const Timestamp &)localWriteTime - baseMutations:(std::vector &&)baseMutations - mutations:(std::vector &&)mutations { - HARD_ASSERT(!mutations.empty(), "Cannot create an empty mutation batch"); - self = [super init]; - if (self) { - _batchID = batchID; - _localWriteTime = localWriteTime; - _baseMutations = std::move(baseMutations); - _mutations = std::move(mutations); - } - return self; -} - -- (const std::vector &)baseMutations { - return _baseMutations; -} - -- (const std::vector &)mutations { - return _mutations; -} - -- (BOOL)isEqual:(id)other { - if (self == other) { - return YES; - } else if (![other isKindOfClass:[FSTMutationBatch class]]) { - return NO; - } - - FSTMutationBatch *otherBatch = (FSTMutationBatch *)other; - return self.batchID == otherBatch.batchID && self.localWriteTime == otherBatch.localWriteTime && - _baseMutations == otherBatch.baseMutations && _mutations == otherBatch.mutations; -} - -- (NSUInteger)hash { - NSUInteger result = (NSUInteger)self.batchID; - result = result * 31 + TimestampInternal::Hash(self.localWriteTime); - for (const Mutation &mutation : _baseMutations) { - result = result * 31 + mutation.Hash(); - } - for (const Mutation &mutation : _mutations) { - result = result * 31 + mutation.Hash(); - } - return result; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"", - self.batchID, self.localWriteTime.ToString().c_str(), - objc::Description(_mutations)]; -} - -- (absl::optional)applyToRemoteDocument:(absl::optional)maybeDoc - documentKey:(const DocumentKey &)documentKey - mutationBatchResult: - (FSTMutationBatchResult *_Nullable)mutationBatchResult { - HARD_ASSERT(!maybeDoc || maybeDoc->key() == documentKey, - "applyTo: key %s doesn't match maybeDoc key %s", documentKey.ToString(), - maybeDoc->key().ToString()); - - HARD_ASSERT(mutationBatchResult.mutationResults.size() == _mutations.size(), - "Mismatch between mutations length (%s) and results length (%s)", _mutations.size(), - mutationBatchResult.mutationResults.size()); - - for (size_t i = 0; i < _mutations.size(); i++) { - const Mutation &mutation = _mutations[i]; - const MutationResult &mutationResult = mutationBatchResult.mutationResults[i]; - if (mutation.key() == documentKey) { - maybeDoc = mutation.ApplyToRemoteDocument(maybeDoc, mutationResult); - } - } - return maybeDoc; -} - -- (absl::optional)applyToLocalDocument:(absl::optional)maybeDoc - documentKey:(const DocumentKey &)documentKey { - HARD_ASSERT(!maybeDoc || maybeDoc->key() == documentKey, - "applyTo: key %s doesn't match maybeDoc key %s", documentKey.ToString(), - maybeDoc->key().ToString()); - - // First, apply the base state. This allows us to apply non-idempotent transform against a - // consistent set of values. - for (const Mutation &mutation : _baseMutations) { - if (mutation.key() == documentKey) { - maybeDoc = mutation.ApplyToLocalView(maybeDoc, maybeDoc, self.localWriteTime); - } - } - - absl::optional baseDoc = maybeDoc; - - // Second, apply all user-provided mutations. - for (const Mutation &mutation : _mutations) { - if (mutation.key() == documentKey) { - maybeDoc = mutation.ApplyToLocalView(maybeDoc, baseDoc, self.localWriteTime); - } - } - return maybeDoc; -} - -- (MaybeDocumentMap)applyToLocalDocumentSet:(const MaybeDocumentMap &)documentSet { - // TODO(mrschmidt): This implementation is O(n^2). If we iterate through the mutations first (as - // done in `applyToLocalDocument:documentKey:`), we can reduce the complexity to O(n). - - MaybeDocumentMap mutatedDocuments = documentSet; - for (const Mutation &mutation : _mutations) { - const DocumentKey &key = mutation.key(); - - absl::optional previousDocument = mutatedDocuments.get(key); - absl::optional mutatedDocument = - [self applyToLocalDocument:std::move(previousDocument) documentKey:key]; - if (mutatedDocument) { - mutatedDocuments = mutatedDocuments.insert(key, *mutatedDocument); - } - } - return mutatedDocuments; -} - -- (DocumentKeySet)keys { - DocumentKeySet set; - for (const Mutation &mutation : _mutations) { - set = set.insert(mutation.key()); - } - return set; -} - -@end - -#pragma mark - FSTMutationBatchResult - -@interface FSTMutationBatchResult () -- (instancetype)initWithBatch:(FSTMutationBatch *)batch - commitVersion:(SnapshotVersion)commitVersion - mutationResults:(std::vector)mutationResults - streamToken:(nullable NSData *)streamToken - docVersions:(DocumentVersionMap)docVersions NS_DESIGNATED_INITIALIZER; -@end - -@implementation FSTMutationBatchResult { - SnapshotVersion _commitVersion; - std::vector _mutationResults; - DocumentVersionMap _docVersions; -} - -- (instancetype)initWithBatch:(FSTMutationBatch *)batch - commitVersion:(SnapshotVersion)commitVersion - mutationResults:(std::vector)mutationResults - streamToken:(nullable NSData *)streamToken - docVersions:(DocumentVersionMap)docVersions { - if (self = [super init]) { - _batch = batch; - _commitVersion = std::move(commitVersion); - _mutationResults = std::move(mutationResults); - _streamToken = streamToken; - _docVersions = std::move(docVersions); - } - return self; -} - -- (const SnapshotVersion &)commitVersion { - return _commitVersion; -} - -- (const std::vector &)mutationResults { - return _mutationResults; -} - -- (const DocumentVersionMap &)docVersions { - return _docVersions; -} - -+ (instancetype)resultWithBatch:(FSTMutationBatch *)batch - commitVersion:(SnapshotVersion)commitVersion - mutationResults:(std::vector)mutationResults - streamToken:(nullable NSData *)streamToken { - HARD_ASSERT(batch.mutations.size() == mutationResults.size(), - "Mutations sent %s must equal results received %s", batch.mutations.size(), - mutationResults.size()); - - DocumentVersionMap docVersions; - std::vector mutations = batch.mutations; - for (size_t i = 0; i < mutations.size(); i++) { - absl::optional version = mutationResults[i].version(); - if (!version) { - // deletes don't have a version, so we substitute the commitVersion - // of the entire batch. - version = commitVersion; - } - - docVersions[mutations[i].key()] = version.value(); - } - - return [[FSTMutationBatchResult alloc] initWithBatch:batch - commitVersion:std::move(commitVersion) - mutationResults:std::move(mutationResults) - streamToken:streamToken - docVersions:std::move(docVersions)]; -} - -@end -NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/CMakeLists.txt b/Firestore/Source/Public/CMakeLists.txt new file mode 100644 index 00000000000..fec68bd0147 --- /dev/null +++ b/Firestore/Source/Public/CMakeLists.txt @@ -0,0 +1,30 @@ +# Copyright 2019 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. + +if(APPLE) + cc_library( + firebase_firestore_public_objc + HEADER_ONLY + SOURCES + FIRFirestoreErrors.h + ) + + target_include_directories( + firebase_firestore_public_objc + INTERFACE + ${FIREBASE_SOURCE_DIR}/Firestore/Source/Public + ) + +endif() + diff --git a/Firestore/Source/Public/FIRCollectionReference.h b/Firestore/Source/Public/FIRCollectionReference.h index 39be000578e..6623f51236b 100644 --- a/Firestore/Source/Public/FIRCollectionReference.h +++ b/Firestore/Source/Public/FIRCollectionReference.h @@ -66,7 +66,7 @@ NS_SWIFT_NAME(CollectionReference) - (FIRDocumentReference *)documentWithPath:(NSString *)documentPath NS_SWIFT_NAME(document(_:)); /** - * Add a new document to this collection with the specified data, assigning it a document ID + * Adds a new document to this collection with the specified data, assigning it a document ID * automatically. * * @param data An `NSDictionary` containing the data for the new document. @@ -77,7 +77,7 @@ NS_SWIFT_NAME(CollectionReference) NS_SWIFT_NAME(addDocument(data:)); /** - * Add a new document to this collection with the specified data, assigning it a document ID + * Adds a new document to this collection with the specified data, assigning it a document ID * automatically. * * @param data An `NSDictionary` containing the data for the new document. diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h index c110bcd10e7..6afba825528 100644 --- a/Firestore/Source/Public/FIRDocumentReference.h +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -25,6 +25,9 @@ NS_ASSUME_NONNULL_BEGIN +/** + * A block type used to handle snapshot updates. + */ typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable error); diff --git a/Firestore/Source/Public/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h index 9a3f61b19a2..5d9fd31225d 100644 --- a/Firestore/Source/Public/FIRDocumentSnapshot.h +++ b/Firestore/Source/Public/FIRDocumentSnapshot.h @@ -79,7 +79,7 @@ NS_SWIFT_NAME(DocumentSnapshot) * exist. * * Server-provided timestamps that have not yet been set to their final value will be returned as - * `NSNull`. You can use `dataWithOptions()` to configure this behavior. + * `NSNull`. You can use `dataWithServerTimestampBehavior()` to configure this behavior. * * @return An `NSDictionary` containing all fields in the document or `nil` if the document doesn't * exist. @@ -103,7 +103,7 @@ NS_SWIFT_NAME(DocumentSnapshot) * exist. * * The timestamps that have not yet been set to their final value will be returned as `NSNull`. The - * can use `get(_:options:)` to configure this behavior. + * can use `get(_:serverTimestampBehavior:)` to configure this behavior. * * @param field The field to retrieve. * @return The value contained in the field or `nil` if the document or field doesn't exist. @@ -115,7 +115,7 @@ NS_SWIFT_NAME(DocumentSnapshot) * exist. * * The timestamps that have not yet been set to their final value will be returned as `NSNull`. The - * can use `get(_:options:)` to configure this behavior. + * can use `get(_:serverTimestampBehavior:)` to configure this behavior. * * @param field The field to retrieve. * @param serverTimestampBehavior Configures how server timestamps that have not yet been set to @@ -159,7 +159,7 @@ NS_SWIFT_NAME(QueryDocumentSnapshot) * Retrieves all fields in the document as an `NSDictionary`. * * Server-provided timestamps that have not yet been set to their final value will be returned as - * `NSNull`. You can use `dataWithOptions()` to configure this behavior. + * `NSNull`. You can use `dataWithServerTimestampBehavior()` to configure this behavior. * * @return An `NSDictionary` containing all fields in the document. */ diff --git a/Firestore/Source/Public/FIRFieldPath.h b/Firestore/Source/Public/FIRFieldPath.h index 3445f2eef33..9f64fbdc99d 100644 --- a/Firestore/Source/Public/FIRFieldPath.h +++ b/Firestore/Source/Public/FIRFieldPath.h @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN NS_SWIFT_NAME(FieldPath) @interface FIRFieldPath : NSObject +/** :nodoc: */ - (instancetype)init NS_UNAVAILABLE; /** diff --git a/Firestore/Source/Public/FIRFirestore.h b/Firestore/Source/Public/FIRFirestore.h index 0f5c9943f0e..ffc0dcf9735 100644 --- a/Firestore/Source/Public/FIRFirestore.h +++ b/Firestore/Source/Public/FIRFirestore.h @@ -192,6 +192,47 @@ NS_SWIFT_NAME(Firestore) */ - (void)clearPersistenceWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; +/** + * Waits until all currently pending writes for the active user have been acknowledged by the + * backend. + * + * The completion block is called immediately without error if there are no outstanding writes. + * Otherwise, the completion block is called when all previously issued writes (including those + * written in a previous app session) have been acknowledged by the backend. The completion + * block does not wait for writes that were added after the method is called. If you + * wish to wait for additional writes, you have to call `waitForPendingWritesWithCompletion` + * again. + * + * Any outstanding `waitForPendingWritesWithCompletion` completion blocks are called with an + * error during user change. + */ +- (void)waitForPendingWritesWithCompletion:(void (^)(NSError *_Nullable error))completion; + +#pragma mark - Terminating + +/** + * Terminates this `FIRFirestore` instance. + * + * After calling `terminate` only the `clearPersistence` method may be used. Any other method + * will throw an error. + * + * To restart after termination, simply create a new instance of FIRFirestore with + * `firestore` or `firestoreForApp` methods. + * + * Termination does not cancel any pending writes and any tasks that are awaiting a response from + * the server will not be resolved. The next time you start this instance, it will resume + * attempting to send these writes to the server. + * + * Note: Under normal circumstances, calling this method is not required. This + * method is useful only when you want to force this instance to release all of its resources or + * in combination with `clearPersistence` to ensure that all local state is destroyed + * between test runs. + * + * @param completion A block to execute once everything has been terminated. + */ +- (void)terminateWithCompletion:(nullable void (^)(NSError *_Nullable error))completion + NS_SWIFT_NAME(terminate(completion:)); + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestoreSource.h b/Firestore/Source/Public/FIRFirestoreSource.h index c1337475ffa..16a65cc9b90 100644 --- a/Firestore/Source/Public/FIRFirestoreSource.h +++ b/Firestore/Source/Public/FIRFirestoreSource.h @@ -22,27 +22,32 @@ * methods can be configured to fetch results only from the server, only from * the local cache, or attempt to fetch results from the server and fall back to * the cache (which is the default). - * - * Setting the source to `Source.default` causes Firestore to try to retrieve an - * up-to-date (server-retrieved) snapshot, but fall back to returning cached - * data if the server can't be reached. - * - * Setting the source to `Source.server` causes Firestore to avoid the cache, - * generating an error if the server cannot be reached. Note that the cache will - * still be updated if the server request succeeds. Also note that - * latency-compensation still takes effect, so any pending write operations will - * be visible in the returned data (merged into the server-provided data). - * - * Setting the source to `Source.cache` causes Firestore to immediately return a - * value from the cache, ignoring the server completely (implying that the - * returned value may be stale with respect to the value on the server). If - * there is no data in the cache to satisfy the `getDocument[s]` call, - * `DocumentReference.getDocument()` will return an error and - * `QuerySnapshot.getDocuments()` will return an empty `QuerySnapshot` with no - * documents. */ typedef NS_ENUM(NSUInteger, FIRFirestoreSource) { + + /** + * Causes Firestore to try to retrieve an up-to-date (server-retrieved) + * snapshot, but fall back to returning cached data if the server can't be + * reached. + */ FIRFirestoreSourceDefault, + + /** + * Causes Firestore to avoid the cache, generating an error if the server + * cannot be reached. Note that the cache will still be updated if the + * server request succeeds. Also note that latency-compensation still takes + * effect, so any pending write operations will be visible in the returned + * data (merged into the server-provided data). + */ FIRFirestoreSourceServer, + + /** + * Causes Firestore to immediately return a value from the cache, ignoring + * the server completely (implying that the returned value may be stale with + * respect to the value on the server). If there is no data in the cache to + * satisfy the `getDocument[s]` call, `DocumentReference.getDocument()` will + * return an error and `QuerySnapshot.getDocuments()` will return an empty + * `QuerySnapshot` with no documents. + */ FIRFirestoreSourceCache } NS_SWIFT_NAME(FirestoreSource); diff --git a/Firestore/Source/Public/FIRGeoPoint.h b/Firestore/Source/Public/FIRGeoPoint.h index 290b2b45ec2..4454225caf8 100644 --- a/Firestore/Source/Public/FIRGeoPoint.h +++ b/Firestore/Source/Public/FIRGeoPoint.h @@ -39,7 +39,14 @@ NS_SWIFT_NAME(GeoPoint) - (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude NS_DESIGNATED_INITIALIZER; +/** + * The point's latitude. Must be a value between -90 and 90 (inclusive). + */ @property(nonatomic, readonly) double latitude; + +/** + * The point's longitude. Must be a value between -180 and 180 (inclusive). + */ @property(nonatomic, readonly) double longitude; @end diff --git a/Firestore/Source/Public/FIRQuery.h b/Firestore/Source/Public/FIRQuery.h index 436c185b4a2..4233f10327a 100644 --- a/Firestore/Source/Public/FIRQuery.h +++ b/Firestore/Source/Public/FIRQuery.h @@ -26,6 +26,9 @@ NS_ASSUME_NONNULL_BEGIN +/** + * A block type used to handle failable snapshot method callbacks. + */ typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot, NSError *_Nullable error); diff --git a/Firestore/Source/Public/FIRTimestamp.h b/Firestore/Source/Public/FIRTimestamp.h index cea316b9887..967d47c06a2 100644 --- a/Firestore/Source/Public/FIRTimestamp.h +++ b/Firestore/Source/Public/FIRTimestamp.h @@ -62,6 +62,13 @@ NS_SWIFT_NAME(Timestamp) /** Returns a new NSDate corresponding to this timestamp. This may lose precision. */ - (NSDate *)dateValue; +/** + * Returns the result of comparing the receiver with another timestamp. + * @param other the other timestamp to compare. + * @return NSOrderedAscending if `other` is chronologically following self, + * NSOrderedDescending if `other` is chronologically preceding self, + * NSOrderedSame otherwise. + */ - (NSComparisonResult)compare:(FIRTimestamp *)other; /** diff --git a/Firestore/Source/Remote/CMakeLists.txt b/Firestore/Source/Remote/CMakeLists.txt new file mode 100644 index 00000000000..d8262bcbe22 --- /dev/null +++ b/Firestore/Source/Remote/CMakeLists.txt @@ -0,0 +1,28 @@ +# Copyright 2019 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. + +if(APPLE) + cc_library( + firebase_firestore_remote_objc + SOURCES + FSTSerializerBeta.h + FSTSerializerBeta.mm + DEPENDS + absl_algorithm + firebase_firestore_nanopb + firebase_firestore_public_objc + libprotobuf_objc + ) + +endif() diff --git a/Firestore/Source/Remote/FSTSerializerBeta.h b/Firestore/Source/Remote/FSTSerializerBeta.h index 94554a251fa..89604f90447 100644 --- a/Firestore/Source/Remote/FSTSerializerBeta.h +++ b/Firestore/Source/Remote/FSTSerializerBeta.h @@ -22,6 +22,7 @@ #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/core/field_filter.h" #include "Firestore/core/src/firebase/firestore/core/query.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.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/field_mask.h" @@ -33,9 +34,6 @@ #include "Firestore/core/src/firebase/firestore/model/transform_operation.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" -@class FSTMutationBatch; -@class FSTQueryData; - @class GCFSBatchGetDocumentsResponse; @class GCFSDocument; @class GCFSDocumentMask; @@ -54,6 +52,7 @@ @class GPBTimestamp; namespace core = firebase::firestore::core; +namespace local = firebase::firestore::local; namespace model = firebase::firestore::model; namespace remote = firebase::firestore::remote; @@ -110,9 +109,9 @@ NS_ASSUME_NONNULL_BEGIN commitVersion:(const model::SnapshotVersion &)commitVersion; - (nullable NSMutableDictionary *)encodedListenRequestLabelsForQueryData: - (FSTQueryData *)queryData; + (const local::QueryData &)queryData; -- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData; +- (GCFSTarget *)encodedTarget:(const local::QueryData &)queryData; - (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(const core::Query &)query; - (core::Query)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target; diff --git a/Firestore/Source/Remote/FSTSerializerBeta.mm b/Firestore/Source/Remote/FSTSerializerBeta.mm index b2bc7883ad1..54d30412968 100644 --- a/Firestore/Source/Remote/FSTSerializerBeta.mm +++ b/Firestore/Source/Remote/FSTSerializerBeta.mm @@ -34,8 +34,6 @@ #import "FIRFirestoreErrors.h" #import "FIRGeoPoint.h" #import "FIRTimestamp.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" #include "Firestore/core/include/firebase/firestore/firestore_errors.h" #include "Firestore/core/include/firebase/firestore/geo_point.h" @@ -43,6 +41,7 @@ #include "Firestore/core/src/firebase/firestore/core/field_filter.h" #include "Firestore/core/src/firebase/firestore/core/filter.h" #include "Firestore/core/src/firebase/firestore/core/query.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/delete_mutation.h" #include "Firestore/core/src/firebase/firestore/model/document.h" @@ -83,6 +82,8 @@ using firebase::firestore::core::OrderBy; using firebase::firestore::core::OrderByList; using firebase::firestore::core::Query; +using firebase::firestore::local::QueryData; +using firebase::firestore::local::QueryPurpose; using firebase::firestore::model::ArrayTransform; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DeleteMutation; @@ -389,7 +390,7 @@ - (FieldValue)decodedReferenceValue:(NSString *)resourceName { const DocumentKey key{[self localResourcePathForQualifiedResourcePath:path]}; const DatabaseId database_id(project, database); - HARD_ASSERT(database_id == _databaseID, "Database %s:%s cannot encode reference from %s:%s", + HARD_ASSERT(database_id == _databaseID, "Database %s:%s cannot decode reference from %s:%s", _databaseID.project_id(), _databaseID.database_id(), database_id.project_id(), database_id.database_id()); return FieldValue::FromReference(_databaseID, key); @@ -449,7 +450,7 @@ - (ObjectValue)decodedMapValue:(GCFSMapValue *)map { - (ObjectValue)decodedFields:(NSDictionary *)fields { __block ObjectValue result = ObjectValue::Empty(); [fields enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, GCFSValue *_Nonnull obj, - BOOL *_Nonnull stop) { + BOOL *_Nonnull) { FieldPath path{util::MakeString(key)}; FieldValue value = [self decodedFieldValue:obj]; result = result.Set(path, std::move(value)); @@ -533,7 +534,7 @@ - (GCFSWrite *)encodedMutation:(const Mutation &)mutation { proto.transform.fieldTransformsArray = [self encodedFieldTransforms:transform.field_transforms()]; // NOTE: We set a precondition of exists: true as a safety-check, since we always combine - // TransformMutations with an SetMutation or PatchMutation which (if successful) should + // TransformMutations with a SetMutation or PatchMutation which (if successful) should // end up with an existing document. proto.currentDocument.exists = YES; return proto; @@ -724,7 +725,7 @@ - (GCFSArrayValue *)encodedArrayTransformElements:(const std::vector - (std::vector)decodedArrayTransformElements:(GCFSArrayValue *)proto { __block std::vector elements; - [proto.valuesArray enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger idx, BOOL *stop) { + [proto.valuesArray enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger, BOOL *) { elements.push_back([self decodedFieldValue:value]); }]; return elements; @@ -747,11 +748,11 @@ - (MutationResult)decodedMutationResult:(GCFSWriteResult *)mutation return MutationResult(std::move(version), std::move(transformResults)); } -#pragma mark - FSTQueryData => GCFSTarget proto +#pragma mark - QueryData => GCFSTarget proto - (nullable NSMutableDictionary *)encodedListenRequestLabelsForQueryData: - (FSTQueryData *)queryData { - NSString *value = [self encodedLabelForPurpose:queryData.purpose]; + (const QueryData &)queryData { + NSString *value = [self encodedLabelForPurpose:queryData.purpose()]; if (!value) { return nil; } @@ -762,22 +763,22 @@ - (MutationResult)decodedMutationResult:(GCFSWriteResult *)mutation return result; } -- (nullable NSString *)encodedLabelForPurpose:(FSTQueryPurpose)purpose { +- (nullable NSString *)encodedLabelForPurpose:(QueryPurpose)purpose { switch (purpose) { - case FSTQueryPurposeListen: + case QueryPurpose::Listen: return nil; - case FSTQueryPurposeExistenceFilterMismatch: + case QueryPurpose::ExistenceFilterMismatch: return @"existence-filter-mismatch"; - case FSTQueryPurposeLimboResolution: + case QueryPurpose::LimboResolution: return @"limbo-document"; default: HARD_FAIL("Unrecognized query purpose: %s", purpose); } } -- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData { +- (GCFSTarget *)encodedTarget:(const QueryData &)queryData { GCFSTarget *result = [GCFSTarget message]; - const Query &query = queryData.query; + const Query &query = queryData.query(); if (query.IsDocumentQuery()) { result.documents = [self encodedDocumentsTarget:query]; @@ -785,10 +786,8 @@ - (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData { result.query = [self encodedQueryTarget:query]; } - result.targetId = queryData.targetID; - if (queryData.resumeToken.length > 0) { - result.resumeToken = queryData.resumeToken; - } + result.targetId = queryData.target_id(); + result.resumeToken = MakeNullableNSData(queryData.resume_token()); return result; } @@ -1168,11 +1167,11 @@ - (SnapshotVersion)versionFromListenResponse:(GCFSListenResponse *)watchChange { WatchTargetChangeState state = [self decodedWatchTargetChangeState:change.targetChangeType]; __block std::vector targetIDs; - [change.targetIdsArray enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { + [change.targetIdsArray enumerateValuesWithBlock:^(int32_t value, NSUInteger, BOOL *) { targetIDs.push_back(value); }]; - NSData *resumeToken = change.resumeToken; + ByteString resumeToken = MakeByteString(change.resumeToken); util::Status cause; if (change.hasCause) { @@ -1180,7 +1179,7 @@ - (SnapshotVersion)versionFromListenResponse:(GCFSListenResponse *)watchChange { util::Status{static_cast(change.cause.code), util::MakeString(change.cause.message)}; } - return absl::make_unique(state, std::move(targetIDs), resumeToken, + return absl::make_unique(state, std::move(targetIDs), std::move(resumeToken), std::move(cause)); } @@ -1204,7 +1203,7 @@ - (WatchTargetChangeState)decodedWatchTargetChangeState:(GCFSTargetChange_Target - (std::vector)decodedIntegerArray:(GPBInt32Array *)values { __block std::vector result; result.reserve(values.count); - [values enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { + [values enumerateValuesWithBlock:^(int32_t value, NSUInteger, BOOL *) { result.push_back(value); }]; return result; diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.h b/Firestore/Source/Util/FSTAsyncQueryListener.h deleted file mode 100644 index 22754f1cc5c..00000000000 --- a/Firestore/Source/Util/FSTAsyncQueryListener.h +++ /dev/null @@ -1,49 +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 - -#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" -#include "Firestore/core/src/firebase/firestore/util/executor.h" - -namespace core = firebase::firestore::core; -namespace util = firebase::firestore::util; - -NS_ASSUME_NONNULL_BEGIN - -/** - * A wrapper class around QueryListener that dispatches events asynchronously. - */ -@interface FSTAsyncQueryListener : NSObject - -- (instancetype)initWithExecutor:(util::Executor*)executor - snapshotHandler:(core::ViewSnapshotHandler&&)snapshotHandler - NS_DESIGNATED_INITIALIZER; - -- (instancetype)init NS_UNAVAILABLE; - -/** - * Synchronously mutes the listener and raise no further events. This method is thread safe can be - * called from any queue. - */ -- (void)mute; - -/** Creates an asynchronous version of the provided snapshot handler. */ -- (core::ViewSnapshotHandler)asyncSnapshotHandler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Firestore/Swift/Source/Codable/CodableErrors.swift b/Firestore/Swift/Source/Codable/CodableErrors.swift index 0b5dcf070ff..70363a8c796 100644 --- a/Firestore/Swift/Source/Codable/CodableErrors.swift +++ b/Firestore/Swift/Source/Codable/CodableErrors.swift @@ -16,6 +16,7 @@ public enum FirestoreDecodingError: Error { case decodingIsNotSupported + case fieldNameConfict(String) } public enum FirestoreEncodingError: Error { diff --git a/Firestore/Swift/Source/Codable/CollectionReference+WriteEncodable.swift b/Firestore/Swift/Source/Codable/CollectionReference+WriteEncodable.swift new file mode 100644 index 00000000000..596057880ef --- /dev/null +++ b/Firestore/Swift/Source/Codable/CollectionReference+WriteEncodable.swift @@ -0,0 +1,39 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import FirebaseFirestore + +extension CollectionReference { + /// Encodes an instance of `Encodable` and adds a new document to this collection + /// with the encoded data, assigning it a document ID automatically. + /// + /// See `Firestore.Encoder` for more details about the encoding process. + /// + /// - Parameters: + /// - value: An instance of `Encodable` to be encoded to a document. + /// - encoder: An encoder instance to use to run the encoding. + /// - 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. + /// - Returns: A `DocumentReference` pointing to the newly created document. + public func addDocument(from value: T, + encoder: Firestore.Encoder = Firestore.Encoder(), + completion: ((Error?) -> Void)? = nil) throws -> DocumentReference { + return addDocument(data: try encoder.encode(value), completion: completion) + } +} diff --git a/Firestore/Swift/Source/Codable/DocumentSnapshot+ReadDecodable.swift b/Firestore/Swift/Source/Codable/DocumentSnapshot+ReadDecodable.swift index defcd1cddfa..14880f3cee4 100644 --- a/Firestore/Swift/Source/Codable/DocumentSnapshot+ReadDecodable.swift +++ b/Firestore/Swift/Source/Codable/DocumentSnapshot+ReadDecodable.swift @@ -34,7 +34,7 @@ extension DocumentSnapshot { d = Firestore.Decoder() } if let data = data() { - return try d?.decode(T.self, from: data) + return try d?.decode(T.self, from: data, in: reference) } return nil } diff --git a/Firestore/Swift/Source/Codable/ExplicitNull.swift b/Firestore/Swift/Source/Codable/ExplicitNull.swift index 9b742ac3e1d..01e6413d8c3 100644 --- a/Firestore/Swift/Source/Codable/ExplicitNull.swift +++ b/Firestore/Swift/Source/Codable/ExplicitNull.swift @@ -38,8 +38,8 @@ public enum ExplicitNull { } } - /// Get the `Optional` representation of `ExplicitNull`. - public var value: Wrapped? { + /// Returns this value as an `Optional`. + public var optionalValue: Wrapped? { switch self { case .none: return .none diff --git a/Firestore/Swift/Source/Codable/FieldValue+Encodable.swift b/Firestore/Swift/Source/Codable/FieldValue+Encodable.swift index 73d8d6c8454..c74d8419960 100644 --- a/Firestore/Swift/Source/Codable/FieldValue+Encodable.swift +++ b/Firestore/Swift/Source/Codable/FieldValue+Encodable.swift @@ -49,6 +49,19 @@ public enum ServerTimestamp: Codable, Equatable { /// value to `stamp`. case resolved(Timestamp) + /// Returns this value as an `Optional`. + /// + /// If the server timestamp is still pending, the returned optional will be `.none`. + /// Once resolved, the returned optional will be `.some` with the resolved timestamp. + public var timestamp: Timestamp? { + switch self { + case .pending: + return .none + case let .resolved(timestamp): + return .some(timestamp) + } + } + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if container.decodeNil() { diff --git a/Firestore/Swift/Source/Codable/SelfDocumentId.swift b/Firestore/Swift/Source/Codable/SelfDocumentId.swift new file mode 100644 index 00000000000..a15b1628872 --- /dev/null +++ b/Firestore/Swift/Source/Codable/SelfDocumentId.swift @@ -0,0 +1,72 @@ +/* + * Copyright 2019 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 FirebaseFirestore + +/// A value that is populated in Codable objects with a `DocumentReference` by +/// the FirestoreDecoder when a document is read. +/// +/// Note that limitations in Swift compiler-generated Codable implementations +/// prevent using this type wrapped in an Optional. Optional SelfDocumentIDs +/// are possible if you write a custom `init(from: Decoder)` method. +/// +/// If the field name used for this type conflicts with a read document field, +/// an error is thrown. For example, if a custom object has a field `firstName` +/// with type `SelfDocumentID`, and there is a property from the document named +/// `firstName` as well, an error is thrown when you try to read the document. +/// +/// When writing a Codable object containing a `SelfDocumentID`, its value is +/// ignored. This allows you to read a document from one path and write it into +/// another without adjusting the value here. +/// +/// NOTE: Trying to encode/decode this type using encoders/decoders other than +/// FirestoreEncoder leads to an error. +public final class SelfDocumentID: Equatable, Codable { + // MARK: - Initializers + + public init() { + reference = nil + } + + public init(from ref: DocumentReference?) { + reference = ref + } + + // MARK: - `Codable` implemention. + + public init(from decoder: Decoder) throws { + throw FirestoreDecodingError.decodingIsNotSupported + } + + public func encode(to encoder: Encoder) throws { + throw FirestoreEncodingError.encodingIsNotSupported + } + + // MARK: - Properties + + public var id: String? { + return reference?.documentID + } + + public let reference: DocumentReference? + + // MARK: - `Equatable` implementation + + public static func == (lhs: SelfDocumentID, + rhs: SelfDocumentID) -> Bool { + return lhs.reference == rhs.reference + } +} diff --git a/Firestore/Swift/Source/Codable/Transaction+WriteEncodable.swift b/Firestore/Swift/Source/Codable/Transaction+WriteEncodable.swift index 62fe1fa6646..82d46bc4952 100644 --- a/Firestore/Swift/Source/Codable/Transaction+WriteEncodable.swift +++ b/Firestore/Swift/Source/Codable/Transaction+WriteEncodable.swift @@ -29,6 +29,7 @@ extension Transaction { /// - encoder: The encoder instance to use to run the encoding. /// - doc: The document to create/overwrite the encoded data to. /// - Returns: This instance of `Transaction`. Used for chaining method calls. + @discardableResult public func setData(from value: T, forDocument doc: DocumentReference, encoder: Firestore.Encoder = Firestore.Encoder()) throws -> Transaction { @@ -50,6 +51,7 @@ extension Transaction { /// document. /// - encoder: The encoder instance to use to run the encoding. /// - Returns: This instance of `Transaction`. Used for chaining method calls. + @discardableResult public func setData(from value: T, forDocument doc: DocumentReference, merge: Bool, @@ -76,6 +78,7 @@ extension Transaction { /// document. /// - encoder: The encoder instance to use to run the encoding. /// - Returns: This instance of `Transaction`. Used for chaining method calls. + @discardableResult public func setData(from value: T, forDocument doc: DocumentReference, mergeFields: [Any], diff --git a/Firestore/Swift/Source/Codable/WriteBatch+WriteEncodable.swift b/Firestore/Swift/Source/Codable/WriteBatch+WriteEncodable.swift index 38fc704ad4b..6f4d11ffff9 100644 --- a/Firestore/Swift/Source/Codable/WriteBatch+WriteEncodable.swift +++ b/Firestore/Swift/Source/Codable/WriteBatch+WriteEncodable.swift @@ -29,6 +29,7 @@ extension WriteBatch { /// - encoder: The encoder instance to use to run the encoding. /// - doc: The document to create/overwrite the encoded data to. /// - Returns: This instance of `WriteBatch`. Used for chaining method calls. + @discardableResult public func setData(from value: T, forDocument doc: DocumentReference, encoder: Firestore.Encoder = Firestore.Encoder()) throws -> WriteBatch { @@ -50,6 +51,7 @@ extension WriteBatch { /// document. /// - encoder: The encoder instance to use to run the encoding. /// - Returns: This instance of `WriteBatch`. Used for chaining method calls. + @discardableResult public func setData(from value: T, forDocument doc: DocumentReference, merge: Bool, @@ -76,6 +78,7 @@ extension WriteBatch { /// document. /// - encoder: The encoder instance to use to run the encoding. /// - Returns: This instance of `WriteBatch`. Used for chaining method calls. + @discardableResult public func setData(from value: T, forDocument doc: DocumentReference, mergeFields: [Any], diff --git a/Firestore/Swift/Tests/API/BasicCompileTests.swift b/Firestore/Swift/Tests/API/BasicCompileTests.swift index 6196c0eba77..9f9fea2a531 100644 --- a/Firestore/Swift/Tests/API/BasicCompileTests.swift +++ b/Firestore/Swift/Tests/API/BasicCompileTests.swift @@ -63,6 +63,10 @@ func main() { clearPersistence(database: db) types() + + waitForPendingWrites(database: db) + + terminateDb(database: db) } func initializeDb() -> Firestore { @@ -448,3 +452,21 @@ func types() { let _: Transaction let _: WriteBatch } + +func waitForPendingWrites(database db: Firestore) { + db.waitForPendingWrites { error in + if let e = error { + print("Uh oh! \(e)") + return + } + } +} + +func terminateDb(database db: Firestore) { + db.terminate { error in + if let e = error { + print("Uh oh! \(e)") + return + } + } +} diff --git a/Firestore/Swift/Tests/Codable/FirestoreEncoderTests.swift b/Firestore/Swift/Tests/Codable/FirestoreEncoderTests.swift index 4624e248cb8..1513e18e9ec 100644 --- a/Firestore/Swift/Tests/Codable/FirestoreEncoderTests.swift +++ b/Firestore/Swift/Tests/Codable/FirestoreEncoderTests.swift @@ -605,4 +605,69 @@ class FirestoreEncoderTests: XCTestCase { decoded = try Firestore.Decoder().decode(Model.self, from: encoded) XCTAssertEqual(decoded, fieldIsNotNull) } + + func testAutomaticallyPopulatesSelfDocumentIDField() throws { + struct Model: Codable, Equatable { + var name: String + var docId: SelfDocumentID + } + + let decoded = try Firestore.Decoder().decode(Model.self, from: ["name": "abc"], in: FSTTestDocRef("abc/123")) + XCTAssertEqual(decoded, Model(name: "abc", docId: SelfDocumentID(from: FSTTestDocRef("abc/123")))) + } + + func testSelfDocumentIDIgnoredInEncoding() throws { + struct Model: Codable, Equatable { + var name: String + var docId: SelfDocumentID + } + + let model = Model(name: "abc", docId: SelfDocumentID(from: FSTTestDocRef("abc/123"))) + _ = assertEncodes(model, to: ["name": "abc"]) + } + + func testEncodingSelfDocumentIDNotEmbeddedThrows() { + let doc = SelfDocumentID(from: FSTTestDocRef("abc/xyz")) + do { + _ = try Firestore.Encoder().encode(doc) + XCTFail("Failed to throw") + } catch FirestoreEncodingError.encodingIsNotSupported { + return + } catch { + XCTFail("Unrecognized error: \(error)") + } + } + + func testSelfDocumentIDWithJsonEncoderThrows() { + let doc = SelfDocumentID(from: FSTTestDocRef("abc/xyz")) + do { + _ = try JSONEncoder().encode(doc) + XCTFail("Failed to throw") + } catch FirestoreEncodingError.encodingIsNotSupported { + return + } catch { + XCTFail("Unrecognized error: \(error)") + } + } + + func testDecodingSelfDocumentIDWithConfictingFieldsThrows() throws { + struct Model: Codable, Equatable { + var name: String + var docId: SelfDocumentID + } + + do { + _ = try Firestore.Decoder().decode( + Model.self, + from: ["name": "abc", "docId": "Causing conflict"], + in: FSTTestDocRef("abc/123") + ) + XCTFail("Failed to throw") + } catch let FirestoreDecodingError.fieldNameConfict(msg) { + XCTAssertEqual(msg, "Field name [\"docId\"] was found from document \"abc/123\"," + " cannot assign the document reference to this field.") + return + } catch { + XCTFail("Unrecognized error: \(error)") + } + } } diff --git a/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift b/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift index 65b6ed1ade4..6852c1243a5 100644 --- a/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift +++ b/Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift @@ -55,11 +55,11 @@ class CodableIntegrationTests: FSTIntegrationTestCase { doc.firestore.runTransaction({ (transaction, errorPointer) -> Any? in do { if let merge = merge { - _ = try transaction.setData(from: value, forDocument: doc, merge: merge) + try transaction.setData(from: value, forDocument: doc, merge: merge) } else if let mergeFields = mergeFields { - _ = try transaction.setData(from: value, forDocument: doc, mergeFields: mergeFields) + try transaction.setData(from: value, forDocument: doc, mergeFields: mergeFields) } else { - _ = try transaction.setData(from: value, forDocument: doc) + try transaction.setData(from: value, forDocument: doc) } } catch { XCTFail("setData with transaction failed.") @@ -67,7 +67,8 @@ class CodableIntegrationTests: FSTIntegrationTestCase { return nil }) { object, error in completion?(error) - } } + } + } awaitExpectations() } @@ -147,7 +148,7 @@ class CodableIntegrationTests: FSTIntegrationTestCase { struct Model: Encodable { var name: String var explicitNull: ExplicitNull - var optional: Optional + var optional: String? } let model = Model( name: "name", @@ -168,6 +169,30 @@ class CodableIntegrationTests: FSTIntegrationTestCase { } } + func testSelfDocumentID() throws { + struct Model: Codable, Equatable { + var name: String + var docId: SelfDocumentID + } + + let docToWrite = documentRef() + let model = Model( + name: "name", + docId: SelfDocumentID() + ) + + try setData(from: model, forDocument: docToWrite, withFlavor: .docRef) + let data = readDocument(forRef: docToWrite).data() + + // "docId" is ignored during encoding + XCTAssertEqual(data! as! [String: String], ["name": "name"]) + + // Decoded result has "docId" auto-populated. + let decoded = try readDocument(forRef: docToWrite).data(as: Model.self) + XCTAssertEqual(decoded!, Model(name: "name", + docId: SelfDocumentID(from: docToWrite))) + } + func testSetThenMerge() throws { struct Model: Codable, Equatable { var name: String? = nil @@ -198,4 +223,23 @@ class CodableIntegrationTests: FSTIntegrationTestCase { age: 10, hobby: "Play"), "Failed with flavor \(flavor)") } } + + func testAddDocument() throws { + struct Model: Codable, Equatable { + var name: String + } + + let collection = collectionRef() + let model = Model(name: "test") + + let added = expectation(description: "Add document") + let docRef = try collection.addDocument(from: model) { error in + XCTAssertNil(error) + added.fulfill() + } + awaitExpectations() + + let result = try readDocument(forRef: docRef).data(as: Model.self) + XCTAssertEqual(model, result) + } } diff --git a/Firestore/core/include/firebase/firestore/timestamp.h b/Firestore/core/include/firebase/firestore/timestamp.h index 7df104a1aef..49c60d33615 100644 --- a/Firestore/core/include/firebase/firestore/timestamp.h +++ b/Firestore/core/include/firebase/firestore/timestamp.h @@ -236,18 +236,4 @@ std::chrono::time_point Timestamp::ToTimePoint() const { } // namespace firebase -namespace std { -/** Convenience-only specialization of `std::hash`. */ -template <> -struct hash { - /** - * Hashes the given `timestamp`. - * - * @note: specialization of `std::hash` is provided for convenience only. The - * implementation is subject to change. - */ - size_t operator()(const firebase::Timestamp& timestamp) const; -}; -} // namespace std - #endif // FIRESTORE_CORE_INCLUDE_FIREBASE_FIRESTORE_TIMESTAMP_H_ diff --git a/Firestore/core/src/firebase/firestore/api/CMakeLists.txt b/Firestore/core/src/firebase/firestore/api/CMakeLists.txt index 12e4a40132a..cdbca900cbf 100644 --- a/Firestore/core/src/firebase/firestore/api/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/api/CMakeLists.txt @@ -41,12 +41,15 @@ cc_library( SOURCES collection_reference.cc collection_reference.h + collection_reference.cc document_change.cc document_change.h document_snapshot.cc document_snapshot.h query_core.h query_core.mm + query_snapshot.cc + query_snapshot.h settings.cc settings.h snapshot_metadata.cc diff --git a/Firestore/core/src/firebase/firestore/api/document_reference.h b/Firestore/core/src/firebase/firestore/api/document_reference.h index 9d8cd6172bb..d932c8c1295 100644 --- a/Firestore/core/src/firebase/firestore/api/document_reference.h +++ b/Firestore/core/src/firebase/firestore/api/document_reference.h @@ -26,10 +26,9 @@ #include "Firestore/core/src/firebase/firestore/core/listen_options.h" #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/util/nullability.h" #include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" - -NS_ASSUME_NONNULL_BEGIN +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" namespace firebase { namespace firestore { @@ -83,7 +82,7 @@ class DocumentReference { void GetDocument(Source source, DocumentSnapshot::Listener&& callback); - ListenerRegistration AddSnapshotListener( + std::unique_ptr AddSnapshotListener( core::ListenOptions options, DocumentSnapshot::Listener&& listener); private: @@ -97,6 +96,4 @@ bool operator==(const DocumentReference& lhs, const DocumentReference& rhs); } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_DOCUMENT_REFERENCE_H_ diff --git a/Firestore/core/src/firebase/firestore/api/document_reference.mm b/Firestore/core/src/firebase/firestore/api/document_reference.mm index 01e58cd200d..f149a1cbbc0 100644 --- a/Firestore/core/src/firebase/firestore/api/document_reference.mm +++ b/Firestore/core/src/firebase/firestore/api/document_reference.mm @@ -19,13 +19,11 @@ #include // NOLINT(build/c++11) #include -#import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRListenerRegistration+Internal.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" - #include "Firestore/core/src/firebase/firestore/api/collection_reference.h" +#include "Firestore/core/src/firebase/firestore/api/firestore.h" +#include "Firestore/core/src/firebase/firestore/api/query_listener_registration.h" #include "Firestore/core/src/firebase/firestore/api/source.h" +#include "Firestore/core/src/firebase/firestore/core/firestore_client.h" #include "Firestore/core/src/firebase/firestore/core/user_data.h" #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/delete_mutation.h" @@ -40,8 +38,6 @@ #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace api { @@ -97,29 +93,27 @@ void DocumentReference::SetData(core::ParsedSetData&& set_data, util::StatusCallback callback) { - [firestore_->client() writeMutations:std::move(set_data).ToMutations( - key(), Precondition::None()) - callback:std::move(callback)]; + firestore_->client()->WriteMutations( + std::move(set_data).ToMutations(key(), Precondition::None()), + std::move(callback)); } void DocumentReference::UpdateData(core::ParsedUpdateData&& update_data, util::StatusCallback callback) { - [firestore_->client() - writeMutations:std::move(update_data) - .ToMutations(key(), Precondition::Exists(true)) - callback:std::move(callback)]; + firestore_->client()->WriteMutations( + std::move(update_data).ToMutations(key(), Precondition::Exists(true)), + std::move(callback)); } void DocumentReference::DeleteDocument(util::StatusCallback callback) { DeleteMutation mutation(key_, Precondition::None()); - [firestore_->client() writeMutations:{mutation} callback:std::move(callback)]; + firestore_->client()->WriteMutations({mutation}, std::move(callback)); } void DocumentReference::GetDocument(Source source, DocumentSnapshot::Listener&& callback) { if (source == Source::Cache) { - [firestore_->client() getDocumentFromLocalCache:*this - callback:std::move(callback)]; + firestore_->client()->GetDocumentFromLocalCache(*this, std::move(callback)); return; } @@ -144,9 +138,9 @@ void OnEvent(StatusOr maybe_snapshot) override { // Remove query first before passing event to user to avoid user actions // affecting the now stale query. - ListenerRegistration registration = + std::unique_ptr registration = registration_promise_.get_future().get(); - registration.Remove(); + registration->Remove(); if (!snapshot.exists() && snapshot.metadata().from_cache()) { // TODO(dimond): Reconsider how to raise missing documents when @@ -174,7 +168,7 @@ void OnEvent(StatusOr maybe_snapshot) override { } } - void Resolve(ListenerRegistration&& registration) { + void Resolve(std::unique_ptr registration) { registration_promise_.set_value(std::move(registration)); } @@ -182,18 +176,18 @@ void Resolve(ListenerRegistration&& registration) { Source source_; DocumentSnapshot::Listener listener_; - std::promise registration_promise_; + std::promise> registration_promise_; }; auto listener = absl::make_unique(source, std::move(callback)); auto listener_unowned = listener.get(); - ListenerRegistration registration = + std::unique_ptr registration = AddSnapshotListener(std::move(options), std::move(listener)); listener_unowned->Resolve(std::move(registration)); } -ListenerRegistration DocumentReference::AddSnapshotListener( +std::unique_ptr DocumentReference::AddSnapshotListener( ListenOptions options, DocumentSnapshot::Listener&& user_listener) { // Convert from ViewSnapshots to DocumentSnapshots. class Converter : public EventListener { @@ -237,15 +231,16 @@ void OnEvent(StatusOr maybe_snapshot) override { // Call the view_listener on the user Executor. auto async_listener = AsyncEventListener::Create( - firestore_->client().userExecutor, std::move(view_listener)); + firestore_->client()->user_executor(), std::move(view_listener)); core::Query query(key_.path()); std::shared_ptr query_listener = - [firestore_->client() listenToQuery:std::move(query) - options:options - listener:async_listener]; - return ListenerRegistration(firestore_->client(), std::move(async_listener), - std::move(query_listener)); + firestore_->client()->ListenToQuery(std::move(query), options, + async_listener); + + return absl::make_unique( + firestore_->client(), std::move(async_listener), + std::move(query_listener)); } bool operator==(const DocumentReference& lhs, const DocumentReference& rhs) { @@ -255,5 +250,3 @@ void OnEvent(StatusOr maybe_snapshot) override { } // namespace api } // namespace firestore } // namespace firebase - -NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/api/document_snapshot.h b/Firestore/core/src/firebase/firestore/api/document_snapshot.h index b5fad8cc260..8bd889e70f6 100644 --- a/Firestore/core/src/firebase/firestore/api/document_snapshot.h +++ b/Firestore/core/src/firebase/firestore/api/document_snapshot.h @@ -27,7 +27,6 @@ #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/field_value.h" -#include "Firestore/core/src/firebase/firestore/objc/objc_class.h" #include "absl/types/optional.h" namespace firebase { diff --git a/Firestore/core/src/firebase/firestore/api/firestore.h b/Firestore/core/src/firebase/firestore/api/firestore.h index 32b2e0bd139..1f35cada7e5 100644 --- a/Firestore/core/src/firebase/firestore/api/firestore.h +++ b/Firestore/core/src/firebase/firestore/api/firestore.h @@ -17,33 +17,34 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_FIRESTORE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_FIRESTORE_H_ -#include - #include #include // NOLINT(build/c++11) #include #include +#include "Firestore/core/src/firebase/firestore/api/listener_registration.h" #include "Firestore/core/src/firebase/firestore/api/settings.h" #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/core/event_listener.h" #include "Firestore/core/src/firebase/firestore/core/transaction.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/objc/objc_class.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" +#include "Firestore/core/src/firebase/firestore/util/empty.h" +#include "Firestore/core/src/firebase/firestore/util/nullability.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/types/any.h" -NS_ASSUME_NONNULL_BEGIN - -OBJC_CLASS(FIRQuery); -OBJC_CLASS(FIRTransaction); -OBJC_CLASS(FSTFirestoreClient); -OBJC_CLASS(NSString); - namespace firebase { namespace firestore { +namespace core { + +class FirestoreClient; +class Query; + +} // namespace core + namespace api { class CollectionReference; @@ -68,7 +69,7 @@ class Firestore : public std::enable_shared_from_this { return persistence_key_; } - FSTFirestoreClient* client(); + const std::shared_ptr& client(); const std::shared_ptr& worker_queue(); @@ -84,13 +85,16 @@ class Firestore : public std::enable_shared_from_this { CollectionReference GetCollection(absl::string_view collection_path); DocumentReference GetDocument(absl::string_view document_path); WriteBatch GetBatch(); - FIRQuery* GetCollectionGroup(std::string collection_id); + core::Query GetCollectionGroup(std::string collection_id); void RunTransaction(core::TransactionUpdateCallback update_callback, core::TransactionResultCallback result_callback); - void Shutdown(util::StatusCallback callback); + void Terminate(util::StatusCallback callback); void ClearPersistence(util::StatusCallback callback); + void WaitForPendingWrites(util::StatusCallback callback); + std::unique_ptr AddSnapshotsInSyncListener( + std::unique_ptr> listener); void EnableNetwork(util::StatusCallback callback); void DisableNetwork(util::StatusCallback callback); @@ -102,7 +106,7 @@ class Firestore : public std::enable_shared_from_this { model::DatabaseId database_id_; std::shared_ptr credentials_provider_; std::string persistence_key_; - objc::Handle client_; + std::shared_ptr client_; std::shared_ptr user_executor_; std::shared_ptr worker_queue_; @@ -118,6 +122,4 @@ class Firestore : public std::enable_shared_from_this { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_FIRESTORE_H_ diff --git a/Firestore/core/src/firebase/firestore/api/firestore.mm b/Firestore/core/src/firebase/firestore/api/firestore.mm index 0c49b9e5602..b261017dd0f 100644 --- a/Firestore/core/src/firebase/firestore/api/firestore.mm +++ b/Firestore/core/src/firebase/firestore/api/firestore.mm @@ -16,24 +16,20 @@ #include "Firestore/core/src/firebase/firestore/api/firestore.h" -#import "Firestore/Source/API/FIRCollectionReference+Internal.h" -#import "Firestore/Source/API/FIRDocumentReference+Internal.h" -#import "Firestore/Source/API/FIRFirestore+Internal.h" -#import "Firestore/Source/API/FIRQuery+Internal.h" -#import "Firestore/Source/API/FIRTransaction+Internal.h" -#import "Firestore/Source/Core/FSTFirestoreClient.h" -#import "Firestore/Source/Local/FSTLevelDB.h" - #include "Firestore/core/src/firebase/firestore/api/collection_reference.h" #include "Firestore/core/src/firebase/firestore/api/document_reference.h" #include "Firestore/core/src/firebase/firestore/api/settings.h" +#include "Firestore/core/src/firebase/firestore/api/snapshots_in_sync_listener_registration.h" #include "Firestore/core/src/firebase/firestore/api/write_batch.h" #include "Firestore/core/src/firebase/firestore/auth/firebase_credentials_provider_apple.h" +#include "Firestore/core/src/firebase/firestore/core/firestore_client.h" +#include "Firestore/core/src/firebase/firestore/core/query.h" #include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" #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/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "absl/memory/memory.h" @@ -43,13 +39,16 @@ namespace api { using auth::CredentialsProvider; +using core::AsyncEventListener; using core::DatabaseInfo; +using core::FirestoreClient; using core::Transaction; +using local::LevelDbPersistence; using model::DocumentKey; using model::ResourcePath; using util::AsyncQueue; +using util::Empty; using util::Executor; -using util::ExecutorLibdispatch; using util::Status; Firestore::Firestore(model::DatabaseId database_id, @@ -64,7 +63,7 @@ extension_{extension} { } -FSTFirestoreClient* Firestore::client() { +const std::shared_ptr& Firestore::client() { HARD_ASSERT(client_, "Client is not yet configured."); return client_; } @@ -89,8 +88,7 @@ settings_ = settings; } -void Firestore::set_user_executor( - std::unique_ptr user_executor) { +void Firestore::set_user_executor(std::unique_ptr user_executor) { std::lock_guard lock{mutex_}; HARD_ASSERT(!client_ && user_executor, "set_user_executor() must be called with a valid executor, " @@ -116,13 +114,11 @@ return WriteBatch(shared_from_this()); } -FIRQuery* Firestore::GetCollectionGroup(std::string collection_id) { +core::Query Firestore::GetCollectionGroup(std::string collection_id) { EnsureClientConfigured(); - core::Query query(ResourcePath::Empty(), std::make_shared( - std::move(collection_id))); - return [[FIRQuery alloc] initWithQuery:std::move(query) - firestore:shared_from_this()]; + return core::Query(ResourcePath::Empty(), std::make_shared( + std::move(collection_id))); } void Firestore::RunTransaction( @@ -130,16 +126,20 @@ core::TransactionResultCallback result_callback) { EnsureClientConfigured(); - [client_ transactionWithRetries:5 - updateCallback:std::move(update_callback) - resultCallback:std::move(result_callback)]; + client_->Transaction(5, std::move(update_callback), + std::move(result_callback)); } -void Firestore::Shutdown(util::StatusCallback callback) { +void Firestore::Terminate(util::StatusCallback callback) { // The client must be initialized to ensure that all subsequent API usage // throws an exception. EnsureClientConfigured(); - [client_ shutdownWithCallback:std::move(callback)]; + client_->Terminate(std::move(callback)); +} + +void Firestore::WaitForPendingWrites(util::StatusCallback callback) { + EnsureClientConfigured(); + client_->WaitForPendingWrites(std::move(callback)); } void Firestore::ClearPersistence(util::StatusCallback callback) { @@ -152,7 +152,7 @@ { std::lock_guard lock{mutex_}; - if (client_ && !client().isShutdown) { + if (client_ && !client()->is_terminated()) { Yield(util::Status( Error::FailedPrecondition, "Persistence cannot be cleared while the client is running.")); @@ -160,18 +160,28 @@ } } - Yield([FSTLevelDB clearPersistence:MakeDatabaseInfo()]); + Yield(LevelDbPersistence::ClearPersistence(MakeDatabaseInfo())); }); } void Firestore::EnableNetwork(util::StatusCallback callback) { EnsureClientConfigured(); - [client_ enableNetworkWithCallback:std::move(callback)]; + client_->EnableNetwork(std::move(callback)); } void Firestore::DisableNetwork(util::StatusCallback callback) { EnsureClientConfigured(); - [client_ disableNetworkWithCallback:std::move(callback)]; + client_->DisableNetwork(std::move(callback)); +} + +std::unique_ptr Firestore::AddSnapshotsInSyncListener( + std::unique_ptr> listener) { + EnsureClientConfigured(); + auto async_listener = AsyncEventListener::Create( + client_->user_executor(), std::move(listener)); + client_->AddSnapshotsInSyncListener(std::move(async_listener)); + return absl::make_unique( + client_, std::move(async_listener)); } void Firestore::EnsureClientConfigured() { @@ -179,12 +189,9 @@ if (!client_) { HARD_ASSERT(worker_queue_, "Expected non-null worker queue"); - client_ = [FSTFirestoreClient - clientWithDatabaseInfo:MakeDatabaseInfo() - settings:settings_ - credentialsProvider:std::move(credentials_provider_) - userExecutor:user_executor_ - workerQueue:worker_queue_]; + client_ = FirestoreClient::Create(MakeDatabaseInfo(), settings_, + std::move(credentials_provider_), + user_executor_, worker_queue_); } } diff --git a/Firestore/core/src/firebase/firestore/api/listener_registration.h b/Firestore/core/src/firebase/firestore/api/listener_registration.h index 2c3cda0c127..0daef042839 100644 --- a/Firestore/core/src/firebase/firestore/api/listener_registration.h +++ b/Firestore/core/src/firebase/firestore/api/listener_registration.h @@ -17,28 +17,21 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_LISTENER_REGISTRATION_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_LISTENER_REGISTRATION_H_ -#include -#include - -#include "Firestore/core/src/firebase/firestore/core/event_listener.h" -#include "Firestore/core/src/firebase/firestore/core/query_listener.h" -#include "Firestore/core/src/firebase/firestore/objc/objc_class.h" - -OBJC_CLASS(FSTFirestoreClient); - -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { +namespace core { +class FirestoreClient; +} + namespace api { /** * An internal handle that encapsulates a user's ability to request that we - * stop listening to a query. When a user calls Remove(), ListenerRegistration - * will synchronously mute the listener and then send a request to the - * FirestoreClient to actually unlisten. + * stop listening to a listener. When a user calls Remove(), + * ListenerRegistration will synchronously mute the listener and then send a + * request to the FirestoreClient to actually unlisten. * - * ListenerRegistration will not automaticlaly stop listening if it is + * ListenerRegistration will not automatically stop listening if it is * destroyed. We allow users to fire and forget listens if they never want to * stop them. * @@ -52,33 +45,17 @@ namespace api { */ class ListenerRegistration { public: - ListenerRegistration( - FSTFirestoreClient* client, - std::shared_ptr> - async_listener, - std::shared_ptr query_listener); + virtual ~ListenerRegistration() = default; /** - * Removes the listener being tracked by this FIRListenerRegistration. After + * Removes the listener being tracked in this ListenerRegistration. After * the initial call, subsequent calls have no effect. */ - void Remove(); - - private: - /** The client that was used to register this listen. */ - objc::Handle client_; - - /** The async listener that is used to mute events synchronously. */ - std::weak_ptr> async_listener_; - - /** The internal QueryListener that can be used to unlisten the query. */ - std::weak_ptr query_listener_; + virtual void Remove() = 0; }; } // namespace api } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_LISTENER_REGISTRATION_H_ diff --git a/Firestore/core/src/firebase/firestore/api/query_core.h b/Firestore/core/src/firebase/firestore/api/query_core.h index c647d9d7065..7e1c1c37c8c 100644 --- a/Firestore/core/src/firebase/firestore/api/query_core.h +++ b/Firestore/core/src/firebase/firestore/api/query_core.h @@ -77,8 +77,8 @@ class Query { * * @return A ListenerRegistration that can be used to remove this listener. */ - ListenerRegistration AddSnapshotListener(core::ListenOptions options, - QuerySnapshot::Listener&& listener); + std::unique_ptr AddSnapshotListener( + core::ListenOptions options, QuerySnapshot::Listener&& listener); /** * Creates and returns a new `Query` with the additional filter that documents diff --git a/Firestore/core/src/firebase/firestore/api/query_core.mm b/Firestore/core/src/firebase/firestore/api/query_core.mm index 31e1b7ed456..3cb95c5cf5a 100644 --- a/Firestore/core/src/firebase/firestore/api/query_core.mm +++ b/Firestore/core/src/firebase/firestore/api/query_core.mm @@ -21,17 +21,15 @@ #include #include -#import "Firestore/Source/Core/FSTFirestoreClient.h" - #include "Firestore/core/src/firebase/firestore/api/firestore.h" +#include "Firestore/core/src/firebase/firestore/api/query_listener_registration.h" #include "Firestore/core/src/firebase/firestore/core/field_filter.h" #include "Firestore/core/src/firebase/firestore/core/filter.h" +#include "Firestore/core/src/firebase/firestore/core/firestore_client.h" #include "Firestore/core/src/firebase/firestore/core/operator.h" #include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "absl/algorithm/container.h" -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace api { @@ -69,8 +67,8 @@ void Query::GetDocuments(Source source, QuerySnapshot::Listener&& callback) { if (source == Source::Cache) { - [firestore_->client() getDocumentsFromLocalCache:*this - callback:std::move(callback)]; + firestore_->client()->GetDocumentsFromLocalCache(*this, + std::move(callback)); return; } @@ -95,9 +93,9 @@ void OnEvent(StatusOr maybe_snapshot) override { // Remove query first before passing event to user to avoid user actions // affecting the now stale query. - ListenerRegistration registration = + std::unique_ptr registration = registration_promise_.get_future().get(); - registration.Remove(); + registration->Remove(); if (snapshot.metadata().from_cache() && source_ == Source::Server) { listener_->OnEvent(Status{ @@ -110,7 +108,7 @@ void OnEvent(StatusOr maybe_snapshot) override { } }; - void Resolve(ListenerRegistration&& registration) { + void Resolve(std::unique_ptr registration) { registration_promise_.set_value(std::move(registration)); } @@ -118,19 +116,19 @@ void Resolve(ListenerRegistration&& registration) { Source source_; QuerySnapshot::Listener listener_; - std::promise registration_promise_; + std::promise> registration_promise_; }; auto listener = absl::make_unique(source, std::move(callback)); auto listener_unowned = listener.get(); - ListenerRegistration registration = + std::unique_ptr registration = AddSnapshotListener(std::move(options), std::move(listener)); listener_unowned->Resolve(std::move(registration)); } -ListenerRegistration Query::AddSnapshotListener( +std::unique_ptr Query::AddSnapshotListener( ListenOptions options, QuerySnapshot::Listener&& user_listener) { // Convert from ViewSnapshots to QuerySnapshots. class Converter : public EventListener { @@ -167,15 +165,15 @@ QuerySnapshot result(firestore_, query_, std::move(snapshot), // Call the view_listener on the user Executor. auto async_listener = AsyncEventListener::Create( - firestore_->client().userExecutor, std::move(view_listener)); + firestore_->client()->user_executor(), std::move(view_listener)); std::shared_ptr query_listener = - [firestore_->client() listenToQuery:this->query() - options:options - listener:async_listener]; + firestore_->client()->ListenToQuery(this->query(), options, + async_listener); - return ListenerRegistration(firestore_->client(), std::move(async_listener), - std::move(query_listener)); + return absl::make_unique( + firestore_->client(), std::move(async_listener), + std::move(query_listener)); } Query Query::Filter(FieldPath field_path, @@ -416,5 +414,3 @@ QuerySnapshot result(firestore_, query_, std::move(snapshot), } // namespace api } // namespace firestore } // namespace firebase - -NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/api/query_listener_registration.h b/Firestore/core/src/firebase/firestore/api/query_listener_registration.h new file mode 100644 index 00000000000..ca60c76168b --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/query_listener_registration.h @@ -0,0 +1,68 @@ +/* + * Copyright 2019 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_API_QUERY_LISTENER_REGISTRATION_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_QUERY_LISTENER_REGISTRATION_H_ + +#include +#include + +#include "Firestore/core/src/firebase/firestore/api/listener_registration.h" +#include "Firestore/core/src/firebase/firestore/core/event_listener.h" +#include "Firestore/core/src/firebase/firestore/core/query_listener.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" + +namespace firebase { +namespace firestore { +namespace core { +class FirestoreClient; +} + +namespace api { + +/** + * An internal handle that encapsulates a user's ability to request that we + * stop listening to a query. + */ +class QueryListenerRegistration : public ListenerRegistration { + public: + QueryListenerRegistration( + std::shared_ptr client, + std::shared_ptr> + async_listener, + std::shared_ptr query_listener); + + /** + * Removes the listener being tracked by this QueryListenerRegistration. + */ + void Remove() override; + + private: + /** The client that was used to register this listen. */ + std::shared_ptr client_; + + /** The async listener that is used to mute events synchronously. */ + std::weak_ptr> async_listener_; + + /** The internal QueryListener that can be used to unlisten the query. */ + std::weak_ptr query_listener_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_QUERY_LISTENER_REGISTRATION_H_ diff --git a/Firestore/core/src/firebase/firestore/api/listener_registration.mm b/Firestore/core/src/firebase/firestore/api/query_listener_registration.mm similarity index 75% rename from Firestore/core/src/firebase/firestore/api/listener_registration.mm rename to Firestore/core/src/firebase/firestore/api/query_listener_registration.mm index 36d8708a085..a7af8791099 100644 --- a/Firestore/core/src/firebase/firestore/api/listener_registration.mm +++ b/Firestore/core/src/firebase/firestore/api/query_listener_registration.mm @@ -14,25 +14,24 @@ * limitations under the License. */ -#include "Firestore/core/src/firebase/firestore/api/listener_registration.h" - -#import "Firestore/Source/Core/FSTFirestoreClient.h" +#include "Firestore/core/src/firebase/firestore/api/query_listener_registration.h" +#include "Firestore/core/src/firebase/firestore/core/firestore_client.h" namespace firebase { namespace firestore { namespace api { -ListenerRegistration::ListenerRegistration( - FSTFirestoreClient* client, +QueryListenerRegistration::QueryListenerRegistration( + std::shared_ptr client, std::shared_ptr> async_listener, std::shared_ptr query_listener) - : client_(client), + : client_(std::move(client)), async_listener_(std::move(async_listener)), query_listener_(std::move(query_listener)) { } -void ListenerRegistration::Remove() { +void QueryListenerRegistration::Remove() { auto async_listener = async_listener_.lock(); if (async_listener) { async_listener->Mute(); @@ -41,11 +40,11 @@ auto query_listener = query_listener_.lock(); if (query_listener) { - [client_ removeListener:query_listener]; + client_->RemoveListener(query_listener); query_listener_.reset(); } - client_.Release(); + client_.reset(); } } // namespace api diff --git a/Firestore/core/src/firebase/firestore/api/query_snapshot.cc b/Firestore/core/src/firebase/firestore/api/query_snapshot.cc index 81afc453a52..398865ac6c7 100644 --- a/Firestore/core/src/firebase/firestore/api/query_snapshot.cc +++ b/Firestore/core/src/firebase/firestore/api/query_snapshot.cc @@ -81,12 +81,12 @@ void QuerySnapshot::ForEachDocument( static DocumentChange::Type DocumentChangeTypeForChange( const DocumentViewChange& change) { switch (change.type()) { - case DocumentViewChange::Type::kAdded: + case DocumentViewChange::Type::Added: return DocumentChange::Type::Added; - case DocumentViewChange::Type::kModified: - case DocumentViewChange::Type::kMetadata: + case DocumentViewChange::Type::Modified: + case DocumentViewChange::Type::Metadata: return DocumentChange::Type::Modified; - case DocumentViewChange::Type::kRemoved: + case DocumentViewChange::Type::Removed: return DocumentChange::Type::Removed; } @@ -117,7 +117,7 @@ void QuerySnapshot::ForEachChange( /*from_cache=*/snapshot_.from_cache()); DocumentSnapshot document(firestore_, doc.key(), doc, metadata); - HARD_ASSERT(change.type() == DocumentViewChange::Type::kAdded, + HARD_ASSERT(change.type() == DocumentViewChange::Type::Added, "Invalid event type for first snapshot"); HARD_ASSERT(!last_document || util::Ascending(doc_comparator.Compare( *last_document, change.document())), @@ -134,7 +134,7 @@ void QuerySnapshot::ForEachChange( DocumentSet index_tracker = snapshot_.old_documents(); for (const DocumentViewChange& change : snapshot_.document_changes()) { if (!include_metadata_changes && - change.type() == DocumentViewChange::Type::kMetadata) { + change.type() == DocumentViewChange::Type::Metadata) { continue; } @@ -146,13 +146,13 @@ void QuerySnapshot::ForEachChange( size_t old_index = DocumentChange::npos; size_t new_index = DocumentChange::npos; - if (change.type() != DocumentViewChange::Type::kAdded) { + if (change.type() != DocumentViewChange::Type::Added) { old_index = index_tracker.IndexOf(change.document().key()); HARD_ASSERT(old_index != DocumentSet::npos, "Index for document not found"); index_tracker = index_tracker.erase(change.document().key()); } - if (change.type() != DocumentViewChange::Type::kRemoved) { + if (change.type() != DocumentViewChange::Type::Removed) { index_tracker = index_tracker.insert(change.document()); new_index = index_tracker.IndexOf(change.document().key()); } diff --git a/Firestore/core/src/firebase/firestore/api/snapshots_in_sync_listener_registration.h b/Firestore/core/src/firebase/firestore/api/snapshots_in_sync_listener_registration.h new file mode 100644 index 00000000000..1d94fbf0421 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/snapshots_in_sync_listener_registration.h @@ -0,0 +1,66 @@ +/* + * Copyright 2019 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_API_SNAPSHOTS_IN_SYNC_LISTENER_REGISTRATION_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_SNAPSHOTS_IN_SYNC_LISTENER_REGISTRATION_H_ + +#include + +#include "Firestore/core/src/firebase/firestore/api/listener_registration.h" +#include "Firestore/core/src/firebase/firestore/core/event_listener.h" +#include "Firestore/core/src/firebase/firestore/util/empty.h" + +namespace firebase { +namespace firestore { +namespace core { + +class FirestoreClient; + +} // namespace core + +namespace api { + +/** + * An internal handle that encapsulates a user's ability to request that we + * stop listening to the snapshots-in-sync listener. When a user calls Remove(), + * SnapshotsInSyncListenerRegistration will synchronously mute the listener and + * then send a request to actually unlisten. + */ +class SnapshotsInSyncListenerRegistration : public ListenerRegistration { + public: + SnapshotsInSyncListenerRegistration( + std::shared_ptr client, + std::shared_ptr> async_listener); + + /** + * Removes the listener being tracked by this FIRListenerRegistration. After + * the initial call, subsequent calls have no effect. + */ + void Remove() override; + + private: + /** The client that was used to register this listen. */ + std::shared_ptr client_; + + /** The async listener that is used to mute events synchronously. */ + std::weak_ptr> async_listener_; +}; + +} // namespace api +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_SNAPSHOTS_IN_SYNC_LISTENER_REGISTRATION_H_ diff --git a/Firestore/core/src/firebase/firestore/api/snapshots_in_sync_listener_registration.mm b/Firestore/core/src/firebase/firestore/api/snapshots_in_sync_listener_registration.mm new file mode 100644 index 00000000000..6a688c3b4b8 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/api/snapshots_in_sync_listener_registration.mm @@ -0,0 +1,46 @@ +/* + * Copyright 2019 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 + +#include "Firestore/core/src/firebase/firestore/api/snapshots_in_sync_listener_registration.h" + +#include "Firestore/core/src/firebase/firestore/core/firestore_client.h" + +namespace firebase { +namespace firestore { +namespace api { + +SnapshotsInSyncListenerRegistration::SnapshotsInSyncListenerRegistration( + std::shared_ptr client, + std::shared_ptr> async_listener) + : client_(std::move(client)), async_listener_(std::move(async_listener)) { +} + +void SnapshotsInSyncListenerRegistration::Remove() { + auto async_listener = async_listener_.lock(); + if (async_listener) { + async_listener->Mute(); + async_listener_.reset(); + } + + client_->RemoveSnapshotsInSyncListener(async_listener); + client_.reset(); +} + +} // namespace api +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/api/write_batch.h b/Firestore/core/src/firebase/firestore/api/write_batch.h index f86115be7f9..711d122c8ea 100644 --- a/Firestore/core/src/firebase/firestore/api/write_batch.h +++ b/Firestore/core/src/firebase/firestore/api/write_batch.h @@ -23,10 +23,7 @@ #include "Firestore/core/src/firebase/firestore/api/document_reference.h" #include "Firestore/core/src/firebase/firestore/model/mutation.h" -#include "Firestore/core/src/firebase/firestore/objc/objc_class.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" - -NS_ASSUME_NONNULL_BEGIN +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" namespace firebase { namespace firestore { @@ -72,6 +69,4 @@ class WriteBatch { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_API_WRITE_BATCH_H_ diff --git a/Firestore/core/src/firebase/firestore/api/write_batch.mm b/Firestore/core/src/firebase/firestore/api/write_batch.mm index fb4d087520b..ed953673902 100644 --- a/Firestore/core/src/firebase/firestore/api/write_batch.mm +++ b/Firestore/core/src/firebase/firestore/api/write_batch.mm @@ -18,16 +18,13 @@ #include -#import "Firestore/Source/Core/FSTFirestoreClient.h" - #include "Firestore/core/src/firebase/firestore/api/document_reference.h" #include "Firestore/core/src/firebase/firestore/api/firestore.h" #include "Firestore/core/src/firebase/firestore/api/input_validation.h" +#include "Firestore/core/src/firebase/firestore/core/firestore_client.h" #include "Firestore/core/src/firebase/firestore/core/user_data.h" #include "Firestore/core/src/firebase/firestore/model/delete_mutation.h" -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace api { @@ -70,8 +67,8 @@ VerifyNotCommitted(); committed_ = true; - [firestore_->client() writeMutations:std::move(mutations_) - callback:std::move(callback)]; + firestore_->client()->WriteMutations(std::move(mutations_), + std::move(callback)); } void WriteBatch::VerifyNotCommitted() const { @@ -91,5 +88,3 @@ } // namespace api } // namespace firestore } // namespace firebase - -NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/core/CMakeLists.txt b/Firestore/core/src/firebase/firestore/core/CMakeLists.txt index 111a9cd2160..7ebaa6c2cc4 100644 --- a/Firestore/core/src/firebase/firestore/core/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/core/CMakeLists.txt @@ -45,12 +45,15 @@ cc_library( query_listener.h target_id_generator.cc target_id_generator.h - user_data.h user_data.cc + user_data.h + view.cc + view.h view_snapshot.cc view_snapshot.h DEPENDS absl_strings firebase_firestore_model firebase_firestore_objc + firebase_firestore_remote ) diff --git a/Firestore/core/src/firebase/firestore/core/event_listener.h b/Firestore/core/src/firebase/firestore/core/event_listener.h index 24c8a39899b..24651a19b72 100644 --- a/Firestore/core/src/firebase/firestore/core/event_listener.h +++ b/Firestore/core/src/firebase/firestore/core/event_listener.h @@ -22,8 +22,8 @@ #include #include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" -#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" #include "absl/memory/memory.h" namespace firebase { diff --git a/Firestore/core/src/firebase/firestore/core/event_manager.h b/Firestore/core/src/firebase/firestore/core/event_manager.h index a46ad0735e2..bdc943529d7 100644 --- a/Firestore/core/src/firebase/firestore/core/event_manager.h +++ b/Firestore/core/src/firebase/firestore/core/event_manager.h @@ -19,20 +19,21 @@ #include #include +#include #include #include "Firestore/core/src/firebase/firestore/core/query.h" #include "Firestore/core/src/firebase/firestore/core/query_listener.h" -#include "Firestore/core/src/firebase/firestore/core/sync_engine_callback.h" +#include "Firestore/core/src/firebase/firestore/core/sync_engine.h" #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/objc/objc_class.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/empty.h" +#include "Firestore/core/src/firebase/firestore/util/nullability.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/algorithm/container.h" #include "absl/types/optional.h" -OBJC_CLASS(FSTSyncEngine); - namespace firebase { namespace firestore { namespace core { @@ -44,7 +45,7 @@ namespace core { */ class EventManager : public SyncEngineCallback { public: - explicit EventManager(FSTSyncEngine* sync_engine); + explicit EventManager(QueryEventSource* query_event_source_); /** * Adds a query listener that will be called with new snapshots for the query. @@ -57,16 +58,28 @@ class EventManager : public SyncEngineCallback { model::TargetId AddQueryListener( std::shared_ptr listener); - /** Removes a previously added listener. It's a no-op if the listener is not - * found. */ + /** + * Removes a previously added listener. It's a no-op if the listener is not + * found. + */ void RemoveQueryListener(std::shared_ptr listener); - // Implements `SyncEngineCallback`. + void AddSnapshotsInSyncListener( + const std::shared_ptr>& listener); + void RemoveSnapshotsInSyncListener( + const std::shared_ptr>& listener); + + // Implements `QueryEventCallback`. void HandleOnlineStateChange(model::OnlineState online_state) override; void OnViewSnapshots(std::vector&& snapshots) override; - void OnError(const core::Query& query, util::Status error) override; + void OnError(const core::Query& query, const util::Status& error) override; private: + /** + * Call all global snapshot listeners that have been set. + */ + void RaiseSnapshotsInSyncEvent(); + /** * Holds the listeners and the last received ViewSnapshot for a query being * tracked by EventManager. @@ -98,9 +111,11 @@ class EventManager : public SyncEngineCallback { absl::optional snapshot_; }; - objc::Handle sync_engine_; + QueryEventSource* query_event_source_ = nullptr; model::OnlineState online_state_ = model::OnlineState::Unknown; std::unordered_map queries_; + std::unordered_set>> + snapshots_in_sync_listeners_; }; } // namespace core diff --git a/Firestore/core/src/firebase/firestore/core/event_manager.mm b/Firestore/core/src/firebase/firestore/core/event_manager.mm index e5efad234c2..ee8a374cd31 100644 --- a/Firestore/core/src/firebase/firestore/core/event_manager.mm +++ b/Firestore/core/src/firebase/firestore/core/event_manager.mm @@ -14,19 +14,20 @@ * limitations under the License. */ -#include "Firestore/core/src/firebase/firestore/core/event_manager.h" - #include -#import "Firestore/Source/Core/FSTSyncEngine.h" +#include "Firestore/core/src/firebase/firestore/core/event_manager.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" namespace firebase { namespace firestore { namespace core { -EventManager::EventManager(FSTSyncEngine* sync_engine) - : sync_engine_(sync_engine) { - [sync_engine_ setCallback:this]; +using util::Empty; + +EventManager::EventManager(QueryEventSource* query_event_source) + : query_event_source_(query_event_source) { + query_event_source->SetCallback(this); } model::TargetId EventManager::AddQueryListener( @@ -39,14 +40,19 @@ query_info.listeners.push_back(listener); - listener->OnOnlineStateChanged(online_state_); + bool raised_event = listener->OnOnlineStateChanged(online_state_); + HARD_ASSERT(!raised_event, "onOnlineStateChanged() shouldn't raise an event " + "for brand-new listeners."); if (query_info.view_snapshot().has_value()) { - listener->OnViewSnapshot(query_info.view_snapshot().value()); + raised_event = listener->OnViewSnapshot(query_info.view_snapshot().value()); + if (raised_event) { + RaiseSnapshotsInSyncEvent(); + } } if (first_listen) { - query_info.target_id = [sync_engine_ listenToQuery:query]; + query_info.target_id = query_event_source_->Listen(query); } return query_info.target_id; } @@ -65,48 +71,81 @@ if (last_listen) { queries_.erase(found_iter); - [sync_engine_ stopListeningToQuery:query]; + query_event_source_->StopListening(query); } } +void EventManager::AddSnapshotsInSyncListener( + const std::shared_ptr>& listener) { + snapshots_in_sync_listeners_.insert(listener); + listener->OnEvent(Empty()); +} + +void EventManager::RemoveSnapshotsInSyncListener( + const std::shared_ptr>& listener) { + snapshots_in_sync_listeners_.erase(listener); +} + void EventManager::HandleOnlineStateChange(model::OnlineState online_state) { + bool raised_event = false; online_state_ = online_state; for (auto&& kv : queries_) { QueryListenersInfo& info = kv.second; for (auto&& listener : info.listeners) { - listener->OnOnlineStateChanged(online_state_); + if (listener->OnOnlineStateChanged(online_state_)) { + raised_event = true; + } } } + if (raised_event) { + RaiseSnapshotsInSyncEvent(); + } +} + +void EventManager::RaiseSnapshotsInSyncEvent() { + Empty empty{}; + for (const auto& listener : snapshots_in_sync_listeners_) { + listener->OnEvent(empty); + } } void EventManager::OnViewSnapshots( std::vector&& snapshots) { + bool raised_event = false; for (ViewSnapshot& snapshot : snapshots) { const Query& query = snapshot.query(); auto found_iter = queries_.find(query); if (found_iter != queries_.end()) { QueryListenersInfo& query_info = found_iter->second; for (const auto& listener : query_info.listeners) { - listener->OnViewSnapshot(snapshot); + if (listener->OnViewSnapshot(snapshot)) { + raised_event = true; + } } query_info.set_view_snapshot(std::move(snapshot)); } } + if (raised_event) { + RaiseSnapshotsInSyncEvent(); + } } -void EventManager::OnError(const core::Query& query, util::Status error) { +void EventManager::OnError(const core::Query& query, + const util::Status& error) { auto found_iter = queries_.find(query); - if (found_iter != queries_.end()) { - QueryListenersInfo& query_info = found_iter->second; - for (const auto& listener : query_info.listeners) { - listener->OnError(std::move(error)); - } + if (found_iter == queries_.end()) { + return; + } - // Remove all listeners. NOTE: We don't need to call [FSTSyncEngine - // stopListening] after an error. - queries_.erase(found_iter); + QueryListenersInfo& query_info = found_iter->second; + for (const auto& listener : query_info.listeners) { + listener->OnError(error); } + + // Remove all listeners. NOTE: We don't need to call + // `SyncEngine::StopListening()` after an error. + queries_.erase(found_iter); } } // namespace core diff --git a/Firestore/core/src/firebase/firestore/core/firestore_client.h b/Firestore/core/src/firebase/firestore/core/firestore_client.h new file mode 100644 index 00000000000..5f06dff2e56 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/firestore_client.h @@ -0,0 +1,218 @@ +/* + * Copyright 2019 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_CORE_FIRESTORE_CLIENT_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_FIRESTORE_CLIENT_H_ + +#import + +#include +#include + +#include "Firestore/core/src/firebase/firestore/api/document_reference.h" +#include "Firestore/core/src/firebase/firestore/api/document_snapshot.h" +#include "Firestore/core/src/firebase/firestore/api/listener_registration.h" +#include "Firestore/core/src/firebase/firestore/api/query_core.h" +#include "Firestore/core/src/firebase/firestore/api/settings.h" +#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/core/listen_options.h" +#include "Firestore/core/src/firebase/firestore/core/query.h" +#include "Firestore/core/src/firebase/firestore/core/query_listener.h" +#include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/local/local_store.h" +#include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/mutation.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" +#include "Firestore/core/src/firebase/firestore/util/empty.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" + +namespace firebase { +namespace firestore { +namespace local { + +class Persistence; +class LruDelegate; + +} // namespace local + +namespace remote { + +class RemoteStore; + +} // namespace remote + +namespace core { + +class EventManager; +class SyncEngine; + +/** + * FirestoreClient is a top-level class that constructs and owns all of the + * pieces of the client SDK architecture. + */ +class FirestoreClient : public std::enable_shared_from_this { + public: + /** + * Creates a fully initialized `FirestoreClient`. + * + * PORTING NOTE: We use factory function instead of public constructor + * because `FirestoreClient` is supposed to be managed by shared_ptr, and + * it is invalid to call `shared_from_this()` from constructors. + * The factory function enforces that `FirestoreClient` has to be managed + * by a shared pointer. + */ + static std::shared_ptr Create( + const DatabaseInfo& database_info, + const api::Settings& settings, + std::shared_ptr credentials_provider, + std::shared_ptr user_executor, + std::shared_ptr worker_queue); + + /** + * Terminates this client, cancels all writes / listeners, and releases all + * resources. + */ + void Terminate(util::StatusCallback callback); + + /** + * Passes a callback that is triggered when all the pending writes at the + * time when this method is called received server acknowledgement. + * An acknowledgement can be either acceptance or rejections. + */ + void WaitForPendingWrites(util::StatusCallback callback); + + /** Disables the network connection. Pending operations will not complete. */ + void DisableNetwork(util::StatusCallback callback); + + /** Enables the network connection and requeues all pending operations. */ + void EnableNetwork(util::StatusCallback callback); + + /** Starts listening to a query. */ + std::shared_ptr ListenToQuery( + Query query, + ListenOptions options, + ViewSnapshot::SharedListener&& listener); + + /** Stops listening to a query previously listened to. */ + void RemoveListener(const std::shared_ptr& listener); + + /** + * Retrieves a document from the cache via the indicated callback. If the doc + * doesn't exist, an error will be sent to the callback. + */ + void GetDocumentFromLocalCache(const api::DocumentReference& doc, + api::DocumentSnapshot::Listener&& callback); + + /** + * Retrieves a (possibly empty) set of documents from the cache via the + * indicated callback. + */ + void GetDocumentsFromLocalCache(const api::Query& query, + api::QuerySnapshot::Listener&& callback); + + /** + * Write mutations. callback will be notified when it's written to the + * backend. + */ + void WriteMutations(std::vector&& mutations, + util::StatusCallback callback); + + /** + * Tries to execute the transaction in update_callback up to retries times. + */ + void Transaction(int retries, + TransactionUpdateCallback update_callback, + TransactionResultCallback result_callback); + + /** + * Adds a listener to be called when a snapshots-in-sync event fires. + */ + void AddSnapshotsInSyncListener( + const std::shared_ptr>& listener); + + /** + * Removes a specific listener for snapshots-in-sync events. + */ + void RemoveSnapshotsInSyncListener( + const std::shared_ptr>& listener); + + /** The database ID of the DatabaseInfo this client was initialized with. */ + const model::DatabaseId& database_id() const { + return database_info_.database_id(); + } + + /** + * 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. + */ + const std::shared_ptr& user_executor() const { + return user_executor_; + } + + /** For usage in this class and testing only. */ + const std::shared_ptr& worker_queue() const { + return worker_queue_; + } + bool is_terminated() const; + + private: + FirestoreClient( + const DatabaseInfo& database_info, + std::shared_ptr credentials_provider, + std::shared_ptr user_executor, + std::shared_ptr worker_queue); + + void Initialize(const auth::User& user, const api::Settings& settings); + + void VerifyNotTerminated(); + + void ScheduleLruGarbageCollection(); + + DatabaseInfo database_info_; + std::shared_ptr credentials_provider_; + /** + * Async queue responsible for all of our internal processing. When we get + * incoming work from the user (via public API) or the network (incoming gRPC + * messages), we should always dispatch onto this queue. This ensures our + * internal data structures are never accessed from multiple threads + * simultaneously. + */ + std::shared_ptr worker_queue_; + std::shared_ptr user_executor_; + + std::unique_ptr persistence_; + std::unique_ptr local_store_; + std::unique_ptr remote_store_; + std::unique_ptr sync_engine_; + std::unique_ptr event_manager_; + + std::chrono::milliseconds initial_gc_delay_ = std::chrono::minutes(1); + std::chrono::milliseconds regular_gc_delay_ = std::chrono::minutes(5); + bool gc_has_run_ = false; + local::LruDelegate* _Nullable lru_delegate_; + util::DelayedOperation lru_callback_; +}; + +} // namespace core +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_FIRESTORE_CLIENT_H_ diff --git a/Firestore/core/src/firebase/firestore/core/firestore_client.mm b/Firestore/core/src/firebase/firestore/core/firestore_client.mm new file mode 100644 index 00000000000..854ae4b632e --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/firestore_client.mm @@ -0,0 +1,472 @@ +/* + * Copyright 2019 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/core/firestore_client.h" + +#include // NOLINT(build/c++11) +#include +#include + +#import "Firestore/Source/Local/FSTLocalSerializer.h" +#import "Firestore/Source/Remote/FSTSerializerBeta.h" + +#include "Firestore/core/src/firebase/firestore/api/settings.h" +#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/core/event_manager.h" +#include "Firestore/core/src/firebase/firestore/core/view.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/model/mutation.h" +#include "Firestore/core/src/firebase/firestore/remote/datastore.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "absl/memory/memory.h" + +namespace firebase { +namespace firestore { +namespace core { + +using firestore::Error; +using api::DocumentReference; +using api::DocumentSnapshot; +using api::ListenerRegistration; +using api::QuerySnapshot; +using api::Settings; +using api::SnapshotMetadata; +using api::ThrowIllegalState; +using auth::CredentialsProvider; +using auth::User; +using local::LevelDbPersistence; +using local::LocalStore; +using local::LruParams; +using local::MemoryPersistence; +using model::DatabaseId; +using model::Document; +using model::DocumentKeySet; +using model::DocumentMap; +using model::MaybeDocument; +using model::Mutation; +using model::OnlineState; +using remote::Datastore; +using remote::RemoteStore; +using util::AsyncQueue; +using util::DelayedConstructor; +using util::DelayedOperation; +using util::Empty; +using util::Executor; +using util::Path; +using util::Status; +using util::StatusCallback; +using util::StatusOr; +using util::StatusOrCallback; +using util::TimerId; + +std::shared_ptr FirestoreClient::Create( + const DatabaseInfo& database_info, + const api::Settings& settings, + std::shared_ptr credentials_provider, + std::shared_ptr user_executor, + std::shared_ptr worker_queue) { + // Have to use `new` because `make_shared` cannot access private constructor. + std::shared_ptr shared_client( + new FirestoreClient(database_info, std::move(credentials_provider), + std::move(user_executor), std::move(worker_queue))); + + auto user_promise = std::make_shared>(); + bool credentials_initialized = false; + + std::weak_ptr weak_client(shared_client); + auto credential_change_listener = [credentials_initialized, user_promise, + weak_client](User user) mutable { + auto shared_client = weak_client.lock(); + if (!shared_client) return; + + if (!credentials_initialized) { + credentials_initialized = true; + user_promise->set_value(user); + } else { + shared_client->worker_queue()->Enqueue([shared_client, user] { + shared_client->worker_queue()->VerifyIsCurrentQueue(); + + LOG_DEBUG("Credential Changed. Current user: %s", user.uid()); + shared_client->sync_engine_->HandleCredentialChange(user); + }); + } + }; + + shared_client->credentials_provider_->SetCredentialChangeListener( + credential_change_listener); + + // Defer initialization until we get the current user from the + // credential_change_listener. This is guaranteed to be synchronously + // dispatched onto our worker queue, so we will be initialized before any + // subsequently queued work runs. + shared_client->worker_queue()->Enqueue( + [shared_client, user_promise, settings] { + User user = user_promise->get_future().get(); + shared_client->Initialize(user, settings); + }); + + return shared_client; +} + +FirestoreClient::FirestoreClient( + const DatabaseInfo& database_info, + std::shared_ptr credentials_provider, + std::shared_ptr user_executor, + std::shared_ptr worker_queue) + : database_info_(database_info), + credentials_provider_(std::move(credentials_provider)), + worker_queue_(std::move(worker_queue)), + user_executor_(std::move(user_executor)) { +} + +void FirestoreClient::Initialize(const User& user, const Settings& settings) { + // Do all of our initialization on our own dispatch queue. + worker_queue()->VerifyIsCurrentQueue(); + LOG_DEBUG("Initializing. Current user: %s", user.uid()); + + // Note: The initialization work must all be synchronous (we can't dispatch + // more work) since external write/listen operations could get queued to run + // before that subsequent work completes. + if (settings.persistence_enabled()) { + Path dir = LevelDbPersistence::StorageDirectory( + database_info_, LevelDbPersistence::AppDataDirectory()); + + FSTSerializerBeta* remote_serializer = [[FSTSerializerBeta alloc] + initWithDatabaseID:database_info_.database_id()]; + FSTLocalSerializer* serializer = + [[FSTLocalSerializer alloc] initWithRemoteSerializer:remote_serializer]; + + auto created = LevelDbPersistence::Create( + std::move(dir), serializer, + LruParams::WithCacheSize(settings.cache_size_bytes())); + if (!created.ok()) { + // If leveldb fails to start then just throw up our hands: the error is + // unrecoverable. There's nothing an end-user can do and nearly all + // failures indicate the developer is doing something grossly wrong so we + // should stop them cold in their tracks with a failure they can't ignore. + [NSException + raise:NSInternalInconsistencyException + format:@"Failed to open DB: %s", created.status().ToString().c_str()]; + } + + auto ldb = std::move(created).ValueOrDie(); + lru_delegate_ = ldb->reference_delegate(); + + persistence_ = std::move(ldb); + if (settings.gc_enabled()) { + ScheduleLruGarbageCollection(); + } + } else { + persistence_ = MemoryPersistence::WithEagerGarbageCollector(); + } + + local_store_ = absl::make_unique(persistence_.get(), user); + + auto datastore = std::make_shared(database_info_, worker_queue(), + credentials_provider_); + + std::weak_ptr weak_this(shared_from_this()); + remote_store_ = absl::make_unique( + local_store_.get(), std::move(datastore), worker_queue(), + [weak_this](OnlineState online_state) { + weak_this.lock()->sync_engine_->HandleOnlineStateChange(online_state); + }); + + sync_engine_ = absl::make_unique(local_store_.get(), + remote_store_.get(), user); + + event_manager_ = absl::make_unique(sync_engine_.get()); + + // Setup wiring for remote store. + remote_store_->set_sync_engine(sync_engine_.get()); + + // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, + // refilling mutation queue, etc.) so must be started after LocalStore. + local_store_->Start(); + remote_store_->Start(); +} + +/** + * Schedules a callback to try running LRU garbage collection. Reschedules + * itself after the GC has run. + */ +void FirestoreClient::ScheduleLruGarbageCollection() { + std::chrono::milliseconds delay = + gc_has_run_ ? regular_gc_delay_ : initial_gc_delay_; + auto shared_this = shared_from_this(); + lru_callback_ = worker_queue()->EnqueueAfterDelay( + delay, TimerId::GarbageCollectionDelay, [shared_this] { + shared_this->local_store_->CollectGarbage( + shared_this->lru_delegate_->garbage_collector()); + shared_this->gc_has_run_ = true; + shared_this->ScheduleLruGarbageCollection(); + }); +} + +void FirestoreClient::DisableNetwork(StatusCallback callback) { + VerifyNotTerminated(); + auto shared_this = shared_from_this(); + worker_queue()->Enqueue([shared_this, callback] { + shared_this->remote_store_->DisableNetwork(); + if (callback) { + shared_this->user_executor()->Execute([=] { callback(Status::OK()); }); + } + }); +} + +void FirestoreClient::EnableNetwork(StatusCallback callback) { + VerifyNotTerminated(); + auto shared_this = shared_from_this(); + worker_queue()->Enqueue([shared_this, callback] { + shared_this->remote_store_->EnableNetwork(); + if (callback) { + shared_this->user_executor()->Execute([=] { callback(Status::OK()); }); + } + }); +} + +void FirestoreClient::Terminate(StatusCallback callback) { + auto shared_this = shared_from_this(); + worker_queue()->EnqueueAndInitiateShutdown([shared_this, callback] { + shared_this->credentials_provider_->SetCredentialChangeListener(nullptr); + + // If we've scheduled LRU garbage collection, cancel it. + if (shared_this->lru_callback_) { + shared_this->lru_callback_.Cancel(); + } + shared_this->remote_store_->Shutdown(); + shared_this->persistence_->Shutdown(); + }); + + // This separate enqueue ensures if `terminate` is called multiple times + // every time the callback is triggered. If it is in the above + // enqueue, it might not get executed because after first `terminate` + // all operations are not executed. + worker_queue()->EnqueueEvenAfterShutdown([shared_this, callback] { + if (callback) { + shared_this->user_executor()->Execute([=] { callback(Status::OK()); }); + } + }); +} + +void FirestoreClient::WaitForPendingWrites(StatusCallback callback) { + VerifyNotTerminated(); + + // Dispatch the result back onto the user dispatch queue. + auto shared_this = shared_from_this(); + auto async_callback = [shared_this, callback](util::Status status) { + if (callback) { + shared_this->user_executor()->Execute( + [=] { callback(std::move(status)); }); + } + }; + + worker_queue()->Enqueue([shared_this, async_callback] { + shared_this->sync_engine_->RegisterPendingWritesCallback( + std::move(async_callback)); + }); +} + +void FirestoreClient::VerifyNotTerminated() { + if (is_terminated()) { + ThrowIllegalState("The client has already been terminated."); + } +} + +bool FirestoreClient::is_terminated() const { + // Technically, the worker queue is still running, but only accepting tasks + // related to termination or supposed to be run after termination. It is + // effectively terminated to the eyes of users. + return worker_queue()->is_shutting_down(); +} + +std::shared_ptr FirestoreClient::ListenToQuery( + Query query, + ListenOptions options, + ViewSnapshot::SharedListener&& listener) { + VerifyNotTerminated(); + + auto query_listener = QueryListener::Create( + std::move(query), std::move(options), std::move(listener)); + + auto shared_this = shared_from_this(); + worker_queue()->Enqueue([shared_this, query_listener] { + shared_this->event_manager_->AddQueryListener(std::move(query_listener)); + }); + + return query_listener; +} + +void FirestoreClient::RemoveListener( + const std::shared_ptr& listener) { + // Checks for termination but does not throw error, allowing it to be an no-op + // if client is already terminated. + if (is_terminated()) { + return; + } + auto shared_this = shared_from_this(); + worker_queue()->Enqueue([shared_this, listener] { + shared_this->event_manager_->RemoveQueryListener(listener); + }); +} + +void FirestoreClient::GetDocumentFromLocalCache( + const DocumentReference& doc, DocumentSnapshot::Listener&& callback) { + VerifyNotTerminated(); + + // TODO(c++14): move `callback` into lambda. + auto shared_callback = absl::ShareUniquePtr(std::move(callback)); + auto shared_this = shared_from_this(); + worker_queue()->Enqueue([shared_this, doc, shared_callback] { + absl::optional maybe_document = + shared_this->local_store_->ReadDocument(doc.key()); + StatusOr maybe_snapshot; + + if (maybe_document && maybe_document->is_document()) { + Document document(*maybe_document); + maybe_snapshot = DocumentSnapshot{ + doc.firestore(), doc.key(), document, + /*from_cache=*/true, + /*has_pending_writes=*/document.has_local_mutations()}; + } else if (maybe_document && maybe_document->is_no_document()) { + maybe_snapshot = + DocumentSnapshot{doc.firestore(), doc.key(), absl::nullopt, + /*from_cache=*/true, + /*has_pending_writes=*/false}; + } else { + maybe_snapshot = + Status{Error::Unavailable, + "Failed to get document from cache. (However, this document " + "may exist on the server. Run again without setting source to " + "FirestoreSourceCache to attempt to retrieve the document "}; + } + + if (shared_callback) { + shared_this->user_executor()->Execute( + [=] { shared_callback->OnEvent(std::move(maybe_snapshot)); }); + } + }); +} + +void FirestoreClient::GetDocumentsFromLocalCache( + const api::Query& query, QuerySnapshot::Listener&& callback) { + VerifyNotTerminated(); + + // TODO(c++14): move `callback` into lambda. + auto shared_callback = absl::ShareUniquePtr(std::move(callback)); + auto shared_this = shared_from_this(); + worker_queue()->Enqueue([shared_this, query, shared_callback] { + DocumentMap docs = shared_this->local_store_->ExecuteQuery(query.query()); + + View view(query.query(), DocumentKeySet{}); + ViewDocumentChanges view_doc_changes = + view.ComputeDocumentChanges(docs.underlying_map()); + ViewChange view_change = view.ApplyChanges(view_doc_changes); + HARD_ASSERT( + view_change.limbo_changes().empty(), + "View returned limbo documents during local-only query execution."); + + HARD_ASSERT(view_change.snapshot().has_value(), "Expected a snapshot"); + + ViewSnapshot snapshot = std::move(view_change.snapshot()).value(); + SnapshotMetadata metadata(snapshot.has_pending_writes(), + snapshot.from_cache()); + + QuerySnapshot result(query.firestore(), query.query(), std::move(snapshot), + std::move(metadata)); + + if (shared_callback) { + shared_this->user_executor()->Execute( + [=] { shared_callback->OnEvent(std::move(result)); }); + } + }); +} + +void FirestoreClient::WriteMutations(std::vector&& mutations, + StatusCallback callback) { + VerifyNotTerminated(); + + // TODO(c++14): move `mutations` into lambda (C++14). + auto shared_this = shared_from_this(); + worker_queue()->Enqueue([shared_this, mutations, callback]() mutable { + if (mutations.empty()) { + if (callback) { + shared_this->user_executor()->Execute([=] { callback(Status::OK()); }); + } + } else { + shared_this->sync_engine_->WriteMutations( + std::move(mutations), [callback, shared_this](Status error) { + // Dispatch the result back onto the user dispatch queue. + if (callback) { + shared_this->user_executor()->Execute( + [=] { callback(std::move(error)); }); + } + }); + } + }); +} + +void FirestoreClient::Transaction(int retries, + TransactionUpdateCallback update_callback, + TransactionResultCallback result_callback) { + VerifyNotTerminated(); + + // Dispatch the result back onto the user dispatch queue. + auto shared_this = shared_from_this(); + auto async_callback = [shared_this, + result_callback](StatusOr maybe_value) { + if (result_callback) { + shared_this->user_executor()->Execute( + [=] { result_callback(std::move(maybe_value)); }); + } + }; + + worker_queue()->Enqueue([shared_this, retries, update_callback, + async_callback] { + shared_this->sync_engine_->Transaction(retries, shared_this->worker_queue(), + std::move(update_callback), + std::move(async_callback)); + }); +} + +void FirestoreClient::AddSnapshotsInSyncListener( + const std::shared_ptr>& user_listener) { + auto shared_this = shared_from_this(); + worker_queue()->Enqueue([shared_this, user_listener] { + shared_this->event_manager_->AddSnapshotsInSyncListener( + std::move(user_listener)); + }); +} + +void FirestoreClient::RemoveSnapshotsInSyncListener( + const std::shared_ptr>& user_listener) { + event_manager_->RemoveSnapshotsInSyncListener(user_listener); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/core/query_listener.cc b/Firestore/core/src/firebase/firestore/core/query_listener.cc index 1ebd220f0e0..fe4e9a44ab6 100644 --- a/Firestore/core/src/firebase/firestore/core/query_listener.cc +++ b/Firestore/core/src/firebase/firestore/core/query_listener.cc @@ -40,16 +40,16 @@ QueryListener::QueryListener(Query query, listener_(std::move(listener)) { } -void QueryListener::OnViewSnapshot(ViewSnapshot snapshot) { +bool QueryListener::OnViewSnapshot(ViewSnapshot snapshot) { HARD_ASSERT( !snapshot.document_changes().empty() || snapshot.sync_state_changed(), "We got a new snapshot with no changes?"); - + bool raised_event = false; if (!options_.include_document_metadata_changes()) { // Remove the metadata-only changes. std::vector changes; for (const DocumentViewChange& change : snapshot.document_changes()) { - if (change.type() != DocumentViewChange::Type::kMetadata) { + if (change.type() != DocumentViewChange::Type::Metadata) { changes.push_back(change); } } @@ -67,24 +67,33 @@ void QueryListener::OnViewSnapshot(ViewSnapshot snapshot) { if (!raised_initial_event_) { if (ShouldRaiseInitialEvent(snapshot, online_state_)) { RaiseInitialEvent(snapshot); + raised_event = true; } } else if (ShouldRaiseEvent(snapshot)) { listener_->OnEvent(snapshot); + raised_event = true; } snapshot_ = std::move(snapshot); + return raised_event; } void QueryListener::OnError(Status error) { listener_->OnEvent(std::move(error)); } -void QueryListener::OnOnlineStateChanged(OnlineState online_state) { +/** + * Returns whether a snaphsot was raised. + */ +bool QueryListener::OnOnlineStateChanged(OnlineState online_state) { online_state_ = online_state; + bool raised_event = false; if (snapshot_.has_value() && !raised_initial_event_ && ShouldRaiseInitialEvent(snapshot_.value(), online_state)) { RaiseInitialEvent(snapshot_.value()); + raised_event = true; } + return raised_event; } bool QueryListener::ShouldRaiseInitialEvent(const ViewSnapshot& snapshot, diff --git a/Firestore/core/src/firebase/firestore/core/query_listener.h b/Firestore/core/src/firebase/firestore/core/query_listener.h index cbbd34a2ea6..4fd3a638873 100644 --- a/Firestore/core/src/firebase/firestore/core/query_listener.h +++ b/Firestore/core/src/firebase/firestore/core/query_listener.h @@ -24,12 +24,9 @@ #include "Firestore/core/src/firebase/firestore/core/query.h" #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/types/optional.h" -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace core { @@ -86,9 +83,18 @@ class QueryListener { return snapshot_; } - virtual void OnViewSnapshot(ViewSnapshot snapshot); + /** + * Applies the new ViewSnapshot to this listener, raising a user-facing event + * if applicable (depending on what changed, whether the user has opted into + * metadata-only changes, etc.). Returns true if a user-facing event was + * indeed raised. + */ + virtual bool OnViewSnapshot(ViewSnapshot snapshot); + virtual void OnError(util::Status error); - virtual void OnOnlineStateChanged(model::OnlineState online_state); + + /** Returns whether a snapshot was raised. */ + virtual bool OnOnlineStateChanged(model::OnlineState online_state); private: bool ShouldRaiseInitialEvent(const ViewSnapshot& snapshot, @@ -122,6 +128,4 @@ class QueryListener { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_QUERY_LISTENER_H_ diff --git a/Firestore/core/src/firebase/firestore/core/sync_engine.h b/Firestore/core/src/firebase/firestore/core/sync_engine.h new file mode 100644 index 00000000000..7c24b2e222a --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/sync_engine.h @@ -0,0 +1,324 @@ +/* + * Copyright 2019 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_CORE_SYNC_ENGINE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_SYNC_ENGINE_H_ + +#include +#include +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/core/query.h" +#include "Firestore/core/src/firebase/firestore/core/target_id_generator.h" +#include "Firestore/core/src/firebase/firestore/core/view.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/local/local_store.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" +#include "Firestore/core/src/firebase/firestore/local/reference_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/maybe_document.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "absl/strings/string_view.h" + +namespace firebase { +namespace firestore { +namespace core { + +/** + * Interface implemented by `EventManager` to handle notifications from + * `SyncEngine`. + */ +class SyncEngineCallback { + public: + virtual ~SyncEngineCallback() = default; + + /** Handles a change in online state. */ + virtual void HandleOnlineStateChange(model::OnlineState online_state) = 0; + /** Handles new view snapshots. */ + virtual void OnViewSnapshots(std::vector&& snapshots) = 0; + /** Handles the failure of a query. */ + virtual void OnError(const core::Query& query, const util::Status& error) = 0; +}; + +/** + * Interface implemented by `SyncEngine` to receive requests from + * `EventManager`. + // PORTING NOTE: This is extracted as an interface to allow gmock to mock + // sync engine. + */ +class QueryEventSource { + public: + virtual ~QueryEventSource() = default; + + virtual void SetCallback(SyncEngineCallback* callback) = 0; + + /** + * Initiates a new listen. The LocalStore will be queried for initial data + * and the listen will be sent to the `RemoteStore` to get remote data. The + * registered SyncEngineCallback will be notified of resulting view + * snapshots and/or listen errors. + * + * @return the target ID assigned to the query. + */ + virtual model::TargetId Listen(Query query) = 0; + + /** Stops listening to a query previously listened to via `Listen`. */ + virtual void StopListening(const Query& query) = 0; +}; + +/** + * SyncEngine is the central controller in the client SDK architecture. It is + * the glue code between the EventManager, LocalStore, and RemoteStore. Some of + * SyncEngine's responsibilities include: + * 1. Coordinating client requests and remote events between the EventManager + * and the local and remote data stores. + * 2. Managing a View object for each query, providing the unified view between + * the local and remote data stores. + * 3. Notifying the RemoteStore when the LocalStore has new mutations in its + * queue that need sending to the backend. + * + * The SyncEngine’s methods should only ever be called by methods running on our + * own worker queue. + */ +class SyncEngine : public remote::RemoteStoreCallback, public QueryEventSource { + public: + SyncEngine(local::LocalStore* local_store, + remote::RemoteStore* remote_store, + const auth::User& initial_user); + + // Implements `QueryEventSource`. + void SetCallback(SyncEngineCallback* callback) override { + sync_engine_callback_ = callback; + } + model::TargetId Listen(Query query) override; + void StopListening(const Query& query) override; + + /** + * Initiates the write of local mutation batch which involves adding the + * writes to the mutation queue, notifying the remote store about new + * mutations, and raising events for any changes this write caused. The + * provided callback will be called once the write has been acked or + * rejected by the backend (or failed locally for any other reason). + */ + void WriteMutations(std::vector&& mutations, + util::StatusCallback callback); + + /** + * Registers a user callback that is called when all pending mutations at the + * moment of calling are acknowledged . + */ + void RegisterPendingWritesCallback(util::StatusCallback callback); + + /** + * Runs the given transaction block up to retries times and then calls + * completion. + * + * @param retries The number of times to try before giving up. + * @param worker_queue The queue to dispatch sync engine calls to. + * @param update_callback The callback to call to execute the user's + * transaction. + * @param result_callback The callback to call when the transaction is + * finished or failed. + */ + void Transaction(int retries, + const std::shared_ptr& worker_queue, + core::TransactionUpdateCallback update_callback, + core::TransactionResultCallback result_callback); + + void HandleCredentialChange(const auth::User& user); + + // Implements `RemoteStoreCallback` + void ApplyRemoteEvent(const remote::RemoteEvent& remote_event) override; + void HandleRejectedListen(model::TargetId target_id, + util::Status error) override; + void HandleSuccessfulWrite( + const model::MutationBatchResult& batch_result) override; + void HandleRejectedWrite(model::BatchId batch_id, + util::Status error) override; + void HandleOnlineStateChange(model::OnlineState online_state) override; + model::DocumentKeySet GetRemoteKeys(model::TargetId target_id) const override; + + // For tests only + std::map GetCurrentLimboDocuments() + const { + // Return defensive copy + return limbo_targets_by_key_; + } + + private: + /** + * QueryView contains all of the info that SyncEngine needs to track for a + * particular query and view. + */ + class QueryView { + public: + QueryView(Query query, + model::TargetId target_id, + nanopb::ByteString resume_token, + View view) + : query_(std::move(query)), + target_id_(target_id), + resume_token_(std::move(resume_token)), + view_(std::move(view)) { + } + + const Query& query() const { + return query_; + } + + /** + * The target ID created by the client that is used in the watch stream to + * identify this query. + */ + model::TargetId target_id() const { + return target_id_; + } + + /** + * An identifier from the datastore backend that indicates the last state of + * the results that was received. This can be used to indicate where to + * continue receiving new doc changes for the query. + */ + const nanopb::ByteString& resume_token() const { + return resume_token_; + } + + /** + * The view is responsible for computing the final merged truth of what docs + * are in the query. It gets notified of local and remote changes, and + * applies the query filters and limits to determine the most correct + * possible results. + */ + View& view() { + return view_; + } + + private: + Query query_; + model::TargetId target_id_; + nanopb::ByteString resume_token_; + View view_; + }; + + /** Tracks a limbo resolution. */ + class LimboResolution { + public: + LimboResolution() = default; + + explicit LimboResolution(const model::DocumentKey& key) : key{key} { + } + + model::DocumentKey key; + + /** + * Set to true once we've received a document. This is used in + * remoteKeysForTarget and ultimately used by `WatchChangeAggregator` to + * decide whether it needs to manufacture a delete event for the target once + * the target is CURRENT. + */ + bool document_received = false; + }; + + void AssertCallbackExists(absl::string_view source); + + ViewSnapshot InitializeViewAndComputeSnapshot( + const local::QueryData& query_data); + + void RemoveAndCleanupQuery(const std::shared_ptr& query_view); + + void RemoveLimboTarget(const model::DocumentKey& key); + + void EmitNewSnapshotsAndNotifyLocalStore( + const model::MaybeDocumentMap& changes, + const absl::optional& maybe_remote_event); + + /** Updates the limbo document state for the given target_id. */ + void UpdateTrackedLimboDocuments( + const std::vector& limbo_changes, + model::TargetId target_id); + + void TrackLimboChange(const LimboDocumentChange& limbo_change); + + void NotifyUser(model::BatchId batch_id, util::Status status); + + /** + * Triggers callbacks waiting for this batch id to get acknowledged by + * server, if there are any. + */ + void TriggerPendingWriteCallbacks(model::BatchId batch_id); + void FailOutstandingPendingWriteCallbacks(absl::string_view message); + + /** The local store, used to persist mutations and cached documents. */ + local::LocalStore* local_store_ = nullptr; + + /** The remote store for sending writes, watches, etc. to the backend. */ + remote::RemoteStore* remote_store_ = nullptr; + + auth::User current_user_; + SyncEngineCallback* sync_engine_callback_ = nullptr; + + /** + * Used for creating the TargetId for the listens used to resolve limbo + * documents. + */ + TargetIdGenerator target_id_generator_; + + /** Stores user completion blocks, indexed by User and BatchId. */ + std::unordered_map, + auth::HashUser> + mutation_callbacks_; + + /** Stores user callbacks waiting for pending writes to be acknowledged. */ + std::unordered_map> + pending_writes_callbacks_; + + // Shared pointers are used to avoid creating and storing two copies of the + // same `QueryView` and for consistency with other platforms. + /** QueryViews for all active queries, indexed by query. */ + std::unordered_map> query_views_by_query_; + + /** QueryViews for all active queries, indexed by target ID. */ + std::unordered_map> + query_views_by_target_; + + /** + * When a document is in limbo, we create a special listen to resolve it. This + * maps the DocumentKey of each limbo document to the TargetId of the listen + * resolving it. + */ + std::map limbo_targets_by_key_; + + /** + * Basically the inverse of limbo_targets_by_key_, a map of target ID to a + * LimboResolution (which includes the DocumentKey as well as whether we've + * received a document for the target). + */ + std::map limbo_resolutions_by_target_; + + /** Used to track any documents that are currently in limbo. */ + local::ReferenceSet limbo_document_refs_; +}; + +} // namespace core +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_SYNC_ENGINE_H_ diff --git a/Firestore/core/src/firebase/firestore/core/sync_engine.mm b/Firestore/core/src/firebase/firestore/core/sync_engine.mm new file mode 100644 index 00000000000..321fcab4514 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/sync_engine.mm @@ -0,0 +1,520 @@ +/* + * Copyright 2019 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/core/sync_engine.h" + +#include "Firestore/core/include/firebase/firestore/firestore_errors.h" +#include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/core/transaction_runner.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.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/document_map.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/model/no_document.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" + +namespace firebase { +namespace firestore { +namespace core { + +namespace { + +using firestore::Error; +using auth::User; +using local::LocalStore; +using local::LocalViewChanges; +using local::LocalWriteResult; +using local::QueryData; +using local::QueryPurpose; +using model::BatchId; +using model::DocumentKey; +using model::DocumentKeySet; +using model::DocumentMap; +using model::kBatchIdUnknown; +using model::ListenSequenceNumber; +using model::MaybeDocumentMap; +using model::NoDocument; +using model::SnapshotVersion; +using model::TargetId; +using remote::RemoteEvent; +using remote::TargetChange; +using util::AsyncQueue; +using util::Status; +using util::StatusCallback; + +// Limbo documents don't use persistence, and are eagerly GC'd. So, listens for +// them don't need real sequence numbers. +const ListenSequenceNumber kIrrelevantSequenceNumber = -1; + +bool ErrorIsInteresting(const Status& error) { + bool missing_index = + (error.code() == Error::FailedPrecondition && + error.error_message().find("requires an index") != std::string::npos); + bool no_permission = (error.code() == Error::PermissionDenied); + return missing_index || no_permission; +} + +} // namespace + +SyncEngine::SyncEngine(LocalStore* local_store, + remote::RemoteStore* remote_store, + const auth::User& initial_user) + : local_store_(local_store), + remote_store_(remote_store), + current_user_(initial_user), + target_id_generator_(TargetIdGenerator::SyncEngineTargetIdGenerator()) { +} + +void SyncEngine::AssertCallbackExists(absl::string_view source) { + HARD_ASSERT(sync_engine_callback_, + "Tried to call '%s' before callback was registered.", source); +} + +TargetId SyncEngine::Listen(Query query) { + AssertCallbackExists("Listen"); + + HARD_ASSERT(query_views_by_query_.find(query) == query_views_by_query_.end(), + "We already listen to query: %s", query.ToString()); + + QueryData query_data = local_store_->AllocateQuery(query); + ViewSnapshot view_snapshot = InitializeViewAndComputeSnapshot(query_data); + std::vector snapshots; + // Not using the `std::initializer_list` constructor to avoid extra copies. + snapshots.push_back(std::move(view_snapshot)); + sync_engine_callback_->OnViewSnapshots(std::move(snapshots)); + + // TODO(wuandy): move `query_data` into `Listen`. + remote_store_->Listen(query_data); + return query_data.target_id(); +} + +ViewSnapshot SyncEngine::InitializeViewAndComputeSnapshot( + const local::QueryData& query_data) { + DocumentMap docs = local_store_->ExecuteQuery(query_data.query()); + DocumentKeySet remote_keys = + local_store_->GetRemoteDocumentKeys(query_data.target_id()); + + View view(query_data.query(), std::move(remote_keys)); + ViewDocumentChanges view_doc_changes = + view.ComputeDocumentChanges(docs.underlying_map()); + ViewChange view_change = view.ApplyChanges(view_doc_changes); + HARD_ASSERT(view_change.limbo_changes().empty(), + "View returned limbo docs before target ack from the server."); + + auto query_view = + std::make_shared(query_data.query(), query_data.target_id(), + query_data.resume_token(), std::move(view)); + query_views_by_query_[query_data.query()] = query_view; + query_views_by_target_[query_data.target_id()] = query_view; + + HARD_ASSERT( + view_change.snapshot().has_value(), + "ApplyChanges to documents for new view should always return a snapshot"); + return view_change.snapshot().value(); +} + +void SyncEngine::StopListening(const Query& query) { + AssertCallbackExists("StopListening"); + + auto query_view = query_views_by_query_[query]; + HARD_ASSERT(query_view, "Trying to stop listening to a query not found"); + + local_store_->ReleaseQuery(query); + remote_store_->StopListening(query_view->target_id()); + RemoveAndCleanupQuery(query_view); +} + +void SyncEngine::RemoveAndCleanupQuery( + const std::shared_ptr& query_view) { + query_views_by_query_.erase(query_view->query()); + query_views_by_target_.erase(query_view->target_id()); + + DocumentKeySet limbo_keys = + limbo_document_refs_.ReferencedKeys(query_view->target_id()); + limbo_document_refs_.RemoveReferences(query_view->target_id()); + for (const DocumentKey& key : limbo_keys) { + if (!limbo_document_refs_.ContainsKey(key)) { + // We removed the last reference for this key. + RemoveLimboTarget(key); + } + } +} + +void SyncEngine::WriteMutations(std::vector&& mutations, + StatusCallback callback) { + AssertCallbackExists("WriteMutations"); + + LocalWriteResult result = local_store_->WriteLocally(std::move(mutations)); + mutation_callbacks_[current_user_].insert( + std::make_pair(result.batch_id(), std::move(callback))); + + EmitNewSnapshotsAndNotifyLocalStore(result.changes(), absl::nullopt); + remote_store_->FillWritePipeline(); +} + +void SyncEngine::RegisterPendingWritesCallback(StatusCallback callback) { + if (!remote_store_->CanUseNetwork()) { + LOG_DEBUG("The network is disabled. The task returned by " + "'waitForPendingWrites()' will not " + "complete until the network is enabled."); + } + + int largest_pending_batch_id = + local_store_->GetHighestUnacknowledgedBatchId(); + + if (largest_pending_batch_id == kBatchIdUnknown) { + // Trigger the callback right away if there is no pending writes at the + // moment. + callback(Status::OK()); + return; + } + + pending_writes_callbacks_[largest_pending_batch_id].push_back( + std::move(callback)); +} + +void SyncEngine::Transaction(int retries, + const std::shared_ptr& worker_queue, + TransactionUpdateCallback update_callback, + TransactionResultCallback result_callback) { + worker_queue->VerifyIsCurrentQueue(); + HARD_ASSERT(retries >= 0, "Got negative number of retries for transaction"); + + // Allocate a shared_ptr so that the TransactionRunner can outlive this frame. + auto runner = std::make_shared(worker_queue, remote_store_, + std::move(update_callback), + std::move(result_callback)); + runner->Run(); +} + +void SyncEngine::HandleCredentialChange(const auth::User& user) { + bool user_changed = (current_user_ != user); + current_user_ = user; + + if (user_changed) { + // Fails callbacks waiting for pending writes requested by previous user. + FailOutstandingPendingWriteCallbacks( + "'waitForPendingWrites' callback is cancelled due to a user change."); + // Notify local store and emit any resulting events from swapping out the + // mutation queue. + MaybeDocumentMap changes = local_store_->HandleUserChange(user); + EmitNewSnapshotsAndNotifyLocalStore(changes, absl::nullopt); + } + + // Notify remote store so it can restart its streams. + remote_store_->HandleCredentialChange(); +} + +void SyncEngine::ApplyRemoteEvent(const RemoteEvent& remote_event) { + AssertCallbackExists("HandleRemoteEvent"); + + // Update received document as appropriate for any limbo targets. + for (const auto& entry : remote_event.target_changes()) { + TargetId target_id = entry.first; + const TargetChange& change = entry.second; + auto it = limbo_resolutions_by_target_.find(target_id); + if (it == limbo_resolutions_by_target_.end()) { + continue; + } + + LimboResolution& limbo_resolution = it->second; + // Since this is a limbo resolution lookup, it's for a single document and + // it could be added, modified, or removed, but not a combination. + auto changed_documents_count = change.added_documents().size() + + change.modified_documents().size() + + change.removed_documents().size(); + HARD_ASSERT( + changed_documents_count <= 1, + "Limbo resolution for single document contains multiple changes."); + + if (!change.added_documents().empty()) { + limbo_resolution.document_received = true; + } else if (!change.modified_documents().empty()) { + HARD_ASSERT(limbo_resolution.document_received, + "Received change for limbo target document without add."); + } else if (!change.removed_documents().empty()) { + HARD_ASSERT(limbo_resolution.document_received, + "Received remove for limbo target document without add."); + limbo_resolution.document_received = false; + } else { + // This was probably just a CURRENT target change or similar. + } + } + + MaybeDocumentMap changes = local_store_->ApplyRemoteEvent(remote_event); + EmitNewSnapshotsAndNotifyLocalStore(changes, remote_event); +} + +void SyncEngine::HandleRejectedListen(TargetId target_id, Status error) { + AssertCallbackExists("HandleRejectedListen"); + + auto it = limbo_resolutions_by_target_.find(target_id); + if (it != limbo_resolutions_by_target_.end()) { + DocumentKey limbo_key = it->second.key; + // Since this query failed, we won't want to manually unlisten to it. + // So go ahead and remove it from bookkeeping. + limbo_targets_by_key_.erase(limbo_key); + limbo_resolutions_by_target_.erase(target_id); + + // TODO(dimond): Retry on transient errors? + + // It's a limbo doc. Create a synthetic event saying it was deleted. This is + // kind of a hack. Ideally, we would have a method in the local store to + // purge a document. However, it would be tricky to keep all of the local + // store's invariants with another method. + NoDocument doc(limbo_key, SnapshotVersion::None(), + /* has_committed_mutations= */ false); + DocumentKeySet limbo_documents = DocumentKeySet{limbo_key}; + RemoteEvent event{SnapshotVersion::None(), /*target_changes=*/{}, + /*target_mismatches=*/{}, + /*document_updates=*/{{limbo_key, doc}}, + std::move(limbo_documents)}; + ApplyRemoteEvent(event); + + } else { + auto found = query_views_by_target_.find(target_id); + HARD_ASSERT(found != query_views_by_target_.end(), "Unknown target id: %s", + target_id); + auto query_view = found->second; + const Query& query = query_view->query(); + local_store_->ReleaseQuery(query); + RemoveAndCleanupQuery(query_view); + if (ErrorIsInteresting(error)) { + LOG_WARN("Listen for query at %s failed: %s", + query.path().CanonicalString(), error.error_message()); + } + sync_engine_callback_->OnError(query, std::move(error)); + } +} + +void SyncEngine::HandleSuccessfulWrite( + const model::MutationBatchResult& batch_result) { + AssertCallbackExists("HandleSuccessfulWrite"); + + // The local store may or may not be able to apply the write result and raise + // events immediately (depending on whether the watcher is caught up), so we + // raise user callbacks first so that they consistently happen before listen + // events. + NotifyUser(batch_result.batch().batch_id(), Status::OK()); + + TriggerPendingWriteCallbacks(batch_result.batch().batch_id()); + + MaybeDocumentMap changes = local_store_->AcknowledgeBatch(batch_result); + EmitNewSnapshotsAndNotifyLocalStore(changes, absl::nullopt); +} + +void SyncEngine::HandleRejectedWrite( + firebase::firestore::model::BatchId batch_id, Status error) { + AssertCallbackExists("HandleRejectedWrite"); + + MaybeDocumentMap changes = local_store_->RejectBatch(batch_id); + + if (!changes.empty() && ErrorIsInteresting(error)) { + const DocumentKey& min_key = changes.min()->first; + LOG_WARN("Write at %s failed: %s", min_key.ToString(), + error.error_message()); + } + + // The local store may or may not be able to apply the write result and raise + // events immediately (depending on whether the watcher is caught up), so we + // raise user callbacks first so that they consistently happen before listen + // events. + NotifyUser(batch_id, std::move(error)); + + TriggerPendingWriteCallbacks(batch_id); + + EmitNewSnapshotsAndNotifyLocalStore(changes, absl::nullopt); +} + +void SyncEngine::HandleOnlineStateChange(model::OnlineState online_state) { + AssertCallbackExists("HandleOnlineStateChange"); + + std::vector new_view_snapshot; + for (const auto& entry : query_views_by_query_) { + const auto& query_view = entry.second; + ViewChange view_change = + query_view->view().ApplyOnlineStateChange(online_state); + HARD_ASSERT(view_change.limbo_changes().empty(), + "OnlineState should not affect limbo documents."); + if (view_change.snapshot().has_value()) { + new_view_snapshot.push_back(*std::move(view_change).snapshot()); + } + } + + sync_engine_callback_->OnViewSnapshots(std::move(new_view_snapshot)); + sync_engine_callback_->HandleOnlineStateChange(online_state); +} + +DocumentKeySet SyncEngine::GetRemoteKeys(TargetId target_id) const { + auto it = limbo_resolutions_by_target_.find(target_id); + if (it != limbo_resolutions_by_target_.end() && + it->second.document_received) { + return DocumentKeySet{it->second.key}; + } else { + auto found = query_views_by_target_.find(target_id); + if (found != query_views_by_target_.end()) { + return found->second->view().synced_documents(); + } + return DocumentKeySet{}; + } +} + +void SyncEngine::NotifyUser(BatchId batch_id, Status status) { + auto it = mutation_callbacks_.find(current_user_); + + // NOTE: Mutations restored from persistence won't have callbacks, so + // it's okay for this (or the callback below) to not exist. + if (it == mutation_callbacks_.end()) { + return; + } + + std::unordered_map& callbacks = it->second; + auto callback_it = callbacks.find(batch_id); + if (callback_it != callbacks.end()) { + callback_it->second(std::move(status)); + callbacks.erase(callback_it); + } +} + +void SyncEngine::TriggerPendingWriteCallbacks(BatchId batch_id) { + auto it = pending_writes_callbacks_.find(batch_id); + if (it != pending_writes_callbacks_.end()) { + for (const auto& callback : it->second) { + callback(Status::OK()); + } + + pending_writes_callbacks_.erase(it); + } +} + +void SyncEngine::FailOutstandingPendingWriteCallbacks( + absl::string_view message) { + for (const auto& entry : pending_writes_callbacks_) { + for (const auto& callback : entry.second) { + callback(Status(Error::Cancelled, message)); + } + } + + pending_writes_callbacks_.clear(); +} + +void SyncEngine::EmitNewSnapshotsAndNotifyLocalStore( + const MaybeDocumentMap& changes, + const absl::optional& maybe_remote_event) { + std::vector new_snapshots; + std::vector document_changes_in_all_views; + + for (const auto& entry : query_views_by_query_) { + const auto& query_view = entry.second; + View& view = query_view->view(); + ViewDocumentChanges view_doc_changes = view.ComputeDocumentChanges(changes); + if (view_doc_changes.needs_refill()) { + // The query has a limit and some docs were removed/updated, so we need to + // re-run the query against the local store to make sure we didn't lose + // any good docs that had been past the limit. + DocumentMap docs = local_store_->ExecuteQuery(query_view->query()); + view_doc_changes = + view.ComputeDocumentChanges(docs.underlying_map(), view_doc_changes); + } + + absl::optional target_changes; + if (maybe_remote_event.has_value()) { + const RemoteEvent& remote_event = maybe_remote_event.value(); + auto it = remote_event.target_changes().find(query_view->target_id()); + if (it != remote_event.target_changes().end()) { + target_changes = it->second; + } + } + ViewChange view_change = + view.ApplyChanges(view_doc_changes, target_changes); + + UpdateTrackedLimboDocuments(view_change.limbo_changes(), + query_view->target_id()); + + if (view_change.snapshot().has_value()) { + new_snapshots.push_back(*view_change.snapshot()); + LocalViewChanges doc_changes = LocalViewChanges::FromViewSnapshot( + *view_change.snapshot(), query_view->target_id()); + document_changes_in_all_views.push_back(std::move(doc_changes)); + } + } + + sync_engine_callback_->OnViewSnapshots(std::move(new_snapshots)); + local_store_->NotifyLocalViewChanges(document_changes_in_all_views); +} + +void SyncEngine::UpdateTrackedLimboDocuments( + const std::vector& limbo_changes, TargetId target_id) { + for (const LimboDocumentChange& limbo_change : limbo_changes) { + switch (limbo_change.type()) { + case LimboDocumentChange::Type::Added: + limbo_document_refs_.AddReference(limbo_change.key(), target_id); + TrackLimboChange(limbo_change); + break; + + case LimboDocumentChange::Type::Removed: + LOG_DEBUG("Document no longer in limbo: %s", + limbo_change.key().ToString()); + limbo_document_refs_.RemoveReference(limbo_change.key(), target_id); + if (!limbo_document_refs_.ContainsKey(limbo_change.key())) { + // We removed the last reference for this key + RemoveLimboTarget(limbo_change.key()); + } + break; + + default: + HARD_FAIL("Unknown limbo change type: %s", limbo_change.type()); + } + } +} + +void SyncEngine::TrackLimboChange(const LimboDocumentChange& limbo_change) { + const DocumentKey& key = limbo_change.key(); + + if (limbo_targets_by_key_.find(key) == limbo_targets_by_key_.end()) { + LOG_DEBUG("New document in limbo: %s", key.ToString()); + + TargetId limbo_target_id = target_id_generator_.NextId(); + Query query(key.path()); + QueryData query_data(std::move(query), limbo_target_id, + kIrrelevantSequenceNumber, + QueryPurpose::LimboResolution); + limbo_resolutions_by_target_.emplace(limbo_target_id, LimboResolution{key}); + // TODO(wuandy): move `query_data` into `Listen`. + remote_store_->Listen(query_data); + limbo_targets_by_key_[key] = limbo_target_id; + } +} + +void SyncEngine::RemoveLimboTarget(const DocumentKey& key) { + auto it = limbo_targets_by_key_.find(key); + if (it == limbo_targets_by_key_.end()) { + // This target already got removed, because the query failed. + return; + } + + TargetId limbo_target_id = it->second; + remote_store_->StopListening(limbo_target_id); + limbo_targets_by_key_.erase(key); + limbo_resolutions_by_target_.erase(limbo_target_id); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/core/sync_engine_callback.h b/Firestore/core/src/firebase/firestore/core/sync_engine_callback.h index 0bc7ac26349..d30815221cb 100644 --- a/Firestore/core/src/firebase/firestore/core/sync_engine_callback.h +++ b/Firestore/core/src/firebase/firestore/core/sync_engine_callback.h @@ -22,7 +22,7 @@ #include "Firestore/core/src/firebase/firestore/core/query.h" #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" #include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" namespace firebase { namespace firestore { @@ -30,7 +30,7 @@ namespace core { /** * Interface implemented by `EventManager` to handle notifications from - * `FSTSyncEngine`. + * `SyncEngine`. */ class SyncEngineCallback { public: @@ -39,7 +39,7 @@ class SyncEngineCallback { /** Handles new view snapshots. */ virtual void OnViewSnapshots(std::vector&& snapshots) = 0; /** Handles the failure of a query. */ - virtual void OnError(const core::Query& query, util::Status error) = 0; + virtual void OnError(const core::Query& query, const util::Status& error) = 0; }; } // namespace core diff --git a/Firestore/core/src/firebase/firestore/core/transaction.h b/Firestore/core/src/firebase/firestore/core/transaction.h index 5a1460fae26..5537c52e0d3 100644 --- a/Firestore/core/src/firebase/firestore/core/transaction.h +++ b/Firestore/core/src/firebase/firestore/core/transaction.h @@ -28,8 +28,6 @@ #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/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" -#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" #include "absl/types/any.h" #include "absl/types/optional.h" diff --git a/Firestore/core/src/firebase/firestore/core/transaction_runner.h b/Firestore/core/src/firebase/firestore/core/transaction_runner.h new file mode 100644 index 00000000000..eb5fca6b7a3 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/transaction_runner.h @@ -0,0 +1,78 @@ +/* + * Copyright 2019 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_CORE_TRANSACTION_RUNNER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_TRANSACTION_RUNNER_H_ + +#include + +#include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/remote/exponential_backoff.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_store.h" +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" + +namespace firebase { +namespace firestore { +namespace core { + +/** + * TransactionRunner encapsulates the logic needed to run and retry transactions + * with backoff. + * + * TransactionRunner manages its own lifetime by keeping itself alive until all + * retries are completed. It must be allocated via + * std::make_shared because the implementation expects to be + * able to call std::shared_from_this to create additional references that will + * keep it alive. + */ +class TransactionRunner + : public std::enable_shared_from_this { + public: + TransactionRunner(const std::shared_ptr& queue, + remote::RemoteStore* remote_store, + core::TransactionUpdateCallback update_callback, + core::TransactionResultCallback result_callback); + + /** + * Runs the transaction and calls the result_callback_ with the result. + */ + void Run(); + + private: + void ContinueCommit(const std::shared_ptr transaction, + const util::StatusOr maybe_result); + + void DispatchResult(const std::shared_ptr transaction, + util::Status status, + const util::StatusOr maybe_result); + + void HandleTransactionError(const std::shared_ptr transaction, + util::Status status); + + std::shared_ptr queue_; + remote::RemoteStore* remote_store_; + core::TransactionUpdateCallback update_callback_; + core::TransactionResultCallback result_callback_; + remote::ExponentialBackoff backoff_; + int retries_left_; +}; + +} // namespace core +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_TRANSACTION_RUNNER_H_ diff --git a/Firestore/core/src/firebase/firestore/core/transaction_runner.mm b/Firestore/core/src/firebase/firestore/core/transaction_runner.mm new file mode 100644 index 00000000000..79fbbd8f7d0 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/transaction_runner.mm @@ -0,0 +1,114 @@ +/* + * Copyright 2019 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 + +#include "Firestore/core/src/firebase/firestore/core/transaction_runner.h" +#include "Firestore/core/src/firebase/firestore/remote/exponential_backoff.h" +#include "absl/algorithm/container.h" + +namespace firebase { +namespace firestore { +namespace core { +namespace { + +using remote::RemoteStore; +using util::AsyncQueue; +using util::TimerId; +using util::Status; + +/** Maximum number of times a transaction can be retried before failing. */ +constexpr int kRetryCount = 5; + +bool IsRetryableTransactionError(const util::Status& error) { + // In transactions, the backend will fail outdated reads with + // FAILED_PRECONDITION and non-matching document versions with ABORTED. These + // errors should be retried. + Error code = error.code(); + return code == Error::Aborted || code == Error::FailedPrecondition || + !remote::Datastore::IsPermanentError(error); +} +} // namespace + +TransactionRunner::TransactionRunner(const std::shared_ptr& queue, + RemoteStore* remote_store, + TransactionUpdateCallback update_callback, + TransactionResultCallback result_callback) + : queue_{queue}, + remote_store_{remote_store}, + update_callback_{update_callback}, + result_callback_{result_callback}, + backoff_{queue_, TimerId::RetryTransaction}, + retries_left_{kRetryCount} { +} + +void TransactionRunner::Run() { + queue_->VerifyIsCurrentQueue(); + + auto shared_this = this->shared_from_this(); + backoff_.BackoffAndRun([shared_this] { + std::shared_ptr transaction = + shared_this->remote_store_->CreateTransaction(); + shared_this->update_callback_( + transaction, + [transaction, shared_this](util::StatusOr maybe_result) { + shared_this->queue_->Enqueue( + [transaction, shared_this, maybe_result] { + shared_this->ContinueCommit(transaction, maybe_result); + }); + }); + }); +} + +void TransactionRunner::ContinueCommit( + const std::shared_ptr transaction, + const util::StatusOr maybe_result) { + if (!maybe_result.ok()) { + HandleTransactionError(transaction, maybe_result.status()); + } else { + auto shared_this = this->shared_from_this(); + transaction->Commit( + [shared_this, transaction, maybe_result](Status status) { + shared_this->DispatchResult(transaction, status, maybe_result); + }); + } +} + +void TransactionRunner::DispatchResult( + const std::shared_ptr transaction, + Status status, + const util::StatusOr maybe_result) { + if (status.ok()) { + result_callback_(std::move(maybe_result)); + } else { + HandleTransactionError(transaction, status); + } +} + +void TransactionRunner::HandleTransactionError( + const std::shared_ptr transaction, Status status) { + if (retries_left_ > 0 && IsRetryableTransactionError(status) && + !transaction->IsPermanentlyFailed()) { + retries_left_ -= 1; + Run(); + } else { + result_callback_(std::move(status)); + } +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/core/user_data.h b/Firestore/core/src/firebase/firestore/core/user_data.h index b8d78074531..e7c6fe26825 100644 --- a/Firestore/core/src/firebase/firestore/core/user_data.h +++ b/Firestore/core/src/firebase/firestore/core/user_data.h @@ -182,7 +182,7 @@ class ParseAccumulator { class ParseContext { public: /** - * Initializes a FSTParseContext with the given source and path. + * Initializes a ParseContext with the given source and path. * * @param path A path within the object being parsed. This could be an empty * path (in which case the context represents the root of the data being @@ -227,7 +227,7 @@ class ParseContext { std::string FieldDescription() const; - // Helpers to get a FSTParseContext for a child field. + // Helpers to get a ParseContext for a child field. ParseContext ChildContext(const std::string& field_name); ParseContext ChildContext(const model::FieldPath& field_path); ParseContext ChildContext(size_t array_index); diff --git a/Firestore/core/src/firebase/firestore/core/view.cc b/Firestore/core/src/firebase/firestore/core/view.cc new file mode 100644 index 00000000000..6ec2fa5e1bc --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/view.cc @@ -0,0 +1,394 @@ +/* + * Copyright 2019 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/core/view.h" + +#include + +#include "Firestore/core/src/firebase/firestore/model/document_set.h" + +namespace firebase { +namespace firestore { +namespace core { + +using model::Document; +using model::DocumentKey; +using model::DocumentKeySet; +using model::DocumentSet; +using model::MaybeDocument; +using model::MaybeDocumentMap; +using model::OnlineState; +using remote::TargetChange; +using util::ComparisonResult; + +// MARK: - LimboDocumentChange + +LimboDocumentChange::LimboDocumentChange( + firebase::firestore::core::LimboDocumentChange::Type type, + firebase::firestore::model::DocumentKey key) + : type_(type), key_(std::move(key)) { +} + +bool operator==(const LimboDocumentChange& lhs, + const LimboDocumentChange& rhs) { + return lhs.type() == rhs.type() && lhs.key() == rhs.key(); +} + +// MARK: - ViewDocumentChanges + +ViewDocumentChanges::ViewDocumentChanges(model::DocumentSet new_documents, + DocumentViewChangeSet changes, + model::DocumentKeySet mutated_keys, + bool needs_refill) + : document_set_(std::move(new_documents)), + change_set_(std::move(changes)), + mutated_keys_(std::move(mutated_keys)), + needs_refill_(needs_refill) { +} + +// MARK: - View + +namespace { + +int GetDocumentViewChangeTypePosition(DocumentViewChange::Type change_type) { + switch (change_type) { + case DocumentViewChange::Type::Removed: + return 0; + case DocumentViewChange::Type::Added: + return 1; + case DocumentViewChange::Type::Modified: + return 2; + case DocumentViewChange::Type::Metadata: + // A metadata change is converted to a modified change at the public API + // layer. Since we sort by document key and then change type, metadata and + // modified changes must be sorted equivalently. + return 2; + } + HARD_FAIL("Unknown DocumentViewChange::Type %s", change_type); +} + +} // namespace + +View::View(Query query, DocumentKeySet remote_documents) + : query_(std::move(query)), + document_set_(query_.Comparator()), + synced_documents_(std::move(remote_documents)) { +} + +ComparisonResult View::Compare(const Document& lhs, const Document& rhs) const { + return document_set_.comparator().Compare(lhs, rhs); +} + +ViewDocumentChanges View::ComputeDocumentChanges( + const MaybeDocumentMap& doc_changes, + const absl::optional& previous_changes) const { + DocumentViewChangeSet change_set; + if (previous_changes) { + change_set = previous_changes->change_set(); + } + DocumentSet old_document_set = + previous_changes ? previous_changes->document_set() : document_set_; + + DocumentKeySet new_mutated_keys = + previous_changes ? previous_changes->mutated_keys() : mutated_keys_; + DocumentKeySet old_mutated_keys = mutated_keys_; + DocumentSet new_document_set = old_document_set; + bool needs_refill = false; + + // Track the last doc in a (full) limit. This is necessary, because some + // update (a delete, or an update moving a doc past the old limit) might mean + // there is some other document in the local cache that either should come (1) + // between the old last limit doc and the new last document, in the case of + // updates, or (2) after the new last document, in the case of deletes. So we + // keep this doc at the old limit to compare the updates to. + // + // Note that this should never get used in a refill (when previous_changes is + // set), because there will only be adds -- no deletes or updates. + absl::optional last_doc_in_limit; + if (query_.limit() != Query::kNoLimit && + old_document_set.size() == static_cast(query_.limit())) { + last_doc_in_limit = old_document_set.GetLastDocument(); + } + + for (const auto& kv : doc_changes) { + const DocumentKey& key = kv.first; + const MaybeDocument& maybe_new_doc = kv.second; + + absl::optional old_doc = old_document_set.GetDocument(key); + absl::optional new_doc; + if (maybe_new_doc.is_document()) { + new_doc = Document(maybe_new_doc); + } + if (new_doc) { + HARD_ASSERT(key == new_doc->key(), + "Mismatching key in document changes: %s != %s", + key.ToString(), new_doc->key().ToString()); + if (!query_.Matches(*new_doc)) { + new_doc = absl::nullopt; + } + } + + bool old_doc_had_pending_mutations = + old_doc && old_mutated_keys.contains(key); + + // We only consider committed mutations for documents that were mutated + // during the lifetime of the view. + bool new_doc_has_pending_mutations = + new_doc && (new_doc->has_local_mutations() || + (old_mutated_keys.contains(key) && + new_doc->has_committed_mutations())); + + bool change_applied = false; + // Calculate change + if (old_doc && new_doc) { + bool docs_equal = old_doc->data() == new_doc->data(); + if (!docs_equal) { + if (!ShouldWaitForSyncedDocument(*new_doc, *old_doc)) { + change_set.AddChange( + DocumentViewChange{*new_doc, DocumentViewChange::Type::Modified}); + change_applied = true; + + if (last_doc_in_limit && + util::Descending(Compare(*new_doc, *last_doc_in_limit))) { + // This doc moved from inside the limit to after the limit. That + // means there may be some doc in the local cache that's actually + // less than this one. + needs_refill = true; + } + } + } else if (old_doc_had_pending_mutations != + new_doc_has_pending_mutations) { + change_set.AddChange( + DocumentViewChange{*new_doc, DocumentViewChange::Type::Metadata}); + change_applied = true; + } + + } else if (!old_doc && new_doc) { + change_set.AddChange( + DocumentViewChange{*new_doc, DocumentViewChange::Type::Added}); + change_applied = true; + } else if (old_doc && !new_doc) { + change_set.AddChange( + DocumentViewChange{*old_doc, DocumentViewChange::Type::Removed}); + change_applied = true; + + if (last_doc_in_limit) { + // A doc was removed from a full limit query. We'll need to re-query + // from the local cache to see if we know about some other doc that + // should be in the results. + needs_refill = true; + } + } + + if (change_applied) { + if (new_doc) { + new_document_set = new_document_set.insert(new_doc); + if (new_doc->has_local_mutations()) { + new_mutated_keys = new_mutated_keys.insert(key); + } else { + new_mutated_keys = new_mutated_keys.erase(key); + } + } else { + new_document_set = new_document_set.erase(key); + new_mutated_keys = new_mutated_keys.erase(key); + } + } + } + + int32_t limit = query_.limit(); + if (limit != Query::kNoLimit && + new_document_set.size() > static_cast(limit)) { + for (size_t i = new_document_set.size() - limit; i > 0; --i) { + absl::optional found = new_document_set.GetLastDocument(); + const Document& old_doc = *found; + new_document_set = new_document_set.erase(old_doc.key()); + new_mutated_keys = new_mutated_keys.erase(old_doc.key()); + change_set.AddChange( + DocumentViewChange{old_doc, DocumentViewChange::Type::Removed}); + } + } + + HARD_ASSERT(!needs_refill || !previous_changes, + "View was refilled using docs that themselves needed refilling."); + + return ViewDocumentChanges(std::move(new_document_set), std::move(change_set), + new_mutated_keys, needs_refill); +} + +bool View::ShouldWaitForSyncedDocument(const Document& new_doc, + const Document& old_doc) const { + // We suppress the initial change event for documents that were modified as + // part of a write acknowledgment (e.g. when the value of a server transform + // is applied) as Watch will send us the same document again. By suppressing + // the event, we only raise two user visible events (one with + // `has_pending_writes` and the final state of the document) instead of three + // (one with `has_pending_writes`, the modified document with + // `has_pending_writes` and the final state of the document). + return (old_doc.has_local_mutations() && new_doc.has_committed_mutations() && + !new_doc.has_local_mutations()); +} + +ViewChange View::ApplyChanges(const ViewDocumentChanges& doc_changes) { + return ApplyChanges(doc_changes, {}); +} + +ViewChange View::ApplyChanges( + const ViewDocumentChanges& doc_changes, + const absl::optional& target_change) { + HARD_ASSERT(!doc_changes.needs_refill(), + "Cannot apply changes that need a refill"); + + DocumentSet old_documents = document_set_; + document_set_ = doc_changes.document_set(); + mutated_keys_ = doc_changes.mutated_keys(); + + // Sort changes based on type and query comparator. + std::vector changes = + doc_changes.change_set().GetChanges(); + std::sort( + changes.begin(), changes.end(), + [this](const DocumentViewChange& lhs, const DocumentViewChange& rhs) { + int pos1 = GetDocumentViewChangeTypePosition(lhs.type()); + int pos2 = GetDocumentViewChangeTypePosition(rhs.type()); + if (pos1 != pos2) { + return pos1 < pos2; + } + return util::Ascending(Compare(lhs.document(), rhs.document())); + }); + + ApplyTargetChange(target_change); + std::vector limbo_changes = UpdateLimboDocuments(); + bool synced = limbo_documents_.empty() && current_; + SyncState new_sync_state = synced ? SyncState::Synced : SyncState::Local; + bool sync_state_changed = new_sync_state != sync_state_; + sync_state_ = new_sync_state; + + if (changes.empty() && !sync_state_changed) { + // No changes. + return ViewChange(absl::nullopt, std::move(limbo_changes)); + } else { + ViewSnapshot snapshot{query_, + doc_changes.document_set(), + old_documents, + std::move(changes), + doc_changes.mutated_keys(), + /*from_cache=*/new_sync_state == SyncState::Local, + sync_state_changed, + /*excludes_metadata_changes=*/false}; + + return ViewChange(std::move(snapshot), std::move(limbo_changes)); + } +} + +ViewChange View::ApplyOnlineStateChange(OnlineState online_state) { + if (current_ && online_state == OnlineState::Offline) { + // If we're offline, set `current_` to false and then call ApplyChanges to + // refresh our sync state and generate a ViewChange as appropriate. We are + // guaranteed to get a new `TargetChange` that sets `current_` back to true + // once the client is back online. + current_ = false; + return ApplyChanges( + ViewDocumentChanges(document_set_, DocumentViewChangeSet{}, + mutated_keys_, /* needs_refill= */ false)); + } else { + // No effect, just return a no-op ViewChange. + return ViewChange(absl::nullopt, {}); + } +} + +// MARK: Private Methods + +/** Returns whether the doc for the given key should be in limbo. */ +bool View::ShouldBeInLimbo(const DocumentKey& key) const { + // If the remote end says it's part of this query, it's not in limbo. + if (synced_documents_.contains(key)) { + return false; + } + // The local store doesn't think it's a result, so it shouldn't be in limbo. + if (!document_set_.ContainsKey(key)) { + return false; + } + // If there are local changes to the doc, they might explain why the server + // doesn't know that it's part of the query. So don't put it in limbo. + // TODO(klimt): Ideally, we would only consider changes that might actually + // affect this specific query. + if (document_set_.GetDocument(key)->has_local_mutations()) { + return false; + } + // Everything else is in limbo. + return true; +} + +/** + * Updates synced_documents_ and current based on the given change. + */ +void View::ApplyTargetChange( + const absl::optional& maybe_target_change) { + if (maybe_target_change.has_value()) { + const TargetChange& target_change = maybe_target_change.value(); + + for (const DocumentKey& key : target_change.added_documents()) { + synced_documents_ = synced_documents_.insert(key); + } + for (const DocumentKey& key : target_change.modified_documents()) { + HARD_ASSERT(synced_documents_.find(key) != synced_documents_.end(), + "Modified document %s not found in view.", key.ToString()); + } + for (const DocumentKey& key : target_change.removed_documents()) { + synced_documents_ = synced_documents_.erase(key); + } + + current_ = target_change.current(); + } +} + +/** Updates limbo_documents_ and returns any changes as LimboDocumentChanges. */ +std::vector View::UpdateLimboDocuments() { + // We can only determine limbo documents when we're in-sync with the server. + if (!current_) { + return {}; + } + + // TODO(klimt): Do this incrementally so that it's not quadratic when updating + // many documents. + DocumentKeySet old_limbo_documents = std::move(limbo_documents_); + limbo_documents_ = DocumentKeySet{}; + for (const Document& doc : document_set_) { + if (ShouldBeInLimbo(doc.key())) { + limbo_documents_ = limbo_documents_.insert(doc.key()); + } + } + + // Diff the new limbo docs with the old limbo docs. + std::vector changes; + changes.reserve(old_limbo_documents.size() + limbo_documents_.size()); + + for (const DocumentKey& key : old_limbo_documents) { + if (!limbo_documents_.contains(key)) { + changes.push_back(LimboDocumentChange::Removed(key)); + } + } + for (const DocumentKey& key : limbo_documents_) { + if (!old_limbo_documents.contains(key)) { + changes.push_back(LimboDocumentChange::Added(key)); + } + } + return changes; +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/core/view.h b/Firestore/core/src/firebase/firestore/core/view.h new file mode 100644 index 00000000000..2979fefd703 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/view.h @@ -0,0 +1,233 @@ +/* + * Copyright 2019 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_CORE_VIEW_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_VIEW_H_ + +#include +#include + +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" + +namespace firebase { +namespace firestore { +namespace core { + +/** A change to a particular document wrt whether it is in "limbo". */ +class LimboDocumentChange { + public: + enum class Type { + Added, + Removed, + }; + + static LimboDocumentChange Added(model::DocumentKey key) { + return {Type::Added, std::move(key)}; + } + + static LimboDocumentChange Removed(model::DocumentKey key) { + return {Type::Removed, std::move(key)}; + } + + LimboDocumentChange(Type type, model::DocumentKey key); + + Type type() const { + return type_; + } + + const model::DocumentKey& key() const { + return key_; + } + + friend bool operator==(const LimboDocumentChange& lhs, + const LimboDocumentChange& rhs); + + private: + Type type_; + model::DocumentKey key_; +}; + +/** The result of applying a set of doc changes to a view. */ +class ViewDocumentChanges { + public: + ViewDocumentChanges(model::DocumentSet new_documents, + DocumentViewChangeSet changes, + model::DocumentKeySet mutated_keys, + bool needs_refill); + + /** The new set of docs that should be in the view. */ + const model::DocumentSet& document_set() const { + return document_set_; + } + + /** The diff of these docs with the previous set of docs. */ + const core::DocumentViewChangeSet& change_set() const { + return change_set_; + } + + const model::DocumentKeySet& mutated_keys() const { + return mutated_keys_; + } + + /** + * Whether the set of documents passed in was not sufficient to calculate the + * new state of the view and there needs to be another pass based on the local + * cache. + */ + bool needs_refill() const { + return needs_refill_; + } + + private: + model::DocumentSet document_set_; + core::DocumentViewChangeSet change_set_; + model::DocumentKeySet mutated_keys_; + bool needs_refill_ = false; +}; + +/** A set of changes to a view. */ +class ViewChange { + public: + ViewChange(absl::optional snapshot, + std::vector limbo_changes) + : snapshot_(std::move(snapshot)), + limbo_changes_(std::move(limbo_changes)) { + } + + const absl::optional snapshot() const& { + return snapshot_; + } + + absl::optional&& snapshot() && { + return std::move(snapshot_); + } + + const std::vector limbo_changes() const { + return limbo_changes_; + } + + private: + absl::optional snapshot_; + std::vector limbo_changes_; +}; + +/** + * View is responsible for computing the final merged truth of what docs are in + * a query. It gets notified of local and remote changes to docs, and applies + * the query filters and limits to determine the most correct possible results. + */ +class View { + public: + View(Query query, model::DocumentKeySet remote_documents); + + /** + * The set of remote documents that the server has told us belongs to the + * target associated with this view. + */ + const model::DocumentKeySet& synced_documents() const { + return synced_documents_; + } + + /** + * Iterates over a set of doc changes, applies the query limit, and computes + * what the new results should be, what the changes were, and whether we may + * need to go back to the local cache for more results. Does not make any + * changes to the view. + * + * @param doc_changes The doc changes to apply to this view. + * @param previous_changes If this is being called with a refill, then start + * with this set of docs and changes instead of the current view. + * @return a new set of docs, changes, and refill flag. + */ + core::ViewDocumentChanges ComputeDocumentChanges( + const model::MaybeDocumentMap& doc_changes, + const absl::optional& previous_changes = + absl::nullopt) const; + + /** + * Updates the view with the given ViewDocumentChanges. + * + * @param doc_changes The set of changes to make to the view's docs. + * @return A new ViewChange with the given docs, changes, and sync state. + */ + ViewChange ApplyChanges(const core::ViewDocumentChanges& doc_changes); + + /** + * Updates the view with the given ViewDocumentChanges and updates limbo docs + * and sync state from the given (optional) target change. + * + * @param doc_changes The set of changes to make to the view's docs. + * @param target_change A target change to apply for computing limbo docs and + * sync state. + * @return A new ViewChange with the given docs, changes, and sync state. + */ + ViewChange ApplyChanges( + const core::ViewDocumentChanges& doc_changes, + const absl::optional& target_change); + + /** + * Applies an OnlineState change to the view, potentially generating an + * ViewChange if the view's sync_state_ changes as a result. + */ + core::ViewChange ApplyOnlineStateChange(model::OnlineState online_state); + + private: + util::ComparisonResult Compare(const model::Document& lhs, + const model::Document& rhs) const; + + bool ShouldBeInLimbo(const model::DocumentKey& key) const; + + bool ShouldWaitForSyncedDocument(const model::Document& new_doc, + const model::Document& old_doc) const; + + void ApplyTargetChange( + const absl::optional& maybe_target_change); + + std::vector UpdateLimboDocuments(); + + Query query_; + + model::DocumentSet document_set_; + + /** Documents included in the remote target. */ + model::DocumentKeySet synced_documents_; + + /** Documents in the view but not in the remote target */ + model::DocumentKeySet limbo_documents_; + + /** Document Keys that have local changes. */ + model::DocumentKeySet mutated_keys_; + + SyncState sync_state_ = SyncState::None; + + /** + * A flag whether the view is current with the backend. A view is considered + * current after it has seen the current flag from the backend and did not + * lose consistency within the watch stream (e.g. because of an existence + * filter mismatch). + */ + bool current_ = false; +}; + +} // namespace core +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_VIEW_H_ diff --git a/Firestore/core/src/firebase/firestore/core/view_snapshot.cc b/Firestore/core/src/firebase/firestore/core/view_snapshot.cc index d4817869707..d148cbb8bc0 100644 --- a/Firestore/core/src/firebase/firestore/core/view_snapshot.cc +++ b/Firestore/core/src/firebase/firestore/core/view_snapshot.cc @@ -71,41 +71,41 @@ void DocumentViewChangeSet::AddChange(DocumentViewChange&& change) { DocumentViewChange::Type new_type = change.type(); // Merge the new change with the existing change. - if (new_type != DocumentViewChange::Type::kAdded && - old_type == DocumentViewChange::Type::kMetadata) { + if (new_type != DocumentViewChange::Type::Added && + old_type == DocumentViewChange::Type::Metadata) { change_map_ = change_map_.insert(key, change); - } else if (new_type == DocumentViewChange::Type::kMetadata && - old_type != DocumentViewChange::Type::kRemoved) { + } else if (new_type == DocumentViewChange::Type::Metadata && + old_type != DocumentViewChange::Type::Removed) { DocumentViewChange new_change{change.document(), old_type}; change_map_ = change_map_.insert(key, new_change); - } else if (new_type == DocumentViewChange::Type::kModified && - old_type == DocumentViewChange::Type::kModified) { + } else if (new_type == DocumentViewChange::Type::Modified && + old_type == DocumentViewChange::Type::Modified) { DocumentViewChange new_change{change.document(), - DocumentViewChange::Type::kModified}; + DocumentViewChange::Type::Modified}; change_map_ = change_map_.insert(key, new_change); - } else if (new_type == DocumentViewChange::Type::kModified && - old_type == DocumentViewChange::Type::kAdded) { + } else if (new_type == DocumentViewChange::Type::Modified && + old_type == DocumentViewChange::Type::Added) { DocumentViewChange new_change{change.document(), - DocumentViewChange::Type::kAdded}; + DocumentViewChange::Type::Added}; change_map_ = change_map_.insert(key, new_change); - } else if (new_type == DocumentViewChange::Type::kRemoved && - old_type == DocumentViewChange::Type::kAdded) { + } else if (new_type == DocumentViewChange::Type::Removed && + old_type == DocumentViewChange::Type::Added) { change_map_ = change_map_.erase(key); - } else if (new_type == DocumentViewChange::Type::kRemoved && - old_type == DocumentViewChange::Type::kModified) { + } else if (new_type == DocumentViewChange::Type::Removed && + old_type == DocumentViewChange::Type::Modified) { DocumentViewChange new_change{old.document(), - DocumentViewChange::Type::kRemoved}; + DocumentViewChange::Type::Removed}; change_map_ = change_map_.insert(key, new_change); - } else if (new_type == DocumentViewChange::Type::kAdded && - old_type == DocumentViewChange::Type::kRemoved) { + } else if (new_type == DocumentViewChange::Type::Added && + old_type == DocumentViewChange::Type::Removed) { DocumentViewChange new_change{change.document(), - DocumentViewChange::Type::kModified}; + DocumentViewChange::Type::Modified}; change_map_ = change_map_.insert(key, new_change); } else { @@ -162,7 +162,7 @@ ViewSnapshot ViewSnapshot::FromInitialDocuments( bool excludes_metadata_changes) { std::vector view_changes; for (const Document& doc : documents) { - view_changes.emplace_back(doc, DocumentViewChange::Type::kAdded); + view_changes.emplace_back(doc, DocumentViewChange::Type::Added); } DocumentSet old_documents(query.Comparator()); diff --git a/Firestore/core/src/firebase/firestore/core/view_snapshot.h b/Firestore/core/src/firebase/firestore/core/view_snapshot.h index 9080ccd4fa8..bd088c4b81e 100644 --- a/Firestore/core/src/firebase/firestore/core/view_snapshot.h +++ b/Firestore/core/src/firebase/firestore/core/view_snapshot.h @@ -31,7 +31,6 @@ #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/document_set.h" -#include "Firestore/core/src/firebase/firestore/objc/objc_class.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" namespace firebase { @@ -46,7 +45,7 @@ class DocumentViewChange { * NOTE: We sort document changes by their type, so the ordering of this enum * is significant. */ - enum class Type { kRemoved = 0, kAdded, kModified, kMetadata }; + enum class Type { Removed = 0, Added, Modified, Metadata }; DocumentViewChange() = default; diff --git a/Firestore/core/src/firebase/firestore/immutable/sorted_set.h b/Firestore/core/src/firebase/firestore/immutable/sorted_set.h index 8609e27e855..7907a3bff88 100644 --- a/Firestore/core/src/firebase/firestore/immutable/sorted_set.h +++ b/Firestore/core/src/firebase/firestore/immutable/sorted_set.h @@ -23,6 +23,7 @@ #include "Firestore/core/src/firebase/firestore/immutable/sorted_container.h" #include "Firestore/core/src/firebase/firestore/immutable/sorted_map.h" #include "Firestore/core/src/firebase/firestore/util/comparison.h" +#include "Firestore/core/src/firebase/firestore/util/empty.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/hashing.h" #include "absl/base/attributes.h" @@ -31,20 +32,9 @@ 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 V = impl::Empty, + typename V = util::Empty, typename M = SortedMap> class SortedSet : public SortedContainer { public: diff --git a/Firestore/core/src/firebase/firestore/local/CMakeLists.txt b/Firestore/core/src/firebase/firestore/local/CMakeLists.txt index e1545a2a0b2..1b535876af5 100644 --- a/Firestore/core/src/firebase/firestore/local/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/local/CMakeLists.txt @@ -16,17 +16,22 @@ cc_library( firebase_firestore_local_persistence_leveldb SOURCES leveldb_index_manager.h - #leveldb_index_manager.mm + leveldb_index_manager.mm leveldb_key.cc leveldb_key.h + leveldb_lru_reference_delegate.h + leveldb_lru_reference_delegate.mm leveldb_migrations.cc leveldb_migrations.h leveldb_mutation_queue.h - #leveldb_mutation_queue.mm + leveldb_mutation_queue.mm + leveldb_persistence.mm + leveldb_persistence.h + leveldb_persistence_apple.mm leveldb_query_cache.h - #leveldb_query_cache.mm + leveldb_query_cache.mm leveldb_remote_document_cache.h - #leveldb_remote_document_cache.mm + leveldb_remote_document_cache.mm leveldb_transaction.cc leveldb_transaction.h leveldb_util.cc @@ -44,34 +49,55 @@ cc_library( EXCLUDE_FROM_ALL ) +if(APPLE) + target_link_libraries( + firebase_firestore_local_persistence_leveldb + PRIVATE + firebase_firestore_protos_objc + ) +endif() + cc_library( firebase_firestore_local SOURCES - document_key_reference.h document_key_reference.cc + document_key_reference.h index_manager.h listen_sequence.h local_documents_view.h local_documents_view.mm - local_serializer.h local_serializer.cc + local_serializer.h local_view_changes.cc local_view_changes.h + lru_garbage_collector.cc + lru_garbage_collector.h + memory_eager_reference_delegate.cc + memory_eager_reference_delegate.h memory_index_manager.cc memory_index_manager.h + memory_lru_reference_delegate.cc + memory_lru_reference_delegate.h memory_mutation_queue.h - #memory_mutation_queue.mm + memory_mutation_queue.mm + memory_persistence.cc + memory_persistence.h memory_query_cache.h - #memory_query_cache.mm + memory_query_cache.mm memory_remote_document_cache.h - #memory_remote_document_cache.mm + memory_remote_document_cache.mm mutation_queue.h + persistence.h + proto_sizer.h + proto_sizer.mm query_cache.h query_data.cc query_data.h + reference_delegate.h reference_set.cc reference_set.h remote_document_cache.h + sizer.h DEPENDS # TODO(b/111328563) Force nanopb first to work around ODR violations protobuf-nanopb-static @@ -84,3 +110,10 @@ cc_library( firebase_firestore_remote firebase_firestore_util ) + +if(APPLE) + target_link_libraries( + firebase_firestore_local PRIVATE + firebase_firestore_protos_objc + ) +endif() diff --git a/Firestore/core/src/firebase/firestore/local/document_key_reference.h b/Firestore/core/src/firebase/firestore/local/document_key_reference.h index 1bf2b4a3689..06d25880263 100644 --- a/Firestore/core/src/firebase/firestore/local/document_key_reference.h +++ b/Firestore/core/src/firebase/firestore/local/document_key_reference.h @@ -34,7 +34,7 @@ namespace local { * references. * * A reference can be from either listen targets (identified by their TargetId) - * or mutation batches (identified by their BatchId). See FSTGarbageCollector + * or mutation batches (identified by their BatchId). See GarbageCollector * for more details. * * Not to be confused with FIRDocumentReference. @@ -56,7 +56,7 @@ class DocumentKeyReference { /** * The targetId of a referring target or the batchId of a referring mutation - * batch. (Which this is depends upon which FSTReferenceSet this reference is + * batch. (Which this is depends upon which ReferenceSet this reference is * a part of.) */ int32_t ref_id() const { diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h b/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h index 3b44cbd0d2e..7e6ff378960 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h @@ -17,10 +17,6 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_INDEX_MANAGER_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_INDEX_MANAGER_H_ -#if !defined(__OBJC__) -#error "For now, this file must only be included by ObjC source files." -#endif // !defined(__OBJC__) - #include #include @@ -28,18 +24,16 @@ #include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" -@class FSTLevelDB; - -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace local { +class LevelDbPersistence; + /** A persisted implementation of IndexManager. */ class LevelDbIndexManager : public IndexManager { public: - explicit LevelDbIndexManager(FSTLevelDB* db); + explicit LevelDbIndexManager(LevelDbPersistence* db); void AddToCollectionParentIndex( const model::ResourcePath& collection_path) override; @@ -48,8 +42,8 @@ class LevelDbIndexManager : public IndexManager { const std::string& collection_id) override; private: - // This instance is owned by FSTLevelDB; avoid a retain cycle. - __weak FSTLevelDB* db_; + // The LevelDbIndexManager is owned by LevelDbPersistence. + LevelDbPersistence* db_; /** * An in-memory copy of the index entries we've already written since the SDK @@ -65,6 +59,4 @@ class LevelDbIndexManager : public IndexManager { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_INDEX_MANAGER_H_ diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.mm b/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.mm index ef7773b541e..0e3fe7dd820 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.mm +++ b/Firestore/core/src/firebase/firestore/local/leveldb_index_manager.mm @@ -20,22 +20,19 @@ #include #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" #include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "absl/strings/match.h" -#import "Firestore/Source/Local/FSTLevelDB.h" - -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace local { using model::ResourcePath; -LevelDbIndexManager::LevelDbIndexManager(FSTLevelDB* db) : db_(db) { +LevelDbIndexManager::LevelDbIndexManager(LevelDbPersistence* db) : db_(db) { } void LevelDbIndexManager::AddToCollectionParentIndex( @@ -49,7 +46,7 @@ std::string key = LevelDbCollectionParentKey::Key(collection_id, parent_path); std::string empty_buffer; - db_.currentTransaction->Put(key, empty_buffer); + db_->current_transaction()->Put(key, empty_buffer); } } @@ -57,7 +54,7 @@ const std::string& collection_id) { std::vector results; - auto index_iterator = db_.currentTransaction->NewIterator(); + auto index_iterator = db_->current_transaction()->NewIterator(); std::string index_prefix = LevelDbCollectionParentKey::KeyPrefix(collection_id); LevelDbCollectionParentKey row_key; @@ -77,5 +74,3 @@ } // namespace local } // namespace firestore } // namespace firebase - -NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_key.h b/Firestore/core/src/firebase/firestore/local/leveldb_key.h index 505cb871214..574907b6737 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_key.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_key.h @@ -20,6 +20,7 @@ #include #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "absl/strings/string_view.h" @@ -140,9 +141,8 @@ class LevelDbMutationKey { } private: - // Deliberately uninitialized: will be assigned in Decode std::string user_id_; - model::BatchId batch_id_; + model::BatchId batch_id_ = model::kBatchIdUnknown; }; /** @@ -209,10 +209,9 @@ class LevelDbDocumentMutationKey { } private: - // Deliberately uninitialized: will be assigned in Decode std::string user_id_; model::DocumentKey document_key_; - model::BatchId batch_id_; + model::BatchId batch_id_ = model::kBatchIdUnknown; }; /** @@ -348,7 +347,7 @@ class LevelDbQueryTargetKey { private: // Deliberately uninitialized: will be assigned in Decode std::string canonical_id_; - model::TargetId target_id_; + model::TargetId target_id_ = 0; }; /** @@ -396,7 +395,7 @@ class LevelDbTargetDocumentKey { private: // Deliberately uninitialized: will be assigned in Decode - model::TargetId target_id_; + model::TargetId target_id_ = 0; model::DocumentKey document_key_; }; diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_lru_reference_delegate.h b/Firestore/core/src/firebase/firestore/local/leveldb_lru_reference_delegate.h new file mode 100644 index 00000000000..5d14f861cbb --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/leveldb_lru_reference_delegate.h @@ -0,0 +1,101 @@ +/* + * Copyright 2019 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_LOCAL_LEVELDB_LRU_REFERENCE_DELEGATE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_LRU_REFERENCE_DELEGATE_H_ + +#include + +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" + +namespace firebase { +namespace firestore { +namespace local { + +class LevelDbPersistence; +class ListenSequence; + +class LevelDbLruReferenceDelegate : public LruDelegate { + public: + LevelDbLruReferenceDelegate(LevelDbPersistence* persistence, + LruParams lru_params); + + ~LevelDbLruReferenceDelegate(); + + void Start(); + + // MARK: ReferenceDelegate methods + + model::ListenSequenceNumber current_sequence_number() const override; + + void AddInMemoryPins(ReferenceSet* set) override; + + void AddReference(const model::DocumentKey& key) override; + void RemoveReference(const model::DocumentKey& key) override; + void RemoveMutationReference(const model::DocumentKey& key) override; + void RemoveTarget(const local::QueryData& query_data) override; + + void UpdateLimboDocument(const model::DocumentKey& key) override; + + void OnTransactionStarted(absl::string_view label) override; + void OnTransactionCommitted() override; + + // MARK: LruDelegate methods + + LruGarbageCollector* garbage_collector() override; + + int64_t CalculateByteSize() override; + size_t GetSequenceNumberCount() override; + + void EnumerateTargets(const TargetCallback& callback) override; + void EnumerateOrphanedDocuments( + const OrphanedDocumentCallback& callback) override; + + int RemoveOrphanedDocuments(model::ListenSequenceNumber upper_bound) override; + int RemoveTargets(model::ListenSequenceNumber sequence_number, + const LiveQueryMap& live_queries) override; + + private: + bool IsPinned(const model::DocumentKey& key); + + bool MutationQueuesContainKey(const model::DocumentKey& key); + + void RemoveSentinel(const model::DocumentKey& key); + void WriteSentinel(const model::DocumentKey& key); + + std::unique_ptr gc_; + + // Persistence instances are owned by FirestoreClient + LevelDbPersistence* db_; + + // Additional references are owned by LocalStore + ReferenceSet* additional_references_; + + // This needs to be a pointer because initialization is delayed until after + // we read from the query cache. + std::unique_ptr listen_sequence_; + + // The current sequence number for the currently active transaction. If no + // transaction is active, resets back to kListenSequenceNumberInvalid. + model::ListenSequenceNumber current_sequence_number_ = + kListenSequenceNumberInvalid; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_LRU_REFERENCE_DELEGATE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_lru_reference_delegate.mm b/Firestore/core/src/firebase/firestore/local/leveldb_lru_reference_delegate.mm new file mode 100644 index 00000000000..83d15de925f --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/leveldb_lru_reference_delegate.mm @@ -0,0 +1,189 @@ +/* + * Copyright 2019 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/local/leveldb_lru_reference_delegate.h" + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" +#include "Firestore/core/src/firebase/firestore/local/reference_set.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "absl/strings/match.h" + +namespace firebase { +namespace firestore { +namespace local { + +using model::DocumentKey; +using model::ListenSequenceNumber; +using model::ResourcePath; + +LevelDbLruReferenceDelegate::LevelDbLruReferenceDelegate( + LevelDbPersistence* persistence, LruParams lru_params) + : db_(persistence) { + gc_ = absl::make_unique(this, lru_params); +} + +// Explicit default the destructor after all forward declared types have been +// fully declared. +LevelDbLruReferenceDelegate::~LevelDbLruReferenceDelegate() = default; + +void LevelDbLruReferenceDelegate::Start() { + ListenSequenceNumber highest_sequence_number = + db_->query_cache()->highest_listen_sequence_number(); + listen_sequence_ = absl::make_unique(highest_sequence_number); +} + +void LevelDbLruReferenceDelegate::AddInMemoryPins(ReferenceSet* set) { + // We should be able to assert that additional_references_ is nullptr, but + // due to restarts in spec tests it would fail. + additional_references_ = set; +} + +void LevelDbLruReferenceDelegate::AddReference(const DocumentKey& key) { + WriteSentinel(key); +} + +void LevelDbLruReferenceDelegate::RemoveReference(const DocumentKey& key) { + WriteSentinel(key); +} + +void LevelDbLruReferenceDelegate::RemoveMutationReference( + const DocumentKey& key) { + WriteSentinel(key); +} + +void LevelDbLruReferenceDelegate::RemoveTarget(const QueryData& query_data) { + QueryData updated = + query_data.Copy(query_data.snapshot_version(), query_data.resume_token(), + current_sequence_number()); + db_->query_cache()->UpdateTarget(std::move(updated)); +} + +void LevelDbLruReferenceDelegate::UpdateLimboDocument(const DocumentKey& key) { + WriteSentinel(key); +} + +ListenSequenceNumber LevelDbLruReferenceDelegate::current_sequence_number() + const { + HARD_ASSERT(current_sequence_number_ != kListenSequenceNumberInvalid, + "Asking for a sequence number outside of a transaction"); + return current_sequence_number_; +} + +void LevelDbLruReferenceDelegate::OnTransactionStarted(absl::string_view) { + HARD_ASSERT(current_sequence_number_ == kListenSequenceNumberInvalid, + "Previous sequence number is still in effect"); + current_sequence_number_ = listen_sequence_->Next(); +} + +void LevelDbLruReferenceDelegate::OnTransactionCommitted() { + current_sequence_number_ = kListenSequenceNumberInvalid; +} + +LruGarbageCollector* LevelDbLruReferenceDelegate::garbage_collector() { + return gc_.get(); +} + +int64_t LevelDbLruReferenceDelegate::CalculateByteSize() { + return db_->CalculateByteSize(); +} + +size_t LevelDbLruReferenceDelegate::GetSequenceNumberCount() { + size_t total_count = db_->query_cache()->size(); + EnumerateOrphanedDocuments( + [&total_count](const DocumentKey&, ListenSequenceNumber) { + total_count++; + }); + return total_count; +} + +void LevelDbLruReferenceDelegate::EnumerateTargets( + const TargetCallback& callback) { + db_->query_cache()->EnumerateTargets(callback); +} + +void LevelDbLruReferenceDelegate::EnumerateOrphanedDocuments( + const OrphanedDocumentCallback& callback) { + db_->query_cache()->EnumerateOrphanedDocuments(callback); +} + +int LevelDbLruReferenceDelegate::RemoveOrphanedDocuments( + ListenSequenceNumber upper_bound) { + int count = 0; + db_->query_cache()->EnumerateOrphanedDocuments( + [&](const DocumentKey& key, ListenSequenceNumber sequence_number) { + if (sequence_number <= upper_bound) { + if (!IsPinned(key)) { + count++; + db_->remote_document_cache()->Remove(key); + RemoveSentinel(key); + } + } + }); + return count; +} + +int LevelDbLruReferenceDelegate::RemoveTargets( + ListenSequenceNumber sequence_number, const LiveQueryMap& live_queries) { + return db_->query_cache()->RemoveTargets(sequence_number, live_queries); +} + +bool LevelDbLruReferenceDelegate::IsPinned(const DocumentKey& key) { + if (additional_references_->ContainsKey(key)) { + return true; + } + return MutationQueuesContainKey(key); +} + +bool LevelDbLruReferenceDelegate::MutationQueuesContainKey( + const DocumentKey& key) { + const std::set& users = db_->users(); + const ResourcePath& path = key.path(); + std::string buffer; + auto it = db_->current_transaction()->NewIterator(); + // For each user, if there is any batch that contains this document in any + // batch, we know it's pinned. + for (const std::string& user : users) { + std::string mutation_key = + LevelDbDocumentMutationKey::KeyPrefix(user, path); + it->Seek(mutation_key); + if (it->Valid() && absl::StartsWith(it->key(), mutation_key)) { + return true; + } + } + return false; +} + +void LevelDbLruReferenceDelegate::RemoveSentinel(const DocumentKey& key) { + db_->current_transaction()->Delete( + LevelDbDocumentTargetKey::SentinelKey(key)); +} + +void LevelDbLruReferenceDelegate::WriteSentinel(const DocumentKey& key) { + std::string sentinel_key = LevelDbDocumentTargetKey::SentinelKey(key); + std::string encoded_sequence_number = + LevelDbDocumentTargetKey::EncodeSentinelValue(current_sequence_number()); + db_->current_transaction()->Put(sentinel_key, encoded_sequence_number); +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h index 84b705f8890..9985cea76ae 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h @@ -37,9 +37,7 @@ #include "absl/strings/string_view.h" #include "leveldb/db.h" -@class FSTLevelDB; @class FSTLocalSerializer; -@class FSTMutationBatch; @class FSTPBMutationQueue; NS_ASSUME_NONNULL_BEGIN @@ -48,6 +46,8 @@ namespace firebase { namespace firestore { namespace local { +class LevelDbPersistence; + /** * Returns one larger than the largest batch ID that has been stored. If there * are no mutations returns 0. Note that batch IDs are global. @@ -57,52 +57,54 @@ model::BatchId LoadNextBatchIdFromDb(leveldb::DB* db); class LevelDbMutationQueue : public MutationQueue { public: LevelDbMutationQueue(const auth::User& user, - FSTLevelDB* db, + LevelDbPersistence* db, FSTLocalSerializer* serializer); void Start() override; bool IsEmpty() override; - void AcknowledgeBatch(FSTMutationBatch* batch, - NSData* _Nullable stream_token) override; + void AcknowledgeBatch(const model::MutationBatch& batch, + const nanopb::ByteString& stream_token) override; - FSTMutationBatch* AddMutationBatch( + model::MutationBatch AddMutationBatch( const Timestamp& local_write_time, std::vector&& base_mutations, std::vector&& mutations) override; - void RemoveMutationBatch(FSTMutationBatch* batch) override; + void RemoveMutationBatch(const model::MutationBatch& batch) override; - std::vector AllMutationBatches() override; + std::vector AllMutationBatches() override; - std::vector AllMutationBatchesAffectingDocumentKeys( + std::vector AllMutationBatchesAffectingDocumentKeys( const model::DocumentKeySet& document_keys) override; - std::vector AllMutationBatchesAffectingDocumentKey( + std::vector AllMutationBatchesAffectingDocumentKey( const model::DocumentKey& key) override; - std::vector AllMutationBatchesAffectingQuery( + std::vector AllMutationBatchesAffectingQuery( const core::Query& query) override; - FSTMutationBatch* _Nullable LookupMutationBatch( + absl::optional LookupMutationBatch( model::BatchId batch_id) override; - FSTMutationBatch* _Nullable NextMutationBatchAfterBatchId( + absl::optional NextMutationBatchAfterBatchId( model::BatchId batch_id) override; + model::BatchId GetHighestUnacknowledgedBatchId() override; + void PerformConsistencyCheck() override; - NSData* _Nullable GetLastStreamToken() override; + nanopb::ByteString GetLastStreamToken() override; - void SetLastStreamToken(NSData* _Nullable stream_token) override; + void SetLastStreamToken(const nanopb::ByteString& stream_token) override; private: /** - * Constructs a vector of matching batches, sorted by batchID to ensure that + * Constructs a vector of matching batches, sorted by batch_id to ensure that * multiple mutations affecting the same document key are applied in order. */ - std::vector AllMutationBatchesWithIds( + std::vector AllMutationBatchesWithIds( const std::set& batch_ids); std::string mutation_queue_key() { @@ -116,16 +118,16 @@ class LevelDbMutationQueue : public MutationQueue { /** Parses the MutationQueue metadata from the given LevelDB row contents. */ FSTPBMutationQueue* _Nullable MetadataForKey(const std::string& key); - FSTMutationBatch* ParseMutationBatch(absl::string_view encoded); + model::MutationBatch ParseMutationBatch(absl::string_view encoded); - // This instance is owned by FSTLevelDB; avoid a retain cycle. - __weak FSTLevelDB* db_; + // The LevelDbMutationQueue instance is owned by LevelDbPersistence. + LevelDbPersistence* db_; FSTLocalSerializer* serializer_; /** - * The normalized userID (e.g. nil UID => @"" userID) used in our LevelDB - * keys. + * The normalized user_id (i.e. after converting null to empty) as used in our + * LevelDB keys. */ std::string user_id_; diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm index 3eaa5ada8b0..a46f1258bc3 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm +++ b/Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.mm @@ -20,13 +20,15 @@ #include #import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Source/Local/FSTLevelDB.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" #include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/util/string_util.h" #include "Firestore/core/src/firebase/firestore/util/to_string.h" #include "absl/strings/match.h" @@ -47,7 +49,11 @@ using model::DocumentKeySet; using model::kBatchIdUnknown; using model::Mutation; +using model::MutationBatch; using model::ResourcePath; +using nanopb::ByteString; +using nanopb::MakeByteString; +using nanopb::MakeNSData; BatchId LoadNextBatchIdFromDb(DB* db) { // TODO(gsoltis): implement Prev() and SeekToLast() on @@ -58,7 +64,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { std::string table_key = LevelDbMutationKey::KeyPrefix(); LevelDbMutationKey row_key; - BatchId max_batch_id = kBatchIdUnknown; + BatchId max_batch_id = 0; bool more_user_ids = false; std::string next_user_id; @@ -69,7 +75,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { next_user_id = row_key.user_id(); } - // This loop assumes that nextUserId contains the next username at the start + // This loop assumes that next_user_id contains the next username at the start // of the iteration. while (more_user_ids) { // Compute the first key after the last mutation for next_user_id. @@ -86,8 +92,8 @@ BatchId LoadNextBatchIdFromDb(DB* db) { // the current user's mutation sequence. if (!it->Valid()) { // The iterator is past the last row altogether (there are no additional - // userIDs and now rows in any table after mutations). The last row will - // have the highest batchID. + // user_ids and now rows in any table after mutations). The last row will + // have the highest batch_id. more_user_ids = false; it->SeekToLast(); @@ -119,7 +125,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { } LevelDbMutationQueue::LevelDbMutationQueue(const User& user, - FSTLevelDB* db, + LevelDbPersistence* db, FSTLocalSerializer* serializer) : db_(db), serializer_(serializer), @@ -127,7 +133,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { } void LevelDbMutationQueue::Start() { - next_batch_id_ = LoadNextBatchIdFromDb(db_.ptr); + next_batch_id_ = LoadNextBatchIdFromDb(db_->ptr()); std::string key = mutation_queue_key(); FSTPBMutationQueue* metadata = MetadataForKey(key); @@ -140,7 +146,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { bool LevelDbMutationQueue::IsEmpty() { std::string user_key = LevelDbMutationKey::KeyPrefix(user_id_); - auto it = db_.currentTransaction->NewIterator(); + auto it = db_->current_transaction()->NewIterator(); it->Seek(user_key); bool empty = true; @@ -150,25 +156,23 @@ BatchId LoadNextBatchIdFromDb(DB* db) { return empty; } -void LevelDbMutationQueue::AcknowledgeBatch(FSTMutationBatch* batch, - NSData* _Nullable stream_token) { +void LevelDbMutationQueue::AcknowledgeBatch(const MutationBatch&, + const ByteString& stream_token) { SetLastStreamToken(stream_token); } -FSTMutationBatch* LevelDbMutationQueue::AddMutationBatch( +MutationBatch LevelDbMutationQueue::AddMutationBatch( const Timestamp& local_write_time, std::vector&& base_mutations, std::vector&& mutations) { BatchId batch_id = next_batch_id_; next_batch_id_++; - FSTMutationBatch* batch = - [[FSTMutationBatch alloc] initWithBatchID:batch_id - localWriteTime:local_write_time - baseMutations:std::move(base_mutations) - mutations:std::move(mutations)]; + MutationBatch batch(batch_id, local_write_time, std::move(base_mutations), + std::move(mutations)); std::string key = mutation_batch_key(batch_id); - db_.currentTransaction->Put(key, [serializer_ encodedMutationBatch:batch]); + db_->current_transaction()->Put(key, + [serializer_ encodedMutationBatch:batch]); // 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 @@ -176,21 +180,21 @@ BatchId LoadNextBatchIdFromDb(DB* db) { // buffer (and the parser will see all default values). std::string empty_buffer; - for (const Mutation& mutation : [batch mutations]) { + for (const Mutation& mutation : batch.mutations()) { key = LevelDbDocumentMutationKey::Key(user_id_, mutation.key(), batch_id); - db_.currentTransaction->Put(key, empty_buffer); + db_->current_transaction()->Put(key, empty_buffer); - db_.indexManager->AddToCollectionParentIndex( + db_->index_manager()->AddToCollectionParentIndex( mutation.key().path().PopLast()); } return batch; } -void LevelDbMutationQueue::RemoveMutationBatch(FSTMutationBatch* batch) { - auto check_iterator = db_.currentTransaction->NewIterator(); +void LevelDbMutationQueue::RemoveMutationBatch(const MutationBatch& batch) { + auto check_iterator = db_->current_transaction()->NewIterator(); - BatchId batch_id = batch.batchID; + BatchId batch_id = batch.batch_id(); std::string key = mutation_batch_key(batch_id); // As a sanity check, verify that the mutation batch exists before deleting @@ -203,28 +207,28 @@ BatchId LoadNextBatchIdFromDb(DB* db) { "Mutation batch %s not found; found %s", DescribeKey(key), DescribeKey(check_iterator->key())); - db_.currentTransaction->Delete(key); + db_->current_transaction()->Delete(key); - for (const Mutation& mutation : [batch mutations]) { + for (const Mutation& mutation : batch.mutations()) { key = LevelDbDocumentMutationKey::Key(user_id_, mutation.key(), batch_id); - db_.currentTransaction->Delete(key); - [db_.referenceDelegate removeMutationReference:mutation.key()]; + db_->current_transaction()->Delete(key); + db_->reference_delegate()->RemoveMutationReference(mutation.key()); } } -std::vector LevelDbMutationQueue::AllMutationBatches() { +std::vector LevelDbMutationQueue::AllMutationBatches() { std::string user_key = LevelDbMutationKey::KeyPrefix(user_id_); - auto it = db_.currentTransaction->NewIterator(); + auto it = db_->current_transaction()->NewIterator(); it->Seek(user_key); - std::vector result; + std::vector result; for (; it->Valid() && absl::StartsWith(it->key(), user_key); it->Next()) { result.push_back(ParseMutationBatch(it->value())); } return result; } -std::vector +std::vector LevelDbMutationQueue::AllMutationBatchesAffectingDocumentKeys( const DocumentKeySet& document_keys) { // Take a pass through the document keys and collect the set of unique @@ -232,7 +236,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { // one key. std::set batch_ids; - auto index_iterator = db_.currentTransaction->NewIterator(); + auto index_iterator = db_->current_transaction()->NewIterator(); LevelDbDocumentMutationKey row_key; for (const DocumentKey& document_key : document_keys) { std::string index_prefix = @@ -263,13 +267,13 @@ BatchId LoadNextBatchIdFromDb(DB* db) { return AllMutationBatchesWithIds(batch_ids); } -std::vector +std::vector LevelDbMutationQueue::AllMutationBatchesAffectingDocumentKey( const DocumentKey& key) { return AllMutationBatchesAffectingDocumentKeys(DocumentKeySet{key}); } -std::vector +std::vector LevelDbMutationQueue::AllMutationBatchesAffectingQuery(const Query& query) { HARD_ASSERT(!query.IsDocumentQuery(), "Document queries shouldn't go down this path"); @@ -292,23 +296,23 @@ BatchId LoadNextBatchIdFromDb(DB* db) { // current approach is to just return all mutation batches that affect // documents in the collection being queried. // - // Unlike allMutationBatchesAffectingDocumentKey, this iteration will scan the + // Unlike AllMutationBatchesAffectingDocumentKey, this iteration will scan the // document-mutation index for more than a single document so the associated - // batchIDs will be neither necessarily unique nor in order. This means an + // batch_ids will be neither necessarily unique nor in order. This means an // efficient simultaneous scan isn't possible. std::string index_prefix = LevelDbDocumentMutationKey::KeyPrefix(user_id_, query_path); - auto index_iterator = db_.currentTransaction->NewIterator(); + auto index_iterator = db_->current_transaction()->NewIterator(); index_iterator->Seek(index_prefix); LevelDbDocumentMutationKey row_key; - // Collect up unique batchIDs encountered during a scan of the index. Use a - // set to accumulate batch IDs so they can be traversed in order in a + // Collect up unique batch_ids encountered during a scan of the index. Use a + // set to accumulate the IDs so they can be traversed in order in a // scan of the main table. // // This method is faster than performing lookups of the keys with _db->Get and - // keeping a hash of batchIDs that have already been looked up. The + // keeping a hash of batch_ids that have already been looked up. The // performance difference is minor for small numbers of keys but > 30% faster // for larger numbers of keys. std::set unique_batch_ids; @@ -334,15 +338,15 @@ BatchId LoadNextBatchIdFromDb(DB* db) { return AllMutationBatchesWithIds(unique_batch_ids); } -FSTMutationBatch* _Nullable LevelDbMutationQueue::LookupMutationBatch( +absl::optional LevelDbMutationQueue::LookupMutationBatch( model::BatchId batch_id) { std::string key = mutation_batch_key(batch_id); std::string value; - Status status = db_.currentTransaction->Get(key, &value); + Status status = db_->current_transaction()->Get(key, &value); if (!status.ok()) { if (status.IsNotFound()) { - return nil; + return absl::nullopt; } HARD_FAIL("Lookup mutation batch (%s, %s) failed with status: %s", user_id_, batch_id, status.ToString()); @@ -351,23 +355,23 @@ BatchId LoadNextBatchIdFromDb(DB* db) { return ParseMutationBatch(value); } -FSTMutationBatch* _Nullable LevelDbMutationQueue::NextMutationBatchAfterBatchId( - model::BatchId batch_id) { +absl::optional +LevelDbMutationQueue::NextMutationBatchAfterBatchId(model::BatchId batch_id) { BatchId next_batch_id = batch_id + 1; std::string key = mutation_batch_key(next_batch_id); - auto it = db_.currentTransaction->NewIterator(); + auto it = db_->current_transaction()->NewIterator(); it->Seek(key); LevelDbMutationKey row_key; if (!it->Valid() || !row_key.Decode(it->key())) { // Past the last row in the DB or out of the mutations table - return nil; + return absl::nullopt; } if (row_key.user_id() != user_id_) { // Jumped past the last mutation for this user - return nil; + return absl::nullopt; } HARD_ASSERT(row_key.batch_id() >= next_batch_id, @@ -375,6 +379,25 @@ BatchId LoadNextBatchIdFromDb(DB* db) { return ParseMutationBatch(it->value()); } +BatchId LevelDbMutationQueue::GetHighestUnacknowledgedBatchId() { + std::unique_ptr it( + db_->ptr()->NewIterator(LevelDbTransaction::DefaultReadOptions())); + + std::string next_user_key = + util::PrefixSuccessor(LevelDbMutationKey::KeyPrefix(user_id_)); + + LevelDbMutationKey row_key; + + it->Seek(next_user_key); + it->Prev(); + if (it->Valid() && row_key.Decode(MakeStringView(it->key())) && + row_key.user_id() == user_id_) { + return row_key.batch_id(); + } + + return kBatchIdUnknown; +} + void LevelDbMutationQueue::PerformConsistencyCheck() { if (!IsEmpty()) { return; @@ -383,7 +406,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { // Verify that there are no entries in the document-mutation index if the // queue is empty. std::string index_prefix = LevelDbDocumentMutationKey::KeyPrefix(user_id_); - auto index_iterator = db_.currentTransaction->NewIterator(); + auto index_iterator = db_->current_transaction()->NewIterator(); index_iterator->Seek(index_prefix); std::vector dangling_mutation_references; @@ -404,23 +427,23 @@ BatchId LoadNextBatchIdFromDb(DB* db) { util::ToString(dangling_mutation_references)); } -NSData* _Nullable LevelDbMutationQueue::GetLastStreamToken() { - return metadata_.lastStreamToken; +ByteString LevelDbMutationQueue::GetLastStreamToken() { + return MakeByteString(metadata_.lastStreamToken); } -void LevelDbMutationQueue::SetLastStreamToken(NSData* _Nullable stream_token) { - metadata_.lastStreamToken = stream_token; +void LevelDbMutationQueue::SetLastStreamToken(const ByteString& stream_token) { + metadata_.lastStreamToken = MakeNullableNSData(stream_token); - db_.currentTransaction->Put(mutation_queue_key(), metadata_); + db_->current_transaction()->Put(mutation_queue_key(), metadata_); } -std::vector LevelDbMutationQueue::AllMutationBatchesWithIds( +std::vector LevelDbMutationQueue::AllMutationBatchesWithIds( const std::set& batch_ids) { - std::vector result; + std::vector result; - // Given an ordered set of unique batchIDs perform a skipping scan over the + // Given an ordered set of unique batch_ids perform a skipping scan over the // main table to find the mutation batches. - auto mutation_iterator = db_.currentTransaction->NewIterator(); + auto mutation_iterator = db_->current_transaction()->NewIterator(); for (BatchId batch_id : batch_ids) { std::string mutation_key = mutation_batch_key(batch_id); mutation_iterator->Seek(mutation_key); @@ -439,7 +462,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { FSTPBMutationQueue* _Nullable LevelDbMutationQueue::MetadataForKey( const std::string& key) { std::string value; - Status status = db_.currentTransaction->Get(key, &value); + Status status = db_->current_transaction()->Get(key, &value); if (status.ok()) { NSData* data = [[NSData alloc] initWithBytesNoCopy:(void*)value.data() length:value.size() @@ -460,7 +483,7 @@ BatchId LoadNextBatchIdFromDb(DB* db) { } } -FSTMutationBatch* LevelDbMutationQueue::ParseMutationBatch( +MutationBatch LevelDbMutationQueue::ParseMutationBatch( absl::string_view encoded) { NSData* data = [[NSData alloc] initWithBytesNoCopy:(void*)encoded.data() length:encoded.size() diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_persistence.h b/Firestore/core/src/firebase/firestore/local/leveldb_persistence.h new file mode 100644 index 00000000000..7da026b2c0c --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/leveldb_persistence.h @@ -0,0 +1,169 @@ +/* + * Copyright 2019 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_LOCAL_LEVELDB_PERSISTENCE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_PERSISTENCE_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_lru_reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_mutation_queue.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_transaction.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" +#include "Firestore/core/src/firebase/firestore/util/path.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" + +@class FSTLocalSerializer; + +namespace firebase { +namespace firestore { +namespace core { + +class DatabaseInfo; + +} // namespace core + +namespace local { + +class LevelDbLruReferenceDelegate; +struct LruParams; + +/** A LevelDB-backed implementation of the Persistence interface. */ +class LevelDbPersistence : public Persistence { + public: + /** + * Creates a LevelDB in the given directory and returns it or a Status object + * containing details of the failure. + */ + static util::StatusOr> Create( + util::Path dir, + FSTLocalSerializer* serializer, + const LruParams& lru_params); + + /** + * Finds a suitable directory to serve as the root of all Firestore local + * storage. + */ + static util::Path AppDataDirectory(); + + /** + * Computes a unique storage directory for the given identifying components of + * local storage. + * + * @param database_info The identifying information for the local storage + * instance. + * @param documents_dir The root document directory relative to which + * the storage directory will be created. Usually just + * `LevelDbPersistence::AppDataDirectory()`. + * @return A storage directory unique to the instance identified by + * `database_info`. + */ + static util::Path StorageDirectory(const core::DatabaseInfo& database_info, + const util::Path& documents_dir); + + LevelDbTransaction* current_transaction(); + + leveldb::DB* ptr() { + return db_.get(); + } + + const std::set users() const { + return users_; + } + + static util::Status ClearPersistence(const core::DatabaseInfo& database_info); + + int64_t CalculateByteSize(); + + // MARK: Persistence overrides + + model::ListenSequenceNumber current_sequence_number() const override; + + void Shutdown() override; + + LevelDbMutationQueue* GetMutationQueueForUser( + const auth::User& user) override; + + LevelDbQueryCache* query_cache() override; + + LevelDbRemoteDocumentCache* remote_document_cache() override; + + LevelDbIndexManager* index_manager() override; + + LevelDbLruReferenceDelegate* reference_delegate() override; + + protected: + void RunInternal(absl::string_view label, + std::function block) override; + + private: + LevelDbPersistence(std::unique_ptr db, + util::Path directory, + std::set users, + FSTLocalSerializer* serializer, + const LruParams& lru_params); + + /** + * Ensures that the given directory exists. + */ + static util::Status EnsureDirectory(const util::Path& dir); + + /** + * Marks the given directory as excluded from platform-specific backup schemes + * like iCloud backup. + */ + static util::Status ExcludeFromBackups(const util::Path& dir); + + /** Opens the database within the given directory. */ + static util::StatusOr> OpenDb( + const util::Path& dir); + + static constexpr const char* kReservedPathComponent = "firestore"; + + std::unique_ptr db_; + + util::Path directory_; + std::set users_; + FSTLocalSerializer* serializer_; + bool started_ = false; + + std::unique_ptr current_mutation_queue_; + std::unique_ptr query_cache_; + std::unique_ptr document_cache_; + std::unique_ptr index_manager_; + std::unique_ptr reference_delegate_; + + std::unique_ptr transaction_; +}; + +/** Returns a standard set of read options. */ +leveldb::ReadOptions StandardReadOptions(); + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LEVELDB_PERSISTENCE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_persistence.mm b/Firestore/core/src/firebase/firestore/local/leveldb_persistence.mm new file mode 100644 index 00000000000..5b6b5a48edd --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/leveldb_persistence.mm @@ -0,0 +1,284 @@ +/* + * Copyright 2019 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/local/leveldb_persistence.h" + +#include +#include + +#include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/core/database_info.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_lru_reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_migrations.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" +#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/sizer.h" +#include "Firestore/core/src/firebase/firestore/util/filesystem.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/string_util.h" +#include "absl/memory/memory.h" +#include "absl/strings/match.h" + +namespace firebase { +namespace firestore { +namespace local { +namespace { + +using auth::User; +using leveldb::DB; +using model::ListenSequenceNumber; +using util::Path; +using util::Status; +using util::StatusOr; +using util::StringFormat; + +/** + * Finds all user ids in the database based on the existence of a mutation + * queue. + */ +std::set CollectUserSet(LevelDbTransaction* transaction) { + std::set result; + + std::string table_prefix = LevelDbMutationKey::KeyPrefix(); + auto it = transaction->NewIterator(); + it->Seek(table_prefix); + + LevelDbMutationKey row_key; + while (it->Valid() && absl::StartsWith(it->key(), table_prefix) && + row_key.Decode(it->key())) { + result.insert(row_key.user_id()); + + auto user_end = LevelDbMutationKey::KeyPrefix(row_key.user_id()); + user_end = util::PrefixSuccessor(user_end); + it->Seek(user_end); + } + return result; +} + +} // namespace + +util::StatusOr> LevelDbPersistence::Create( + util::Path dir, + FSTLocalSerializer* serializer, + const LruParams& lru_params) { + Status status = EnsureDirectory(dir); + if (!status.ok()) return status; + + status = ExcludeFromBackups(dir); + if (!status.ok()) return status; + + StatusOr> created = OpenDb(dir); + if (!created.ok()) return created.status(); + + std::unique_ptr db = std::move(created).ValueOrDie(); + LevelDbMigrations::RunMigrations(db.get()); + + LevelDbTransaction transaction(db.get(), "Start LevelDB"); + std::set users = CollectUserSet(&transaction); + transaction.Commit(); + + // Explicit conversion is required to allow the StatusOr to be created. + std::unique_ptr result(new LevelDbPersistence( + std::move(db), std::move(dir), std::move(users), serializer, lru_params)); + return std::move(result); +} + +LevelDbPersistence::LevelDbPersistence(std::unique_ptr db, + util::Path directory, + std::set users, + FSTLocalSerializer* serializer, + const LruParams& lru_params) + : db_(std::move(db)), + directory_(std::move(directory)), + users_(std::move(users)), + serializer_(serializer) { + query_cache_ = absl::make_unique(this, serializer_); + document_cache_ = + absl::make_unique(this, serializer_); + index_manager_ = absl::make_unique(this); + reference_delegate_ = + absl::make_unique(this, lru_params); + + // TODO(gsoltis): set up a leveldb transaction for these operations. + query_cache_->Start(); + reference_delegate_->Start(); + started_ = true; +} + +// MARK: - Storage location + +#if !defined(__APPLE__) + +Path LevelDbPersistence::AppDataDirectory() { +#error "This does not yet support non-Apple platforms." +} + +#endif // !defined(__APPLE__) + +util::Path LevelDbPersistence::StorageDirectory( + const core::DatabaseInfo& database_info, const util::Path& documents_dir) { + // Use two different path formats: + // + // * persistence_key / project_id . database_id / name + // * persistence_key / project_id / name + // + // project_ids are DNS-compatible names and cannot contain dots so there's + // no danger of collisions. + std::string project_key = database_info.database_id().project_id(); + if (!database_info.database_id().IsDefaultDatabase()) { + absl::StrAppend(&project_key, ".", + database_info.database_id().database_id()); + } + + // Reserve one additional path component to allow multiple physical databases + return Path::JoinUtf8(documents_dir, database_info.persistence_key(), + project_key, "main"); +} + +// MARK: - Startup + +Status LevelDbPersistence::EnsureDirectory(const Path& dir) { + Status status = util::RecursivelyCreateDir(dir); + if (!status.ok()) { + return Status{Error::Internal, "Failed to create persistence directory"} + .CausedBy(status); + } + + return Status::OK(); +} + +#if !defined(__APPLE__) + +Status LevelDbPersistence::ExcludeFromBackups(const Path& directory) { + // Non-Apple platforms don't yet implement exclusion from backups. + return Status::OK(); +} + +#endif + +StatusOr> LevelDbPersistence::OpenDb(const Path& dir) { + leveldb::Options options; + options.create_if_missing = true; + + DB* database = nullptr; + leveldb::Status status = DB::Open(options, dir.ToUtf8String(), &database); + if (!status.ok()) { + return Status{Error::Internal, + StringFormat("Failed to open LevelDB database at %s", + dir.ToUtf8String())} + .CausedBy(ConvertStatus(status)); + } + + return std::unique_ptr(database); +} + +// MARK: - LevelDB utilities + +LevelDbTransaction* LevelDbPersistence::current_transaction() { + HARD_ASSERT(transaction_ != nullptr, + "Attempting to access transaction before one has started"); + return transaction_.get(); +} + +util::Status LevelDbPersistence::ClearPersistence( + const core::DatabaseInfo& database_info) { + Path leveldb_dir = StorageDirectory(database_info, AppDataDirectory()); + LOG_DEBUG("Clearing persistence for path: %s", leveldb_dir.ToUtf8String()); + return util::RecursivelyDelete(leveldb_dir); +} + +int64_t LevelDbPersistence::CalculateByteSize() { + int64_t count = 0; + auto iter = util::DirectoryIterator::Create(directory_); + for (; iter->Valid(); iter->Next()) { + int64_t file_size = util::FileSize(iter->file()).ValueOrDie(); + count += file_size; + } + + HARD_ASSERT(iter->status().ok(), "Failed to iterate leveldb directory: %s", + iter->status().error_message().c_str()); + HARD_ASSERT(count >= 0 && count <= std::numeric_limits::max(), + "Overflowed counting bytes cached"); + return count; +} + +// MARK: - Persistence + +model::ListenSequenceNumber LevelDbPersistence::current_sequence_number() + const { + return reference_delegate_->current_sequence_number(); +} + +void LevelDbPersistence::Shutdown() { + HARD_ASSERT(started_, "LevelDbPersistence shutdown without start!"); + started_ = false; + db_.reset(); +} + +LevelDbMutationQueue* LevelDbPersistence::GetMutationQueueForUser( + const auth::User& user) { + users_.insert(user.uid()); + current_mutation_queue_ = + absl::make_unique(user, this, serializer_); + return current_mutation_queue_.get(); +} + +LevelDbQueryCache* LevelDbPersistence::query_cache() { + return query_cache_.get(); +} + +LevelDbRemoteDocumentCache* LevelDbPersistence::remote_document_cache() { + return document_cache_.get(); +} + +LevelDbIndexManager* LevelDbPersistence::index_manager() { + return index_manager_.get(); +} + +LevelDbLruReferenceDelegate* LevelDbPersistence::reference_delegate() { + return reference_delegate_.get(); +} + +void LevelDbPersistence::RunInternal(absl::string_view label, + std::function block) { + HARD_ASSERT(transaction_ == nullptr, + "Starting a transaction while one is already in progress"); + + transaction_ = absl::make_unique(db_.get(), label); + reference_delegate_->OnTransactionStarted(label); + + block(); + + reference_delegate_->OnTransactionCommitted(); + transaction_->Commit(); + transaction_.reset(); +} + +constexpr const char* LevelDbPersistence::kReservedPathComponent; + +leveldb::ReadOptions StandardReadOptions() { + // For now this is paranoid, but perhaps disable that in production builds. + leveldb::ReadOptions options; + options.verify_checksums = true; + return options; +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_persistence_apple.mm b/Firestore/core/src/firebase/firestore/local/leveldb_persistence_apple.mm new file mode 100644 index 00000000000..125171da1ae --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/leveldb_persistence_apple.mm @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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/local/leveldb_persistence.h" + +#import + +#include "Firestore/core/src/firebase/firestore/util/path.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" + +#if defined(__APPLE__) + +namespace firebase { +namespace firestore { +namespace local { + +using util::Path; +using util::Status; + +Path LevelDbPersistence::AppDataDirectory() { +#if TARGET_OS_IOS + NSArray* directories = NSSearchPathForDirectoriesInDomains( + NSDocumentDirectory, NSUserDomainMask, YES); + return Path::FromNSString(directories[0]).AppendUtf8(kReservedPathComponent); + +#elif TARGET_OS_TV + NSArray* directories = NSSearchPathForDirectoriesInDomains( + NSCachesDirectory, NSUserDomainMask, YES); + return Path::FromNSString(directories[0]).AppendUtf8(kReservedPathComponent); + +#elif TARGET_OS_OSX + std::string dot_prefixed = absl::StrCat(".", kReservedPathComponent); + return Path::FromNSString(NSHomeDirectory()).AppendUtf8(dot_prefixed); + +#else +#error "Don't know where to store documents on this platform." + +#endif +} + +Status LevelDbPersistence::ExcludeFromBackups(const Path& dir) { + NSURL* dir_url = [NSURL fileURLWithPath:dir.ToNSString()]; + NSError* error = nil; + if (![dir_url setResourceValue:@YES + forKey:NSURLIsExcludedFromBackupKey + error:&error]) { + return Status{ + Error::Internal, + "Failed to mark persistence directory as excluded from backups"} + .CausedBy(Status::FromNSError(error)); + } + + return Status::OK(); +} + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // __APPLE__ diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h b/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h index 0369ff0ae2b..b060f0146d8 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.h @@ -27,15 +27,14 @@ #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" #include "Firestore/core/src/firebase/firestore/local/query_cache.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.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/types.h" #include "absl/strings/string_view.h" #include "leveldb/db.h" -@class FSTLevelDB; @class FSTLocalSerializer; -@class FSTQueryData; NS_ASSUME_NONNULL_BEGIN @@ -43,6 +42,8 @@ namespace firebase { namespace firestore { namespace local { +class LevelDbPersistence; + /** Cached Queries backed by LevelDB. */ class LevelDbQueryCache : public QueryCache { public: @@ -58,21 +59,21 @@ class LevelDbQueryCache : public QueryCache { * * @param db The LevelDB in which to create the cache. */ - LevelDbQueryCache(FSTLevelDB* db, FSTLocalSerializer* serializer); + LevelDbQueryCache(LevelDbPersistence* db, FSTLocalSerializer* serializer); // Target-related methods - void AddTarget(FSTQueryData* query_data) override; + void AddTarget(const QueryData& query_data) override; - void UpdateTarget(FSTQueryData* query_data) override; + void UpdateTarget(const QueryData& query_data) override; - void RemoveTarget(FSTQueryData* query_data) override; + void RemoveTarget(const QueryData& query_data) override; - FSTQueryData* _Nullable GetTarget(const core::Query& query) override; + absl::optional GetTarget(const core::Query& query) override; void EnumerateTargets(const TargetCallback& callback) override; int RemoveTargets(model::ListenSequenceNumber upper_bound, - const std::unordered_map& + const std::unordered_map& live_targets) override; // Key-related methods @@ -120,17 +121,17 @@ class LevelDbQueryCache : public QueryCache { void EnumerateOrphanedDocuments(const OrphanedDocumentCallback& callback); private: - void Save(FSTQueryData* query_data); - bool UpdateMetadata(FSTQueryData* query_data); + void Save(const QueryData& query_data); + bool UpdateMetadata(const QueryData& query_data); void SaveMetadata(); /** * Parses the given bytes as an FSTPBTarget protocol buffer and then converts * to the equivalent query data. */ - FSTQueryData* DecodeTarget(absl::string_view encoded); + QueryData DecodeTarget(absl::string_view encoded); - // This instance is owned by FSTLevelDB; avoid a retain cycle. - __weak FSTLevelDB* db_; + // The LevelDbQueryCache is owned by LevelDbPersistence. + LevelDbPersistence* db_; FSTLocalSerializer* serializer_; /** A write-through cached copy of the metadata for the query cache. */ FSTPBTargetGlobal* metadata_; diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.mm b/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.mm index 05fbe9f2d8d..08a1c47c3c0 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.mm +++ b/Firestore/core/src/firebase/firestore/local/leveldb_query_cache.mm @@ -20,10 +20,12 @@ #include #import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" -#import "Firestore/Source/Local/FSTLevelDB.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Local/FSTQueryData.h" + #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.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/util/string_apple.h" @@ -45,7 +47,7 @@ FSTPBTargetGlobal* LevelDbQueryCache::ReadMetadata(leveldb::DB* db) { std::string key = LevelDbTargetGlobalKey::Key(); std::string value; - Status status = db->Get([FSTLevelDB standardReadOptions], key, &value); + Status status = db->Get(StandardReadOptions(), key, &value); if (status.IsNotFound()) { return nil; } else if (!status.ok()) { @@ -67,16 +69,14 @@ return proto; } -LevelDbQueryCache::LevelDbQueryCache(FSTLevelDB* db, +LevelDbQueryCache::LevelDbQueryCache(LevelDbPersistence* db, FSTLocalSerializer* serializer) : db_(db), serializer_(serializer) { } -// TODO(gsoltis): revisit having a Start method vs a static factory function -// that returns a started instance. void LevelDbQueryCache::Start() { - // TODO(gsoltis): switch this usage of ptr to currentTransaction - metadata_ = ReadMetadata(db_.ptr); + // TODO(gsoltis): switch this usage of ptr to current_transaction() + metadata_ = ReadMetadata(db_->ptr()); HARD_ASSERT(metadata_ != nil, "Found nil metadata, expected schema to be at version 0 which " "ensures metadata existence"); @@ -84,21 +84,21 @@ [serializer_ decodedVersion:metadata_.lastRemoteSnapshotVersion]; } -void LevelDbQueryCache::AddTarget(FSTQueryData* query_data) { +void LevelDbQueryCache::AddTarget(const QueryData& query_data) { Save(query_data); - const std::string& canonical_id = query_data.query.CanonicalId(); + const std::string& canonical_id = query_data.query().CanonicalId(); std::string index_key = - LevelDbQueryTargetKey::Key(canonical_id, query_data.targetID); + LevelDbQueryTargetKey::Key(canonical_id, query_data.target_id()); std::string empty_buffer; - db_.currentTransaction->Put(index_key, empty_buffer); + db_->current_transaction()->Put(index_key, empty_buffer); metadata_.targetCount++; UpdateMetadata(query_data); SaveMetadata(); } -void LevelDbQueryCache::UpdateTarget(FSTQueryData* query_data) { +void LevelDbQueryCache::UpdateTarget(const QueryData& query_data) { Save(query_data); if (UpdateMetadata(query_data)) { @@ -106,49 +106,50 @@ } } -void LevelDbQueryCache::RemoveTarget(FSTQueryData* query_data) { - TargetId target_id = query_data.targetID; +void LevelDbQueryCache::RemoveTarget(const QueryData& query_data) { + TargetId target_id = query_data.target_id(); RemoveAllKeysForTarget(target_id); std::string key = LevelDbTargetKey::Key(target_id); - db_.currentTransaction->Delete(key); + db_->current_transaction()->Delete(key); std::string index_key = - LevelDbQueryTargetKey::Key(query_data.query.CanonicalId(), target_id); - db_.currentTransaction->Delete(index_key); + LevelDbQueryTargetKey::Key(query_data.query().CanonicalId(), target_id); + db_->current_transaction()->Delete(index_key); metadata_.targetCount--; SaveMetadata(); } -FSTQueryData* _Nullable LevelDbQueryCache::GetTarget(const Query& query) { +absl::optional LevelDbQueryCache::GetTarget(const Query& query) { // Scan the query-target index starting with a prefix starting with the given - // query's canonicalID. Note that this is a scan rather than a get because - // canonicalIDs are not required to be unique per target. + // query's canonical_id. Note that this is a scan rather than a get because + // canonical_ids are not required to be unique per target. const std::string& canonical_id = query.CanonicalId(); - auto index_iterator = db_.currentTransaction->NewIterator(); + auto index_iterator = db_->current_transaction()->NewIterator(); std::string index_prefix = LevelDbQueryTargetKey::KeyPrefix(canonical_id); index_iterator->Seek(index_prefix); // Simultaneously scan the targets table. This works because each - // (canonicalID, targetID) pair is unique and ordered, so when scanning a - // table prefixed by exactly one canonicalID, all the targetIDs will be unique - // and in order. + // (canonical_id, target_id) pair is unique and ordered, so when scanning a + // table prefixed by exactly one canonical_id, all the target_ids will be + // unique and in order. std::string target_prefix = LevelDbTargetKey::KeyPrefix(); - auto target_iterator = db_.currentTransaction->NewIterator(); + auto target_iterator = db_->current_transaction()->NewIterator(); LevelDbQueryTargetKey row_key; for (; index_iterator->Valid(); index_iterator->Next()) { - // Only consider rows matching exactly the specific canonicalID of interest. + // Only consider rows matching exactly the specific canonical_id of + // interest. if (!absl::StartsWith(index_iterator->key(), index_prefix) || !row_key.Decode(index_iterator->key()) || canonical_id != row_key.canonical_id()) { - // End of this canonicalID's possible targets. + // End of this canonical_id's possible targets. break; } - // Each row is a unique combination of canonicalID and targetID, so this + // Each row is a unique combination of canonical_id and target_id, so this // foreign key reference can only occur once. std::string target_key = LevelDbTargetKey::Key(row_key.target_id()); target_iterator->Seek(target_key); @@ -161,39 +162,39 @@ // Finally after finding a potential match, check that the query is actually // equal to the requested query. - FSTQueryData* target = DecodeTarget(target_iterator->value()); - if (target.query == query) { + QueryData target = DecodeTarget(target_iterator->value()); + if (target.query() == query) { return target; } } - return nil; + return absl::nullopt; } void LevelDbQueryCache::EnumerateTargets(const TargetCallback& callback) { // Enumerate all targets, give their sequence numbers. std::string target_prefix = LevelDbTargetKey::KeyPrefix(); - auto it = db_.currentTransaction->NewIterator(); + auto it = db_->current_transaction()->NewIterator(); it->Seek(target_prefix); for (; it->Valid() && absl::StartsWith(it->key(), target_prefix); it->Next()) { - FSTQueryData* target = DecodeTarget(it->value()); + QueryData target = DecodeTarget(it->value()); callback(target); } } int LevelDbQueryCache::RemoveTargets( ListenSequenceNumber upper_bound, - const std::unordered_map& live_targets) { + const std::unordered_map& live_targets) { int count = 0; std::string target_prefix = LevelDbTargetKey::KeyPrefix(); - auto it = db_.currentTransaction->NewIterator(); + auto it = db_->current_transaction()->NewIterator(); it->Seek(target_prefix); for (; it->Valid() && absl::StartsWith(it->key(), target_prefix); it->Next()) { - FSTQueryData* query_data = DecodeTarget(it->value()); - if (query_data.sequenceNumber <= upper_bound && - live_targets.find(query_data.targetID) == live_targets.end()) { + QueryData query_data = DecodeTarget(it->value()); + if (query_data.sequence_number() <= upper_bound && + live_targets.find(query_data.target_id()) == live_targets.end()) { RemoveTarget(query_data); count++; } @@ -210,50 +211,50 @@ std::string empty_buffer; for (const DocumentKey& key : keys) { - db_.currentTransaction->Put(LevelDbTargetDocumentKey::Key(target_id, key), - empty_buffer); - db_.currentTransaction->Put(LevelDbDocumentTargetKey::Key(key, target_id), - empty_buffer); - [db_.referenceDelegate addReference:key]; + db_->current_transaction()->Put( + LevelDbTargetDocumentKey::Key(target_id, key), empty_buffer); + db_->current_transaction()->Put( + LevelDbDocumentTargetKey::Key(key, target_id), empty_buffer); + db_->reference_delegate()->AddReference(key); }; } void LevelDbQueryCache::RemoveMatchingKeys(const DocumentKeySet& keys, TargetId target_id) { for (const DocumentKey& key : keys) { - db_.currentTransaction->Delete( + db_->current_transaction()->Delete( LevelDbTargetDocumentKey::Key(target_id, key)); - db_.currentTransaction->Delete( + db_->current_transaction()->Delete( LevelDbDocumentTargetKey::Key(key, target_id)); - [db_.referenceDelegate removeReference:key]; + db_->reference_delegate()->RemoveReference(key); } } void LevelDbQueryCache::RemoveAllKeysForTarget(TargetId target_id) { std::string index_prefix = LevelDbTargetDocumentKey::KeyPrefix(target_id); - auto index_iterator = db_.currentTransaction->NewIterator(); + auto index_iterator = db_->current_transaction()->NewIterator(); index_iterator->Seek(index_prefix); LevelDbTargetDocumentKey row_key; for (; index_iterator->Valid(); index_iterator->Next()) { absl::string_view index_key = index_iterator->key(); - // Only consider rows matching this specific targetID. + // Only consider rows matching this specific target_id. if (!row_key.Decode(index_key) || row_key.target_id() != target_id) { break; } const DocumentKey& document_key = row_key.document_key(); // Delete both index rows - db_.currentTransaction->Delete(index_key); - db_.currentTransaction->Delete( + db_->current_transaction()->Delete(index_key); + db_->current_transaction()->Delete( LevelDbDocumentTargetKey::Key(document_key, target_id)); } } DocumentKeySet LevelDbQueryCache::GetMatchingKeys(TargetId target_id) { std::string index_prefix = LevelDbTargetDocumentKey::KeyPrefix(target_id); - auto index_iterator = db_.currentTransaction->NewIterator(); + auto index_iterator = db_->current_transaction()->NewIterator(); index_iterator->Seek(index_prefix); DocumentKeySet result; @@ -277,7 +278,7 @@ // Sentinel row just says the document exists, not that it's a member of any // particular target. std::string index_prefix = LevelDbDocumentTargetKey::KeyPrefix(key.path()); - auto index_iterator = db_.currentTransaction->NewIterator(); + auto index_iterator = db_->current_transaction()->NewIterator(); index_iterator->Seek(index_prefix); for (; index_iterator->Valid() && @@ -307,7 +308,7 @@ void LevelDbQueryCache::EnumerateOrphanedDocuments( const OrphanedDocumentCallback& callback) { std::string document_target_prefix = LevelDbDocumentTargetKey::KeyPrefix(); - auto it = db_.currentTransaction->NewIterator(); + auto it = db_->current_transaction()->NewIterator(); it->Seek(document_target_prefix); ListenSequenceNumber next_to_report = 0; DocumentKey key_to_report; @@ -340,21 +341,22 @@ } } -void LevelDbQueryCache::Save(FSTQueryData* query_data) { - TargetId target_id = query_data.targetID; +void LevelDbQueryCache::Save(const QueryData& query_data) { + TargetId target_id = query_data.target_id(); std::string key = LevelDbTargetKey::Key(target_id); - db_.currentTransaction->Put(key, [serializer_ encodedQueryData:query_data]); + db_->current_transaction()->Put(key, + [serializer_ encodedQueryData:query_data]); } -bool LevelDbQueryCache::UpdateMetadata(FSTQueryData* query_data) { +bool LevelDbQueryCache::UpdateMetadata(const QueryData& query_data) { bool updated = false; - if (query_data.targetID > metadata_.highestTargetId) { - metadata_.highestTargetId = query_data.targetID; + if (query_data.target_id() > metadata_.highestTargetId) { + metadata_.highestTargetId = query_data.target_id(); updated = true; } - if (query_data.sequenceNumber > metadata_.highestListenSequenceNumber) { - metadata_.highestListenSequenceNumber = query_data.sequenceNumber; + if (query_data.sequence_number() > metadata_.highestListenSequenceNumber) { + metadata_.highestListenSequenceNumber = query_data.sequence_number(); updated = true; } @@ -362,10 +364,10 @@ } void LevelDbQueryCache::SaveMetadata() { - db_.currentTransaction->Put(LevelDbTargetGlobalKey::Key(), metadata_); + db_->current_transaction()->Put(LevelDbTargetGlobalKey::Key(), metadata_); } -FSTQueryData* LevelDbQueryCache::DecodeTarget(absl::string_view encoded) { +QueryData LevelDbQueryCache::DecodeTarget(absl::string_view encoded) { NSData* data = [[NSData alloc] initWithBytesNoCopy:(void*)encoded.data() length:encoded.size() freeWhenDone:NO]; diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h b/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h index c779947431f..1a703e76120 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.h @@ -31,7 +31,6 @@ #include "Firestore/core/src/firebase/firestore/model/types.h" #include "absl/strings/string_view.h" -@class FSTLevelDB; @class FSTLocalSerializer; NS_ASSUME_NONNULL_BEGIN @@ -40,10 +39,13 @@ namespace firebase { namespace firestore { namespace local { +class LevelDbPersistence; + /** Cached Remote Documents backed by leveldb. */ class LevelDbRemoteDocumentCache : public RemoteDocumentCache { public: - LevelDbRemoteDocumentCache(FSTLevelDB* db, FSTLocalSerializer* serializer); + LevelDbRemoteDocumentCache(LevelDbPersistence* db, + FSTLocalSerializer* serializer); void Add(const model::MaybeDocument& document) override; void Remove(const model::DocumentKey& key) override; @@ -58,8 +60,8 @@ class LevelDbRemoteDocumentCache : public RemoteDocumentCache { model::MaybeDocument DecodeMaybeDocument(absl::string_view encoded, const model::DocumentKey& key); - // This instance is owned by FSTLevelDB; avoid a retain cycle. - __weak FSTLevelDB* db_; + // The LevelDbRemoteDocumentCache instance is owned by LevelDbPersistence. + LevelDbPersistence* db_; FSTLocalSerializer* serializer_; }; diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.mm b/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.mm index 6634c7b4a47..5d1b63a73de 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.mm +++ b/Firestore/core/src/firebase/firestore/local/leveldb_remote_document_cache.mm @@ -21,10 +21,10 @@ #include #import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" -#import "Firestore/Source/Local/FSTLevelDB.h" #import "Firestore/Source/Local/FSTLocalSerializer.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_key.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "leveldb/db.h" @@ -43,28 +43,29 @@ using leveldb::Status; LevelDbRemoteDocumentCache::LevelDbRemoteDocumentCache( - FSTLevelDB* db, FSTLocalSerializer* serializer) + LevelDbPersistence* db, FSTLocalSerializer* serializer) : db_(db), serializer_(serializer) { } void LevelDbRemoteDocumentCache::Add(const MaybeDocument& document) { std::string ldb_key = LevelDbRemoteDocumentKey::Key(document.key()); - db_.currentTransaction->Put(ldb_key, - [serializer_ encodedMaybeDocument:document]); + db_->current_transaction()->Put(ldb_key, + [serializer_ encodedMaybeDocument:document]); - db_.indexManager->AddToCollectionParentIndex(document.key().path().PopLast()); + db_->index_manager()->AddToCollectionParentIndex( + document.key().path().PopLast()); } void LevelDbRemoteDocumentCache::Remove(const DocumentKey& key) { std::string ldb_key = LevelDbRemoteDocumentKey::Key(key); - db_.currentTransaction->Delete(ldb_key); + db_->current_transaction()->Delete(ldb_key); } absl::optional LevelDbRemoteDocumentCache::Get( const DocumentKey& key) { std::string ldb_key = LevelDbRemoteDocumentKey::Key(key); std::string value; - Status status = db_.currentTransaction->Get(ldb_key, &value); + Status status = db_->current_transaction()->Get(ldb_key, &value); if (status.IsNotFound()) { return absl::nullopt; } else if (status.ok()) { @@ -79,13 +80,13 @@ const DocumentKeySet& keys) { OptionalMaybeDocumentMap results; - LevelDbRemoteDocumentKey currentKey; - auto it = db_.currentTransaction->NewIterator(); + LevelDbRemoteDocumentKey current_key; + auto it = db_->current_transaction()->NewIterator(); for (const DocumentKey& key : keys) { it->Seek(LevelDbRemoteDocumentKey::Key(key)); - if (!it->Valid() || !currentKey.Decode(it->key()) || - currentKey.document_key() != key) { + if (!it->Valid() || !current_key.Decode(it->key()) || + current_key.document_key() != key) { results = results.insert(key, absl::nullopt); } else { results = results.insert(key, DecodeMaybeDocument(it->value(), key)); @@ -109,7 +110,7 @@ // Documents are ordered by key, so we can use a prefix scan to narrow down // the documents we need to match the query against. std::string start_key = LevelDbRemoteDocumentKey::KeyPrefix(query_path); - auto it = db_.currentTransaction->NewIterator(); + auto it = db_->current_transaction()->NewIterator(); it->Seek(start_key); LevelDbRemoteDocumentKey current_key; @@ -148,11 +149,11 @@ HARD_FAIL("FSTPBMaybeDocument failed to parse: %s", error); } - MaybeDocument maybeDocument = [serializer_ decodedMaybeDocument:proto]; - HARD_ASSERT(maybeDocument.key() == key, + MaybeDocument maybe_document = [serializer_ decodedMaybeDocument:proto]; + HARD_ASSERT(maybe_document.key() == key, "Read document has key (%s) instead of expected key (%s).", - maybeDocument.key().ToString(), key.ToString()); - return maybeDocument; + maybe_document.key().ToString(), key.ToString()); + return maybe_document; } } // namespace local diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_util.cc b/Firestore/core/src/firebase/firestore/local/leveldb_util.cc index 49b9cd94188..cad14b2c4fd 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_util.cc +++ b/Firestore/core/src/firebase/firestore/local/leveldb_util.cc @@ -17,6 +17,7 @@ #include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" #include "Firestore/core/include/firebase/firestore/firestore_errors.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" #include "absl/strings/str_cat.h" namespace firebase { diff --git a/Firestore/core/src/firebase/firestore/local/leveldb_util.h b/Firestore/core/src/firebase/firestore/local/leveldb_util.h index 24a7537f97e..f2e79f6fb35 100644 --- a/Firestore/core/src/firebase/firestore/local/leveldb_util.h +++ b/Firestore/core/src/firebase/firestore/local/leveldb_util.h @@ -19,7 +19,7 @@ #include -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/strings/string_view.h" #include "leveldb/slice.h" #include "leveldb/status.h" diff --git a/Firestore/core/src/firebase/firestore/local/local_documents_view.h b/Firestore/core/src/firebase/firestore/local/local_documents_view.h index e6a5e95bfc5..d4013dae3be 100644 --- a/Firestore/core/src/firebase/firestore/local/local_documents_view.h +++ b/Firestore/core/src/firebase/firestore/local/local_documents_view.h @@ -17,8 +17,6 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LOCAL_DOCUMENTS_VIEW_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LOCAL_DOCUMENTS_VIEW_H_ -#import - #include #include "Firestore/core/src/firebase/firestore/core/query.h" @@ -29,10 +27,6 @@ #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/document_map.h" -NS_ASSUME_NONNULL_BEGIN - -@class FSTMutationBatch; - namespace firebase { namespace firestore { namespace local { @@ -41,7 +35,7 @@ namespace local { * A readonly view of the local state of all documents we're tracking (i.e. we * have a cached version in remoteDocumentCache or local mutations for the * document). The view is computed by applying the mutations in the - * FSTMutationQueue to the FSTRemoteDocumentCache. + * MutationQueue to the RemoteDocumentCache. */ class LocalDocumentsView { public: @@ -84,7 +78,7 @@ class LocalDocumentsView { /** Internal version of GetDocument that allows re-using batches. */ absl::optional GetDocument( const model::DocumentKey& key, - const std::vector& batches); + const std::vector& batches); /** * Returns the view of the given `docs` as they would appear after applying @@ -92,7 +86,7 @@ class LocalDocumentsView { */ model::OptionalMaybeDocumentMap ApplyLocalMutationsToDocuments( const model::OptionalMaybeDocumentMap& docs, - const std::vector& batches); + const std::vector& batches); /** Performs a simple document lookup for the given path. */ model::DocumentMap GetDocumentsMatchingDocumentQuery( @@ -115,7 +109,7 @@ class LocalDocumentsView { * lead to missing results for the query. */ model::DocumentMap AddMissingBaseDocuments( - const std::vector& matching_batches, + const std::vector& matching_batches, model::DocumentMap existing_docs); RemoteDocumentCache* remote_document_cache_; @@ -127,6 +121,4 @@ class LocalDocumentsView { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LOCAL_DOCUMENTS_VIEW_H_ diff --git a/Firestore/core/src/firebase/firestore/local/local_documents_view.mm b/Firestore/core/src/firebase/firestore/local/local_documents_view.mm index 34a29cd954a..40645309e9d 100644 --- a/Firestore/core/src/firebase/firestore/local/local_documents_view.mm +++ b/Firestore/core/src/firebase/firestore/local/local_documents_view.mm @@ -19,8 +19,6 @@ #include #include -#import "Firestore/Source/Model/FSTMutationBatch.h" - #include "Firestore/core/src/firebase/firestore/local/mutation_queue.h" #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" #include "Firestore/core/src/firebase/firestore/model/document.h" @@ -31,8 +29,6 @@ #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace local { @@ -45,6 +41,7 @@ using model::MaybeDocument; using model::MaybeDocumentMap; using model::Mutation; +using model::MutationBatch; using model::NoDocument; using model::OptionalMaybeDocumentMap; using model::ResourcePath; @@ -53,16 +50,16 @@ absl::optional LocalDocumentsView::GetDocument( const DocumentKey& key) { - std::vector batches = + std::vector batches = mutation_queue_->AllMutationBatchesAffectingDocumentKey(key); return GetDocument(key, batches); } absl::optional LocalDocumentsView::GetDocument( - const DocumentKey& key, const std::vector& batches) { + const DocumentKey& key, const std::vector& batches) { absl::optional document = remote_document_cache_->Get(key); - for (FSTMutationBatch* batch : batches) { - document = [batch applyToLocalDocument:document documentKey:key]; + for (const MutationBatch& batch : batches) { + document = batch.ApplyToLocalDocument(document, key); } return document; @@ -70,14 +67,14 @@ OptionalMaybeDocumentMap LocalDocumentsView::ApplyLocalMutationsToDocuments( const OptionalMaybeDocumentMap& docs, - const std::vector& batches) { + const std::vector& batches) { OptionalMaybeDocumentMap results; for (const auto& kv : docs) { const DocumentKey& key = kv.first; absl::optional local_view = kv.second; - for (FSTMutationBatch* batch : batches) { - local_view = [batch applyToLocalDocument:local_view documentKey:key]; + for (const MutationBatch& batch : batches) { + local_view = batch.ApplyToLocalDocument(local_view, key); } results = results.insert(key, local_view); } @@ -99,7 +96,7 @@ for (const auto& kv : base_docs) { all_keys = all_keys.insert(kv.first); } - std::vector batches = + std::vector batches = mutation_queue_->AllMutationBatchesAffectingDocumentKeys(all_keys); OptionalMaybeDocumentMap docs = @@ -172,13 +169,13 @@ const Query& query) { DocumentMap results = remote_document_cache_->GetMatching(query); // Get locally persisted mutation batches. - std::vector matchingBatches = + std::vector matching_batches = mutation_queue_->AllMutationBatchesAffectingQuery(query); - results = AddMissingBaseDocuments(matchingBatches, std::move(results)); + results = AddMissingBaseDocuments(matching_batches, std::move(results)); - for (FSTMutationBatch* batch : matchingBatches) { - for (const Mutation& mutation : [batch mutations]) { + for (const MutationBatch& batch : matching_batches) { + for (const Mutation& mutation : batch.mutations()) { // Only process documents belonging to the collection. if (!query.path().IsImmediateParentOf(mutation.key().path())) { continue; @@ -190,8 +187,8 @@ absl::optional base_doc = results.underlying_map().get(key); - absl::optional mutated_doc = - mutation.ApplyToLocalView(base_doc, base_doc, batch.localWriteTime); + absl::optional mutated_doc = mutation.ApplyToLocalView( + base_doc, base_doc, batch.local_write_time()); if (mutated_doc && mutated_doc->is_document()) { results = results.insert(key, Document(*mutated_doc)); @@ -218,11 +215,11 @@ } DocumentMap LocalDocumentsView::AddMissingBaseDocuments( - const std::vector& matching_batches, + const std::vector& matching_batches, DocumentMap existing_docs) { DocumentKeySet missing_doc_keys; - for (FSTMutationBatch* batch : matching_batches) { - for (const Mutation& mutation : [batch mutations]) { + for (const MutationBatch& batch : matching_batches) { + for (const Mutation& mutation : batch.mutations()) { const DocumentKey& key = mutation.key(); if (mutation.type() == Mutation::Type::Patch && !existing_docs.underlying_map().contains(key)) { @@ -246,5 +243,3 @@ } // namespace local } // namespace firestore } // namespace firebase - -NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/local/local_serializer.cc b/Firestore/core/src/firebase/firestore/local/local_serializer.cc index dceb135a2aa..983ab0c859d 100644 --- a/Firestore/core/src/firebase/firestore/local/local_serializer.cc +++ b/Firestore/core/src/firebase/firestore/local/local_serializer.cc @@ -123,8 +123,7 @@ google_firestore_v1_Document LocalSerializer::EncodeDocument( const Document& doc) const { google_firestore_v1_Document result{}; - result.name = - rpc_serializer_.EncodeString(rpc_serializer_.EncodeKey(doc.key())); + result.name = rpc_serializer_.EncodeKey(doc.key()); // Encode Document.fields (unless it's empty) pb_size_t count = CheckedSize(doc.data().GetInternalValue().size()); @@ -148,8 +147,7 @@ firestore_client_NoDocument LocalSerializer::EncodeNoDocument( const NoDocument& no_doc) const { firestore_client_NoDocument result{}; - result.name = - rpc_serializer_.EncodeString(rpc_serializer_.EncodeKey(no_doc.key())); + result.name = rpc_serializer_.EncodeKey(no_doc.key()); result.read_time = rpc_serializer_.EncodeVersion(no_doc.version()); return result; @@ -163,9 +161,7 @@ NoDocument LocalSerializer::DecodeNoDocument( // TODO(rsgowman): Fix hardcoding of has_committed_mutations. // Instead, we should grab this from the proto (see other ports). However, // we'll defer until the nanopb-master gets merged to master. - return NoDocument(rpc_serializer_.DecodeKey( - reader, rpc_serializer_.DecodeString(proto.name)), - version, + return NoDocument(rpc_serializer_.DecodeKey(reader, proto.name), version, /*has_committed_mutations=*/false); } @@ -173,8 +169,7 @@ firestore_client_UnknownDocument LocalSerializer::EncodeUnknownDocument( const UnknownDocument& unknown_doc) const { firestore_client_UnknownDocument result{}; - result.name = rpc_serializer_.EncodeString( - rpc_serializer_.EncodeKey(unknown_doc.key())); + result.name = rpc_serializer_.EncodeKey(unknown_doc.key()); result.version = rpc_serializer_.EncodeVersion(unknown_doc.version()); return result; @@ -185,8 +180,7 @@ UnknownDocument LocalSerializer::DecodeUnknownDocument( SnapshotVersion version = rpc_serializer_.DecodeSnapshotVersion(reader, proto.version); - return UnknownDocument(rpc_serializer_.DecodeKey( - reader, rpc_serializer_.DecodeString(proto.name)), + return UnknownDocument(rpc_serializer_.DecodeKey(reader, proto.name), version); } @@ -249,7 +243,7 @@ QueryData LocalSerializer::DecodeQueryData( if (!reader->status().ok()) return QueryData::Invalid(); return QueryData(std::move(query), target_id, sequence_number, - QueryPurpose::kListen, version, std::move(resume_token)); + QueryPurpose::Listen, version, std::move(resume_token)); } firestore_client_WriteBatch LocalSerializer::EncodeMutationBatch( @@ -257,15 +251,25 @@ firestore_client_WriteBatch LocalSerializer::EncodeMutationBatch( firestore_client_WriteBatch result{}; result.batch_id = mutation_batch.batch_id(); - pb_size_t count = CheckedSize(mutation_batch.mutations().size()); + + pb_size_t count = CheckedSize(mutation_batch.base_mutations().size()); + result.base_writes_count = count; + result.base_writes = MakeArray(count); + int i = 0; + for (const auto& mutation : mutation_batch.base_mutations()) { + result.base_writes[i] = rpc_serializer_.EncodeMutation(mutation); + i++; + } + + count = CheckedSize(mutation_batch.mutations().size()); result.writes_count = count; result.writes = MakeArray(count); - int i = 0; + i = 0; for (const auto& mutation : mutation_batch.mutations()) { - HARD_ASSERT(mutation.is_valid(), "Invalid mutation encountered."); result.writes[i] = rpc_serializer_.EncodeMutation(mutation); i++; } + result.local_write_time = rpc_serializer_.EncodeTimestamp(mutation_batch.local_write_time()); @@ -277,13 +281,21 @@ MutationBatch LocalSerializer::DecodeMutationBatch( int batch_id = proto.batch_id; Timestamp local_write_time = rpc_serializer_.DecodeTimestamp(reader, proto.local_write_time); + + std::vector base_mutations; + for (size_t i = 0; i < proto.base_writes_count; i++) { + base_mutations.push_back( + rpc_serializer_.DecodeMutation(reader, proto.base_writes[i])); + } + std::vector mutations; for (size_t i = 0; i < proto.writes_count; i++) { mutations.push_back( rpc_serializer_.DecodeMutation(reader, proto.writes[i])); } - return MutationBatch(batch_id, local_write_time, std::move(mutations)); + return MutationBatch(batch_id, local_write_time, std::move(base_mutations), + std::move(mutations)); } } // namespace local diff --git a/Firestore/core/src/firebase/firestore/local/local_serializer.h b/Firestore/core/src/firebase/firestore/local/local_serializer.h index 443dde9cf2b..e32d1e39d43 100644 --- a/Firestore/core/src/firebase/firestore/local/local_serializer.h +++ b/Firestore/core/src/firebase/firestore/local/local_serializer.h @@ -33,7 +33,7 @@ #include "Firestore/core/src/firebase/firestore/nanopb/reader.h" #include "Firestore/core/src/firebase/firestore/nanopb/writer.h" #include "Firestore/core/src/firebase/firestore/remote/serializer.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/local/local_store.h b/Firestore/core/src/firebase/firestore/local/local_store.h new file mode 100644 index 00000000000..49faadb0789 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/local_store.h @@ -0,0 +1,264 @@ +/* + * Copyright 2019 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_LOCAL_LOCAL_STORE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LOCAL_STORE_H_ + +#include +#include +#include + +#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/local/local_documents_view.h" +#include "Firestore/core/src/firebase/firestore/local/local_view_changes.h" +#include "Firestore/core/src/firebase/firestore/local/local_write_result.h" +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" +#include "Firestore/core/src/firebase/firestore/local/reference_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" +#include "Firestore/core/src/firebase/firestore/model/mutation.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch_result.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace local { + +/** + * Local storage in the Firestore client. Coordinates persistence components + * like the mutation queue and remote document cache to present a latency + * compensated view of stored data. + * + * The LocalStore is responsible for accepting mutations from the SyncEngine. + * Writes from the client are put into a queue as provisional Mutations until + * they are processed by the RemoteStore and confirmed as having been written to + * the server. + * + * The local store provides the local version of documents that have been + * modified locally. It maintains the constraint: + * + * LocalDocument = RemoteDocument + Active(LocalMutations) + * + * (Active mutations are those that are enqueued and have not been previously + * acknowledged or rejected). + * + * The RemoteDocument ("ground truth") state is provided via the + * ApplyChangeBatch method. It will be some version of a server-provided + * document OR will be a server-provided document PLUS acknowledged mutations: + * + * RemoteDocument' = RemoteDocument + Acknowledged(LocalMutations) + * + * Note that this "dirty" version of a RemoteDocument will not be identical to a + * server base version, since it has LocalMutations added to it pending getting + * an authoritative copy from the server. + * + * Since LocalMutations can be rejected by the server, we have to be able to + * revert a LocalMutation that has already been applied to the LocalDocument + * (typically done by replaying all remaining LocalMutations to the + * RemoteDocument to re-apply). + * + * It also maintains the persistence of mapping queries to resume tokens and + * target ids. + * + * The LocalStore must be able to efficiently execute queries against its local + * cache of the documents, to provide the initial set of results before any + * remote changes have been received. + */ +class LocalStore { + public: + LocalStore(Persistence* persistence, const auth::User& initial_user); + + /** Performs any initial startup actions required by the local store. */ + void Start(); + + /** + * Tells the LocalStore that the currently authenticated user has changed. + * + * In response the local store switches the mutation queue to the new user and + * returns any resulting document changes. + */ + model::MaybeDocumentMap HandleUserChange(const auth::User& user); + + /** Accepts locally generated Mutations and commits them to storage. */ + local::LocalWriteResult WriteLocally( + std::vector&& mutations); + + /** + * Returns the current value of a document with a given key, or `nullopt` if + * not found. + */ + absl::optional ReadDocument( + const model::DocumentKey& key); + + /** + * Acknowledges the given batch. + * + * On the happy path when a batch is acknowledged, the local store will: + * + * + remove the batch from the mutation queue; + * + apply the changes to the remote document cache; + * + recalculate the latency compensated view implied by those changes (there + * may be mutations in the queue that affect the documents but haven't been + * acknowledged yet); and + * + give the changed documents back the sync engine + * + * @return The resulting (modified) documents. + */ + model::MaybeDocumentMap AcknowledgeBatch( + const model::MutationBatchResult& batch_result); + + /** + * Removes mutations from the MutationQueue for the specified batch. + * LocalDocuments will be recalculated. + * + * @return The resulting (modified) documents. + */ + model::MaybeDocumentMap RejectBatch(model::BatchId batch_id); + + /** Returns the last recorded stream token for the current user. */ + nanopb::ByteString GetLastStreamToken(); + + /** + * Sets the stream token for the current user without acknowledging any + * mutation batch. This is usually only useful after a stream handshake or in + * response to an error that requires clearing the stream token. + */ + void SetLastStreamToken(const nanopb::ByteString& stream_token); + + /** + * Returns the last consistent snapshot processed (used by the RemoteStore to + * determine whether to buffer incoming snapshots from the backend). + */ + const model::SnapshotVersion& GetLastRemoteSnapshotVersion() const; + + /** + * Updates the "ground-state" (remote) documents. We assume that the remote + * event reflects any write batches that have been acknowledged or rejected + * (i.e. we do not re-apply local mutations to updates from this event). + * + * LocalDocuments are re-calculated if there are remaining mutations in the + * queue. + */ + model::MaybeDocumentMap ApplyRemoteEvent( + const remote::RemoteEvent& remote_event); + + /** + * Returns the keys of the documents that are associated with the given + * targetID in the remote table. + */ + model::DocumentKeySet GetRemoteDocumentKeys(model::TargetId target_id); + + /** + * Assigns a query an internal ID so that its results can be pinned so they + * don't get GC'd. A query must be allocated in the local store before the + * store can be used to manage its view. + */ + local::QueryData AllocateQuery(core::Query query); + + /** Unpin all the documents associated with a query. */ + void ReleaseQuery(const core::Query& query); + + /** + * Runs a query against all the documents in the local store and returns the + * results. + */ + model::DocumentMap ExecuteQuery(const core::Query& query); + + /** + * Notify the local store of the changed views to locally pin / unpin + * documents. + */ + void NotifyLocalViewChanges( + const std::vector& view_changes); + + /** + * Gets the mutation batch after the passed in batchId in the mutation queue + * or `nullopt` if empty. + * + * @param batch_id The batch to search after, or `kBatchIdUnknown` for the + * first mutation in the queue. + * @return the next mutation or `nullopt` if there wasn't one. + */ + absl::optional GetNextMutationBatch( + model::BatchId batch_id); + + /** + * Returns the largest (latest) batch id in mutation queue that is pending + * server response. Returns `kBatchIdUnknown` if the queue is empty. + */ + model::BatchId GetHighestUnacknowledgedBatchId(); + + local::LruResults CollectGarbage( + local::LruGarbageCollector* garbage_collector); + + private: + void StartMutationQueue(); + void ApplyBatchResult(const model::MutationBatchResult& batch_result); + + /** + * Returns true if the new_query_data should be persisted during an update of + * an active target. QueryData should always be persisted when a target is + * being released and should not call this function. + * + * While the target is active, QueryData updates can be omitted when nothing + * about the target has changed except metadata like the resume token or + * snapshot version. Occasionally it's worth the extra write to prevent these + * values from getting too stale after a crash, but this doesn't have to be + * too frequent. + */ + bool ShouldPersistQueryData(const QueryData& new_query_data, + const local::QueryData& old_query_data, + const remote::TargetChange& change) const; + + /** Manages our in-memory or durable persistence. Owned by FirestoreClient. */ + Persistence* persistence_ = nullptr; + + /** Used to generate targetIDs for queries tracked locally. */ + core::TargetIdGenerator target_id_generator_; + + /** + * The set of all mutations that have been sent but not yet been applied to + * the backend. + */ + MutationQueue* mutation_queue_ = nullptr; + + /** The set of all cached remote documents. */ + RemoteDocumentCache* remote_document_cache_ = nullptr; + + /** Maps a query to the data about that query. */ + QueryCache* query_cache_ = nullptr; + + /** + * The "local" view of all documents (layering mutation queue on top of + * remote_document_cache_). + */ + std::unique_ptr local_documents_; + + /** The set of document references maintained by any local views. */ + ReferenceSet local_view_references_; + + /** Maps target ids to data about their queries. */ + std::unordered_map target_ids; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LOCAL_STORE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/local_store.mm b/Firestore/core/src/firebase/firestore/local/local_store.mm new file mode 100644 index 00000000000..1d4ae32408b --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/local_store.mm @@ -0,0 +1,491 @@ +/* + * Copyright 2019 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/local/local_store.h" + +#include + +#include "Firestore/core/src/firebase/firestore/model/patch_mutation.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" + +namespace firebase { +namespace firestore { +namespace local { + +namespace { + +using auth::User; +using core::Query; +using core::TargetIdGenerator; +using model::BatchId; +using model::DocumentKey; +using model::DocumentKeySet; +using model::DocumentMap; +using model::DocumentVersionMap; +using model::ListenSequenceNumber; +using model::MaybeDocumentMap; +using model::MaybeDocument; +using model::Mutation; +using model::MutationBatch; +using model::MutationBatchResult; +using model::ObjectValue; +using model::OptionalMaybeDocumentMap; +using model::PatchMutation; +using model::Precondition; +using model::SnapshotVersion; +using model::TargetId; +using nanopb::ByteString; +using remote::TargetChange; + +/** + * The maximum time to leave a resume token buffered without writing it out. + * This value is arbitrary: it's long enough to avoid several writes (possibly + * indefinitely if updates come more frequently than this) but short enough that + * restarting after crashing will still have a pretty recent resume token. + */ +const int64_t kResumeTokenMaxAgeSeconds = 5 * 60; // 5 minutes + +} // namespace + +LocalStore::LocalStore(Persistence* persistence, const User& initial_user) + : persistence_(persistence), + mutation_queue_(persistence->GetMutationQueueForUser(initial_user)), + remote_document_cache_(persistence->remote_document_cache()), + query_cache_(persistence->query_cache()), + local_documents_( + absl::make_unique(remote_document_cache_, + mutation_queue_, + persistence->index_manager())) { + persistence->reference_delegate()->AddInMemoryPins(&local_view_references_); + target_id_generator_ = TargetIdGenerator::QueryCacheTargetIdGenerator(0); +} + +void LocalStore::Start() { + StartMutationQueue(); + TargetId target_id = query_cache_->highest_target_id(); + target_id_generator_ = + TargetIdGenerator::QueryCacheTargetIdGenerator(target_id); +} + +void LocalStore::StartMutationQueue() { + persistence_->Run("Start MutationQueue", [&] { mutation_queue_->Start(); }); +} + +MaybeDocumentMap LocalStore::HandleUserChange(const User& user) { + // Swap out the mutation queue, grabbing the pending mutation batches before + // and after. + std::vector old_batches = persistence_->Run( + "OldBatches", [&] { return mutation_queue_->AllMutationBatches(); }); + + // The old one has a reference to the mutation queue, so null it out first. + local_documents_.reset(); + mutation_queue_ = persistence_->GetMutationQueueForUser(user); + + StartMutationQueue(); + + return persistence_->Run("NewBatches", [&] { + std::vector new_batches = + mutation_queue_->AllMutationBatches(); + + // Recreate our LocalDocumentsView using the new MutationQueue. + local_documents_ = absl::make_unique( + remote_document_cache_, mutation_queue_, persistence_->index_manager()); + + // Union the old/new changed keys. + DocumentKeySet changed_keys; + for (const std::vector* batches : + {&old_batches, &new_batches}) { + for (const MutationBatch& batch : *batches) { + for (const Mutation& mutation : batch.mutations()) { + changed_keys = changed_keys.insert(mutation.key()); + } + } + } + + // Return the set of all (potentially) changed documents as the result of + // the user change. + return local_documents_->GetDocuments(changed_keys); + }); +} + +LocalWriteResult LocalStore::WriteLocally(std::vector&& mutations) { + Timestamp local_write_time = Timestamp::Now(); + DocumentKeySet keys; + for (const Mutation& mutation : mutations) { + keys = keys.insert(mutation.key()); + } + + return persistence_->Run("Locally write mutations", [&] { + // Load and apply all existing mutations. This lets us compute the current + // base state for all non-idempotent transforms before applying any + // additional user-provided writes. + MaybeDocumentMap existing_documents = local_documents_->GetDocuments(keys); + + // For non-idempotent mutations (such as `FieldValue.increment()`), we + // record the base state in a separate patch mutation. This is later used to + // guarantee consistent values and prevents flicker even if the backend + // sends us an update that already includes our transform. + std::vector base_mutations; + for (const Mutation& mutation : mutations) { + absl::optional base_document = + existing_documents.get(mutation.key()); + + absl::optional base_value = + mutation.ExtractBaseValue(base_document); + if (base_value) { + // NOTE: The base state should only be applied if there's some existing + // document to override, so use a Precondition of exists=true + base_mutations.push_back(PatchMutation(mutation.key(), *base_value, + base_value->ToFieldMask(), + Precondition::Exists(true))); + } + } + + MutationBatch batch = mutation_queue_->AddMutationBatch( + local_write_time, std::move(base_mutations), std::move(mutations)); + MaybeDocumentMap changed_documents = + batch.ApplyToLocalDocumentSet(existing_documents); + return LocalWriteResult{batch.batch_id(), std::move(changed_documents)}; + }); +} + +MaybeDocumentMap LocalStore::AcknowledgeBatch( + const MutationBatchResult& batch_result) { + return persistence_->Run("Acknowledge batch", [&] { + const MutationBatch& batch = batch_result.batch(); + mutation_queue_->AcknowledgeBatch(batch, batch_result.stream_token()); + ApplyBatchResult(batch_result); + mutation_queue_->PerformConsistencyCheck(); + + return local_documents_->GetDocuments(batch.keys()); + }); +} + +void LocalStore::ApplyBatchResult(const MutationBatchResult& batch_result) { + const MutationBatch& batch = batch_result.batch(); + DocumentKeySet doc_keys = batch.keys(); + const DocumentVersionMap& versions = batch_result.doc_versions(); + + for (const DocumentKey& doc_key : doc_keys) { + absl::optional remote_doc = + remote_document_cache_->Get(doc_key); + absl::optional doc = remote_doc; + + auto ack_version_iter = versions.find(doc_key); + HARD_ASSERT(ack_version_iter != versions.end(), + "doc_versions should contain every doc in the write."); + const SnapshotVersion& ack_version = ack_version_iter->second; + + if (!doc || doc->version() < ack_version) { + doc = batch.ApplyToRemoteDocument(doc, doc_key, batch_result); + if (!doc) { + HARD_ASSERT( + !remote_doc, + "Mutation batch %s applied to document %s resulted in nullopt.", + batch.ToString(), util::ToString(remote_doc)); + } else { + remote_document_cache_->Add(*doc); + } + } + } + + mutation_queue_->RemoveMutationBatch(batch); +} + +MaybeDocumentMap LocalStore::RejectBatch(BatchId batch_id) { + return persistence_->Run("Reject batch", [&] { + absl::optional to_reject = + mutation_queue_->LookupMutationBatch(batch_id); + HARD_ASSERT(to_reject.has_value(), "Attempt to reject nonexistent batch!"); + + mutation_queue_->RemoveMutationBatch(*to_reject); + mutation_queue_->PerformConsistencyCheck(); + + return local_documents_->GetDocuments(to_reject->keys()); + }); +} + +ByteString LocalStore::GetLastStreamToken() { + return mutation_queue_->GetLastStreamToken(); +} + +void LocalStore::SetLastStreamToken(const ByteString& stream_token) { + persistence_->Run("Set stream token", + [&] { mutation_queue_->SetLastStreamToken(stream_token); }); +} + +const SnapshotVersion& LocalStore::GetLastRemoteSnapshotVersion() const { + return query_cache_->GetLastRemoteSnapshotVersion(); +} + +model::MaybeDocumentMap LocalStore::ApplyRemoteEvent( + const remote::RemoteEvent& remote_event) { + const SnapshotVersion& last_remote_version = + query_cache_->GetLastRemoteSnapshotVersion(); + + return persistence_->Run("Apply remote event", [&] { + // TODO(gsoltis): move the sequence number into the reference delegate. + ListenSequenceNumber sequence_number = + persistence_->current_sequence_number(); + + for (const auto& entry : remote_event.target_changes()) { + TargetId target_id = entry.first; + const TargetChange& change = entry.second; + + auto found = target_ids.find(target_id); + if (found == target_ids.end()) { + // We don't update the remote keys if the query is not active. This + // ensures that we persist the updated query data along with the updated + // assignment. + continue; + } + + QueryData old_query_data = found->second; + + query_cache_->RemoveMatchingKeys(change.removed_documents(), target_id); + query_cache_->AddMatchingKeys(change.added_documents(), target_id); + + // 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 reviously + // _removed from this target. + const ByteString& resume_token = change.resume_token(); + // Update the resume token if the change includes one. + if (!resume_token.empty()) { + QueryData new_query_data = old_query_data.Copy( + remote_event.snapshot_version(), resume_token, sequence_number); + target_ids[target_id] = new_query_data; + + // Update the query data if there are target changes (or if sufficient + // time has passed since the last update). + if (ShouldPersistQueryData(new_query_data, old_query_data, change)) { + query_cache_->UpdateTarget(new_query_data); + } + } + } + + OptionalMaybeDocumentMap changed_docs; + const DocumentKeySet& limbo_documents = + remote_event.limbo_document_changes(); + DocumentKeySet updated_keys; + for (const auto& kv : remote_event.document_updates()) { + updated_keys = updated_keys.insert(kv.first); + } + // Each loop iteration only affects its "own" doc, so it's safe to get all + // the remote documents in advance in a single call. + OptionalMaybeDocumentMap existing_docs = + remote_document_cache_->GetAll(updated_keys); + + for (const auto& kv : remote_event.document_updates()) { + const DocumentKey& key = kv.first; + const MaybeDocument& doc = kv.second; + absl::optional existing_doc; + auto found_existing = existing_docs.get(key); + if (found_existing) { + existing_doc = *found_existing; + } + + // Note: The order of the steps below is important, since we want to + // ensure that rejected limbo resolutions (which fabricate NoDocuments + // with SnapshotVersion::None) never add documents to cache. + if (doc.type() == MaybeDocument::Type::NoDocument && + doc.version() == SnapshotVersion::None()) { + // NoDocuments with SnapshotVersion::None are used in manufactured + // events. We remove these documents from cache since we lost access. + remote_document_cache_->Remove(key); + changed_docs = changed_docs.insert(key, doc); + } else if (!existing_doc || doc.version() > existing_doc->version() || + (doc.version() == existing_doc->version() && + existing_doc->has_pending_writes())) { + // TODO(index-free): Comment in this assert when we enable Index-Free + // queries HARD_ASSERT(remoteEvent.snapshot_version() != + // SnapshotVersion::None(), + // "Cannot add a document when the remote version is zero"); + remote_document_cache_->Add(doc); + changed_docs = changed_docs.insert(key, doc); + } else { + LOG_DEBUG("LocalStore Ignoring outdated watch update for %s. " + "Current version: %s Watch version: %s", + key.ToString(), existing_doc->version().ToString(), + doc.version().ToString()); + } + + // If this was a limbo resolution, make sure we mark when it was accessed. + if (limbo_documents.contains(key)) { + persistence_->reference_delegate()->UpdateLimboDocument(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. + const SnapshotVersion& remote_version = remote_event.snapshot_version(); + if (remote_version != SnapshotVersion::None()) { + HARD_ASSERT(remote_version >= last_remote_version, + "Watch stream reverted to previous snapshot?? (%s < %s)", + remote_version.ToString(), last_remote_version.ToString()); + query_cache_->SetLastRemoteSnapshotVersion(remote_version); + } + + return local_documents_->GetLocalViewOfDocuments(changed_docs); + }); +} + +bool LocalStore::ShouldPersistQueryData(const QueryData& new_query_data, + const QueryData& old_query_data, + const TargetChange& change) const { + // Avoid clearing any existing value + HARD_ASSERT(!new_query_data.resume_token().empty(), + "Attempted to persist query data with empty resume token"); + + // Always persist query data if we don't already have a resume token. + if (old_query_data.resume_token().empty()) return true; + + // Don't allow resume token changes to be buffered indefinitely. This allows + // us to be reasonably up-to-date after a crash and avoids needing to loop + // over all active queries on shutdown. Especially in the browser we may not + // get time to do anything interesting while the current tab is closing. + int64_t new_seconds = new_query_data.snapshot_version().timestamp().seconds(); + int64_t old_seconds = old_query_data.snapshot_version().timestamp().seconds(); + int64_t time_delta = new_seconds - old_seconds; + if (time_delta >= kResumeTokenMaxAgeSeconds) return true; + + // Otherwise if the only thing that has changed about a target is its resume + // token then it's not worth persisting. Note that the RemoteStore keeps an + // in-memory view of the currently active targets which includes the current + // resume token, so stream failure or user changes will still use an + // up-to-date resume token regardless of what we do here. + size_t changes = change.added_documents().size() + + change.modified_documents().size() + + change.removed_documents().size(); + return changes > 0; +} + +void LocalStore::NotifyLocalViewChanges( + const std::vector& view_changes) { + persistence_->Run("NotifyLocalViewChanges", [&] { + for (const LocalViewChanges& view_change : view_changes) { + for (const DocumentKey& key : view_change.removed_keys()) { + persistence_->reference_delegate()->RemoveReference(key); + } + local_view_references_.AddReferences(view_change.added_keys(), + view_change.target_id()); + local_view_references_.RemoveReferences(view_change.removed_keys(), + view_change.target_id()); + } + }); +} + +absl::optional LocalStore::GetNextMutationBatch( + BatchId batch_id) { + return persistence_->Run("NextMutationBatchAfterBatchID", [&] { + return mutation_queue_->NextMutationBatchAfterBatchId(batch_id); + }); +} + +absl::optional LocalStore::ReadDocument(const DocumentKey& key) { + return persistence_->Run("ReadDocument", + [&] { return local_documents_->GetDocument(key); }); +} + +BatchId LocalStore::GetHighestUnacknowledgedBatchId() { + return persistence_->Run("getHighestUnacknowledgedBatchId", [&] { + return mutation_queue_->GetHighestUnacknowledgedBatchId(); + }); +} + +QueryData LocalStore::AllocateQuery(Query query) { + QueryData query_data = persistence_->Run("Allocate query", [&] { + absl::optional cached = query_cache_->GetTarget(query); + // TODO(mcg): freshen last accessed date if cached exists? + if (!cached) { + cached = QueryData(query, target_id_generator_.NextId(), + persistence_->current_sequence_number(), + QueryPurpose::Listen); + query_cache_->AddTarget(*cached); + } + return *cached; + }); + + // Sanity check to ensure that even when resuming a query it's not currently + // active. + TargetId target_id = query_data.target_id(); + HARD_ASSERT(target_ids.find(target_id) == target_ids.end(), + "Tried to allocate an already allocated query: %s", + query.ToString()); + target_ids[target_id] = query_data; + return query_data; +} + +void LocalStore::ReleaseQuery(const Query& query) { + persistence_->Run("Release query", [&] { + absl::optional query_data = query_cache_->GetTarget(query); + HARD_ASSERT(query_data, "Tried to release nonexistent query: %s", + query.ToString()); + + TargetId target_id = query_data->target_id(); + + auto found = target_ids.find(target_id); + if (found != target_ids.end()) { + const QueryData& cached_query_data = found->second; + + if (cached_query_data.snapshot_version() > + query_data->snapshot_version()) { + // If we've been avoiding persisting the resumeToken (see + // ShouldPersistQueryData for conditions and rationale) we need to + // persist the token now because there will no longer be an in-memory + // version to fall back on. + query_data = cached_query_data; + query_cache_->UpdateTarget(*query_data); + } + } + + // References for documents sent via Watch are automatically removed when we + // delete a query's target data from the reference delegate. Since this does + // not remove references for locally mutated documents, we have to remove + // the target associations for these documents manually. + DocumentKeySet removed = local_view_references_.RemoveReferences(target_id); + for (const DocumentKey& key : removed) { + persistence_->reference_delegate()->RemoveReference(key); + } + target_ids.erase(target_id); + persistence_->reference_delegate()->RemoveTarget(*query_data); + }); +} + +DocumentMap LocalStore::ExecuteQuery(const Query& query) { + return persistence_->Run("ExecuteQuery", [&] { + return local_documents_->GetDocumentsMatchingQuery(query); + }); +} + +DocumentKeySet LocalStore::GetRemoteDocumentKeys(TargetId target_id) { + return persistence_->Run("RemoteDocumentKeysForTarget", [&] { + return query_cache_->GetMatchingKeys(target_id); + }); +} + +LruResults LocalStore::CollectGarbage(LruGarbageCollector* garbage_collector) { + return persistence_->Run("Collect garbage", [&] { + return garbage_collector->Collect(target_ids); + }); +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/local_view_changes.cc b/Firestore/core/src/firebase/firestore/local/local_view_changes.cc index 542ef1074c9..1048b7c9680 100644 --- a/Firestore/core/src/firebase/firestore/local/local_view_changes.cc +++ b/Firestore/core/src/firebase/firestore/local/local_view_changes.cc @@ -34,11 +34,11 @@ LocalViewChanges LocalViewChanges::FromViewSnapshot( for (const DocumentViewChange& doc_change : snapshot.document_changes()) { switch (doc_change.type()) { - case DocumentViewChange::Type::kAdded: + case DocumentViewChange::Type::Added: added_keys = added_keys.insert(doc_change.document().key()); break; - case DocumentViewChange::Type::kRemoved: + case DocumentViewChange::Type::Removed: removed_keys = removed_keys.insert(doc_change.document().key()); break; diff --git a/Firestore/core/src/firebase/firestore/local/local_write_result.h b/Firestore/core/src/firebase/firestore/local/local_write_result.h index b9630156ec9..4e303a27971 100644 --- a/Firestore/core/src/firebase/firestore/local/local_write_result.h +++ b/Firestore/core/src/firebase/firestore/local/local_write_result.h @@ -33,6 +33,8 @@ class LocalWriteResult { : batch_id_(batch_id), changes_(std::move(changes)) { } + LocalWriteResult() = default; + /** The batch ID of the local write. */ model::BatchId batch_id() const { return batch_id_; diff --git a/Firestore/core/src/firebase/firestore/local/lru_garbage_collector.cc b/Firestore/core/src/firebase/firestore/local/lru_garbage_collector.cc new file mode 100644 index 00000000000..2d27a7aa1de --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/lru_garbage_collector.cc @@ -0,0 +1,209 @@ +/* + * Copyright 2019 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/local/lru_garbage_collector.h" + +#include // NOLINT(build/c++11) +#include +#include +#include + +#include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/src/firebase/firestore/api/settings.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/util/log.h" + +namespace firebase { +namespace firestore { +namespace local { +namespace { + +using firebase::firestore::api::Settings; +using firebase::firestore::model::DocumentKey; +using firebase::firestore::model::ListenSequenceNumber; +using firebase::firestore::model::TargetId; + +using Millis = std::chrono::milliseconds; + +static Millis::rep MillisecondsBetween(const Timestamp& start, + const Timestamp& end) { + return std::chrono::duration_cast(end.ToTimePoint() - + start.ToTimePoint()) + .count(); +} + +/** + * RollingSequenceNumberBuffer tracks the nth sequence number in a series. + * Sequence numbers may be added out of order. + */ +class RollingSequenceNumberBuffer { + public: + explicit RollingSequenceNumberBuffer(size_t max_elements) + : queue_(std::priority_queue()), + max_elements_(max_elements) { + } + + RollingSequenceNumberBuffer(const RollingSequenceNumberBuffer& other) = + delete; + + void AddElement(ListenSequenceNumber sequence_number) { + if (queue_.size() < max_elements_) { + queue_.push(sequence_number); + } else { + ListenSequenceNumber highest_value = queue_.top(); + if (sequence_number < highest_value) { + queue_.pop(); + queue_.push(sequence_number); + } + } + } + + ListenSequenceNumber max_value() const { + return queue_.top(); + } + + size_t size() const { + return queue_.size(); + } + + private: + std::priority_queue queue_; + const size_t max_elements_; +}; + +} // namespace + +const ListenSequenceNumber kListenSequenceNumberInvalid = -1; + +LruParams LruParams::Default() { + return LruParams{100 * 1024 * 1024, 10, 1000}; +} + +LruParams LruParams::Disabled() { + return LruParams{api::Settings::CacheSizeUnlimited, 0, 0}; +} + +LruParams LruParams::WithCacheSize(int64_t cache_size) { + LruParams params = Default(); + params.min_bytes_threshold = cache_size; + return params; +} + +LruGarbageCollector::LruGarbageCollector(LruDelegate* delegate, + LruParams params) + : delegate_(delegate), params_(std::move(params)) { +} + +LruResults LruGarbageCollector::Collect(const LiveQueryMap& live_targets) { + if (params_.min_bytes_threshold == Settings::CacheSizeUnlimited) { + LOG_DEBUG("Garbage collection skipped; disabled"); + return LruResults::DidNotRun(); + } + + int64_t current_size = CalculateByteSize(); + if (current_size < params_.min_bytes_threshold) { + // Not enough on disk to warrant collection. Wait another timeout cycle. + LOG_DEBUG( + "Garbage collection skipped; Cache size %s is lower than threshold %s", + current_size, params_.min_bytes_threshold); + return LruResults::DidNotRun(); + } + + LOG_DEBUG("Running garbage collection on cache of size: %s", current_size); + return RunGarbageCollection(live_targets); +} + +LruResults LruGarbageCollector::RunGarbageCollection( + const LiveQueryMap& live_targets) { + Timestamp start = Timestamp::Now(); + + // Cap at the configured max + int sequence_numbers = QueryCountForPercentile(params_.percentile_to_collect); + if (sequence_numbers > params_.maximum_sequence_numbers_to_collect) { + sequence_numbers = params_.maximum_sequence_numbers_to_collect; + } + Timestamp counted_targets = Timestamp::Now(); + + ListenSequenceNumber upper_bound = + SequenceNumberForQueryCount(sequence_numbers); + Timestamp found_upper_bound = Timestamp::Now(); + + int num_targets_removed = RemoveTargets(upper_bound, live_targets); + Timestamp removed_targets = Timestamp::Now(); + + int num_documents_removed = RemoveOrphanedDocuments(upper_bound); + Timestamp removed_documents = Timestamp::Now(); + + std::string desc = "LRU Garbage Collection:\n"; + absl::StrAppend(&desc, "\tCounted targets in ", + MillisecondsBetween(start, counted_targets), "ms\n"); + absl::StrAppend(&desc, "\tDetermined least recently used ", sequence_numbers, + " sequence numbers in ", + MillisecondsBetween(counted_targets, found_upper_bound), + "ms\n"); + absl::StrAppend(&desc, "\tRemoved ", num_targets_removed, " targets in ", + MillisecondsBetween(found_upper_bound, removed_targets), + "ms\n"); + absl::StrAppend(&desc, "\tRemoved ", num_documents_removed, " documents in ", + MillisecondsBetween(removed_targets, removed_documents), + "ms\n"); + absl::StrAppend(&desc, "Total duration: ", + MillisecondsBetween(start, removed_documents), "ms"); + LOG_DEBUG(desc.c_str()); + + return LruResults{/* did_run= */ true, sequence_numbers, num_targets_removed, + num_documents_removed}; +} + +int LruGarbageCollector::QueryCountForPercentile(int percentile) { + size_t total_count = delegate_->GetSequenceNumberCount(); + return static_cast((percentile / 100.0f) * total_count); +} + +ListenSequenceNumber LruGarbageCollector::SequenceNumberForQueryCount( + int query_count) { + if (query_count == 0) { + return kListenSequenceNumberInvalid; + } + + RollingSequenceNumberBuffer buffer(query_count); + + delegate_->EnumerateTargets([&buffer](const QueryData& query_data) { + buffer.AddElement(query_data.sequence_number()); + }); + + delegate_->EnumerateOrphanedDocuments( + [&buffer](const DocumentKey& doc_key, + ListenSequenceNumber sequence_number) { + buffer.AddElement(sequence_number); + }); + + return buffer.max_value(); +} + +int LruGarbageCollector::RemoveTargets(ListenSequenceNumber sequence_number, + const LiveQueryMap& live_queries) { + return delegate_->RemoveTargets(sequence_number, live_queries); +} + +int LruGarbageCollector::RemoveOrphanedDocuments( + ListenSequenceNumber sequence_number) { + return delegate_->RemoveOrphanedDocuments(sequence_number); +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h b/Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h new file mode 100644 index 00000000000..1c38366612d --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h @@ -0,0 +1,162 @@ +/* + * Copyright 2019 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_LOCAL_LRU_GARBAGE_COLLECTOR_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LRU_GARBAGE_COLLECTOR_H_ + +#include + +#include "Firestore/core/src/firebase/firestore/local/query_cache.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" + +namespace firebase { +namespace firestore { +namespace local { + +class LruGarbageCollector; + +ABSL_CONST_INIT extern const model::ListenSequenceNumber + kListenSequenceNumberInvalid; + +struct LruParams { + static LruParams Default(); + + static LruParams Disabled(); + + static LruParams WithCacheSize(int64_t cache_size); + + int64_t min_bytes_threshold; + int percentile_to_collect; + int maximum_sequence_numbers_to_collect; +}; + +struct LruResults { + static LruResults DidNotRun() { + return LruResults{/* did_run= */ false, 0, 0, 0}; + } + + bool did_run; + int sequence_numbers_collected; + int targets_removed; + int documents_removed; +}; + +using LiveQueryMap = std::unordered_map; + +/** + * Persistence layers intending to use LRU Garbage collection should implement + * this interface. This interface defines the operations that the LRU garbage + * collector needs from the persistence layer. + */ +class LruDelegate : public ReferenceDelegate { + public: + virtual ~LruDelegate() = default; + + /** Access to the underlying LRU Garbage collector instance. */ + virtual LruGarbageCollector* garbage_collector() = 0; + + virtual int64_t CalculateByteSize() = 0; + + /** Returns the number of targets and orphaned documents cached. */ + virtual size_t GetSequenceNumberCount() = 0; + + /** + * Enumerates all the targets that the delegate is aware of. This is typically + * all of the targets in an QueryCache. + */ + virtual void EnumerateTargets(const TargetCallback& callback) = 0; + + /** + * Enumerates all of the outstanding mutations. + */ + virtual void EnumerateOrphanedDocuments( + const OrphanedDocumentCallback& callback) = 0; + + /** + * Removes all unreferenced documents from the cache that have a sequence + * number less than or equal to the given sequence number. Returns the number + * of documents removed. + */ + virtual int RemoveOrphanedDocuments( + model::ListenSequenceNumber sequence_number) = 0; + + /** + * Removes all targets that are not currently being listened to and have a + * sequence number less than or equal to the given sequence number. Returns + * the number of targets removed. + */ + virtual int RemoveTargets(model::ListenSequenceNumber sequence_number, + const LiveQueryMap& live_queries) = 0; +}; + +/** + * LruGarbageCollector defines the LRU algorithm used to clean up old documents + * and targets. It is persistence-agnostic, as long as proper delegate is + * provided. + */ +class LruGarbageCollector { + public: + LruGarbageCollector(LruDelegate* delegate, LruParams params); + + int64_t CalculateByteSize() const { + return delegate_->CalculateByteSize(); + } + + /** + * Given a target percentile, return the number of queries that make up that + * percentage of the queries that are cached. For instance, if 20 queries are + * cached, and the percentile is 40, the result will be 8. + */ + int QueryCountForPercentile(int percentile); + + /** + * Given a number of queries n, return the nth sequence number in the cache. + */ + model::ListenSequenceNumber SequenceNumberForQueryCount(int query_count); + + /** + * Removes queries that are not currently live (as indicated by presence in + * the live_queries map) and have a sequence number less than or equal to the + * given sequence number. + */ + int RemoveTargets(model::ListenSequenceNumber sequence_number, + const LiveQueryMap& live_queries); + + /** + * Removes all unreferenced documents from the cache that have a sequence + * number less than or equal to the given sequence number. Returns the number + * of documents removed. + */ + int RemoveOrphanedDocuments(model::ListenSequenceNumber sequence_number); + + local::LruResults Collect(const LiveQueryMap& live_targets); + + private: + LruResults RunGarbageCollection(const LiveQueryMap& live_targets); + + // Delegate owns the LruGarbageCollector; this is a back pointer. + LruDelegate* delegate_; + + LruParams params_ = LruParams::Default(); +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_LRU_GARBAGE_COLLECTOR_H_ diff --git a/Firestore/core/src/firebase/firestore/local/memory_eager_reference_delegate.cc b/Firestore/core/src/firebase/firestore/local/memory_eager_reference_delegate.cc new file mode 100644 index 00000000000..ebceed3b324 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/memory_eager_reference_delegate.cc @@ -0,0 +1,118 @@ +/* + * Copyright 2019 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/local/memory_eager_reference_delegate.h" + +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" +#include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/reference_set.h" +#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" + +namespace firebase { +namespace firestore { +namespace local { + +using model::DocumentKey; +using model::DocumentKeyHash; +using model::ListenSequenceNumber; + +MemoryEagerReferenceDelegate::MemoryEagerReferenceDelegate( + MemoryPersistence* persistence) + : persistence_(persistence) { +} + +ListenSequenceNumber MemoryEagerReferenceDelegate::current_sequence_number() + const { + return kListenSequenceNumberInvalid; +} + +void MemoryEagerReferenceDelegate::AddInMemoryPins(ReferenceSet* set) { + // We should be able to assert that additional_references_ is nullptr, but due + // to restarts in spec tests it would fail. + additional_references_ = set; +} + +void MemoryEagerReferenceDelegate::RemoveTarget(const QueryData& query_data) { + for (const DocumentKey& doc_key : + persistence_->query_cache()->GetMatchingKeys(query_data.target_id())) { + orphaned_->insert(doc_key); + } + persistence_->query_cache()->RemoveTarget(query_data); +} + +void MemoryEagerReferenceDelegate::AddReference(const DocumentKey& key) { + orphaned_->erase(key); +} + +void MemoryEagerReferenceDelegate::RemoveReference(const DocumentKey& key) { + orphaned_->insert(key); +} + +void MemoryEagerReferenceDelegate::RemoveMutationReference( + const DocumentKey& key) { + orphaned_->insert(key); +} + +bool MemoryEagerReferenceDelegate::IsReferenced(const DocumentKey& key) const { + if (persistence_->query_cache()->Contains(key)) { + return true; + } + if (MutationQueuesContainKey(key)) { + return true; + } + if (additional_references_ && additional_references_->ContainsKey(key)) { + return true; + } + return false; +} + +void MemoryEagerReferenceDelegate::UpdateLimboDocument(const DocumentKey& key) { + if (IsReferenced(key)) { + orphaned_->erase(key); + } else { + orphaned_->insert(key); + } +} + +void MemoryEagerReferenceDelegate::OnTransactionStarted(absl::string_view) { + // Constructs the unordered map, in place, with no arguments. + orphaned_.emplace(); +} + +void MemoryEagerReferenceDelegate::OnTransactionCommitted() { + for (const auto& key : *orphaned_) { + if (!IsReferenced(key)) { + persistence_->remote_document_cache()->Remove(key); + } + } + orphaned_.reset(); +} + +bool MemoryEagerReferenceDelegate::MutationQueuesContainKey( + const DocumentKey& key) const { + const auto& queues = persistence_->mutation_queues(); + for (const auto& entry : queues) { + if (entry.second->ContainsKey(key)) { + return true; + } + } + return false; +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/memory_eager_reference_delegate.h b/Firestore/core/src/firebase/firestore/local/memory_eager_reference_delegate.h new file mode 100644 index 00000000000..96824f96b34 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/memory_eager_reference_delegate.h @@ -0,0 +1,73 @@ +/* + * Copyright 2019 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_LOCAL_MEMORY_EAGER_REFERENCE_DELEGATE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_EAGER_REFERENCE_DELEGATE_H_ + +#include +#include + +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace local { + +class MemoryPersistence; + +/** + * Provides the eager GC implementation for memory persistence. + */ +class MemoryEagerReferenceDelegate : public ReferenceDelegate { + public: + explicit MemoryEagerReferenceDelegate(MemoryPersistence* persistence); + + model::ListenSequenceNumber current_sequence_number() const override; + + void AddInMemoryPins(ReferenceSet* set) override; + + void AddReference(const model::DocumentKey& key) override; + void RemoveReference(const model::DocumentKey& key) override; + void RemoveMutationReference(const model::DocumentKey& key) override; + void RemoveTarget(const QueryData& query_data) override; + + void UpdateLimboDocument(const model::DocumentKey& key) override; + + void OnTransactionStarted(absl::string_view label) override; + void OnTransactionCommitted() override; + + private: + bool IsReferenced(const model::DocumentKey& key) const; + + bool MutationQueuesContainKey(const model::DocumentKey& key) const; + + absl::optional> + orphaned_; + + // This instance is owned by MemoryPersistence. + MemoryPersistence* persistence_; + + // The ReferenceSet is owned by LocalStore. + ReferenceSet* additional_references_; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_EAGER_REFERENCE_DELEGATE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/memory_lru_reference_delegate.cc b/Firestore/core/src/firebase/firestore/local/memory_lru_reference_delegate.cc new file mode 100644 index 00000000000..02926776901 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/memory_lru_reference_delegate.cc @@ -0,0 +1,191 @@ +/* + * Copyright 2019 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/local/memory_lru_reference_delegate.h" + +#include + +#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" +#include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/local/reference_set.h" +#include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/local/sizer.h" + +namespace firebase { +namespace firestore { +namespace local { + +using model::DocumentKey; +using model::ListenSequenceNumber; + +MemoryLruReferenceDelegate::MemoryLruReferenceDelegate( + MemoryPersistence* persistence, + LruParams lru_params, + std::unique_ptr sizer) + : persistence_(persistence), + sizer_(std::move(sizer)), + gc_(this, lru_params) { + // Theoretically this is always 0, since this is all in-memory... + ListenSequenceNumber highest_sequence_number = + persistence_->query_cache()->highest_listen_sequence_number(); + listen_sequence_ = absl::make_unique(highest_sequence_number); +} + +LruGarbageCollector* MemoryLruReferenceDelegate::garbage_collector() { + return &gc_; +} + +ListenSequenceNumber MemoryLruReferenceDelegate::current_sequence_number() + const { + HARD_ASSERT(current_sequence_number_ != kListenSequenceNumberInvalid, + "Asking for a sequence number outside of a transaction"); + return current_sequence_number_; +} + +void MemoryLruReferenceDelegate::AddInMemoryPins(ReferenceSet* set) { + // We should be able to assert that additional_references_ is nullptr, but due + // to restarts in spec tests it would fail. + additional_references_ = set; +} + +void MemoryLruReferenceDelegate::RemoveTarget(const QueryData& query_data) { + QueryData updated = + query_data.Copy(query_data.snapshot_version(), query_data.resume_token(), + current_sequence_number_); + persistence_->query_cache()->UpdateTarget(updated); +} + +void MemoryLruReferenceDelegate::UpdateLimboDocument( + const model::DocumentKey& key) { + sequence_numbers_[key] = current_sequence_number_; +} + +void MemoryLruReferenceDelegate::OnTransactionStarted(absl::string_view label) { + current_sequence_number_ = listen_sequence_->Next(); +} + +void MemoryLruReferenceDelegate::OnTransactionCommitted() { + current_sequence_number_ = kListenSequenceNumberInvalid; +} + +void MemoryLruReferenceDelegate::EnumerateTargets( + const TargetCallback& callback) { + return persistence_->query_cache()->EnumerateTargets(callback); +} + +void MemoryLruReferenceDelegate::EnumerateOrphanedDocuments( + const OrphanedDocumentCallback& callback) { + for (const auto& entry : sequence_numbers_) { + const DocumentKey& key = entry.first; + ListenSequenceNumber sequence_number = entry.second; + // Pass in the exact sequence number as the upper bound so we know it won't + // be pinned by being too recent. + if (!IsPinnedAtSequenceNumber(sequence_number, key)) { + callback(key, sequence_number); + } + } +} + +size_t MemoryLruReferenceDelegate::GetSequenceNumberCount() { + size_t total_count = persistence_->query_cache()->size(); + EnumerateOrphanedDocuments( + [&total_count](const DocumentKey& key, + ListenSequenceNumber sequence_number) { total_count++; }); + return total_count; +} + +int MemoryLruReferenceDelegate::RemoveTargets( + model::ListenSequenceNumber sequence_number, + const LiveQueryMap& live_queries) { + return persistence_->query_cache()->RemoveTargets(sequence_number, + live_queries); +} + +int MemoryLruReferenceDelegate::RemoveOrphanedDocuments( + model::ListenSequenceNumber upper_bound) { + std::vector removed = + persistence_->remote_document_cache()->RemoveOrphanedDocuments( + this, upper_bound); + for (const auto& key : removed) { + sequence_numbers_.erase(key); + } + return static_cast(removed.size()); +} + +void MemoryLruReferenceDelegate::AddReference(const DocumentKey& key) { + sequence_numbers_[key] = current_sequence_number_; +} + +void MemoryLruReferenceDelegate::RemoveReference(const DocumentKey& key) { + sequence_numbers_[key] = current_sequence_number_; +} + +bool MemoryLruReferenceDelegate::MutationQueuesContainKey( + const DocumentKey& key) const { + const auto& queues = persistence_->mutation_queues(); + for (const auto& entry : queues) { + if (entry.second->ContainsKey(key)) { + return true; + } + } + return false; +} + +void MemoryLruReferenceDelegate::RemoveMutationReference( + const DocumentKey& key) { + sequence_numbers_[key] = current_sequence_number_; +} + +bool MemoryLruReferenceDelegate::IsPinnedAtSequenceNumber( + ListenSequenceNumber upper_bound, const DocumentKey& key) const { + if (MutationQueuesContainKey(key)) { + return true; + } + if (additional_references_ && additional_references_->ContainsKey(key)) { + return true; + } + if (persistence_->query_cache()->Contains(key)) { + return true; + } + + auto it = sequence_numbers_.find(key); + if (it != sequence_numbers_.end() && it->second > upper_bound) { + return true; + } + return false; +} + +int64_t MemoryLruReferenceDelegate::CalculateByteSize() { + // Note that this method is only used for testing because this delegate is + // only used for testing. The algorithm here (loop through everything, + // serialize it and count bytes) is inefficient and inexact, but won't run in + // production. + int64_t count = 0; + count += persistence_->query_cache()->CalculateByteSize(*sizer_); + count += persistence_->remote_document_cache()->CalculateByteSize(*sizer_); + const auto& queues = persistence_->mutation_queues(); + for (const auto& entry : queues) { + count += entry.second->CalculateByteSize(*sizer_); + } + return count; +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/memory_lru_reference_delegate.h b/Firestore/core/src/firebase/firestore/local/memory_lru_reference_delegate.h new file mode 100644 index 00000000000..4400b21d89c --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/memory_lru_reference_delegate.h @@ -0,0 +1,112 @@ +/* + * Copyright 2019 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_LOCAL_MEMORY_LRU_REFERENCE_DELEGATE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_LRU_REFERENCE_DELEGATE_H_ + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" + +namespace firebase { +namespace firestore { +namespace local { + +class ListenSequence; +class MemoryPersistence; +class Sizer; + +/** + * Provides the LRU GC implementation for memory persistence. + */ +class MemoryLruReferenceDelegate : public LruDelegate { + public: + MemoryLruReferenceDelegate(MemoryPersistence* persistence, + LruParams lru_params, + std::unique_ptr sizer); + + bool IsPinnedAtSequenceNumber(model::ListenSequenceNumber upper_bound, + const model::DocumentKey& key) const; + + // MARK: ReferenceDelegate overrides + + model::ListenSequenceNumber current_sequence_number() const override; + + void AddInMemoryPins(ReferenceSet* set) override; + + void AddReference(const model::DocumentKey& key) override; + void RemoveReference(const model::DocumentKey& key) override; + void RemoveMutationReference(const model::DocumentKey& key) override; + void RemoveTarget(const QueryData& query_data) override; + + void UpdateLimboDocument(const model::DocumentKey& key) override; + + void OnTransactionStarted(absl::string_view label) override; + void OnTransactionCommitted() override; + + // MARK: LruDelegate overrides + + LruGarbageCollector* garbage_collector() override; + + int64_t CalculateByteSize() override; + size_t GetSequenceNumberCount() override; + + void EnumerateTargets(const TargetCallback& callback) override; + void EnumerateOrphanedDocuments( + const OrphanedDocumentCallback& callback) override; + + int RemoveOrphanedDocuments(model::ListenSequenceNumber upper_bound) override; + int RemoveTargets(model::ListenSequenceNumber sequence_number, + const LiveQueryMap& live_queries) override; + + private: + bool MutationQueuesContainKey(const model::DocumentKey& key) const; + + // This instance is owned by MemoryPersistence. + MemoryPersistence* persistence_ = nullptr; + + std::unique_ptr sizer_; + + LruGarbageCollector gc_; + + // Tracks sequence numbers of when documents are used. Equivalent to sentinel + // rows in the leveldb implementation. + std::unordered_map + sequence_numbers_; + + // This ReferenceSet is owned by LocalStore. + ReferenceSet* additional_references_ = nullptr; + + // This needs to be a pointer because initialization is delayed until after + // we read from the query cache. + std::unique_ptr listen_sequence_; + + // The current sequence number for the currently active transaction. If no + // transaction is active, resets back to kListenSequenceNumberInvalid. + model::ListenSequenceNumber current_sequence_number_ = + kListenSequenceNumberInvalid; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_LRU_REFERENCE_DELEGATE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h index c8e0cc0c057..56d2353ee9f 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h @@ -17,12 +17,6 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_MUTATION_QUEUE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_MUTATION_QUEUE_H_ -#if !defined(__OBJC__) -#error "For now, this file must only be included by ObjC source files." -#endif // !defined(__OBJC__) - -#import - #include #include @@ -34,71 +28,70 @@ #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/types.h" -@class FSTLocalSerializer; -@class FSTMemoryPersistence; -@class FSTMutationBatch; - -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace local { +class MemoryPersistence; +class Sizer; + class MemoryMutationQueue : public MutationQueue { public: - explicit MemoryMutationQueue(FSTMemoryPersistence* persistence); + explicit MemoryMutationQueue(MemoryPersistence* persistence); void Start() override; bool IsEmpty() override; - void AcknowledgeBatch(FSTMutationBatch* batch, - NSData* _Nullable stream_token) override; + void AcknowledgeBatch(const model::MutationBatch& batch, + const nanopb::ByteString& stream_token) override; - FSTMutationBatch* AddMutationBatch( + model::MutationBatch AddMutationBatch( const Timestamp& local_write_time, std::vector&& base_mutations, std::vector&& mutations) override; - void RemoveMutationBatch(FSTMutationBatch* batch) override; + void RemoveMutationBatch(const model::MutationBatch& batch) override; - std::vector AllMutationBatches() override { + std::vector AllMutationBatches() override { return queue_; } - std::vector AllMutationBatchesAffectingDocumentKeys( + std::vector AllMutationBatchesAffectingDocumentKeys( const model::DocumentKeySet& document_keys) override; - std::vector AllMutationBatchesAffectingDocumentKey( + std::vector AllMutationBatchesAffectingDocumentKey( const model::DocumentKey& key) override; - std::vector AllMutationBatchesAffectingQuery( + std::vector AllMutationBatchesAffectingQuery( const core::Query& query) override; - FSTMutationBatch* _Nullable LookupMutationBatch( + absl::optional LookupMutationBatch( model::BatchId batch_id) override; - FSTMutationBatch* _Nullable NextMutationBatchAfterBatchId( + absl::optional NextMutationBatchAfterBatchId( model::BatchId batch_id) override; + model::BatchId GetHighestUnacknowledgedBatchId() override; + void PerformConsistencyCheck() override; bool ContainsKey(const model::DocumentKey& key); - size_t CalculateByteSize(FSTLocalSerializer* serializer); + int64_t CalculateByteSize(const Sizer& sizer); - NSData* _Nullable GetLastStreamToken() override; - void SetLastStreamToken(NSData* _Nullable token) override; + nanopb::ByteString GetLastStreamToken() override; + void SetLastStreamToken(const nanopb::ByteString& token) override; private: using DocumentKeyReferenceSet = immutable::SortedSet; - std::vector AllMutationBatchesWithIds( + std::vector AllMutationBatchesWithIds( const std::set& batch_ids); /** - * Finds the index of the given batchID in the mutation queue. This operation + * Finds the index of the given batch_id in the mutation queue. This operation * is O(1). * * @return The computed index of the batch with the given BatchID, based on @@ -108,8 +101,9 @@ class MemoryMutationQueue : public MutationQueue { */ int IndexOfBatchId(model::BatchId batch_id); - // This instance is owned by FSTMemoryPersistence; avoid a retain cycle. - __weak FSTMemoryPersistence* persistence_; + // This instance is owned by MemoryPersistence. + MemoryPersistence* persistence_; + /** * A FIFO queue of all mutations to apply to the backend. Mutations are added * to the end of the queue as they're written, and removed from the front of @@ -128,7 +122,7 @@ class MemoryMutationQueue : public MutationQueue { * Once the held write acknowledgements become visible they are removed from * the head of the queue along with any tombstones that follow. */ - std::vector queue_; + std::vector queue_; /** * The next value to use when assigning sequential IDs to each mutation @@ -141,7 +135,7 @@ class MemoryMutationQueue : public MutationQueue { * responses the client has processed. Stream tokens are opaque checkpoint * markers whose only real value is their inclusion in the next request. */ - NSData* _Nullable last_stream_token_; + nanopb::ByteString last_stream_token_; /** An ordered mapping between documents and the mutation batch IDs. */ DocumentKeyReferenceSet batches_by_document_key_; @@ -151,6 +145,4 @@ class MemoryMutationQueue : public MutationQueue { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_MUTATION_QUEUE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm index 0261de3e84e..5bef3162761 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm +++ b/Firestore/core/src/firebase/firestore/local/memory_mutation_queue.mm @@ -18,17 +18,15 @@ #include -#import "Firestore/Protos/objc/firestore/local/Mutation.pbobjc.h" -#import "Firestore/Source/Local/FSTLocalSerializer.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - #include "Firestore/core/src/firebase/firestore/local/document_key_reference.h" +#include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/sizer.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace local { @@ -37,10 +35,13 @@ using model::BatchId; using model::DocumentKey; using model::DocumentKeySet; +using model::kBatchIdUnknown; using model::Mutation; +using model::MutationBatch; using model::ResourcePath; +using nanopb::ByteString; -MemoryMutationQueue::MemoryMutationQueue(FSTMemoryPersistence* persistence) +MemoryMutationQueue::MemoryMutationQueue(MemoryPersistence* persistence) : persistence_(persistence) { } @@ -50,16 +51,16 @@ return queue_.empty(); } -void MemoryMutationQueue::AcknowledgeBatch(FSTMutationBatch* batch, - NSData* _Nullable stream_token) { +void MemoryMutationQueue::AcknowledgeBatch(const MutationBatch& batch, + const ByteString& stream_token) { HARD_ASSERT(!queue_.empty(), "Cannot acknowledge batch on an empty queue"); // Guaranteed to exist, due to above assert - FSTMutationBatch* check = queue_.front(); + const MutationBatch& check = queue_.front(); // Verify that the batch in the queue is the one to be acknowledged. - HARD_ASSERT(batch.batchID == check.batchID, + HARD_ASSERT(batch.batch_id() == check.batch_id(), "Queue ordering failure: expected batch %s, got batch %s", - batch.batchID, check.batchID); + batch.batch_id(), check.batch_id()); last_stream_token_ = stream_token; } @@ -67,13 +68,13 @@ // Note: The queue may be shutdown / started multiple times, since we maintain // the queue for the duration of the app session in case a user logs out / // back in. To behave like the LevelDB-backed MutationQueue (and accommodate - // tests that expect as much), we reset nextBatchID if the queue is empty. + // tests that expect as much), we reset next_batch_id_ if the queue is empty. if (IsEmpty()) { next_batch_id_ = 1; } } -FSTMutationBatch* MemoryMutationQueue::AddMutationBatch( +MutationBatch MemoryMutationQueue::AddMutationBatch( const Timestamp& local_write_time, std::vector&& base_mutations, std::vector&& mutations) { @@ -83,50 +84,47 @@ next_batch_id_++; if (!queue_.empty()) { - FSTMutationBatch* prior = queue_.back(); - HARD_ASSERT(prior.batchID < batch_id, - "Mutation batchIDs must be in monotonically increasing order"); + const MutationBatch& prior = queue_.back(); + HARD_ASSERT(prior.batch_id() < batch_id, + "Mutation batch_ids must be in monotonically increasing order"); } - FSTMutationBatch* batch = - [[FSTMutationBatch alloc] initWithBatchID:batch_id - localWriteTime:local_write_time - baseMutations:std::move(base_mutations) - mutations:std::move(mutations)]; + MutationBatch batch(batch_id, local_write_time, std::move(base_mutations), + std::move(mutations)); queue_.push_back(batch); // Track references by document key and index collection parents. - for (const Mutation& mutation : [batch mutations]) { + for (const Mutation& mutation : batch.mutations()) { batches_by_document_key_ = batches_by_document_key_.insert( DocumentKeyReference{mutation.key(), batch_id}); - persistence_.indexManager->AddToCollectionParentIndex( + persistence_->index_manager()->AddToCollectionParentIndex( mutation.key().path().PopLast()); } return batch; } -void MemoryMutationQueue::RemoveMutationBatch(FSTMutationBatch* batch) { +void MemoryMutationQueue::RemoveMutationBatch(const MutationBatch& batch) { // Can only remove the first batch HARD_ASSERT(!queue_.empty(), "Trying to remove batch from empty queue"); - FSTMutationBatch* head = queue_.front(); - HARD_ASSERT(head.batchID == batch.batchID, + const MutationBatch& head = queue_.front(); + HARD_ASSERT(head.batch_id() == batch.batch_id(), "Can only remove the first entry of the mutation queue"); queue_.erase(queue_.begin()); // Remove entries from the index too. - for (const Mutation& mutation : [batch mutations]) { + for (const Mutation& mutation : batch.mutations()) { const DocumentKey& key = mutation.key(); - [persistence_.referenceDelegate removeMutationReference:key]; + persistence_->reference_delegate()->RemoveMutationReference(key); - DocumentKeyReference reference{key, batch.batchID}; + DocumentKeyReference reference{key, batch.batch_id()}; batches_by_document_key_ = batches_by_document_key_.erase(reference); } } -std::vector +std::vector MemoryMutationQueue::AllMutationBatchesAffectingDocumentKeys( const DocumentKeySet& document_keys) { // First find the set of affected batch IDs. @@ -144,23 +142,24 @@ return AllMutationBatchesWithIds(batch_ids); } -std::vector +std::vector MemoryMutationQueue::AllMutationBatchesAffectingDocumentKey( const DocumentKey& key) { - std::vector result; + std::vector result; DocumentKeyReference start{key, 0}; for (const auto& reference : batches_by_document_key_.values_from(start)) { if (key != reference.key()) break; - FSTMutationBatch* batch = LookupMutationBatch(reference.ref_id()); - HARD_ASSERT(batch, "Batches in the index must exist in the main table"); - result.push_back(batch); + auto batch = LookupMutationBatch(reference.ref_id()); + HARD_ASSERT(batch.has_value(), + "Batches in the index must exist in the main table"); + result.push_back(*batch); } return result; } -std::vector +std::vector MemoryMutationQueue::AllMutationBatchesAffectingQuery(const Query& query) { HARD_ASSERT( !query.IsCollectionGroupQuery(), @@ -180,7 +179,7 @@ } DocumentKeyReference start{DocumentKey{start_path}, 0}; - // Find unique batchIDs referenced by all documents potentially matching the + // Find unique batch_ids referenced by all documents potentially matching the // query. std::set unique_batch_ids; for (const auto& reference : batches_by_document_key_.values_from(start)) { @@ -204,30 +203,38 @@ return AllMutationBatchesWithIds(unique_batch_ids); } -FSTMutationBatch* _Nullable MemoryMutationQueue::NextMutationBatchAfterBatchId( - BatchId batch_id) { +absl::optional +MemoryMutationQueue::NextMutationBatchAfterBatchId(BatchId batch_id) { BatchId next_batch_id = batch_id + 1; - // The requested batchID may still be out of range so normalize it to the + // The requested batch_id may still be out of range so normalize it to the // start of the queue. int raw_index = IndexOfBatchId(next_batch_id); - int index = raw_index < 0 ? 0 : raw_index; - return queue_.size() > index ? queue_[index] : nil; + size_t index = raw_index < 0 ? 0 : static_cast(raw_index); + if (queue_.size() <= index) { + return absl::nullopt; + } + + return queue_[index]; +} + +BatchId MemoryMutationQueue::GetHighestUnacknowledgedBatchId() { + return IsEmpty() ? kBatchIdUnknown : next_batch_id_ - 1; } -FSTMutationBatch* _Nullable MemoryMutationQueue::LookupMutationBatch( +absl::optional MemoryMutationQueue::LookupMutationBatch( BatchId batch_id) { if (queue_.empty()) { - return nil; + return absl::nullopt; } int index = IndexOfBatchId(batch_id); - if (index < 0 || index >= queue_.size()) { - return nil; + if (index < 0 || static_cast(index) >= queue_.size()) { + return absl::nullopt; } - FSTMutationBatch* batch = queue_[index]; - HARD_ASSERT(batch.batchID == batch_id, "If found, batch must match"); + const MutationBatch& batch = queue_[index]; + HARD_ASSERT(batch.batch_id() == batch_id, "If found, batch must match"); return batch; } @@ -248,29 +255,29 @@ return begin != range.end() && begin->key() == key; } -size_t MemoryMutationQueue::CalculateByteSize(FSTLocalSerializer* serializer) { - size_t count = 0; +int64_t MemoryMutationQueue::CalculateByteSize(const Sizer& sizer) { + int64_t count = 0; for (const auto& batch : queue_) { - count += [[serializer encodedMutationBatch:batch] serializedSize]; + count += sizer.CalculateByteSize(batch); }; return count; } -NSData* _Nullable MemoryMutationQueue::GetLastStreamToken() { +ByteString MemoryMutationQueue::GetLastStreamToken() { return last_stream_token_; } -void MemoryMutationQueue::SetLastStreamToken(NSData* _Nullable token) { +void MemoryMutationQueue::SetLastStreamToken(const ByteString& token) { last_stream_token_ = token; } -std::vector MemoryMutationQueue::AllMutationBatchesWithIds( +std::vector MemoryMutationQueue::AllMutationBatchesWithIds( const std::set& batch_ids) { - std::vector result; + std::vector result; for (BatchId batch_id : batch_ids) { - FSTMutationBatch* batch = LookupMutationBatch(batch_id); - if (batch) { - result.push_back(batch); + auto batch = LookupMutationBatch(batch_id); + if (batch.has_value()) { + result.push_back(*batch); } } @@ -284,15 +291,13 @@ } // Examine the front of the queue to figure out the difference between the - // batchID and indexes in the array. Note that since the queue is ordered by - // batchID, if the first batch has a larger batchID then the requested batchID - // doesn't exist in the queue. - FSTMutationBatch* first_batch = queue_.front(); - return batch_id - first_batch.batchID; + // batch_id and indexes in the array. Note that since the queue is ordered by + // batch_id, if the first batch has a larger batch_id then the requested + // batch_id doesn't exist in the queue. + const MutationBatch& first_batch = queue_.front(); + return batch_id - first_batch.batch_id(); } } // namespace local } // namespace firestore } // namespace firebase - -NS_ASSUME_NONNULL_END diff --git a/Firestore/core/src/firebase/firestore/local/memory_persistence.cc b/Firestore/core/src/firebase/firestore/local/memory_persistence.cc new file mode 100644 index 00000000000..88b380f9f45 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/memory_persistence.cc @@ -0,0 +1,110 @@ +/* + * Copyright 2019 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/local/memory_persistence.h" + +#include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/listen_sequence.h" +#include "Firestore/core/src/firebase/firestore/local/lru_garbage_collector.h" +#include "Firestore/core/src/firebase/firestore/local/memory_eager_reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/memory_lru_reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" +#include "Firestore/core/src/firebase/firestore/local/memory_query_cache.h" +#include "Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/sizer.h" +#include "absl/memory/memory.h" + +namespace firebase { +namespace firestore { +namespace local { + +using auth::User; +using model::ListenSequenceNumber; + +std::unique_ptr +MemoryPersistence::WithEagerGarbageCollector() { + std::unique_ptr persistence(new MemoryPersistence()); + auto delegate = + absl::make_unique(persistence.get()); + persistence->set_reference_delegate(std::move(delegate)); + return persistence; +} + +std::unique_ptr MemoryPersistence::WithLruGarbageCollector( + LruParams lru_params, std::unique_ptr sizer) { + std::unique_ptr persistence(new MemoryPersistence()); + auto delegate = absl::make_unique( + persistence.get(), lru_params, std::move(sizer)); + persistence->set_reference_delegate(std::move(delegate)); + return persistence; +} + +MemoryPersistence::MemoryPersistence() + : query_cache_(this), remote_document_cache_(this), started_(true) { +} + +ListenSequenceNumber MemoryPersistence::current_sequence_number() const { + return reference_delegate_->current_sequence_number(); +} + +void MemoryPersistence::Shutdown() { + // No durable state to ensure is closed on shutdown. + HARD_ASSERT(started_, "MemoryPersistence shutdown without start!"); + started_ = false; +} + +MemoryMutationQueue* MemoryPersistence::GetMutationQueueForUser( + const User& user) { + auto iter = mutation_queues_.find(user); + if (iter == mutation_queues_.end()) { + auto queue = absl::make_unique(this); + MemoryMutationQueue* result = queue.get(); + + mutation_queues_.emplace(user, std::move(queue)); + return result; + } else { + return iter->second.get(); + } +} + +MemoryQueryCache* MemoryPersistence::query_cache() { + return &query_cache_; +} + +MemoryRemoteDocumentCache* MemoryPersistence::remote_document_cache() { + return &remote_document_cache_; +} + +MemoryIndexManager* MemoryPersistence::index_manager() { + return &index_manager_; +} + +ReferenceDelegate* MemoryPersistence::reference_delegate() { + return reference_delegate_.get(); +} + +void MemoryPersistence::RunInternal(absl::string_view label, + std::function block) { + TransactionGuard guard(reference_delegate_.get(), label); + + block(); +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/memory_persistence.h b/Firestore/core/src/firebase/firestore/local/memory_persistence.h new file mode 100644 index 00000000000..3a4dbc16f80 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/memory_persistence.h @@ -0,0 +1,125 @@ +/* + * Copyright 2019 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_LOCAL_MEMORY_PERSISTENCE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_PERSISTENCE_H_ + +#include +#include +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/auth/user.h" +#include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/memory_mutation_queue.h" +#include "Firestore/core/src/firebase/firestore/local/memory_query_cache.h" +#include "Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" + +namespace firebase { +namespace firestore { +namespace local { + +struct LruParams; +class MemoryIndexManager; +class MemoryMutationQueue; +class MemoryQueryCache; +class MemoryRemoteDocumentCache; +class MutationQueue; +class QueryCache; +class ReferenceDelegate; +class RemoteDocumentCache; +class Sizer; + +/** + * An in-memory implementation of the Persistence interface. Values are stored + * only in RAM and are never persisted to any durable storage. + */ +class MemoryPersistence : public Persistence { + public: + using MutationQueues = + std::unordered_map, + auth::HashUser>; + + static std::unique_ptr WithEagerGarbageCollector(); + + static std::unique_ptr WithLruGarbageCollector( + LruParams params, std::unique_ptr sizer); + + const MutationQueues& mutation_queues() const { + return mutation_queues_; + } + + // MARK: Persistence overrides + + model::ListenSequenceNumber current_sequence_number() const override; + + void Shutdown() override; + + MemoryMutationQueue* GetMutationQueueForUser(const auth::User& user) override; + + MemoryQueryCache* query_cache() override; + + MemoryRemoteDocumentCache* remote_document_cache() override; + + MemoryIndexManager* index_manager() override; + + ReferenceDelegate* reference_delegate() override; + + protected: + void RunInternal(absl::string_view label, + std::function block) override; + + private: + MemoryPersistence(); + + void set_reference_delegate(std::unique_ptr delegate) { + reference_delegate_ = std::move(delegate); + } + + MutationQueues mutation_queues_; + + /** + * The QueryCache representing the persisted cache of queries. + * + * Note that this is retained here to make it easier to write tests affecting + * both the in-memory and LevelDB-backed persistence layers. Tests can create + * a new LocalStore wrapping this Persistence instance and this will make + * the in-memory persistence layer behave as if it were actually persisting + * values. + */ + MemoryQueryCache query_cache_; + + /** + * The RemoteDocumentCache representing the persisted cache of remote + * documents. + */ + MemoryRemoteDocumentCache remote_document_cache_; + + MemoryIndexManager index_manager_; + + std::unique_ptr reference_delegate_; + + bool started_ = false; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_PERSISTENCE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/memory_query_cache.h b/Firestore/core/src/firebase/firestore/local/memory_query_cache.h index 40197c905be..7636b2e164b 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_query_cache.h +++ b/Firestore/core/src/firebase/firestore/local/memory_query_cache.h @@ -17,12 +17,6 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_QUERY_CACHE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_QUERY_CACHE_H_ -#if !defined(__OBJC__) -#error "For now, this file must only be included by ObjC source files." -#endif // !defined(__OBJC__) - -#import - #include #include #include @@ -32,35 +26,31 @@ #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" -#include "Firestore/core/src/firebase/firestore/objc/objc_compatibility.h" - -@class FSTLocalSerializer; -@class FSTMemoryPersistence; -@class FSTQueryData; - -NS_ASSUME_NONNULL_BEGIN namespace firebase { namespace firestore { namespace local { +class MemoryPersistence; +class Sizer; + class MemoryQueryCache : public QueryCache { public: - explicit MemoryQueryCache(FSTMemoryPersistence* persistence); + explicit MemoryQueryCache(MemoryPersistence* persistence); // Target-related methods - void AddTarget(FSTQueryData* query_data) override; + void AddTarget(const QueryData& query_data) override; - void UpdateTarget(FSTQueryData* query_data) override; + void UpdateTarget(const QueryData& query_data) override; - void RemoveTarget(FSTQueryData* query_data) override; + void RemoveTarget(const QueryData& query_data) override; - FSTQueryData* _Nullable GetTarget(const core::Query& query) override; + absl::optional GetTarget(const core::Query& query) override; void EnumerateTargets(const TargetCallback& callback) override; int RemoveTargets(model::ListenSequenceNumber upper_bound, - const std::unordered_map& + const std::unordered_map& live_targets) override; // Key-related methods @@ -75,7 +65,7 @@ class MemoryQueryCache : public QueryCache { bool Contains(const model::DocumentKey& key) override; // Other methods and accessors - size_t CalculateByteSize(FSTLocalSerializer* serializer); + int64_t CalculateByteSize(const Sizer& sizer); size_t size() const override { return queries_.size(); @@ -94,8 +84,8 @@ class MemoryQueryCache : public QueryCache { void SetLastRemoteSnapshotVersion(model::SnapshotVersion version) override; private: - // This instance is owned by FSTMemoryPersistence; avoid a retain cycle. - __weak FSTMemoryPersistence* persistence_; + // This instance is owned by MemoryPersistence. + MemoryPersistence* persistence_; /** The highest sequence number encountered */ model::ListenSequenceNumber highest_listen_sequence_number_; @@ -105,7 +95,7 @@ class MemoryQueryCache : public QueryCache { model::SnapshotVersion last_remote_snapshot_version_; /** Maps a query to the data about that query. */ - std::unordered_map queries_; + std::unordered_map queries_; /** * A ordered bidirectional mapping between documents and the remote target @@ -118,6 +108,4 @@ class MemoryQueryCache : public QueryCache { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_QUERY_CACHE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/memory_query_cache.mm b/Firestore/core/src/firebase/firestore/local/memory_query_cache.mm index 2f25b430d8b..28662a2f05a 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_query_cache.mm +++ b/Firestore/core/src/firebase/firestore/local/memory_query_cache.mm @@ -16,30 +16,26 @@ #include "Firestore/core/src/firebase/firestore/local/memory_query_cache.h" -#import - #include -#import "Firestore/Protos/objc/firestore/local/Target.pbobjc.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" -#import "Firestore/Source/Local/FSTQueryData.h" - +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/sizer.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" -using firebase::firestore::core::Query; -using firebase::firestore::model::DocumentKey; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::ListenSequenceNumber; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::TargetId; - namespace firebase { namespace firestore { namespace local { -NS_ASSUME_NONNULL_BEGIN +using core::Query; +using model::DocumentKey; +using model::DocumentKeySet; +using model::ListenSequenceNumber; +using model::SnapshotVersion; +using model::TargetId; -MemoryQueryCache::MemoryQueryCache(FSTMemoryPersistence* persistence) +MemoryQueryCache::MemoryQueryCache(MemoryPersistence* persistence) : persistence_(persistence), highest_listen_sequence_number_(ListenSequenceNumber(0)), highest_target_id_(TargetId(0)), @@ -47,29 +43,29 @@ queries_() { } -void MemoryQueryCache::AddTarget(FSTQueryData* query_data) { - queries_[query_data.query] = query_data; - if (query_data.targetID > highest_target_id_) { - highest_target_id_ = query_data.targetID; +void MemoryQueryCache::AddTarget(const QueryData& query_data) { + queries_[query_data.query()] = query_data; + if (query_data.target_id() > highest_target_id_) { + highest_target_id_ = query_data.target_id(); } - if (query_data.sequenceNumber > highest_listen_sequence_number_) { - highest_listen_sequence_number_ = query_data.sequenceNumber; + if (query_data.sequence_number() > highest_listen_sequence_number_) { + highest_listen_sequence_number_ = query_data.sequence_number(); } } -void MemoryQueryCache::UpdateTarget(FSTQueryData* query_data) { +void MemoryQueryCache::UpdateTarget(const QueryData& query_data) { // For the memory query cache, adds and updates are treated the same. AddTarget(query_data); } -void MemoryQueryCache::RemoveTarget(FSTQueryData* query_data) { - queries_.erase(query_data.query); - references_.RemoveReferences(query_data.targetID); +void MemoryQueryCache::RemoveTarget(const QueryData& query_data) { + queries_.erase(query_data.query()); + references_.RemoveReferences(query_data.target_id()); } -FSTQueryData* _Nullable MemoryQueryCache::GetTarget(const Query& query) { +absl::optional MemoryQueryCache::GetTarget(const Query& query) { auto iter = queries_.find(query); - return iter == queries_.end() ? nil : iter->second; + return iter == queries_.end() ? absl::optional{} : iter->second; } void MemoryQueryCache::EnumerateTargets(const TargetCallback& callback) { @@ -80,16 +76,16 @@ int MemoryQueryCache::RemoveTargets( model::ListenSequenceNumber upper_bound, - const std::unordered_map& live_targets) { + const std::unordered_map& live_targets) { std::vector to_remove; for (const auto& kv : queries_) { const Query& query = kv.first; - FSTQueryData* query_data = kv.second; + const QueryData& query_data = kv.second; - if (query_data.sequenceNumber <= upper_bound) { - if (live_targets.find(query_data.targetID) == live_targets.end()) { + if (query_data.sequence_number() <= upper_bound) { + if (live_targets.find(query_data.target_id()) == live_targets.end()) { to_remove.push_back(&query); - references_.RemoveReferences(query_data.targetID); + references_.RemoveReferences(query_data.target_id()); } } } @@ -104,7 +100,7 @@ TargetId target_id) { references_.AddReferences(keys, target_id); for (const DocumentKey& key : keys) { - [persistence_.referenceDelegate addReference:key]; + persistence_->reference_delegate()->AddReference(key); } } @@ -112,7 +108,7 @@ TargetId target_id) { references_.RemoveReferences(keys, target_id); for (const DocumentKey& key : keys) { - [persistence_.referenceDelegate removeReference:key]; + persistence_->reference_delegate()->RemoveReference(key); } } @@ -124,11 +120,10 @@ return references_.ContainsKey(key); } -size_t MemoryQueryCache::CalculateByteSize(FSTLocalSerializer* serializer) { - size_t count = 0; +int64_t MemoryQueryCache::CalculateByteSize(const Sizer& sizer) { + int64_t count = 0; for (const auto& kv : queries_) { - FSTQueryData* query_data = kv.second; - count += [[serializer encodedQueryData:query_data] serializedSize]; + count += sizer.CalculateByteSize(kv.second); } return count; } @@ -141,8 +136,6 @@ last_remote_snapshot_version_ = std::move(version); } -NS_ASSUME_NONNULL_END - } // namespace local } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h b/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h index 28de7123fb1..e5abca65af4 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h +++ b/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.h @@ -17,10 +17,6 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_REMOTE_DOCUMENT_CACHE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_REMOTE_DOCUMENT_CACHE_H_ -#if !defined(__OBJC__) -#error "For now, this file must only be included by ObjC source files." -#endif // !defined(__OBJC__) - #include #include "Firestore/core/src/firebase/firestore/local/remote_document_cache.h" @@ -29,19 +25,17 @@ #include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/model/types.h" -@class FSTLocalSerializer; -@class FSTMemoryLRUReferenceDelegate; -@class FSTMemoryPersistence; - -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace local { +class MemoryLruReferenceDelegate; +class MemoryPersistence; +class Sizer; + class MemoryRemoteDocumentCache : public RemoteDocumentCache { public: - explicit MemoryRemoteDocumentCache(FSTMemoryPersistence* persistence); + explicit MemoryRemoteDocumentCache(MemoryPersistence* persistence); void Add(const model::MaybeDocument& document) override; void Remove(const model::DocumentKey& key) override; @@ -53,23 +47,21 @@ class MemoryRemoteDocumentCache : public RemoteDocumentCache { model::DocumentMap GetMatching(const core::Query& query) override; std::vector RemoveOrphanedDocuments( - FSTMemoryLRUReferenceDelegate* reference_delegate, + MemoryLruReferenceDelegate* reference_delegate, model::ListenSequenceNumber upper_bound); - size_t CalculateByteSize(FSTLocalSerializer* serializer); + int64_t CalculateByteSize(const Sizer& sizer); private: /** Underlying cache of documents. */ model::MaybeDocumentMap docs_; - // This instance is owned by FSTMemoryPersistence; avoid a retain cycle. - __weak FSTMemoryPersistence* persistence_; + // This instance is owned by MemoryPersistence; avoid a retain cycle. + MemoryPersistence* persistence_; }; } // namespace local } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MEMORY_REMOTE_DOCUMENT_CACHE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.mm b/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.mm index 7c66bfec82f..a2e14dd3e4f 100644 --- a/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.mm +++ b/Firestore/core/src/firebase/firestore/local/memory_remote_document_cache.mm @@ -18,15 +18,14 @@ #include -#import "Firestore/Protos/objc/firestore/local/MaybeDocument.pbobjc.h" -#import "Firestore/Source/Local/FSTMemoryPersistence.h" - +#include "Firestore/core/src/firebase/firestore/local/memory_lru_reference_delegate.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/sizer.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" namespace firebase { namespace firestore { namespace local { -namespace { using core::Query; using model::Document; @@ -38,29 +37,15 @@ using model::MaybeDocumentMap; using model::OptionalMaybeDocumentMap; -/** - * Returns an estimate of the number of bytes used to store the given - * document key in memory. This is only an estimate and includes the size - * of the segments of the path, but not any object overhead or path separators. - */ -size_t DocumentKeyByteSize(const DocumentKey& key) { - size_t count = 0; - for (const auto& segment : key.path()) { - count += segment.size(); - } - return count; -} -} // namespace - MemoryRemoteDocumentCache::MemoryRemoteDocumentCache( - FSTMemoryPersistence* persistence) { + MemoryPersistence* persistence) { persistence_ = persistence; } void MemoryRemoteDocumentCache::Add(const MaybeDocument& document) { docs_ = docs_.insert(document.key(), document); - persistence_.indexManager->AddToCollectionParentIndex( + persistence_->index_manager()->AddToCollectionParentIndex( document.key().path().PopLast()); } @@ -114,14 +99,13 @@ size_t DocumentKeyByteSize(const DocumentKey& key) { } std::vector MemoryRemoteDocumentCache::RemoveOrphanedDocuments( - FSTMemoryLRUReferenceDelegate* reference_delegate, + MemoryLruReferenceDelegate* reference_delegate, ListenSequenceNumber upper_bound) { std::vector removed; MaybeDocumentMap updated_docs = docs_; for (const auto& kv : docs_) { const DocumentKey& key = kv.first; - if (![reference_delegate isPinnedAtSequenceNumber:upper_bound - document:key]) { + if (!reference_delegate->IsPinnedAtSequenceNumber(upper_bound, key)) { updated_docs = updated_docs.erase(key); removed.push_back(key); } @@ -130,12 +114,10 @@ size_t DocumentKeyByteSize(const DocumentKey& key) { return removed; } -size_t MemoryRemoteDocumentCache::CalculateByteSize( - FSTLocalSerializer* serializer) { - size_t count = 0; +int64_t MemoryRemoteDocumentCache::CalculateByteSize(const Sizer& sizer) { + int64_t count = 0; for (const auto& kv : docs_) { - count += DocumentKeyByteSize(kv.first); - count += [[serializer encodedMaybeDocument:kv.second] serializedSize]; + count += sizer.CalculateByteSize(kv.second); } return count; } diff --git a/Firestore/core/src/firebase/firestore/local/mutation_queue.h b/Firestore/core/src/firebase/firestore/local/mutation_queue.h index 328b3018894..97e3a6e6185 100644 --- a/Firestore/core/src/firebase/firestore/local/mutation_queue.h +++ b/Firestore/core/src/firebase/firestore/local/mutation_queue.h @@ -17,24 +17,16 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MUTATION_QUEUE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MUTATION_QUEUE_H_ -#if !defined(__OBJC__) -#error "For now, this file must only be included by ObjC source files." -#endif // !defined(__OBJC__) - -#import - #include #include "Firestore/core/include/firebase/firestore/timestamp.h" #include "Firestore/core/src/firebase/firestore/core/query.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/mutation.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" #include "Firestore/core/src/firebase/firestore/model/types.h" - -@class FSTMutationBatch; - -NS_ASSUME_NONNULL_BEGIN +#include "Firestore/core/src/firebase/firestore/nanopb/byte_string.h" +#include "absl/types/optional.h" namespace firebase { namespace firestore { @@ -56,8 +48,8 @@ class MutationQueue { virtual bool IsEmpty() = 0; /** Acknowledges the given batch. */ - virtual void AcknowledgeBatch(FSTMutationBatch* batch, - NSData* _Nullable stream_token) = 0; + virtual void AcknowledgeBatch(const model::MutationBatch& batch, + const nanopb::ByteString& stream_token) = 0; /** * Creates a new mutation batch and adds it to this mutation queue. @@ -68,7 +60,7 @@ class MutationQueue { * overwrite values that are persisted in the remote document cache. * @param mutations The user-provided mutations in this mutation batch. */ - virtual FSTMutationBatch* AddMutationBatch( + virtual model::MutationBatch AddMutationBatch( const Timestamp& local_write_time, std::vector&& base_mutations, std::vector&& mutations) = 0; @@ -80,12 +72,12 @@ class MutationQueue { * + Removing applied mutations from the head of the queue * + Removing rejected mutations from anywhere in the queue */ - virtual void RemoveMutationBatch(FSTMutationBatch* batch) = 0; + virtual void RemoveMutationBatch(const model::MutationBatch& batch) = 0; /** Gets all mutation batches in the mutation queue. */ // TODO(mikelehen): PERF: Current consumer only needs mutated keys; if we can // provide that cheaply, we should replace this. - virtual std::vector AllMutationBatches() = 0; + virtual std::vector AllMutationBatches() = 0; /** * Finds all mutation batches that could @em possibly affect the given @@ -98,7 +90,7 @@ class MutationQueue { * if it's convenient. */ // TODO(mcg): This should really return an iterator - virtual std::vector + virtual std::vector AllMutationBatchesAffectingDocumentKeys( const model::DocumentKeySet& document_keys) = 0; @@ -113,8 +105,8 @@ class MutationQueue { * convenient. */ // TODO(mcg): This should really return an iterator - virtual std::vector AllMutationBatchesAffectingDocumentKey( - const model::DocumentKey& key) = 0; + virtual std::vector + AllMutationBatchesAffectingDocumentKey(const model::DocumentKey& key) = 0; /** * Finds all mutation batches that could affect the results for the given @@ -131,25 +123,35 @@ class MutationQueue { */ // TODO(mikelehen): This should perhaps return an iterator, though I'm not // sure we can avoid loading them all in memory. - virtual std::vector AllMutationBatchesAffectingQuery( + virtual std::vector AllMutationBatchesAffectingQuery( const core::Query& query) = 0; /** Loads the mutation batch with the given batch_id. */ - virtual FSTMutationBatch* _Nullable LookupMutationBatch( + virtual absl::optional LookupMutationBatch( model::BatchId batch_id) = 0; /** - * Gets the first unacknowledged mutation batch after the passed in batchId in - * the mutation queue or nil if empty. + * Gets the first unacknowledged mutation batch after the passed in batch_id + * in the mutation queue or nil if empty. * * @param batch_id The batch to search after, or kBatchIdUnknown for the first * mutation in the queue. * * @return the next mutation or nil if there wasn't one. */ - virtual FSTMutationBatch* _Nullable NextMutationBatchAfterBatchId( + virtual absl::optional NextMutationBatchAfterBatchId( model::BatchId batch_id) = 0; + /** + * Gets the largest (latest) batch id in mutation queue for the current user + * that is pending server response, returns `kBatchIdUnknown` if the queue + * is empty. + * + * @return the largest batch id in the mutation queue that is not + * acknowledged. + */ + virtual model::BatchId GetHighestUnacknowledgedBatchId() = 0; + /** * Performs a consistency check, examining the mutation queue for any leaks, * if possible. @@ -157,16 +159,14 @@ class MutationQueue { virtual void PerformConsistencyCheck() = 0; /** Returns the current stream token for this mutation queue. */ - virtual NSData* _Nullable GetLastStreamToken() = 0; + virtual nanopb::ByteString GetLastStreamToken() = 0; /** Sets the stream token for this mutation queue. */ - virtual void SetLastStreamToken(NSData* _Nullable stream_token) = 0; + virtual void SetLastStreamToken(const nanopb::ByteString& stream_token) = 0; }; } // namespace local } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_MUTATION_QUEUE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/persistence.h b/Firestore/core/src/firebase/firestore/local/persistence.h new file mode 100644 index 00000000000..b05dc7fcfdb --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/persistence.h @@ -0,0 +1,162 @@ +/* + * Copyright 2019 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_LOCAL_PERSISTENCE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_PERSISTENCE_H_ + +#include +#include + +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "absl/strings/string_view.h" + +namespace firebase { +namespace firestore { +namespace auth { + +class User; + +} // namespace auth + +namespace local { + +class IndexManager; +class MutationQueue; +class QueryCache; +class ReferenceDelegate; +class RemoteDocumentCache; + +/** + * Persistence is the lowest-level shared interface to data storage in + * Firestore. + * + * Persistence creates MutationQueue and RemoteDocumentCache instances backed + * by some underlying storage mechanism (which might be in-memory or LevelDB). + * + * Persistence also exposes an API to run transactions against the backing + * store. All read and write operations must be wrapped in a transaction. + * Implementations of Persistence only need to guarantee that writes made + * against the transaction are not made to durable storage until the transaction + * commits. Since memory-only storage components do not alter durable storage, + * they are free to ignore the transaction. + * + * This contract is enough to allow the LocalStore to be written independently + * of whether or not the stored state actually is durably persisted. If a user + * enables persistent storage, writes are grouped together to avoid inconsistent + * state that could cause crashes. + * + * Concretely, when persistent storage is enabled, the durable versions of + * MutationQueue, RemoteDocumentCache, and others (the mutators) will group + * their writes in a transaction. Once the local store has completed one logical + * operation, it commits the transaction. + * + * When persistent storage is disabled, the non-durable versions of the mutators + * ignore the transaction. This short-cut is allowed because memory-only storage + * leaves no state so it cannot be inconsistent. + * + * This simplifies the implementations of the mutators and allows memory-only + * implementations to supplement the durable ones without requiring any special + * dual-store implementation of Persistence. The cost is that LocalStore needs + * to be slightly careful about the order of its reads and writes in order to + * avoid relying on being able to read back uncommitted writes. + */ +class Persistence { + public: + virtual ~Persistence() = default; + + virtual model::ListenSequenceNumber current_sequence_number() const = 0; + + /** Releases any resources held during eager shutdown. */ + virtual void Shutdown() = 0; + + /** + * Returns a MutationQueue representing the persisted mutations for the given + * user. + * + * Note: The implementation is free to return the same instance every time + * this is called for a given user. In particular, the memory-backed + * implementation does this to emulate the persisted implementation to the + * extent possible (e.g. in the case of UID switching from sally=>jack=>sally, + * sally's mutation queue will be preserved). + */ + virtual MutationQueue* GetMutationQueueForUser(const auth::User& user) = 0; + + /** Returns a QueryCache representing the persisted cache of queries. */ + virtual QueryCache* query_cache() = 0; + + /** + * Returns a RemoteDocumentCache representing the persisted cache of remote + * documents. + */ + virtual RemoteDocumentCache* remote_document_cache() = 0; + + /** Returns an IndexManager that manages our persisted query indexes. */ + virtual IndexManager* index_manager() = 0; + + /** + * This property provides access to hooks around the document reference + * lifecycle. + */ + virtual ReferenceDelegate* reference_delegate() = 0; + + /** + * Accepts a function and runs it within a transaction. When called, a + * transaction will be started before a block is run, and committed after the + * block has executed. + * + * @param label A semi-unique name for the transaction, for logging. + * @param block A void-returning function to be executed within the + * transaction. + */ + template + auto Run(absl::string_view label, F block) -> + typename std::enable_if::value, + void>::type { + RunInternal(label, std::forward(block)); + } + + /** + * Accepts a function and runs it within a transaction. When called, a + * transaction will be started before a block is run, and committed after the + * block has executed. + * + * @param label A semi-unique name for the transaction, for logging. + * @param block A function to be executed within the transaction whose return + * value will be the result of the transaction. The type of the return + * value must be default constructible and copy- or move-assignable. + * @return The value returned from the invocation of `block`. + */ + template + auto Run(absl::string_view label, F block) -> + typename std::enable_if::value, + decltype(block())>::type { + decltype(block()) result; + + RunInternal(label, [&]() mutable { result = block(); }); + + return result; + } + + private: + virtual void RunInternal(absl::string_view label, + std::function block) = 0; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_PERSISTENCE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/proto_sizer.h b/Firestore/core/src/firebase/firestore/local/proto_sizer.h new file mode 100644 index 00000000000..04577cf22dc --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/proto_sizer.h @@ -0,0 +1,56 @@ +/* + * Copyright 2019 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_LOCAL_PROTO_SIZER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_PROTO_SIZER_H_ + +#if !defined(__OBJC__) +#error "This header only supports Objective-C++" +#endif // !defined(__OBJC__) + +#include "Firestore/core/src/firebase/firestore/local/sizer.h" + +@class FSTLocalSerializer; + +namespace firebase { +namespace firestore { +namespace local { + +/** + * Estimates the stored size of documents and queries by translating to protos + * and using the serialized sizes to estimate. + */ +class ProtoSizer : public Sizer { + public: + explicit ProtoSizer(FSTLocalSerializer* serializer); + + int64_t CalculateByteSize( + const model::MaybeDocument& maybe_doc) const override; + + int64_t CalculateByteSize( + const model::MutationBatch& mutation_batch) const override; + + int64_t CalculateByteSize(const QueryData& query_data) const override; + + private: + FSTLocalSerializer* serializer_; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_PROTO_SIZER_H_ diff --git a/Firestore/core/src/firebase/firestore/local/proto_sizer.mm b/Firestore/core/src/firebase/firestore/local/proto_sizer.mm new file mode 100644 index 00000000000..e54be5d15b8 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/proto_sizer.mm @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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/local/proto_sizer.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" +#import "Firestore/Source/Local/FSTLocalSerializer.h" + +#include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/maybe_document.h" + +namespace firebase { +namespace firestore { +namespace local { + +using model::DocumentKey; +using model::MaybeDocument; + +ProtoSizer::ProtoSizer(FSTLocalSerializer* serializer) + : serializer_(serializer) { +} + +int64_t ProtoSizer::CalculateByteSize(const MaybeDocument& maybe_doc) const { + return [[serializer_ encodedMaybeDocument:maybe_doc] serializedSize]; +} + +int64_t ProtoSizer::CalculateByteSize(const model::MutationBatch& batch) const { + return [[serializer_ encodedMutationBatch:batch] serializedSize]; +} + +int64_t ProtoSizer::CalculateByteSize(const QueryData& query_data) const { + return [[serializer_ encodedQueryData:query_data] serializedSize]; +} + +} // namespace local +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/query_cache.h b/Firestore/core/src/firebase/firestore/local/query_cache.h index b81821f95ab..3e1bee38cdf 100644 --- a/Firestore/core/src/firebase/firestore/local/query_cache.h +++ b/Firestore/core/src/firebase/firestore/local/query_cache.h @@ -17,25 +17,16 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_QUERY_CACHE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_QUERY_CACHE_H_ -#if !defined(__OBJC__) -#error "For now, this file must only be included by ObjC source files." -#endif // !defined(__OBJC__) - -#import - #include #include #include "Firestore/core/src/firebase/firestore/core/query.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.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" #include "Firestore/core/src/firebase/firestore/model/types.h" -@class FSTQueryData; - -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace local { @@ -43,15 +34,14 @@ namespace local { using OrphanedDocumentCallback = std::function; -using TargetCallback = std::function; +using TargetCallback = std::function; /** * Represents cached targets received from the remote backend. This contains * both a mapping between targets and the documents that matched them according * to the server, but also metadata about the targets. * - * The cache is keyed by Query and entries in the cache are FSTQueryData - * instances. + * The cache is keyed by Query and entries in the cache are QueryData instances. */ class QueryCache { public: @@ -66,39 +56,39 @@ class QueryCache { * The cache key is extracted from `queryData.query`. The key must not already * exist in the cache. * - * @param query_data A new FSTQueryData instance to put in the cache. + * @param query_data A new QueryData instance to put in the cache. */ - virtual void AddTarget(FSTQueryData* query_data) = 0; + virtual void AddTarget(const QueryData& query_data) = 0; /** * Updates an entry in the cache. * * The cache key is extracted from `queryData.query`. The entry must already * exist in the cache, and it will be replaced. - * @param query_data An FSTQueryData instance to replace an existing entry in - * the cache + * + * @param query_data A QueryData instance to replace an existing entry in + * the cache */ - virtual void UpdateTarget(FSTQueryData* query_data) = 0; + virtual void UpdateTarget(const QueryData& query_data) = 0; /** Removes the cached entry for the given query data. The entry must already * exist in the cache. */ - virtual void RemoveTarget(FSTQueryData* query_data) = 0; + virtual void RemoveTarget(const QueryData& query_data) = 0; /** - * Looks up an FSTQueryData entry in the cache. + * Looks up a QueryData entry in the cache. * * @param query The query corresponding to the entry to look up. - * @return The cached FSTQueryData entry, or nil if the cache has no entry for - * the query. + * @return The cached QueryData entry, or nullopt if the cache has no entry + * for the query. */ - virtual FSTQueryData* _Nullable GetTarget(const core::Query& query) = 0; + virtual absl::optional GetTarget(const core::Query& query) = 0; virtual void EnumerateTargets(const TargetCallback& callback) = 0; virtual int RemoveTargets( model::ListenSequenceNumber upper_bound, - const std::unordered_map& - live_targets) = 0; + const std::unordered_map& live_targets) = 0; // Key-related methods virtual void AddMatchingKeys(const model::DocumentKeySet& keys, @@ -156,6 +146,4 @@ class QueryCache { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_QUERY_CACHE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/query_data.cc b/Firestore/core/src/firebase/firestore/local/query_data.cc index 871a81629fe..8d5c6a9ddee 100644 --- a/Firestore/core/src/firebase/firestore/local/query_data.cc +++ b/Firestore/core/src/firebase/firestore/local/query_data.cc @@ -16,6 +16,8 @@ #include "Firestore/core/src/firebase/firestore/local/query_data.h" +#include +#include #include namespace firebase { @@ -26,12 +28,37 @@ using core::Query; using model::SnapshotVersion; using nanopb::ByteString; -QueryData::QueryData(Query&& query, +// MARK: - QueryPurpose + +namespace { + +const char* ToString(QueryPurpose purpose) { + switch (purpose) { + case QueryPurpose::Listen: + return "Listen"; + case QueryPurpose::ExistenceFilterMismatch: + return "ExistenceFilterMismatch"; + case QueryPurpose::LimboResolution: + return "LimboResolution"; + } + + UNREACHABLE(); +} + +} // namespace + +std::ostream& operator<<(std::ostream& os, QueryPurpose purpose) { + return os << ToString(purpose); +} + +// MARK: - QueryData + +QueryData::QueryData(Query query, model::TargetId target_id, model::ListenSequenceNumber sequence_number, QueryPurpose purpose, SnapshotVersion snapshot_version, - ByteString&& resume_token) + ByteString resume_token) : query_(std::move(query)), target_id_(target_id), sequence_number_(sequence_number), @@ -40,29 +67,57 @@ QueryData::QueryData(Query&& query, resume_token_(std::move(resume_token)) { } -// TODO(rsgowman): Implement once WatchStream::EmptyResumeToken exists. -/* -QueryData::QueryData(const Query& query, int target_id, QueryPurpose purpose) - : QueryData(query, +QueryData::QueryData(Query query, + int target_id, + model::ListenSequenceNumber sequence_number, + QueryPurpose purpose) + : QueryData(std::move(query), target_id, + sequence_number, purpose, model::SnapshotVersion::None(), - WatchStream::EmptyResumeToken()) { + ByteString()) { } -*/ QueryData QueryData::Invalid() { return QueryData(Query::Invalid(), /*target_id=*/-1, /*sequence_number=*/-1, - QueryPurpose::kListen, + QueryPurpose::Listen, SnapshotVersion(SnapshotVersion::None()), {}); } -QueryData QueryData::Copy(SnapshotVersion&& snapshot_version, - ByteString&& resume_token) const { - return QueryData(Query(query_), target_id_, sequence_number_, purpose_, +QueryData QueryData::Copy(SnapshotVersion snapshot_version, + ByteString resume_token, + model::ListenSequenceNumber sequence_number) const { + return QueryData(Query(query_), target_id_, sequence_number, purpose_, std::move(snapshot_version), std::move(resume_token)); } +bool operator==(const QueryData& lhs, const QueryData& rhs) { + return lhs.query() == rhs.query() && lhs.target_id() == rhs.target_id() && + lhs.sequence_number() == rhs.sequence_number() && + lhs.purpose() == rhs.purpose() && + lhs.snapshot_version() == rhs.snapshot_version() && + lhs.resume_token() == rhs.resume_token(); +} + +size_t QueryData::Hash() const { + return util::Hash(query_, target_id_, sequence_number_, purpose_, + snapshot_version_, resume_token_); +} + +std::string QueryData::ToString() const { + std::ostringstream ss; + ss << *this; + return ss.str(); +} + +std::ostream& operator<<(std::ostream& os, const QueryData& value) { + return os << "QueryData(query=," << value.query_ + << ", target=" << value.target_id_ << ", purpose=" << value.purpose_ + << ", version=" << value.snapshot_version_ + << ", resume_token=" << value.resume_token_ << ")"; +} + } // namespace local } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/local/query_data.h b/Firestore/core/src/firebase/firestore/local/query_data.h index 96e95196537..7789d886909 100644 --- a/Firestore/core/src/firebase/firestore/local/query_data.h +++ b/Firestore/core/src/firebase/firestore/local/query_data.h @@ -17,7 +17,8 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_QUERY_DATA_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_QUERY_DATA_H_ -#include +#include +#include #include #include "Firestore/core/src/firebase/firestore/core/query.h" @@ -32,17 +33,19 @@ namespace local { /** An enumeration for the different purposes we have for queries. */ enum class QueryPurpose { /** A regular, normal query. */ - kListen, + Listen, /** * The query was used to refill a query after an existence filter mismatch. */ - kExistenceFilterMismatch, + ExistenceFilterMismatch, /** The query was used to resolve a limbo document. */ - kLimboResolution, + LimboResolution, }; +std::ostream& operator<<(std::ostream& os, QueryPurpose purpose); + /** * An immutable set of metadata that the store will need to keep track of for * each query. @@ -62,19 +65,26 @@ class QueryData { * data that matches the query. The resume token essentially identifies a * point in time from which the server should resume sending results. */ - QueryData(core::Query&& query, + QueryData(core::Query query, model::TargetId target_id, model::ListenSequenceNumber sequence_number, QueryPurpose purpose, model::SnapshotVersion snapshot_version, - nanopb::ByteString&& resume_token); + nanopb::ByteString resume_token); /** * Convenience constructor for use when creating a QueryData for the first * time. */ - // TODO(rsgowman): Define once WatchStream::EmptyResumeToken exists. - // QueryData(const core::Query& query, int target_id, QueryPurpose purpose); + QueryData(core::Query query, + int target_id, + model::ListenSequenceNumber sequence_number, + QueryPurpose purpose); + + /** + * Creates an invalid QueryData. Prefer QueryData::Invalid() for readability. + */ + QueryData() = default; /** * Constructs an invalid QueryData. Reading any properties of the returned @@ -82,10 +92,15 @@ class QueryData { */ static QueryData Invalid(); + /** The query being listened to. */ const core::Query& query() const { return query_; } + /** + * The TargetId to which the query corresponds, assigned by the LocalStore for + * user queries or the SyncEngine for limbo queries. + */ model::TargetId target_id() const { return target_id_; } @@ -94,20 +109,37 @@ class QueryData { return sequence_number_; } + /** The purpose of the query. */ QueryPurpose purpose() const { return purpose_; } + /** The latest snapshot version seen for this target. */ const model::SnapshotVersion& snapshot_version() const { return snapshot_version_; } + /** + * 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. + */ const nanopb::ByteString& resume_token() const { return resume_token_; } - QueryData Copy(model::SnapshotVersion&& snapshot_version, - nanopb::ByteString&& resume_token) const; + QueryData Copy(model::SnapshotVersion snapshot_version, + nanopb::ByteString resume_token, + model::ListenSequenceNumber sequence_number) const; + + friend bool operator==(const QueryData& lhs, const QueryData& rhs); + + size_t Hash() const; + + std::string ToString() const; + + friend std::ostream& operator<<(std::ostream& os, const QueryData& value); private: core::Query query_; @@ -118,14 +150,6 @@ class QueryData { nanopb::ByteString resume_token_; }; -inline bool operator==(const QueryData& lhs, const QueryData& rhs) { - return lhs.query() == rhs.query() && lhs.target_id() == rhs.target_id() && - lhs.sequence_number() == rhs.sequence_number() && - lhs.purpose() == rhs.purpose() && - lhs.snapshot_version() == rhs.snapshot_version() && - lhs.resume_token() == rhs.resume_token(); -} - inline bool operator!=(const QueryData& lhs, const QueryData& rhs) { return !(lhs == rhs); } diff --git a/Firestore/core/src/firebase/firestore/local/reference_delegate.h b/Firestore/core/src/firebase/firestore/local/reference_delegate.h new file mode 100644 index 00000000000..0e4e270413c --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/reference_delegate.h @@ -0,0 +1,124 @@ +/* + * Copyright 2019 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_LOCAL_REFERENCE_DELEGATE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_REFERENCE_DELEGATE_H_ + +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "absl/strings/string_view.h" + +namespace firebase { +namespace firestore { +namespace model { + +class DocumentKey; + +} // namespace model + +namespace local { + +class QueryData; +class ReferenceSet; + +/** + * A ReferenceDelegate 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. + */ +class ReferenceDelegate { + public: + virtual ~ReferenceDelegate() = default; + + virtual model::ListenSequenceNumber current_sequence_number() const = 0; + + /** + * Registers a ReferenceSet of documents that should be considered + * 'referenced' and not eligible for removal during garbage collection. + */ + virtual void AddInMemoryPins(ReferenceSet* set) = 0; + + /** + * Notifies the delegate that the given document was added to a target. + */ + virtual void AddReference(const model::DocumentKey& key) = 0; + + /** + * Notifies the delegate that the given document was removed from a target. + */ + virtual void RemoveReference(const model::DocumentKey& key) = 0; + + /** + * Notifies the delegate that a document is no longer being mutated by the + * user. + */ + virtual void RemoveMutationReference(const model::DocumentKey& key) = 0; + + /** + * Notifies the delegate that a target was removed. + */ + virtual void RemoveTarget(const local::QueryData& query_data) = 0; + + /** + * Notifies the delegate that a limbo document was updated. + */ + virtual void UpdateLimboDocument(const model::DocumentKey& key) = 0; + + /** + * Lifecycle hook that notifies the delegate that a transaction has started. + */ + virtual void OnTransactionStarted(absl::string_view label) = 0; + + /** + * Lifecycle hook that notifies the delegate that a transaction has committed. + */ + virtual void OnTransactionCommitted() = 0; +}; + +/** + * Calls `OnTransactionStarted` in its constructor and then ensures that + * `OnTransactionCommitted` is called at the close of any block in which it is + * declared. + */ +struct TransactionGuard { + TransactionGuard(ReferenceDelegate* reference_delegate, + absl::string_view label) + : reference_delegate_(reference_delegate) { + reference_delegate_->OnTransactionStarted(label); + } + + ~TransactionGuard() { + reference_delegate_->OnTransactionCommitted(); + } + + TransactionGuard(const TransactionGuard&) = delete; + TransactionGuard& operator=(const TransactionGuard&) = delete; + + private: + ReferenceDelegate* reference_delegate_; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_REFERENCE_DELEGATE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/remote_document_cache.h b/Firestore/core/src/firebase/firestore/local/remote_document_cache.h index 68ee23f8108..741e1e46c35 100644 --- a/Firestore/core/src/firebase/firestore/local/remote_document_cache.h +++ b/Firestore/core/src/firebase/firestore/local/remote_document_cache.h @@ -17,20 +17,12 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_REMOTE_DOCUMENT_CACHE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_REMOTE_DOCUMENT_CACHE_H_ -#if !defined(__OBJC__) -#error "For now, this file must only be included by ObjC source files." -#endif // !defined(__OBJC__) - -#import - #include "Firestore/core/src/firebase/firestore/core/query.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/document_map.h" #include "Firestore/core/src/firebase/firestore/model/types.h" -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace local { @@ -99,6 +91,4 @@ class RemoteDocumentCache { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_REMOTE_DOCUMENT_CACHE_H_ diff --git a/Firestore/core/src/firebase/firestore/local/sizer.h b/Firestore/core/src/firebase/firestore/local/sizer.h new file mode 100644 index 00000000000..3a990245ae6 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/local/sizer.h @@ -0,0 +1,65 @@ +/* + * Copyright 2019 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_LOCAL_SIZER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_SIZER_H_ + +#include + +namespace firebase { +namespace firestore { +namespace model { + +class MaybeDocument; +class MutationBatch; + +} // namespace model + +namespace local { + +class QueryData; + +/** + * Estimates the stored size of documents and queries. + */ +class Sizer { + public: + virtual ~Sizer() = default; + + /** + * Calculates the size of the given maybe_doc in bytes. Note that even + * NoDocuments have an associated size. + */ + virtual int64_t CalculateByteSize( + const model::MaybeDocument& maybe_doc) const = 0; + + /** + * Calculates the size of the given mutation_batch in bytes. + */ + virtual int64_t CalculateByteSize( + const model::MutationBatch& batch) const = 0; + + /** + * Calculates the size of the given query_data in bytes. + */ + virtual int64_t CalculateByteSize(const QueryData& query_data) const = 0; +}; + +} // namespace local +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_LOCAL_SIZER_H_ diff --git a/Firestore/core/src/firebase/firestore/model/CMakeLists.txt b/Firestore/core/src/firebase/firestore/model/CMakeLists.txt index 0ce29810668..d4b014de131 100644 --- a/Firestore/core/src/firebase/firestore/model/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/model/CMakeLists.txt @@ -44,6 +44,8 @@ cc_library( mutation.h mutation_batch.cc mutation_batch.h + mutation_batch_result.cc + mutation_batch_result.h no_document.cc no_document.h patch_mutation.cc diff --git a/Firestore/core/src/firebase/firestore/model/database_id.cc b/Firestore/core/src/firebase/firestore/model/database_id.cc index 2d27bbc79cf..efec02eb4da 100644 --- a/Firestore/core/src/firebase/firestore/model/database_id.cc +++ b/Firestore/core/src/firebase/firestore/model/database_id.cc @@ -16,6 +16,8 @@ #include "Firestore/core/src/firebase/firestore/model/database_id.h" +#include + #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/hashing.h" @@ -40,6 +42,14 @@ util::ComparisonResult DatabaseId::CompareTo( return util::Compare(database_id(), rhs.database_id()); } +std::string DatabaseId::ToString() const { + return absl::StrCat("DatabaseId(", project_id(), ":", database_id(), ")"); +} + +std::ostream& operator<<(std::ostream& out, const DatabaseId& database_id) { + return out << database_id.ToString(); +} + size_t DatabaseId::Hash() const { return util::Hash(project_id(), database_id()); } diff --git a/Firestore/core/src/firebase/firestore/model/database_id.h b/Firestore/core/src/firebase/firestore/model/database_id.h index 3ec1be80815..6787f912db5 100644 --- a/Firestore/core/src/firebase/firestore/model/database_id.h +++ b/Firestore/core/src/firebase/firestore/model/database_id.h @@ -17,6 +17,7 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DATABASE_ID_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_DATABASE_ID_H_ +#include #include #include #include @@ -60,6 +61,10 @@ class DatabaseId : public util::Comparable { util::ComparisonResult CompareTo(const DatabaseId& rhs) const; + std::string ToString() const; + friend std::ostream& operator<<(std::ostream& out, + const DatabaseId& database_id); + size_t Hash() const; private: diff --git a/Firestore/core/src/firebase/firestore/model/document_map.h b/Firestore/core/src/firebase/firestore/model/document_map.h index 7df3c142ec7..3c4edfdcbee 100644 --- a/Firestore/core/src/firebase/firestore/model/document_map.h +++ b/Firestore/core/src/firebase/firestore/model/document_map.h @@ -23,7 +23,6 @@ #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/maybe_document.h" -#include "Firestore/core/src/firebase/firestore/objc/objc_class.h" #include "absl/base/attributes.h" #include "absl/types/optional.h" diff --git a/Firestore/core/src/firebase/firestore/model/field_path.cc b/Firestore/core/src/firebase/firestore/model/field_path.cc index 375c14bb924..f3a6c65193e 100644 --- a/Firestore/core/src/firebase/firestore/model/field_path.cc +++ b/Firestore/core/src/firebase/firestore/model/field_path.cc @@ -81,7 +81,7 @@ struct JoinEscaped { }; } // namespace -constexpr char FieldPath::kDocumentKeyPath[]; +constexpr const char* FieldPath::kDocumentKeyPath; FieldPath FieldPath::FromDotSeparatedString(absl::string_view path) { if (path.find_first_of("~*/[]") != absl::string_view::npos) { diff --git a/Firestore/core/src/firebase/firestore/model/field_path.h b/Firestore/core/src/firebase/firestore/model/field_path.h index 88072254d3f..3f1981043bd 100644 --- a/Firestore/core/src/firebase/firestore/model/field_path.h +++ b/Firestore/core/src/firebase/firestore/model/field_path.h @@ -38,7 +38,7 @@ class FieldPath : public impl::BasePath, public util::Comparable { public: /** The field path string that represents the document's key. */ - static constexpr char kDocumentKeyPath[] = "__name__"; + static constexpr const char* kDocumentKeyPath = "__name__"; // Note: Xcode 8.2 requires explicit specification of the constructor. FieldPath() : impl::BasePath() { diff --git a/Firestore/core/src/firebase/firestore/model/mutation.cc b/Firestore/core/src/firebase/firestore/model/mutation.cc index 1403190cbf9..28018417563 100644 --- a/Firestore/core/src/firebase/firestore/model/mutation.cc +++ b/Firestore/core/src/firebase/firestore/model/mutation.cc @@ -18,6 +18,7 @@ #include #include +#include #include #include "Firestore/core/src/firebase/firestore/model/document.h" @@ -25,11 +26,23 @@ #include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/model/no_document.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" +#include "absl/strings/str_cat.h" namespace firebase { namespace firestore { namespace model { +std::string MutationResult::ToString() const { + return absl::StrCat( + "MutationResult(version=", version_.ToString(), + ", transform_results=", util::ToString(transform_results_), ")"); +} + +std::ostream& operator<<(std::ostream& os, const MutationResult& result) { + return os << result.ToString(); +} + Mutation::Rep::Rep(DocumentKey&& key, Precondition&& precondition) : key_(std::move(key)), precondition_(std::move(precondition)) { } diff --git a/Firestore/core/src/firebase/firestore/model/mutation.h b/Firestore/core/src/firebase/firestore/model/mutation.h index 3868e897661..acd597212f0 100644 --- a/Firestore/core/src/firebase/firestore/model/mutation.h +++ b/Firestore/core/src/firebase/firestore/model/mutation.h @@ -78,6 +78,11 @@ class MutationResult { return transform_results_; } + std::string ToString() const; + + friend std::ostream& operator<<(std::ostream& os, + const MutationResult& result); + private: SnapshotVersion version_; absl::optional> transform_results_; diff --git a/Firestore/core/src/firebase/firestore/model/mutation_batch.cc b/Firestore/core/src/firebase/firestore/model/mutation_batch.cc index b674358fc33..1ceede2ebba 100644 --- a/Firestore/core/src/firebase/firestore/model/mutation_batch.cc +++ b/Firestore/core/src/firebase/firestore/model/mutation_batch.cc @@ -19,6 +19,7 @@ #include #include +#include "Firestore/core/src/firebase/firestore/model/mutation_batch_result.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/to_string.h" @@ -28,23 +29,110 @@ namespace model { MutationBatch::MutationBatch(int batch_id, Timestamp local_write_time, - std::vector&& mutations) + std::vector base_mutations, + std::vector mutations) : batch_id_(batch_id), local_write_time_(std::move(local_write_time)), + base_mutations_(std::move(base_mutations)), mutations_(std::move(mutations)) { HARD_ASSERT(!mutations_.empty(), "Cannot create an empty mutation batch"); } +absl::optional MutationBatch::ApplyToRemoteDocument( + absl::optional maybe_doc, + const DocumentKey& document_key, + const MutationBatchResult& mutation_batch_result) const { + HARD_ASSERT(!maybe_doc || maybe_doc->key() == document_key, + "ApplyTo: key %s doesn't match maybe_doc key %s", + document_key.ToString(), maybe_doc->key().ToString()); + + const auto& mutation_results = mutation_batch_result.mutation_results(); + HARD_ASSERT(mutation_results.size() == mutations_.size(), + "Mismatch between mutations length (%s) and results length (%s)", + mutations_.size(), mutation_results.size()); + + for (size_t i = 0; i < mutations_.size(); i++) { + const Mutation& mutation = mutations_[i]; + const MutationResult& mutation_result = mutation_results[i]; + if (mutation.key() == document_key) { + maybe_doc = mutation.ApplyToRemoteDocument(maybe_doc, mutation_result); + } + } + return maybe_doc; +} + +absl::optional MutationBatch::ApplyToLocalDocument( + absl::optional maybe_doc, + const DocumentKey& document_key) const { + HARD_ASSERT(!maybe_doc || maybe_doc->key() == document_key, + "key %s doesn't match maybe_doc key %s", document_key.ToString(), + maybe_doc->key().ToString()); + + // First, apply the base state. This allows us to apply non-idempotent + // transform against a consistent set of values. + for (const Mutation& mutation : base_mutations_) { + if (mutation.key() == document_key) { + maybe_doc = + mutation.ApplyToLocalView(maybe_doc, maybe_doc, local_write_time_); + } + } + + absl::optional base_doc = maybe_doc; + + // Second, apply all user-provided mutations. + for (const Mutation& mutation : mutations_) { + if (mutation.key() == document_key) { + maybe_doc = + mutation.ApplyToLocalView(maybe_doc, base_doc, local_write_time_); + } + } + return maybe_doc; +} + +MaybeDocumentMap MutationBatch::ApplyToLocalDocumentSet( + const MaybeDocumentMap& document_set) const { + // TODO(mrschmidt): This implementation is O(n^2). If we iterate through the + // mutations first (as done in `applyToLocalDocument:documentKey:`), we can + // reduce the complexity to O(n). + + MaybeDocumentMap mutated_documents = document_set; + for (const Mutation& mutation : mutations_) { + const DocumentKey& key = mutation.key(); + + absl::optional previous_document = + mutated_documents.get(key); + absl::optional mutated_document = + ApplyToLocalDocument(std::move(previous_document), key); + if (mutated_document) { + mutated_documents = mutated_documents.insert(key, *mutated_document); + } + } + return mutated_documents; +} + +DocumentKeySet MutationBatch::keys() const { + DocumentKeySet set; + for (const Mutation& mutation : mutations_) { + set = set.insert(mutation.key()); + } + return set; +} + bool operator==(const MutationBatch& lhs, const MutationBatch& rhs) { return lhs.batch_id() == rhs.batch_id() && lhs.local_write_time() == rhs.local_write_time() && + lhs.base_mutations() == rhs.base_mutations() && lhs.mutations() == rhs.mutations(); } +std::string MutationBatch::ToString() const { + return absl::StrCat("MutationBatch(id=", batch_id_, + ", local_write_time=", local_write_time_.ToString(), + ", mutations=", util::ToString(mutations_), ")"); +} + std::ostream& operator<<(std::ostream& os, const MutationBatch& batch) { - return os << "MutationBatch(id=" << batch.batch_id_ - << ", local_write_time=" << batch.local_write_time_ - << ", mutations=" << util::ToString(batch.mutations_) << ")"; + return os << batch.ToString(); } } // namespace model diff --git a/Firestore/core/src/firebase/firestore/model/mutation_batch.h b/Firestore/core/src/firebase/firestore/model/mutation_batch.h index 3055d451c17..3d3d8515884 100644 --- a/Firestore/core/src/firebase/firestore/model/mutation_batch.h +++ b/Firestore/core/src/firebase/firestore/model/mutation_batch.h @@ -19,9 +19,12 @@ #include #include +#include #include #include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" #include "Firestore/core/src/firebase/firestore/model/mutation.h" #include "Firestore/core/src/firebase/firestore/model/types.h" @@ -29,9 +32,11 @@ namespace firebase { namespace firestore { namespace model { +class MutationBatchResult; + /** - * A BatchID that was searched for and not found or a batch ID value known to be - * before all known batches. + * A BatchID that was searched for and not found or a batch ID value known to + * be before all known batches. * * BatchId values from the local store are non-negative so this value is before * all batches. @@ -48,8 +53,8 @@ constexpr BatchId kBatchIdUnknown = -1; class MutationBatch { public: /** - * A batch ID that was searched for and not found or a batch ID value known to - * be before all known batches. + * A batch ID that was searched for and not found or a batch ID value known + * to be before all known batches. * * Batch ID values from the local store are non-negative so this value is * before all batches. @@ -58,12 +63,10 @@ class MutationBatch { MutationBatch(int batch_id, Timestamp local_write_time, - std::vector&& mutations); - - // TODO(rsgowman): Port ApplyToRemoteDocument() - // TODO(rsgowman): Port ApplyToLocalView() - // TODO(rsgowman): Port GetKeys() + std::vector base_mutations, + std::vector mutations); + /** The unique ID of this mutation batch. */ int batch_id() const { return batch_id_; } @@ -76,17 +79,77 @@ class MutationBatch { return local_write_time_; } + /** + * Mutations that are used to populate the base values when this mutation is + * applied locally. This can be used to locally overwrite values that are + * persisted in the remote document cache. Base mutations are never sent to + * the backend. + */ + const std::vector& base_mutations() const { + return base_mutations_; + } + + /** + * The user-provided mutations in this mutation batch. User-provided + * mutations are applied both locally and remotely on the backend. + */ const std::vector& mutations() const { return mutations_; } + /** + * Applies all the mutations in this MutationBatch to the specified document + * to create a new remote document. + * + * @param maybe_doc The document to which to apply mutations or nullopt if + * there's no existing document. + * @param document_key The key of the document to apply mutations to. + * @param mutation_batch_result The result of applying the MutationBatch to + * the backend. + */ + absl::optional ApplyToRemoteDocument( + absl::optional maybe_doc, + const DocumentKey& document_key, + const MutationBatchResult& mutation_batch_result) const; + + /** + * Estimates the latency compensated view of all the mutations in this batch + * applied to the given MaybeDocument. + * + * Unlike ApplyToRemoteDocument, this method is used before the mutation has + * been committed and so it's possible that the mutation is operating on a + * locally non-existent document and may produce a non-existent document. + * + * @param maybe_doc The document to which to apply mutations or nullopt if + * there's no existing document. + * @param document_key The key of the document to apply mutations to. + */ + absl::optional ApplyToLocalDocument( + absl::optional maybe_doc, + const DocumentKey& document_key) const; + + /** + * Computes the local view for all provided documents given the mutations in + * this batch. + */ + MaybeDocumentMap ApplyToLocalDocumentSet( + const MaybeDocumentMap& document_set) const; + + /** + * Returns the set of unique keys referenced by all mutations in the batch. + */ + DocumentKeySet keys() const; + friend bool operator==(const MutationBatch& lhs, const MutationBatch& rhs); + std::string ToString() const; + friend std::ostream& operator<<(std::ostream& os, const MutationBatch& batch); private: int batch_id_; - const Timestamp local_write_time_; + Timestamp local_write_time_; + std::vector base_mutations_; std::vector mutations_; }; diff --git a/Firestore/core/src/firebase/firestore/model/mutation_batch_result.cc b/Firestore/core/src/firebase/firestore/model/mutation_batch_result.cc new file mode 100644 index 00000000000..d79b041ee23 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/model/mutation_batch_result.cc @@ -0,0 +1,72 @@ +/* + * Copyright 2019 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/model/mutation_batch_result.h" + +#include +#include + +#include "Firestore/core/src/firebase/firestore/model/mutation.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" +#include "absl/strings/str_cat.h" + +namespace firebase { +namespace firestore { +namespace model { + +MutationBatchResult::MutationBatchResult( + MutationBatch batch, + model::SnapshotVersion commit_version, + std::vector mutation_results, + nanopb::ByteString stream_token) + : batch_(std::move(batch)), + commit_version_(commit_version), + mutation_results_(std::move(mutation_results)), + stream_token_(std::move(stream_token)) { + HARD_ASSERT(batch_.mutations().size() == mutation_results_.size(), + "Number of mutations sent %s must equal results received %s", + batch_.mutations().size(), mutation_results_.size()); + + const auto& mutations = batch_.mutations(); + for (size_t i = 0; i < mutations.size(); i++) { + absl::optional version = mutation_results_[i].version(); + if (!version) { + // Deletes don't have a version, so we substitute the commit_version + // of the entire batch. + version = commit_version_; + } + + const DocumentKey& key = mutations[i].key(); + doc_versions_[key] = *version; + } +} + +std::string MutationBatchResult::ToString() const { + return absl::StrCat("MutationBatchResult(batch=", batch_.ToString(), + ", commit_version=", commit_version_.ToString(), + ", mutation_results=", util::ToString(mutation_results_), + ", stream_token=", util::ToString(stream_token_), ")"); +} + +std::ostream& operator<<(std::ostream& os, const MutationBatchResult& result) { + return os << result.ToString(); +} + +} // namespace model +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/model/mutation_batch_result.h b/Firestore/core/src/firebase/firestore/model/mutation_batch_result.h new file mode 100644 index 00000000000..447dfab507e --- /dev/null +++ b/Firestore/core/src/firebase/firestore/model/mutation_batch_result.h @@ -0,0 +1,96 @@ +/* + * Copyright 2019 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_MODEL_MUTATION_BATCH_RESULT_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_MUTATION_BATCH_RESULT_H_ + +#include +#include +#include +#include +#include + +#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/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" +#include "Firestore/core/src/firebase/firestore/model/mutation.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" + +namespace firebase { +namespace firestore { +namespace model { + +using DocumentVersionMap = + std::unordered_map; + +/** + * The result of applying a mutation batch to the backend. + * + * Note that unlike most classes in firebase::firestore::model, this class is + * just a grouping of result values and is not optimized for copying. + */ +class MutationBatchResult { + public: + /** + * Creates a new MutationBatchResult for the given batch and results. There + * must be one result for each mutation in the batch. This constructor caches + * a document=>version mapping (as doc_versions()). + */ + MutationBatchResult(MutationBatch batch, + SnapshotVersion commit_version, + std::vector mutation_results, + nanopb::ByteString stream_token); + + const MutationBatch& batch() const { + return batch_; + } + + const SnapshotVersion& commit_version() const { + return commit_version_; + } + + const std::vector& mutation_results() const { + return mutation_results_; + } + + const nanopb::ByteString& stream_token() const { + return stream_token_; + } + + const DocumentVersionMap& doc_versions() const { + return doc_versions_; + } + + std::string ToString() const; + + friend std::ostream& operator<<(std::ostream& os, + const MutationBatchResult& result); + + private: + MutationBatch batch_; + SnapshotVersion commit_version_; + std::vector mutation_results_; + nanopb::ByteString stream_token_; + DocumentVersionMap doc_versions_; +}; + +} // namespace model +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_MODEL_MUTATION_BATCH_RESULT_H_ diff --git a/Firestore/core/src/firebase/firestore/model/snapshot_version.cc b/Firestore/core/src/firebase/firestore/model/snapshot_version.cc index 93667588b14..eed605faff7 100644 --- a/Firestore/core/src/firebase/firestore/model/snapshot_version.cc +++ b/Firestore/core/src/firebase/firestore/model/snapshot_version.cc @@ -18,6 +18,8 @@ #include +#include "Firestore/core/src/firebase/firestore/util/hashing.h" + namespace firebase { namespace firestore { namespace model { @@ -37,7 +39,7 @@ util::ComparisonResult SnapshotVersion::CompareTo( } size_t SnapshotVersion::Hash() const { - return std::hash{}(timestamp_); + return util::Hash(timestamp_.seconds(), timestamp_.nanoseconds()); } std::string SnapshotVersion::ToString() const { diff --git a/Firestore/core/src/firebase/firestore/model/types.h b/Firestore/core/src/firebase/firestore/model/types.h index 39eb96acbef..d1f2bb7a5fb 100644 --- a/Firestore/core/src/firebase/firestore/model/types.h +++ b/Firestore/core/src/firebase/firestore/model/types.h @@ -19,10 +19,6 @@ #include -#if defined(__OBJC__) -#import -#endif - namespace firebase { namespace firestore { namespace model { diff --git a/Firestore/core/src/firebase/firestore/nanopb/byte_string.cc b/Firestore/core/src/firebase/firestore/nanopb/byte_string.cc index 5c119a4f1e0..6123c002097 100644 --- a/Firestore/core/src/firebase/firestore/nanopb/byte_string.cc +++ b/Firestore/core/src/firebase/firestore/nanopb/byte_string.cc @@ -16,9 +16,12 @@ #include "Firestore/core/src/firebase/firestore/nanopb/byte_string.h" +#include #include #include +#include #include +#include #include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/util/hashing.h" @@ -87,14 +90,18 @@ size_t ByteString::Hash() const { } std::string ByteString::ToString() const { - std::string hex = absl::BytesToHexString(MakeStringView(*this)); - return absl::StrCat("<", hex, ">"); + return absl::CEscape(MakeStringView(*this)); } std::ostream& operator<<(std::ostream& out, const ByteString& str) { return out << str.ToString(); } +std::string ByteString::ToHexString() const { + std::string hex = absl::BytesToHexString(MakeStringView(*this)); + return absl::StrCat("<", hex, ">"); +} + } // namespace nanopb } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/nanopb/byte_string.h b/Firestore/core/src/firebase/firestore/nanopb/byte_string.h index 3d01f97ae4a..d22a6fdbf9c 100644 --- a/Firestore/core/src/firebase/firestore/nanopb/byte_string.h +++ b/Firestore/core/src/firebase/firestore/nanopb/byte_string.h @@ -147,9 +147,14 @@ class ByteString : public util::Comparable { size_t Hash() const; + // Interprets the value as an ASCII string; the way control characters are + // represented is implementation-defined. std::string ToString() const; friend std::ostream& operator<<(std::ostream& out, const ByteString& str); + // Represents the value as hexademical values. + std::string ToHexString() const; + private: /** * Private constructor directly assigns to bytes_. The extra integer tag diff --git a/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.cc b/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.cc index a38c84d7e2d..f23b100b758 100644 --- a/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.cc +++ b/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.cc @@ -22,12 +22,14 @@ namespace firebase { namespace firestore { namespace nanopb { -pb_bytes_array_t* CopyBytesArray(const pb_bytes_array_t* buffer) { +pb_bytes_array_t* _Nullable CopyBytesArray( + const pb_bytes_array_t* _Nullable buffer) { if (buffer == nullptr) return nullptr; return MakeBytesArray(buffer->bytes, buffer->size); } -pb_bytes_array_t* MakeBytesArray(const void* data, size_t size) { +pb_bytes_array_t* _Nullable MakeBytesArray(const void* _Nullable data, + size_t size) { if (size == 0) return nullptr; pb_size_t pb_size = CheckedSize(size); @@ -47,7 +49,7 @@ pb_bytes_array_t* MakeBytesArray(const void* data, size_t size) { return result; } -std::string MakeString(const pb_bytes_array_t* str) { +std::string MakeString(const pb_bytes_array_t* _Nullable str) { if (str == nullptr) return ""; auto bytes = reinterpret_cast(str->bytes); @@ -55,7 +57,7 @@ std::string MakeString(const pb_bytes_array_t* str) { return std::string{bytes, size}; } -absl::string_view MakeStringView(const pb_bytes_array_t* str) { +absl::string_view MakeStringView(const pb_bytes_array_t* _Nullable str) { if (str == nullptr) return absl::string_view(nullptr, 0); auto bytes = reinterpret_cast(str->bytes); diff --git a/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h b/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h index 02383d8b612..c8bbbe83832 100644 --- a/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h +++ b/Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h @@ -26,6 +26,7 @@ #include "Firestore/core/src/firebase/firestore/nanopb/byte_string.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/nullability.h" namespace firebase { namespace firestore { @@ -45,37 +46,40 @@ inline pb_size_t CheckedSize(size_t size) { * Creates a new, null-terminated byte array that's a copy of the bytes in the * given buffer. Returns a null instance if the given buffer is null or empty. */ -pb_bytes_array_t* CopyBytesArray(const pb_bytes_array_t* buffer); +pb_bytes_array_t* _Nullable CopyBytesArray( + const pb_bytes_array_t* _Nullable buffer); /** * Creates a new, null-terminated byte array that's a copy of the given bytes. * Returns a null instance if the given size is zero. */ -pb_bytes_array_t* MakeBytesArray(const void* data, size_t size); +pb_bytes_array_t* _Nullable MakeBytesArray(const void* _Nullable data, + size_t size); /** * Creates a new, null-terminated byte array that's a copy of the given bytes. * Returns a null instance if the size of the given vector is zero. */ -inline pb_bytes_array_t* MakeBytesArray(const std::vector& bytes) { +inline pb_bytes_array_t* _Nullable MakeBytesArray( + const std::vector& bytes) { return MakeBytesArray(bytes.data(), bytes.size()); } /** * Creates a string_view of the given nanopb bytes. */ -absl::string_view MakeStringView(const pb_bytes_array_t* str); +absl::string_view MakeStringView(const pb_bytes_array_t* _Nullable str); /** * Creates a string_view of the given nanopb bytes. */ absl::string_view MakeStringView(const ByteString& bytes); -inline pb_bytes_array_t* MakeBytesArray(const std::string& str) { +inline pb_bytes_array_t* _Nullable MakeBytesArray(const std::string& str) { return MakeBytesArray(str.data(), str.size()); } -std::string MakeString(const pb_bytes_array_t* str); +std::string MakeString(const pb_bytes_array_t* _Nullable str); /** * Copies the backing byte array into a new vector of bytes. @@ -85,14 +89,21 @@ inline std::vector MakeVector(const ByteString& str) { } #if __OBJC__ -inline ByteString MakeByteString(NSData* value) { +inline ByteString MakeByteString(NSData* _Nullable value) { + if (value == nil) return ByteString(); + auto size = static_cast(value.length); return ByteString::Take(MakeBytesArray(value.bytes, size)); } -inline NSData* MakeNSData(const ByteString& str) { +inline NSData* _Nonnull MakeNSData(const ByteString& str) { return [[NSData alloc] initWithBytes:str.data() length:str.size()]; } + +inline NSData* _Nullable MakeNullableNSData(const ByteString& str) { + if (str.empty()) return nil; + return MakeNSData(str); +} #endif } // namespace nanopb diff --git a/Firestore/core/src/firebase/firestore/objc/objc_class.h b/Firestore/core/src/firebase/firestore/objc/objc_class.h index 11f60b529df..bc3a7619019 100644 --- a/Firestore/core/src/firebase/firestore/objc/objc_class.h +++ b/Firestore/core/src/firebase/firestore/objc/objc_class.h @@ -154,20 +154,6 @@ bool Equals(const Handle& lhs, const Handle& rhs) { } #endif -// Define NS_ASSUME_NONNULL_BEGIN for straight C++ so that everything gets the -// correct nullability specifier. -#if !defined(NS_ASSUME_NONNULL_BEGIN) -#if __clang__ -#define NS_ASSUME_NONNULL_BEGIN _Pragma("clang assume_nonnull begin") -#define NS_ASSUME_NONNULL_END _Pragma("clang assume_nonnull end") - -#else // !__clang__ -#define NS_ASSUME_NONNULL_BEGIN -#define NS_ASSUME_NONNULL_END -#define _Nullable -#endif // __clang__ -#endif // !defined(NS_ASSUME_NONNULL_BEGIN) - } // namespace objc } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/remote/CMakeLists.txt b/Firestore/core/src/firebase/firestore/remote/CMakeLists.txt index 0c221c955bd..20ce0c6705a 100644 --- a/Firestore/core/src/firebase/firestore/remote/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/remote/CMakeLists.txt @@ -82,6 +82,8 @@ cc_library( grpc_util.cc grpc_util.cc grpc_util.h + remote_event.cc + remote_event.h serializer.cc serializer.h watch_change.h diff --git a/Firestore/core/src/firebase/firestore/remote/connectivity_monitor_apple.mm b/Firestore/core/src/firebase/firestore/remote/connectivity_monitor_apple.mm index ccf53db53b1..3f6c0dfe29f 100644 --- a/Firestore/core/src/firebase/firestore/remote/connectivity_monitor_apple.mm +++ b/Firestore/core/src/firebase/firestore/remote/connectivity_monitor_apple.mm @@ -24,7 +24,6 @@ #include -#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "absl/memory/memory.h" @@ -37,7 +36,6 @@ using NetworkStatus = ConnectivityMonitor::NetworkStatus; using util::AsyncQueue; -using util::ExecutorLibdispatch; NetworkStatus ToNetworkStatus(SCNetworkReachabilityFlags flags) { if (!(flags & kSCNetworkReachabilityFlagsReachable)) { @@ -124,7 +122,7 @@ explicit ConnectivityMonitorApple( } void OnReachabilityChanged(SCNetworkReachabilityFlags flags) { - queue()->ExecuteBlocking( + queue()->Enqueue( [this, flags] { MaybeInvokeCallbacks(ToNetworkStatus(flags)); }); } diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.h b/Firestore/core/src/firebase/firestore/remote/datastore.h index 77444e5737f..e5ff3d6caf8 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.h +++ b/Firestore/core/src/firebase/firestore/remote/datastore.h @@ -21,8 +21,6 @@ #error "This header only supports Objective-C++" #endif // !defined(__OBJC__) -#import - #include #include #include @@ -39,14 +37,11 @@ #include "Firestore/core/src/firebase/firestore/remote/write_stream.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/executor.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/strings/string_view.h" #include "grpcpp/completion_queue.h" #include "grpcpp/support/status.h" -#import "Firestore/Source/Core/FSTTypes.h" - namespace firebase { namespace firestore { namespace remote { @@ -195,7 +190,7 @@ class Datastore : public std::enable_shared_from_this { // shared for all spawned gRPC streams and calls). std::unique_ptr rpc_executor_; grpc::CompletionQueue grpc_queue_; - // TODO(varconst): move `ConnectivityMonitor` to `FSTFirestoreClient`. + // TODO(varconst): move `ConnectivityMonitor` to `FirestoreClient`. std::unique_ptr connectivity_monitor_; GrpcConnection grpc_connection_; diff --git a/Firestore/core/src/firebase/firestore/remote/datastore.mm b/Firestore/core/src/firebase/firestore/remote/datastore.mm index f7563946ad4..fe17ee42cc0 100644 --- a/Firestore/core/src/firebase/firestore/remote/datastore.mm +++ b/Firestore/core/src/firebase/firestore/remote/datastore.mm @@ -35,7 +35,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" -#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/statusor.h" @@ -57,15 +57,12 @@ using util::Status; using util::StatusOr; using util::Executor; -using util::ExecutorLibdispatch; const auto kRpcNameCommit = "/google.firestore.v1.Firestore/Commit"; const auto kRpcNameLookup = "/google.firestore.v1.Firestore/BatchGetDocuments"; std::unique_ptr CreateExecutor() { - auto queue = dispatch_queue_create("com.google.firebase.firestore.rpc", - DISPATCH_QUEUE_SERIAL); - return absl::make_unique(queue); + return Executor::CreateSerial("com.google.firebase.firestore.rpc"); } std::string MakeString(grpc::string_ref grpc_str) { diff --git a/Firestore/core/src/firebase/firestore/remote/exponential_backoff.cc b/Firestore/core/src/firebase/firestore/remote/exponential_backoff.cc index 7ecde33bdce..7239df9e6eb 100644 --- a/Firestore/core/src/firebase/firestore/remote/exponential_backoff.cc +++ b/Firestore/core/src/firebase/firestore/remote/exponential_backoff.cc @@ -27,11 +27,26 @@ namespace firebase { namespace firestore { namespace remote { +namespace { using firebase::firestore::util::AsyncQueue; using firebase::firestore::util::TimerId; +using Milliseconds = util::AsyncQueue::Milliseconds; namespace chr = std::chrono; +/** + * Initial backoff time in milliseconds after an error. Set to 1s according to + * https://cloud.google.com/apis/design/errors. + */ +constexpr Milliseconds kDefaultBackoffInitialDelay = Milliseconds(1000); + +constexpr double kDefaultBackoffFactor = 1.5; + +/** Maximum backoff time in milliseconds. */ +constexpr Milliseconds kDefaultBackoffMaxDelay = Milliseconds(60 * 1000); + +} // namespace + ExponentialBackoff::ExponentialBackoff(const std::shared_ptr& queue, TimerId timer_id, double backoff_factor, @@ -53,6 +68,15 @@ ExponentialBackoff::ExponentialBackoff(const std::shared_ptr& queue, "Initial delay can't be greater than max delay"); } +ExponentialBackoff::ExponentialBackoff(const std::shared_ptr& queue, + TimerId timer_id) + : ExponentialBackoff(queue, + timer_id, + kDefaultBackoffFactor, + kDefaultBackoffInitialDelay, + kDefaultBackoffMaxDelay) { +} + void ExponentialBackoff::BackoffAndRun(AsyncQueue::Operation&& operation) { Cancel(); @@ -89,15 +113,14 @@ void ExponentialBackoff::BackoffAndRun(AsyncQueue::Operation&& operation) { chr::duration_cast(current_base_ * backoff_factor_)); } -ExponentialBackoff::Milliseconds ExponentialBackoff::GetDelayWithJitter() { +Milliseconds ExponentialBackoff::GetDelayWithJitter() { std::uniform_real_distribution distribution; double random_double = distribution(secure_random_); return chr::duration_cast((random_double - 0.5) * current_base_); } -ExponentialBackoff::Milliseconds ExponentialBackoff::ClampDelay( - Milliseconds delay) const { +Milliseconds ExponentialBackoff::ClampDelay(Milliseconds delay) const { if (delay < initial_delay_) { return initial_delay_; } diff --git a/Firestore/core/src/firebase/firestore/remote/exponential_backoff.h b/Firestore/core/src/firebase/firestore/remote/exponential_backoff.h index 34eff0572f9..bc27a83c4bb 100644 --- a/Firestore/core/src/firebase/firestore/remote/exponential_backoff.h +++ b/Firestore/core/src/firebase/firestore/remote/exponential_backoff.h @@ -61,6 +61,12 @@ class ExponentialBackoff { util::AsyncQueue::Milliseconds initial_delay, util::AsyncQueue::Milliseconds max_delay); + /** + * Instantiates the exponential backoff with the default values. + */ + ExponentialBackoff(const std::shared_ptr& queue, + util::TimerId timer_id); + /** * Resets the backoff delay. * @@ -94,7 +100,6 @@ class ExponentialBackoff { private: using Milliseconds = util::AsyncQueue::Milliseconds; - // Returns a random value in the range [-current_base_/2, current_base_/2]. Milliseconds GetDelayWithJitter(); Milliseconds ClampDelay(Milliseconds delay) const; diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_call.h b/Firestore/core/src/firebase/firestore/remote/grpc_call.h index 6cd0c33c379..8c2b38e2647 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_call.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_call.h @@ -19,7 +19,7 @@ #include -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "grpcpp/client_context.h" #include "grpcpp/support/string_ref.h" diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_completion.h b/Firestore/core/src/firebase/firestore/remote/grpc_completion.h index 4cd0b6e45b8..72c9b8b3dcf 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_completion.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_completion.h @@ -24,7 +24,7 @@ #include #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "grpcpp/support/byte_buffer.h" namespace firebase { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_connection.cc b/Firestore/core/src/firebase/firestore/remote/grpc_connection.cc index 5e15fe588cf..09028713222 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_connection.cc +++ b/Firestore/core/src/firebase/firestore/remote/grpc_connection.cc @@ -29,6 +29,7 @@ #include "Firestore/core/src/firebase/firestore/util/filesystem.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "Firestore/core/src/firebase/firestore/util/string_format.h" #include "absl/memory/memory.h" #include "grpcpp/create_channel.h" diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_stream.cc b/Firestore/core/src/firebase/firestore/remote/grpc_stream.cc index 778db1836da..85c4ebec563 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_stream.cc +++ b/Firestore/core/src/firebase/firestore/remote/grpc_stream.cc @@ -22,6 +22,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_util.h" #include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_stream.h b/Firestore/core/src/firebase/firestore/remote/grpc_stream.h index 186233aa39d..9f7d251ce32 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_stream.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_stream.h @@ -31,7 +31,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_completion.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_stream_observer.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/types/optional.h" #include "grpcpp/client_context.h" SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_stream_observer.h b/Firestore/core/src/firebase/firestore/remote/grpc_stream_observer.h index 0b0980b84be..52981206cea 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_stream_observer.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_stream_observer.h @@ -17,7 +17,7 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_GRPC_STREAM_OBSERVER_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_GRPC_STREAM_OBSERVER_H_ -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "grpcpp/support/byte_buffer.h" namespace firebase { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.cc b/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.cc index 22e70b20f36..5551143ff81 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.cc +++ b/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.cc @@ -20,6 +20,8 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.h b/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.h index 078c2c10adf..ca662576dcc 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.h @@ -26,8 +26,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_stream.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_stream_observer.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "grpcpp/client_context.h" SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() #include "grpcpp/generic/generic_stub.h" diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.cc b/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.cc index 4b127162264..15d5c73fd1b 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.cc +++ b/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.cc @@ -20,6 +20,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_util.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h b/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h index d3e9bba7c01..751d66f3f61 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h @@ -26,8 +26,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_call.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_completion.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "grpcpp/client_context.h" SUPPRESS_DOCUMENTATION_WARNINGS_BEGIN() #include "grpcpp/generic/generic_stub.h" diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_util.cc b/Firestore/core/src/firebase/firestore/remote/grpc_util.cc index d9ba3905453..a5f88f1e785 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_util.cc +++ b/Firestore/core/src/firebase/firestore/remote/grpc_util.cc @@ -17,6 +17,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_util.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/remote/grpc_util.h b/Firestore/core/src/firebase/firestore/remote/grpc_util.h index 89b8c7f37b9..2cba6e39584 100644 --- a/Firestore/core/src/firebase/firestore/remote/grpc_util.h +++ b/Firestore/core/src/firebase/firestore/remote/grpc_util.h @@ -17,7 +17,7 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_GRPC_UTIL_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_GRPC_UTIL_H_ -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "grpcpp/support/status.h" namespace firebase { diff --git a/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc index afdc69d93d4..36e083b68cd 100644 --- a/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc +++ b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.cc @@ -21,6 +21,7 @@ #include "Firestore/core/src/firebase/firestore/util/executor.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/string_format.h" namespace chr = std::chrono; diff --git a/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h index 6edc66aee20..601a08266e8 100644 --- a/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h +++ b/Firestore/core/src/firebase/firestore/remote/online_state_tracker.h @@ -24,7 +24,7 @@ #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.mm b/Firestore/core/src/firebase/firestore/remote/remote_event.cc similarity index 91% rename from Firestore/core/src/firebase/firestore/remote/remote_event.mm rename to Firestore/core/src/firebase/firestore/remote/remote_event.cc index 1a3341fa788..93fb7876371 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.cc @@ -18,8 +18,7 @@ #include -#import "Firestore/Source/Local/FSTQueryData.h" - +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/no_document.h" namespace firebase { @@ -28,17 +27,20 @@ using core::DocumentViewChange; using core::Query; +using local::QueryData; +using local::QueryPurpose; using model::DocumentKey; using model::DocumentKeySet; using model::MaybeDocument; using model::NoDocument; using model::SnapshotVersion; using model::TargetId; +using nanopb::ByteString; // TargetChange bool operator==(const TargetChange& lhs, const TargetChange& rhs) { - return [lhs.resume_token() isEqualToData:rhs.resume_token()] && + return lhs.resume_token() == rhs.resume_token() && lhs.current() == rhs.current() && lhs.added_documents() == rhs.added_documents() && lhs.modified_documents() == rhs.modified_documents() && @@ -47,13 +49,10 @@ // TargetState -TargetState::TargetState() : resume_token_{[NSData data]} { -} - -void TargetState::UpdateResumeToken(NSData* resume_token) { - if (resume_token.length > 0) { +void TargetState::UpdateResumeToken(ByteString resume_token) { + if (!resume_token.empty()) { has_pending_changes_ = true; - resume_token_ = [resume_token copy]; + resume_token_ = std::move(resume_token); } } @@ -67,13 +66,13 @@ DocumentViewChange::Type change_type = entry.second; switch (change_type) { - case DocumentViewChange::Type::kAdded: + case DocumentViewChange::Type::Added: added_documents = added_documents.insert(document_key); break; - case DocumentViewChange::Type::kModified: + case DocumentViewChange::Type::Modified: modified_documents = modified_documents.insert(document_key); break; - case DocumentViewChange::Type::kRemoved: + case DocumentViewChange::Type::Removed: removed_documents = removed_documents.insert(document_key); break; default: @@ -215,9 +214,9 @@ TargetId target_id = existence_filter.target_id(); int expected_count = existence_filter.filter().count(); - FSTQueryData* query_data = QueryDataForActiveTarget(target_id); + absl::optional query_data = QueryDataForActiveTarget(target_id); if (query_data) { - const Query& query = query_data.query; + const Query& query = query_data->query(); if (query.IsDocumentQuery()) { if (expected_count == 0) { // The existence filter told us the document does not exist. We deduce @@ -256,14 +255,14 @@ TargetId target_id = entry.first; TargetState& target_state = entry.second; - FSTQueryData* query_data = QueryDataForActiveTarget(target_id); + absl::optional query_data = QueryDataForActiveTarget(target_id); if (query_data) { - if (target_state.current() && query_data.query.IsDocumentQuery()) { + if (target_state.current() && query_data->query().IsDocumentQuery()) { // Document queries for document that don't exist can produce an empty // result set. To update our local cache, we synthesize a document // delete if we have not previously received the document. This resolves // the limbo state of the document, removing it from limboDocumentRefs. - DocumentKey key{query_data.query.path()}; + DocumentKey key{query_data->query().path()}; if (pending_document_updates_.find(key) == pending_document_updates_.end() && !TargetContainsDocument(target_id, key)) { @@ -291,8 +290,10 @@ bool is_only_limbo_target = true; for (TargetId target_id : entry.second) { - FSTQueryData* query_data = QueryDataForActiveTarget(target_id); - if (query_data && query_data.purpose != FSTQueryPurposeLimboResolution) { + absl::optional query_data = + QueryDataForActiveTarget(target_id); + if (query_data && + query_data->purpose() != QueryPurpose::LimboResolution) { is_only_limbo_target = false; break; } @@ -325,8 +326,8 @@ DocumentViewChange::Type change_type = TargetContainsDocument(target_id, document.key()) - ? DocumentViewChange::Type::kModified - : DocumentViewChange::Type::kAdded; + ? DocumentViewChange::Type::Modified + : DocumentViewChange::Type::Added; TargetState& target_state = EnsureTargetState(target_id); target_state.AddDocumentChange(document.key(), change_type); @@ -345,7 +346,7 @@ TargetState& target_state = EnsureTargetState(target_id); if (TargetContainsDocument(target_id, key)) { - target_state.AddDocumentChange(key, DocumentViewChange::Type::kRemoved); + target_state.AddDocumentChange(key, DocumentViewChange::Type::Removed); } else { // The document may have entered and left the target before we raised a // snapshot, so we can just ignore the change. @@ -382,15 +383,15 @@ } bool WatchChangeAggregator::IsActiveTarget(TargetId target_id) const { - return QueryDataForActiveTarget(target_id) != nil; + return QueryDataForActiveTarget(target_id) != absl::nullopt; } -FSTQueryData* WatchChangeAggregator::QueryDataForActiveTarget( +absl::optional WatchChangeAggregator::QueryDataForActiveTarget( TargetId target_id) const { auto target_state = target_states_.find(target_id); return target_state != target_states_.end() && target_state->second.IsPending() - ? nil + ? absl::optional{} : target_metadata_provider_->GetQueryDataForTarget(target_id); } diff --git a/Firestore/core/src/firebase/firestore/remote/remote_event.h b/Firestore/core/src/firebase/firestore/remote/remote_event.h index a8483b59120..ed519de6fb0 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_event.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_event.h @@ -17,14 +17,6 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_EVENT_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_EVENT_H_ -#if !defined(__OBJC__) -// TODO(varconst): the only dependency is `NSData` -// (used to represent the resume token). -#error "This header only supports Objective-C++" -#endif // !defined(__OBJC__) - -#import - #include #include #include @@ -32,17 +24,15 @@ #include #include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.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/maybe_document.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/nanopb/byte_string.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" -@class FSTQueryData; - -NS_ASSUME_NONNULL_BEGIN - namespace firebase { namespace firestore { namespace remote { @@ -64,10 +54,10 @@ class TargetMetadataProvider { model::TargetId target_id) const = 0; /** - * Returns the FSTQueryData for an active target ID or 'null' if this query - * has become inactive + * Returns the QueryData for an active target ID or `nullopt` if this query + * has become inactive. */ - virtual FSTQueryData* GetQueryDataForTarget( + virtual absl::optional GetQueryDataForTarget( model::TargetId target_id) const = 0; }; @@ -84,7 +74,7 @@ class TargetChange { public: TargetChange() = default; - TargetChange(NSData* resume_token, + TargetChange(nanopb::ByteString resume_token, bool current, model::DocumentKeySet added_documents, model::DocumentKeySet modified_documents, @@ -102,7 +92,7 @@ class TargetChange { * query. The resume token essentially identifies a point in time from which * the server should resume sending results. */ - NSData* resume_token() const { + const nanopb::ByteString& resume_token() const { return resume_token_; } @@ -140,7 +130,7 @@ class TargetChange { } private: - NSData* resume_token_ = nil; + nanopb::ByteString resume_token_; bool current_ = false; model::DocumentKeySet added_documents_; model::DocumentKeySet modified_documents_; @@ -152,8 +142,6 @@ bool operator==(const TargetChange& lhs, const TargetChange& rhs); /** Tracks the internal state of a Watch target. */ class TargetState { public: - TargetState(); - /** * Whether this target has been marked 'current'. * @@ -167,7 +155,7 @@ class TargetState { } /** The last resume token sent to us for this target. */ - NSData* resume_token() const { + const nanopb::ByteString& resume_token() const { return resume_token_; } @@ -185,7 +173,7 @@ class TargetState { * Applies the resume token to the `TargetChange`, but only when it has a new * value. Empty resume tokens are discarded. */ - void UpdateResumeToken(NSData* resume_token); + void UpdateResumeToken(nanopb::ByteString resume_token); /** * Creates a target change from the current set of changes. @@ -223,7 +211,7 @@ class TargetState { model::DocumentKeyHash> document_changes_; - NSData* resume_token_; + nanopb::ByteString resume_token_; bool current_ = false; @@ -398,11 +386,11 @@ class WatchChangeAggregator { bool IsActiveTarget(model::TargetId target_id) const; /** - * Returns the `FSTQueryData` for an active target (i.e., a target that the - * user is still interested in that has no outstanding target change - * requests). + * Returns the `QueryData` for an active target (i.e., a target that the user + * is still interested in that has no outstanding target change requests). */ - FSTQueryData* QueryDataForActiveTarget(model::TargetId target_id) const; + absl::optional QueryDataForActiveTarget( + model::TargetId target_id) const; /** * Resets the state of a Watch target to its initial state (e.g. sets @@ -445,6 +433,4 @@ class WatchChangeAggregator { } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_EVENT_H_ diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h index 60879fa312f..1c3184126c3 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h @@ -28,15 +28,16 @@ #include #include "Firestore/core/src/firebase/firestore/core/database_info.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/nanopb/byte_string.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" +#include "absl/types/optional.h" #include "grpcpp/support/byte_buffer.h" #import "Firestore/Protos/objc/google/firestore/v1/Firestore.pbobjc.h" -#import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" namespace firebase { @@ -67,7 +68,7 @@ class WatchStreamSerializer { : serializer_{serializer} { } - GCFSListenRequest* CreateWatchRequest(FSTQueryData* query) const; + GCFSListenRequest* CreateWatchRequest(const local::QueryData& query) const; GCFSListenRequest* CreateUnwatchRequest(model::TargetId target_id) const; static grpc::ByteBuffer ToByteBuffer(GCFSListenRequest* request); @@ -100,10 +101,10 @@ class WriteStreamSerializer { } void UpdateLastStreamToken(GCFSWriteResponse* proto); - void SetLastStreamToken(NSData* token) { + void SetLastStreamToken(const nanopb::ByteString& token) { last_stream_token_ = token; } - NSData* GetLastStreamToken() const { + nanopb::ByteString GetLastStreamToken() const { return last_stream_token_; } @@ -132,7 +133,7 @@ class WriteStreamSerializer { private: FSTSerializerBeta* serializer_; - NSData* last_stream_token_; + nanopb::ByteString last_stream_token_; }; /** diff --git a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm index e1481829316..f205e627783 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.mm @@ -25,6 +25,7 @@ #import "Firestore/Source/API/FIRFirestore+Internal.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" @@ -36,12 +37,15 @@ namespace bridge { using core::DatabaseInfo; +using local::QueryData; using model::DocumentKey; using model::MaybeDocument; using model::Mutation; using model::MutationResult; using model::TargetId; using model::SnapshotVersion; +using nanopb::MakeByteString; +using nanopb::MakeNSData; using util::MakeString; using util::MakeNSError; using util::Status; @@ -124,7 +128,7 @@ bool IsLoggingEnabled() { // WatchStreamSerializer GCFSListenRequest* WatchStreamSerializer::CreateWatchRequest( - FSTQueryData* query) const { + const QueryData& query) const { GCFSListenRequest* request = [GCFSListenRequest message]; request.database = [serializer_ encodedDatabaseID]; request.addTarget = [serializer_ encodedTarget:query]; @@ -171,7 +175,7 @@ bool IsLoggingEnabled() { // WriteStreamSerializer void WriteStreamSerializer::UpdateLastStreamToken(GCFSWriteResponse* proto) { - last_stream_token_ = proto.streamToken; + last_stream_token_ = MakeByteString(proto.streamToken); } GCFSWriteRequest* WriteStreamSerializer::CreateHandshake() const { @@ -191,7 +195,7 @@ bool IsLoggingEnabled() { GCFSWriteRequest* request = [GCFSWriteRequest message]; request.writesArray = protos; - request.streamToken = last_stream_token_; + request.streamToken = MakeNullableNSData(last_stream_token_); return request; } diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.h b/Firestore/core/src/firebase/firestore/remote/remote_store.h index f27180fd75d..03fc7f17968 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_store.h +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.h @@ -17,18 +17,15 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_STORE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_STORE_H_ -#if !defined(__OBJC__) -#error "This header only supports Objective-C++" -#endif // !defined(__OBJC__) - -#import - #include #include #include #include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/local/local_store.h" #include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" +#include "Firestore/core/src/firebase/firestore/model/mutation_batch_result.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/datastore.h" @@ -38,93 +35,84 @@ #include "Firestore/core/src/firebase/firestore/remote/watch_stream.h" #include "Firestore/core/src/firebase/firestore/remote/write_stream.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" - -@class FSTLocalStore; -@class FSTMutationBatch; -@class FSTMutationBatchResult; -@class FSTQueryData; -@class FSTTransaction; +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" -NS_ASSUME_NONNULL_BEGIN - -/** - * A protocol that describes the actions the `RemoteStore` needs to perform on - * a cooperating synchronization engine. - */ -@protocol FSTRemoteSyncer - -/** - * Applies one remote event to the sync engine, notifying any views of the - * changes, and releasing any pending mutation batches that would become visible - * because of the snapshot version the remote event contains. - */ -- (void)applyRemoteEvent: - (const firebase::firestore::remote::RemoteEvent&)remoteEvent; +namespace firebase { +namespace firestore { +namespace remote { /** - * Rejects the listen for the given targetID. This can be triggered by the - * backend for any active target. - * - * @param targetID The targetID corresponding to a listen initiated via - * `RemoteStore::Listen`. - * @param error A description of the condition that has forced the rejection. - * Nearly always this will be an indication that the user is no longer - * authorized to see the data matching the target. + * A callback interface for events from remote store. */ -- (void)rejectListenWithTargetID: - (const firebase::firestore::model::TargetId)targetID - error: - (NSError*)error; // NOLINT(readability/casting) +class RemoteStoreCallback { + public: + /** + * Applies a remote event to the sync engine, notifying any views of the + * changes, and releasing any pending mutation batches that would become + * visible because of the snapshot version the remote event contains. + */ + virtual void ApplyRemoteEvent(const RemoteEvent& remote_event) = 0; -/** - * Applies the result of a successful write of a mutation batch to the sync - * engine, emitting snapshots in any views that the mutation applies to, and - * removing the batch from the mutation queue. - */ -- (void)applySuccessfulWriteWithResult: - (FSTMutationBatchResult*)batchResult; // NOLINT(readability/casting) + /** + * Handles rejection of listen for the given target id. This can be triggered + * by the backend for any active target. + * + * @param target_id The target ID corresponding to a listen initiated via + * RemoteStore::Listen() + * @param error A description of the condition that has forced the rejection. + * Nearly always this will be an indication that the user is no longer + * authorized to see the data matching the target. + */ + virtual void HandleRejectedListen(model::TargetId target_id, + util::Status error) = 0; -/** - * Rejects the batch, removing the batch from the mutation queue, recomputing - * the local view of any documents affected by the batch and then, emitting - * snapshots with the reverted value. - */ -- (void) - rejectFailedWriteWithBatchID:(firebase::firestore::model::BatchId)batchID - error: - (NSError*)error; // NOLINT(readability/casting) + /** + * Applies the result of a successful write of a mutation batch to the sync + * engine, emitting snapshots in any views that the mutation applies to, and + * removing the batch from the mutation queue. + */ + virtual void HandleSuccessfulWrite( + const model::MutationBatchResult& batch_result) = 0; -/** - * Returns the set of remote document keys for the given target ID. This list - * includes the documents that were assigned to the target when we received the - * last snapshot. - */ -- (firebase::firestore::model::DocumentKeySet)remoteKeysForTarget: - (firebase::firestore::model::TargetId)targetId; + /** + * Rejects the batch, removing the batch from the mutation queue, recomputing + * the local view of any documents affected by the batch and then, emitting + * snapshots with the reverted value. + */ + virtual void HandleRejectedWrite(model::BatchId batch_id, + util::Status error) = 0; -@end + /** + * Applies an OnlineState change to the sync engine and notifies any views of + * the change. + */ + virtual void HandleOnlineStateChange(model::OnlineState online_state) = 0; -namespace firebase { -namespace firestore { -namespace remote { + /** + * Returns the set of remote document keys for the given target ID. This list + * includes the documents that were assigned to the target when we received + * the last snapshot. + */ + virtual model::DocumentKeySet GetRemoteKeys( + model::TargetId target_id) const = 0; +}; class RemoteStore : public TargetMetadataProvider, public WatchStreamCallback, public WriteStreamCallback { public: - RemoteStore(FSTLocalStore* local_store, + RemoteStore(local::LocalStore* local_store, std::shared_ptr datastore, const std::shared_ptr& worker_queue, std::function online_state_handler); - void set_sync_engine(id sync_engine) { + void set_sync_engine(RemoteStoreCallback* sync_engine) { sync_engine_ = sync_engine; } /** * Starts up the remote store, creating streams, restoring state from - * `FSTLocalStore`, etc. + * `LocalStore`, etc. */ void Start(); @@ -146,6 +134,8 @@ class RemoteStore : public TargetMetadataProvider, */ void EnableNetwork(); + bool CanUseNetwork() const; + /** * Tells the `RemoteStore` that the currently authenticated user has changed. * @@ -155,17 +145,17 @@ class RemoteStore : public TargetMetadataProvider, */ void HandleCredentialChange(); - /** Listens to the target identified by the given `FSTQueryData`. */ - void Listen(FSTQueryData* query_data); + /** Listens to the target identified by the given `QueryData`. */ + void Listen(const local::QueryData& query_data); /** Stops listening to the target with the given target ID. */ void StopListening(model::TargetId target_id); /** - * Attempts to fill our write pipeline with writes from the `FSTLocalStore`. + * Attempts to fill our write pipeline with writes from the `LocalStore`. * * Called internally to bootstrap or refill the write pipeline and by - * `FSTSyncEngine` whenever there are new mutations to process. + * `SyncEngine` whenever there are new mutations to process. * * Starts the write stream if necessary. */ @@ -175,7 +165,7 @@ class RemoteStore : public TargetMetadataProvider, * Queues additional writes to be sent to the write stream, sending them * immediately if the write stream is established. */ - void AddToWritePipeline(FSTMutationBatch* batch); + void AddToWritePipeline(const model::MutationBatch& batch); /** Returns a new transaction backed by this remote store. */ // TODO(c++14): return a plain value when it becomes possible to move @@ -184,7 +174,8 @@ class RemoteStore : public TargetMetadataProvider, model::DocumentKeySet GetRemoteKeysForTarget( model::TargetId target_id) const override; - FSTQueryData* GetQueryDataForTarget(model::TargetId target_id) const override; + absl::optional GetQueryDataForTarget( + model::TargetId target_id) const override; void OnWatchStreamOpen() override; void OnWatchStreamChange( @@ -202,7 +193,7 @@ class RemoteStore : public TargetMetadataProvider, private: void DisableNetworkInternal(); - void SendWatchRequest(FSTQueryData* query_data); + void SendWatchRequest(const local::QueryData& query_data); void SendUnwatchRequest(model::TargetId target_id); /** @@ -231,8 +222,6 @@ class RemoteStore : public TargetMetadataProvider, void HandleHandshakeError(const util::Status& status); void HandleWriteError(const util::Status& status); - bool CanUseNetwork() const; - void StartWatchStream(); /** @@ -243,13 +232,13 @@ class RemoteStore : public TargetMetadataProvider, void CleanUpWatchStreamState(); - id sync_engine_ = nil; + RemoteStoreCallback* sync_engine_ = nullptr; /** * The local store, used to fill the write pipeline with outbound mutations * and resolve existence filter mismatches. */ - FSTLocalStore* local_store_ = nil; + local::LocalStore* local_store_ = nullptr; /** The client-side proxy for interacting with the backend. */ std::shared_ptr datastore_; @@ -263,7 +252,7 @@ class RemoteStore : public TargetMetadataProvider, * to the server. The targets removed with unlistens are removed eagerly * without waiting for confirmation from the listen stream. */ - std::unordered_map listen_targets_; + std::unordered_map listen_targets_; OnlineStateTracker online_state_tracker_; @@ -294,13 +283,11 @@ class RemoteStore : public TargetMetadataProvider, * purely based on order, and so we can just remove writes from the front of * the `write_pipeline_` as we receive responses. */ - std::vector write_pipeline_; + std::vector write_pipeline_; }; } // namespace remote } // namespace firestore } // namespace firebase -NS_ASSUME_NONNULL_END - #endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_REMOTE_STORE_H_ diff --git a/Firestore/core/src/firebase/firestore/remote/remote_store.mm b/Firestore/core/src/firebase/firestore/remote/remote_store.mm index 905a88c1f35..204fdd3d65d 100644 --- a/Firestore/core/src/firebase/firestore/remote/remote_store.mm +++ b/Firestore/core/src/firebase/firestore/remote/remote_store.mm @@ -16,45 +16,40 @@ #include "Firestore/core/src/firebase/firestore/remote/remote_store.h" +#include #include -#import "Firestore/Source/Local/FSTLocalStore.h" -#import "Firestore/Source/Local/FSTQueryData.h" -#import "Firestore/Source/Model/FSTMutationBatch.h" - #include "Firestore/core/src/firebase/firestore/core/transaction.h" +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/mutation_batch.h" +#include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" +#include "Firestore/core/src/firebase/firestore/util/to_string.h" #include "absl/memory/memory.h" -using firebase::firestore::core::Transaction; -using firebase::firestore::model::BatchId; -using firebase::firestore::model::DocumentKeySet; -using firebase::firestore::model::MutationResult; -using firebase::firestore::model::OnlineState; -using firebase::firestore::model::SnapshotVersion; -using firebase::firestore::model::TargetId; -using firebase::firestore::model::kBatchIdUnknown; -using firebase::firestore::remote::Datastore; -using firebase::firestore::remote::WatchStream; -using firebase::firestore::remote::DocumentWatchChange; -using firebase::firestore::remote::ExistenceFilterWatchChange; -using firebase::firestore::remote::OnlineStateTracker; -using firebase::firestore::remote::RemoteEvent; -using firebase::firestore::remote::TargetChange; -using firebase::firestore::remote::WatchChange; -using firebase::firestore::remote::WatchChangeAggregator; -using firebase::firestore::remote::WatchTargetChange; -using firebase::firestore::remote::WatchTargetChangeState; -using firebase::firestore::util::AsyncQueue; -using firebase::firestore::util::Status; - namespace firebase { namespace firestore { namespace remote { +using core::Transaction; +using local::LocalStore; +using local::QueryData; +using local::QueryPurpose; +using model::BatchId; +using model::DocumentKeySet; +using model::MutationBatch; +using model::MutationBatchResult; +using model::MutationResult; +using model::OnlineState; +using model::SnapshotVersion; +using model::TargetId; +using model::kBatchIdUnknown; +using nanopb::ByteString; +using util::AsyncQueue; +using util::Status; + /** * The maximum number of pending writes to allow. * TODO(b/35853402): Negotiate this value with the backend. @@ -62,7 +57,7 @@ constexpr int kMaxPendingWrites = 10; RemoteStore::RemoteStore( - FSTLocalStore* local_store, + LocalStore* local_store, std::shared_ptr datastore, const std::shared_ptr& worker_queue, std::function online_state_handler) @@ -87,7 +82,7 @@ if (CanUseNetwork()) { // Load any saved stream token from persistent storage - write_stream_->SetLastStreamToken([local_store_ lastStreamToken]); + write_stream_->SetLastStreamToken(local_store_->GetLastStreamToken()); if (ShouldStartWatchStream()) { StartWatchStream(); @@ -135,8 +130,8 @@ // Watch Stream -void RemoteStore::Listen(FSTQueryData* query_data) { - TargetId targetKey = query_data.targetID; +void RemoteStore::Listen(const QueryData& query_data) { + TargetId targetKey = query_data.target_id(); HARD_ASSERT(listen_targets_.find(targetKey) == listen_targets_.end(), "Listen called with duplicate target id: %s", targetKey); @@ -172,10 +167,10 @@ } } -void RemoteStore::SendWatchRequest(FSTQueryData* query_data) { +void RemoteStore::SendWatchRequest(const QueryData& query_data) { // We need to increment the the expected number of pending responses we're due // from watch so we wait for the ack to process any messages from this target. - watch_change_aggregator_->RecordPendingTargetRequest(query_data.targetID); + watch_change_aggregator_->RecordPendingTargetRequest(query_data.target_id()); watch_stream_->WatchQuery(query_data); } @@ -263,7 +258,7 @@ } if (snapshot_version != SnapshotVersion::None() && - snapshot_version >= [local_store_ lastRemoteSnapshotVersion]) { + snapshot_version >= local_store_->GetLastRemoteSnapshotVersion()) { // We have received a target change with a global snapshot if the snapshot // version is not equal to `SnapshotVersion::None()`. RaiseWatchSnapshot(snapshot_version); @@ -277,24 +272,24 @@ RemoteEvent remote_event = watch_change_aggregator_->CreateRemoteEvent(snapshot_version); - // Update in-memory resume tokens. `FSTLocalStore` will update the persistent + // Update in-memory resume tokens. `LocalStore` will update the persistent // view of these when applying the completed `RemoteEvent`. for (const auto& entry : remote_event.target_changes()) { const TargetChange& target_change = entry.second; - NSData* resumeToken = target_change.resume_token(); + const ByteString& resumeToken = target_change.resume_token(); - if (resumeToken.length > 0) { + if (!resumeToken.empty()) { TargetId target_id = entry.first; auto found = listen_targets_.find(target_id); - FSTQueryData* query_data = - found != listen_targets_.end() ? found->second : nil; + absl::optional query_data; + if (found != listen_targets_.end()) { + query_data = found->second; + } // A watched target might have been removed already. if (query_data) { - listen_targets_[target_id] = [query_data - queryDataByReplacingSnapshotVersion:snapshot_version - resumeToken:resumeToken - sequenceNumber:query_data.sequenceNumber]; + listen_targets_[target_id] = query_data->Copy( + snapshot_version, resumeToken, query_data->sequence_number()); } } } @@ -307,14 +302,12 @@ // A watched target might have been removed already. continue; } - FSTQueryData* query_data = found->second; + QueryData query_data = found->second; // Clear the resume token for the query, since we're in a known mismatch // state. - query_data = [[FSTQueryData alloc] initWithQuery:query_data.query - targetID:target_id - listenSequenceNumber:query_data.sequenceNumber - purpose:query_data.purpose]; + query_data = QueryData(query_data.query(), target_id, + query_data.sequence_number(), query_data.purpose()); listen_targets_[target_id] = query_data; // Cause a hard reset by unwatching and rewatching immediately, but @@ -325,16 +318,14 @@ // mismatch, but don't actually retain that in listen_targets_. This ensures // that we flag the first re-listen this way without impacting future // listens of this target (that might happen e.g. on reconnect). - FSTQueryData* request_query_data = [[FSTQueryData alloc] - initWithQuery:query_data.query - targetID:target_id - listenSequenceNumber:query_data.sequenceNumber - purpose:FSTQueryPurposeExistenceFilterMismatch]; + QueryData request_query_data(query_data.query(), target_id, + query_data.sequence_number(), + QueryPurpose::ExistenceFilterMismatch); SendWatchRequest(request_query_data); } // Finally handle remote event - [sync_engine_ applyRemoteEvent:remote_event]; + sync_engine_->ApplyRemoteEvent(remote_event); } void RemoteStore::ProcessTargetError(const WatchTargetChange& change) { @@ -346,8 +337,7 @@ if (found != listen_targets_.end()) { listen_targets_.erase(found); watch_change_aggregator_->RemoveTarget(target_id); - [sync_engine_ rejectListenWithTargetID:target_id - error:util::MakeNSError(change.cause())]; + sync_engine_->HandleRejectedListen(target_id, change.cause()); } } } @@ -357,18 +347,18 @@ void RemoteStore::FillWritePipeline() { BatchId last_batch_id_retrieved = write_pipeline_.empty() ? kBatchIdUnknown - : write_pipeline_.back().batchID; + : write_pipeline_.back().batch_id(); while (CanAddToWritePipeline()) { - FSTMutationBatch* batch = - [local_store_ nextMutationBatchAfterBatchID:last_batch_id_retrieved]; + absl::optional batch = + local_store_->GetNextMutationBatch(last_batch_id_retrieved); if (!batch) { if (write_pipeline_.empty()) { write_stream_->MarkIdle(); } break; } - AddToWritePipeline(batch); - last_batch_id_retrieved = batch.batchID; + AddToWritePipeline(*batch); + last_batch_id_retrieved = batch->batch_id(); } if (ShouldStartWriteStream()) { @@ -380,14 +370,14 @@ return CanUseNetwork() && write_pipeline_.size() < kMaxPendingWrites; } -void RemoteStore::AddToWritePipeline(FSTMutationBatch* batch) { +void RemoteStore::AddToWritePipeline(const MutationBatch& batch) { HARD_ASSERT(CanAddToWritePipeline(), "AddToWritePipeline called when pipeline is full"); write_pipeline_.push_back(batch); if (write_stream_->IsOpen() && write_stream_->handshake_complete()) { - write_stream_->WriteMutations(batch.mutations); + write_stream_->WriteMutations(batch.mutations()); } } @@ -408,11 +398,11 @@ void RemoteStore::OnWriteStreamHandshakeComplete() { // Record the stream token. - [local_store_ setLastStreamToken:write_stream_->GetLastStreamToken()]; + local_store_->SetLastStreamToken(write_stream_->GetLastStreamToken()); // Send the write pipeline now that the stream is established. - for (FSTMutationBatch* write : write_pipeline_) { - write_stream_->WriteMutations(write.mutations); + for (const MutationBatch& write : write_pipeline_) { + write_stream_->WriteMutations(write.mutations()); } } @@ -423,15 +413,13 @@ // to the first write in our write pipeline. HARD_ASSERT(!write_pipeline_.empty(), "Got result for empty write pipeline"); - FSTMutationBatch* batch = write_pipeline_.front(); + MutationBatch batch = write_pipeline_.front(); write_pipeline_.erase(write_pipeline_.begin()); - FSTMutationBatchResult* batchResult = [FSTMutationBatchResult - resultWithBatch:batch - commitVersion:commit_version - mutationResults:std::move(mutation_results) - streamToken:write_stream_->GetLastStreamToken()]; - [sync_engine_ applySuccessfulWriteWithResult:batchResult]; + MutationBatchResult batch_result(std::move(batch), commit_version, + std::move(mutation_results), + write_stream_->GetLastStreamToken()); + sync_engine_->HandleSuccessfulWrite(batch_result); // It's possible that with the completion of this mutation another slot has // freed up. @@ -476,14 +464,13 @@ // no longer valid. Note that the handshake does not count as a write: see // comments on `Datastore::IsPermanentWriteError` for details. if (Datastore::IsPermanentError(status)) { - NSString* token = - [write_stream_->GetLastStreamToken() base64EncodedStringWithOptions:0]; + std::string token = util::ToString(write_stream_->GetLastStreamToken()); LOG_DEBUG("RemoteStore %s error before completed handshake; resetting " "stream token %s: " "error code: '%s', details: '%s'", this, token, status.code(), status.error_message()); - write_stream_->SetLastStreamToken(nil); - [local_store_ setLastStreamToken:nil]; + write_stream_->SetLastStreamToken({}); + local_store_->SetLastStreamToken({}); } else { // Some other error, don't reset stream token. Our stream logic will just // retry with exponential backoff. @@ -501,15 +488,14 @@ // If this was a permanent error, the request itself was the problem so it's // not going to succeed if we resend it. - FSTMutationBatch* batch = write_pipeline_.front(); + MutationBatch batch = write_pipeline_.front(); write_pipeline_.erase(write_pipeline_.begin()); // In this case it's also unlikely that the server itself is melting // down--this was just a bad request so inhibit backoff on the next restart. write_stream_->InhibitBackoff(); - [sync_engine_ rejectFailedWriteWithBatchID:batch.batchID - error:util::MakeNSError(status)]; + sync_engine_->HandleRejectedWrite(batch.batch_id(), status); // It's possible that with the completion of this mutation another slot has // freed up. @@ -527,19 +513,21 @@ } DocumentKeySet RemoteStore::GetRemoteKeysForTarget(TargetId target_id) const { - return [sync_engine_ remoteKeysForTarget:target_id]; + return sync_engine_->GetRemoteKeys(target_id); } -FSTQueryData* RemoteStore::GetQueryDataForTarget(TargetId target_id) const { +absl::optional RemoteStore::GetQueryDataForTarget( + TargetId target_id) const { auto found = listen_targets_.find(target_id); - return found != listen_targets_.end() ? found->second : nil; + return found != listen_targets_.end() ? found->second + : absl::optional{}; } void RemoteStore::HandleCredentialChange() { if (CanUseNetwork()) { // Tear down and re-create our network streams. This will ensure we get a // fresh auth token for the new user and re-fill the write pipeline with new - // mutations from the `FSTLocalStore` (since mutations are per-user). + // mutations from the `LocalStore` (since mutations are per-user). LOG_DEBUG("RemoteStore %s restarting streams for new credential", this); is_network_enabled_ = false; DisableNetworkInternal(); diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.cc b/Firestore/core/src/firebase/firestore/remote/serializer.cc index 24add65e886..60cfe463d91 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.cc +++ b/Firestore/core/src/firebase/firestore/remote/serializer.cc @@ -96,9 +96,6 @@ std::string Serializer::DecodeString(const pb_bytes_array_t* str) { namespace { -FieldValue::Map DecodeMapValue(Reader* reader, - const google_firestore_v1_MapValue& map_value); - // There's no f:f::model equivalent of StructuredQuery, so we'll create our // own struct for decoding. We could use nanopb's struct, but it's slightly // inconvenient since it's a fixed size (so uses callbacks to represent @@ -114,66 +111,6 @@ struct StructuredQuery { // TODO(rsgowman): other fields }; -FieldValue::Map::value_type DecodeFieldsEntry( - Reader* reader, const google_firestore_v1_Document_FieldsEntry& fields) { - std::string key = Serializer::DecodeString(fields.key); - FieldValue value = Serializer::DecodeFieldValue(reader, fields.value); - - if (key.empty()) { - reader->Fail( - "Invalid message: Empty key while decoding a Map field value."); - return {}; - } - - return FieldValue::Map::value_type{std::move(key), std::move(value)}; -} - -FieldValue::Map DecodeFields( - Reader* reader, - size_t count, - const google_firestore_v1_Document_FieldsEntry* fields) { - FieldValue::Map result; - for (size_t i = 0; i < count; i++) { - FieldValue::Map::value_type kv = DecodeFieldsEntry(reader, fields[i]); - result = result.insert(std::move(kv.first), std::move(kv.second)); - } - - return result; -} - -google_firestore_v1_MapValue EncodeMapValue(const ObjectValue& object_value) { - google_firestore_v1_MapValue result{}; - - pb_size_t count = CheckedSize(object_value.GetInternalValue().size()); - - result.fields_count = count; - result.fields = MakeArray(count); - - int i = 0; - for (const auto& kv : object_value.GetInternalValue()) { - result.fields[i].key = Serializer::EncodeString(kv.first); - result.fields[i].value = Serializer::EncodeFieldValue(kv.second); - i++; - } - - return result; -} - -FieldValue::Map DecodeMapValue(Reader* reader, - const google_firestore_v1_MapValue& map_value) { - FieldValue::Map result; - - for (size_t i = 0; i < map_value.fields_count; i++) { - std::string key = Serializer::DecodeString(map_value.fields[i].key); - FieldValue value = - Serializer::DecodeFieldValue(reader, map_value.fields[i].value); - - result = result.insert(key, value); - } - - return result; -} - /** * Creates the prefix for a fully qualified resource path, without a local path * on the end. @@ -187,12 +124,12 @@ ResourcePath EncodeDatabaseId(const DatabaseId& 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(); +pb_bytes_array_t* EncodeResourceName(const DatabaseId& database_id, + const ResourcePath& path) { + return Serializer::EncodeString(EncodeDatabaseId(database_id) + .Append("documents") + .Append(path) + .CanonicalString()); } /** @@ -271,76 +208,180 @@ void Serializer::FreeNanopbMessage(const pb_field_t fields[], } google_firestore_v1_Value Serializer::EncodeFieldValue( - const FieldValue& field_value) { - // TODO(rsgowman): some refactoring is in order... but will wait until after a - // non-varint, non-fixed-size (i.e. string) type is present before doing so. - google_firestore_v1_Value result{}; + const FieldValue& field_value) const { switch (field_value.type()) { case FieldValue::Type::Null: - result.which_value_type = google_firestore_v1_Value_null_value_tag; - result.null_value = google_protobuf_NullValue_NULL_VALUE; - return result; + return EncodeNull(); case FieldValue::Type::Boolean: - result.which_value_type = google_firestore_v1_Value_boolean_value_tag; - result.boolean_value = field_value.boolean_value(); - return result; + return EncodeBoolean(field_value.boolean_value()); case FieldValue::Type::Integer: - result.which_value_type = google_firestore_v1_Value_integer_value_tag; - result.integer_value = field_value.integer_value(); - return result; + return EncodeInteger(field_value.integer_value()); case FieldValue::Type::Double: - result.which_value_type = google_firestore_v1_Value_double_value_tag; - result.double_value = field_value.double_value(); - return result; + return EncodeDouble(field_value.double_value()); case FieldValue::Type::Timestamp: - result.which_value_type = google_firestore_v1_Value_timestamp_value_tag; - result.timestamp_value = EncodeTimestamp(field_value.timestamp_value()); - return result; - - case FieldValue::Type::ServerTimestamp: - // TODO(rsgowman): Implement - abort(); + return EncodeTimestampValue(field_value.timestamp_value()); case FieldValue::Type::String: - result.which_value_type = google_firestore_v1_Value_string_value_tag; - result.string_value = EncodeString(field_value.string_value()); - return result; + return EncodeStringValue(field_value.string_value()); case FieldValue::Type::Blob: - result.which_value_type = google_firestore_v1_Value_bytes_value_tag; - // Copy the blob so that pb_release can do the right thing. - result.bytes_value = - nanopb::CopyBytesArray(field_value.blob_value().get()); - return result; + return EncodeBlob(field_value.blob_value()); case FieldValue::Type::Reference: - // TODO(rsgowman): Implement - abort(); + return EncodeReference(field_value.reference_value()); case FieldValue::Type::GeoPoint: - result.which_value_type = google_firestore_v1_Value_geo_point_value_tag; - result.geo_point_value = EncodeGeoPoint(field_value.geo_point_value()); - return result; + return EncodeGeoPoint(field_value.geo_point_value()); - case FieldValue::Type::Array: + case FieldValue::Type::Array: { + google_firestore_v1_Value result{}; result.which_value_type = google_firestore_v1_Value_array_value_tag; result.array_value = EncodeArray(field_value.array_value()); return result; + } - case FieldValue::Type::Object: + case FieldValue::Type::Object: { + google_firestore_v1_Value result{}; result.which_value_type = google_firestore_v1_Value_map_value_tag; result.map_value = EncodeMapValue(ObjectValue(field_value)); return result; + } + + case FieldValue::Type::ServerTimestamp: + HARD_FAIL("Unhandled type %s on %s", field_value.type(), + field_value.ToString()); } UNREACHABLE(); } -FieldValue Serializer::DecodeFieldValue(Reader* reader, - const google_firestore_v1_Value& msg) { +google_firestore_v1_Value Serializer::EncodeNull() const { + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_null_value_tag; + result.null_value = google_protobuf_NullValue_NULL_VALUE; + return result; +} + +google_firestore_v1_Value Serializer::EncodeBoolean(bool value) const { + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_boolean_value_tag; + result.boolean_value = value; + return result; +} + +google_firestore_v1_Value Serializer::EncodeInteger(int64_t value) const { + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_integer_value_tag; + result.integer_value = value; + return result; +} + +google_firestore_v1_Value Serializer::EncodeDouble(double value) const { + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_double_value_tag; + result.double_value = value; + return result; +} + +google_firestore_v1_Value Serializer::EncodeTimestampValue( + Timestamp value) const { + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_timestamp_value_tag; + result.timestamp_value = EncodeTimestamp(value); + return result; +} + +google_firestore_v1_Value Serializer::EncodeStringValue( + const std::string& value) const { + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_string_value_tag; + result.string_value = EncodeString(value); + return result; +} + +google_firestore_v1_Value Serializer::EncodeBlob( + const nanopb::ByteString& value) const { + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_bytes_value_tag; + // Copy the blob so that pb_release can do the right thing. + result.bytes_value = nanopb::CopyBytesArray(value.get()); + return result; +} + +google_firestore_v1_Value Serializer::EncodeReference( + const FieldValue::Reference& value) const { + HARD_ASSERT(database_id_ == value.database_id(), + "Database %s cannot encode reference from %s", + database_id_.ToString(), value.database_id().ToString()); + + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_reference_value_tag; + result.reference_value = + EncodeResourceName(value.database_id(), value.key().path()); + + return result; +} + +google_firestore_v1_Value Serializer::EncodeGeoPoint( + const GeoPoint& value) const { + google_firestore_v1_Value result{}; + result.which_value_type = google_firestore_v1_Value_geo_point_value_tag; + + google_type_LatLng geo_point{}; + geo_point.latitude = value.latitude(); + geo_point.longitude = value.longitude(); + result.geo_point_value = geo_point; + + return result; +} + +FieldValue::Map::value_type Serializer::DecodeFieldsEntry( + Reader* reader, + const google_firestore_v1_Document_FieldsEntry& fields) const { + std::string key = DecodeString(fields.key); + FieldValue value = DecodeFieldValue(reader, fields.value); + + if (key.empty()) { + reader->Fail( + "Invalid message: Empty key while decoding a Map field value."); + return {}; + } + + return FieldValue::Map::value_type{std::move(key), std::move(value)}; +} + +FieldValue::Map Serializer::DecodeFields( + Reader* reader, + size_t count, + const google_firestore_v1_Document_FieldsEntry* fields) const { + FieldValue::Map result; + for (size_t i = 0; i < count; i++) { + FieldValue::Map::value_type kv = DecodeFieldsEntry(reader, fields[i]); + result = result.insert(std::move(kv.first), std::move(kv.second)); + } + + return result; +} + +FieldValue::Map Serializer::DecodeMapValue( + Reader* reader, const google_firestore_v1_MapValue& map_value) const { + FieldValue::Map result; + + for (size_t i = 0; i < map_value.fields_count; i++) { + std::string key = DecodeString(map_value.fields[i].key); + FieldValue value = DecodeFieldValue(reader, map_value.fields[i].value); + + result = result.insert(key, value); + } + + return result; +} + +FieldValue Serializer::DecodeFieldValue( + Reader* reader, const google_firestore_v1_Value& msg) const { switch (msg.which_value_type) { case google_firestore_v1_Value_null_value_tag: if (msg.null_value != google_protobuf_NullValue_NULL_VALUE) { @@ -376,9 +417,7 @@ FieldValue Serializer::DecodeFieldValue(Reader* reader, return FieldValue::FromBlob(ByteString(msg.bytes_value)); case google_firestore_v1_Value_reference_value_tag: - // TODO(b/74243929): Implement remaining types. - HARD_FAIL("Unhandled message field number (tag): %i.", - msg.which_value_type); + return DecodeReference(reader, msg.reference_value); case google_firestore_v1_Value_geo_point_value_tag: return FieldValue::FromGeoPoint( @@ -400,31 +439,44 @@ FieldValue Serializer::DecodeFieldValue(Reader* reader, UNREACHABLE(); } -std::string Serializer::EncodeKey(const DocumentKey& key) const { +pb_bytes_array_t* Serializer::EncodeKey(const DocumentKey& key) const { return EncodeResourceName(database_id_, key.path()); } -DocumentKey Serializer::DecodeKey(Reader* reader, - absl::string_view name) const { - ResourcePath resource = DecodeResourceName(reader, name); - if (resource.size() < 5) { +void Serializer::ValidateDocumentKeyPath( + Reader* reader, const ResourcePath& resource_name) const { + if (resource_name.size() < 5) { reader->Fail( StringFormat("Attempted to decode invalid key: '%s'. Should have at " "least 5 segments.", - name)); - } else if (resource[1] != database_id_.project_id()) { + resource_name.CanonicalString())); + } else if (resource_name[1] != database_id_.project_id()) { reader->Fail( StringFormat("Tried to deserialize key from different project. " "Expected: '%s'. Found: '%s'. (Full key: '%s')", - database_id_.project_id(), resource[1], name)); - } else if (resource[3] != database_id_.database_id()) { + database_id_.project_id(), resource_name[1], + resource_name.CanonicalString())); + } else if (resource_name[3] != database_id_.database_id()) { reader->Fail( StringFormat("Tried to deserialize key from different database. " "Expected: '%s'. Found: '%s'. (Full key: '%s')", - database_id_.database_id(), resource[3], name)); + database_id_.database_id(), resource_name[3], + resource_name.CanonicalString())); } +} - ResourcePath local_path = ExtractLocalPathFromResourceName(reader, resource); +DocumentKey Serializer::DecodeKey(Reader* reader, + const pb_bytes_array_t* name) const { + ResourcePath resource_name = DecodeResourceName(reader, MakeStringView(name)); + ValidateDocumentKeyPath(reader, resource_name); + + return DecodeKey(reader, resource_name); +} + +DocumentKey Serializer::DecodeKey(Reader* reader, + const ResourcePath& resource_name) const { + ResourcePath local_path = + ExtractLocalPathFromResourceName(reader, resource_name); if (!DocumentKey::IsDocumentKey(local_path)) { reader->Fail(StringFormat("Invalid document key path: %s", @@ -436,11 +488,24 @@ DocumentKey Serializer::DecodeKey(Reader* reader, return DocumentKey{std::move(local_path)}; } +DatabaseId Serializer::DecodeDatabaseId( + Reader* reader, const ResourcePath& resource_name) const { + if (resource_name.size() < 4) { + reader->Fail(StringFormat("Tried to deserialize invalid key %s", + resource_name.CanonicalString())); + return DatabaseId{}; + } + + const std::string& project_id = resource_name[1]; + const std::string& database_id = resource_name[3]; + return DatabaseId{project_id, database_id}; +} + google_firestore_v1_Document Serializer::EncodeDocument( const DocumentKey& key, const ObjectValue& object_value) const { google_firestore_v1_Document result{}; - result.name = EncodeString(EncodeKey(key)); + result.name = EncodeKey(key); // Encode Document.fields (unless it's empty) pb_size_t count = CheckedSize(object_value.GetInternalValue().size()); @@ -483,7 +548,7 @@ Document Serializer::DecodeFoundDocument( google_firestore_v1_BatchGetDocumentsResponse_found_tag, "Tried to deserialize a found document from a missing document."); - DocumentKey key = DecodeKey(reader, DecodeString(response.found.name)); + DocumentKey key = DecodeKey(reader, response.found.name); FieldValue::Map value = DecodeFields(reader, response.found.fields_count, response.found.fields); SnapshotVersion version = @@ -504,7 +569,7 @@ NoDocument Serializer::DecodeMissingDocument( google_firestore_v1_BatchGetDocumentsResponse_missing_tag, "Tried to deserialize a missing document from a found document."); - DocumentKey key = DecodeKey(reader, DecodeString(response.missing)); + DocumentKey key = DecodeKey(reader, response.missing); SnapshotVersion version = DecodeSnapshotVersion(reader, response.read_time); if (version == SnapshotVersion::None()) { @@ -523,12 +588,13 @@ Document Serializer::DecodeDocument( SnapshotVersion version = DecodeSnapshotVersion(reader, proto.update_time); return Document(ObjectValue::FromMap(std::move(fields_internal)), - DecodeKey(reader, DecodeString(proto.name)), version, + DecodeKey(reader, proto.name), version, DocumentState::kSynced); } google_firestore_v1_Write Serializer::EncodeMutation( const Mutation& mutation) const { + HARD_ASSERT(mutation.is_valid(), "Invalid mutation encountered."); google_firestore_v1_Write result{}; if (!mutation.precondition().is_none()) { @@ -547,34 +613,33 @@ google_firestore_v1_Write Serializer::EncodeMutation( result.which_operation = google_firestore_v1_Write_update_tag; auto patch_mutation = static_cast(mutation); result.update = EncodeDocument(mutation.key(), patch_mutation.value()); - result.update_mask = EncodeDocumentMask(patch_mutation.mask()); + result.update_mask = EncodeFieldMask(patch_mutation.mask()); return result; } case Mutation::Type::Transform: { - // TODO(rsgowman): Implement transform mutations. Probably like this: - abort(); - /* - result.which_operation = google_firestore_v1_Write_transform_tag; - auto transform = static_cast(mutation); - result.transform.document = EncodeKey(transform.key()); - - size_t count = transform.field_transforms.size(); - result.transform.field_transforms_count = count; - result.transform.field_transforms = - MakeArray(count); - int i = 0; - for (const FieldTransform& field_transform : - transform.field_transforms()) { result.transform.field_transforms[i] = - EncodeFieldTransform(field_transform); i++; - } - return result; - */ + result.which_operation = google_firestore_v1_Write_transform_tag; + auto transform = static_cast(mutation); + result.transform.document = EncodeKey(transform.key()); + + pb_size_t count = CheckedSize(transform.field_transforms().size()); + result.transform.field_transforms_count = count; + result.transform.field_transforms = + MakeArray( + count); + int i = 0; + for (const FieldTransform& field_transform : + transform.field_transforms()) { + result.transform.field_transforms[i] = + EncodeFieldTransform(field_transform); + i++; + } + return result; } case Mutation::Type::Delete: { result.which_operation = google_firestore_v1_Write_delete_tag; - result.delete_ = EncodeString(EncodeKey(mutation.key())); + result.delete_ = EncodeKey(mutation.key()); return result; } } @@ -589,10 +654,10 @@ Mutation Serializer::DecodeMutation( switch (mutation.which_operation) { case google_firestore_v1_Write_update_tag: { - DocumentKey key = DecodeKey(reader, DecodeString(mutation.update.name)); + DocumentKey key = DecodeKey(reader, mutation.update.name); ObjectValue value = ObjectValue::FromMap(DecodeFields( reader, mutation.update.fields_count, mutation.update.fields)); - FieldMask mask = DecodeDocumentMask(mutation.update_mask); + FieldMask mask = DecodeFieldMask(mutation.update_mask); if (mask.size() > 0) { return PatchMutation(std::move(key), std::move(value), std::move(mask), std::move(precondition)); @@ -603,25 +668,23 @@ Mutation Serializer::DecodeMutation( } case google_firestore_v1_Write_delete_tag: - return DeleteMutation(DecodeKey(reader, DecodeString(mutation.delete_)), + return DeleteMutation(DecodeKey(reader, mutation.delete_), std::move(precondition)); - // TODO(rsgowman): Implement transform. Probably like this: - /* - case google_firestore_v1_Write_transform_tag: - std::vector field_transforms; - for (size_t i = 0; i field_transforms; + for (size_t i = 0; i < mutation.transform.field_transforms_count; i++) { + field_transforms.push_back(DecodeFieldTransform( + reader, mutation.transform.field_transforms[i])); + } - HARD_ASSERT(precondition.type() == Precondition::Type::Exists && - precondition.exists(), "Transforms only support precondition \"exists == - true\""); + HARD_ASSERT(precondition.type() == Precondition::Type::Exists && + precondition.exists(), + "Transforms only support precondition \"exists == true\""); - return absl::make_unique( - DecodeKey(reader, mutation.transform.document), - field_transforms); - */ + return TransformMutation(DecodeKey(reader, mutation.transform.document), + field_transforms); + } default: reader->Fail(StringFormat("Unknown mutation operation: %s", @@ -687,7 +750,7 @@ Precondition Serializer::DecodePrecondition( } /* static */ -google_firestore_v1_DocumentMask Serializer::EncodeDocumentMask( +google_firestore_v1_DocumentMask Serializer::EncodeFieldMask( const FieldMask& mask) { google_firestore_v1_DocumentMask result{}; @@ -697,7 +760,7 @@ google_firestore_v1_DocumentMask Serializer::EncodeDocumentMask( int i = 0; for (const FieldPath& path : mask) { - result.field_paths[i] = EncodeString(path.CanonicalString()); + result.field_paths[i] = EncodeFieldPath(path); i++; } @@ -705,14 +768,105 @@ google_firestore_v1_DocumentMask Serializer::EncodeDocumentMask( } /* static */ -model::FieldMask Serializer::DecodeDocumentMask( +FieldMask Serializer::DecodeFieldMask( const google_firestore_v1_DocumentMask& mask) { std::set fields; for (size_t i = 0; i < mask.field_paths_count; i++) { - auto path = DecodeString(mask.field_paths[i]); - fields.insert(FieldPath::FromServerFormat(path)); + fields.insert(DecodeFieldPath(mask.field_paths[i])); } - return model::FieldMask(std::move(fields)); + return FieldMask(std::move(fields)); +} + +google_firestore_v1_DocumentTransform_FieldTransform +Serializer::EncodeFieldTransform(const FieldTransform& field_transform) const { + using Type = TransformOperation::Type; + + google_firestore_v1_DocumentTransform_FieldTransform proto{}; + proto.field_path = EncodeFieldPath(field_transform.path()); + + switch (field_transform.transformation().type()) { + case Type::ServerTimestamp: + proto.which_transform_type = + google_firestore_v1_DocumentTransform_FieldTransform_set_to_server_value_tag; // NOLINT + proto.set_to_server_value = + google_firestore_v1_DocumentTransform_FieldTransform_ServerValue_REQUEST_TIME; // NOLINT + return proto; + + case Type::ArrayUnion: + proto.which_transform_type = + google_firestore_v1_DocumentTransform_FieldTransform_append_missing_elements_tag; // NOLINT + proto.append_missing_elements = EncodeArray( + ArrayTransform(field_transform.transformation()).elements()); + return proto; + + case Type::ArrayRemove: + proto.which_transform_type = + google_firestore_v1_DocumentTransform_FieldTransform_remove_all_from_array_tag; // NOLINT + proto.remove_all_from_array = EncodeArray( + ArrayTransform(field_transform.transformation()).elements()); + return proto; + + case Type::Increment: { + const auto& increment = static_cast( + field_transform.transformation()); + proto.increment = EncodeFieldValue(increment.operand()); + return proto; + } + } + + UNREACHABLE(); +} + +FieldTransform Serializer::DecodeFieldTransform( + nanopb::Reader* reader, + const google_firestore_v1_DocumentTransform_FieldTransform& proto) const { + switch (proto.which_transform_type) { + case google_firestore_v1_DocumentTransform_FieldTransform_set_to_server_value_tag: { // NOLINT + HARD_ASSERT( + proto.set_to_server_value == + google_firestore_v1_DocumentTransform_FieldTransform_ServerValue_REQUEST_TIME, // NOLINT + "Unknown transform setToServerValue: %s", proto.set_to_server_value); + + return FieldTransform(DecodeFieldPath(proto.field_path), + ServerTimestampTransform()); + } + + case google_firestore_v1_DocumentTransform_FieldTransform_append_missing_elements_tag: { // NOLINT + std::vector elements = + DecodeArray(reader, proto.append_missing_elements); + return FieldTransform(DecodeFieldPath(proto.field_path), + ArrayTransform(TransformOperation::Type::ArrayUnion, + std::move(elements))); + } + + case google_firestore_v1_DocumentTransform_FieldTransform_remove_all_from_array_tag: { // NOLINT + std::vector elements = + DecodeArray(reader, proto.remove_all_from_array); + return FieldTransform( + DecodeFieldPath(proto.field_path), + ArrayTransform(TransformOperation::Type::ArrayRemove, + std::move(elements))); + } + + case google_firestore_v1_DocumentTransform_FieldTransform_increment_tag: { + FieldValue operand = DecodeFieldValue(reader, proto.increment); + return FieldTransform(DecodeFieldPath(proto.field_path), + NumericIncrementTransform(std::move(operand))); + } + } + + UNREACHABLE(); +} + +/* static */ +pb_bytes_array_t* Serializer::EncodeFieldPath(const FieldPath& field_path) { + return EncodeString(field_path.CanonicalString()); +} + +/* static */ +FieldPath Serializer::DecodeFieldPath(const pb_bytes_array_t* field_path) { + absl::string_view str = MakeStringView(field_path); + return FieldPath::FromServerFormat(str); } google_firestore_v1_Target_QueryTarget Serializer::EncodeQueryTarget( @@ -723,12 +877,12 @@ google_firestore_v1_Target_QueryTarget Serializer::EncodeQueryTarget( std::string collection_id; // TODO(rsgowman): Port Collection Group Queries logic. if (query.path().empty()) { - result.parent = EncodeString(EncodeQueryPath(ResourcePath::Empty())); + result.parent = EncodeQueryPath(ResourcePath::Empty()); } else { ResourcePath path = query.path(); HARD_ASSERT(path.size() % 2 != 0, "Document queries with filters are not supported."); - result.parent = EncodeString(EncodeQueryPath(path.PopLast())); + result.parent = EncodeQueryPath(path.PopLast()); collection_id = path.last_segment(); } @@ -798,16 +952,16 @@ Query Serializer::DecodeQueryTarget( path = path.Append(query.from[0].collection_id); } - // TODO(rsgowman): Dencode the filters. - // TODO(rsgowman): Dencode the orders. - // TODO(rsgowman): Dencode the limit. - // TODO(rsgowman): Dencode the startat. - // TODO(rsgowman): Dencode the endat. + // TODO(rsgowman): Decode the filters. + // TODO(rsgowman): Decode the orders. + // TODO(rsgowman): Decode the limit. + // TODO(rsgowman): Decode the startat. + // TODO(rsgowman): Decode the endat. return Query(path); } -std::string Serializer::EncodeQueryPath(const ResourcePath& path) const { +pb_bytes_array_t* Serializer::EncodeQueryPath(const ResourcePath& path) const { return EncodeResourceName(database_id_, path); } @@ -849,12 +1003,15 @@ Timestamp Serializer::DecodeTimestamp( return Timestamp{timestamp_proto.seconds, timestamp_proto.nanos}; } -/* static */ -google_type_LatLng Serializer::EncodeGeoPoint(const GeoPoint& geo_point_value) { - google_type_LatLng result{}; - result.latitude = geo_point_value.latitude(); - result.longitude = geo_point_value.longitude(); - return result; +FieldValue Serializer::DecodeReference( + Reader* reader, const pb_bytes_array_t* resource_name_raw) const { + ResourcePath resource_name = + DecodeResourceName(reader, MakeStringView(resource_name_raw)); + ValidateDocumentKeyPath(reader, resource_name); + DatabaseId database_id = DecodeDatabaseId(reader, resource_name); + DocumentKey key = DecodeKey(reader, resource_name); + + return FieldValue::FromReference(std::move(database_id), std::move(key)); } /* static */ @@ -877,9 +1034,8 @@ GeoPoint Serializer::DecodeGeoPoint(nanopb::Reader* reader, return GeoPoint(latitude, longitude); } -/* static */ google_firestore_v1_ArrayValue Serializer::EncodeArray( - const std::vector& array_value) { + const std::vector& array_value) const { google_firestore_v1_ArrayValue result{}; pb_size_t count = CheckedSize(array_value.size()); @@ -894,9 +1050,9 @@ google_firestore_v1_ArrayValue Serializer::EncodeArray( return result; } -/* static */ std::vector Serializer::DecodeArray( - nanopb::Reader* reader, const google_firestore_v1_ArrayValue& array_proto) { + nanopb::Reader* reader, + const google_firestore_v1_ArrayValue& array_proto) const { std::vector result; result.reserve(array_proto.values_count); @@ -907,6 +1063,25 @@ std::vector Serializer::DecodeArray( return result; } +google_firestore_v1_MapValue Serializer::EncodeMapValue( + const ObjectValue& object_value) const { + google_firestore_v1_MapValue result{}; + + pb_size_t count = CheckedSize(object_value.GetInternalValue().size()); + + result.fields_count = count; + result.fields = MakeArray(count); + + int i = 0; + for (const auto& kv : object_value.GetInternalValue()) { + result.fields[i].key = EncodeString(kv.first); + result.fields[i].value = EncodeFieldValue(kv.second); + i++; + } + + return result; +} + } // namespace remote } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/remote/serializer.h b/Firestore/core/src/firebase/firestore/remote/serializer.h index 41711e41dff..b1309800238 100644 --- a/Firestore/core/src/firebase/firestore/remote/serializer.h +++ b/Firestore/core/src/firebase/firestore/remote/serializer.h @@ -41,7 +41,7 @@ #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/hard_assert.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/base/attributes.h" #include "absl/strings/string_view.h" @@ -125,8 +125,8 @@ class Serializer { /** * @brief Converts the FieldValue model passed into bytes. */ - static google_firestore_v1_Value EncodeFieldValue( - const model::FieldValue& field_value); + google_firestore_v1_Value EncodeFieldValue( + const model::FieldValue& field_value) const; /** * @brief Converts from nanopb proto to the model FieldValue format. @@ -135,21 +135,21 @@ class Serializer { // used for is error handling. This seems questionable. We probably need to // rework error handling. Again. But we'll defer that for now and continue // just passing the reader object. - static model::FieldValue DecodeFieldValue( - nanopb::Reader* reader, const google_firestore_v1_Value& proto); + model::FieldValue DecodeFieldValue( + nanopb::Reader* reader, const google_firestore_v1_Value& proto) const; /** * 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( + pb_bytes_array_t* EncodeKey( const firebase::firestore::model::DocumentKey& key) const; /** * Decodes the given document key from a fully qualified name. */ firebase::firestore::model::DocumentKey DecodeKey( - nanopb::Reader* reader, absl::string_view name) const; + nanopb::Reader* reader, const pb_bytes_array_t* name) const; /** * @brief Converts the Document (i.e. key/value) into bytes. @@ -175,11 +175,20 @@ class Serializer { nanopb::Reader* reader, const google_firestore_v1_Precondition& precondition); - static google_firestore_v1_DocumentMask EncodeDocumentMask( + static google_firestore_v1_DocumentMask EncodeFieldMask( const model::FieldMask& mask); - static model::FieldMask DecodeDocumentMask( + static model::FieldMask DecodeFieldMask( const google_firestore_v1_DocumentMask& mask); + google_firestore_v1_DocumentTransform_FieldTransform EncodeFieldTransform( + const model::FieldTransform& field_transform) const; + model::FieldTransform DecodeFieldTransform( + nanopb::Reader* reader, + const google_firestore_v1_DocumentTransform_FieldTransform& proto) const; + + static pb_bytes_array_t* EncodeFieldPath(const model::FieldPath& field_path); + static model::FieldPath DecodeFieldPath(const pb_bytes_array_t* field_path); + /** * @brief Converts the Query into bytes, representing a * firestore::v1::Target::QueryTarget. @@ -206,17 +215,30 @@ class Serializer { nanopb::Reader* reader, const google_firestore_v1_Target_QueryTarget& proto); - static google_type_LatLng EncodeGeoPoint(const GeoPoint& geo_point_value); static GeoPoint DecodeGeoPoint(nanopb::Reader* reader, const google_type_LatLng& latlng_proto); - static google_firestore_v1_ArrayValue EncodeArray( - const std::vector& array_value); - static std::vector DecodeArray( + google_firestore_v1_ArrayValue EncodeArray( + const std::vector& array_value) const; + std::vector DecodeArray( nanopb::Reader* reader, - const google_firestore_v1_ArrayValue& array_proto); + const google_firestore_v1_ArrayValue& array_proto) const; + + google_firestore_v1_MapValue EncodeMapValue( + const model::ObjectValue& object_value) const; private: + google_firestore_v1_Value EncodeNull() const; + google_firestore_v1_Value EncodeBoolean(bool value) const; + google_firestore_v1_Value EncodeInteger(int64_t value) const; + google_firestore_v1_Value EncodeDouble(double value) const; + google_firestore_v1_Value EncodeTimestampValue(Timestamp value) const; + google_firestore_v1_Value EncodeStringValue(const std::string& value) const; + google_firestore_v1_Value EncodeBlob(const nanopb::ByteString& value) const; + google_firestore_v1_Value EncodeReference( + const model::FieldValue::Reference& value) const; + google_firestore_v1_Value EncodeGeoPoint(const GeoPoint& value) const; + model::Document DecodeFoundDocument( nanopb::Reader* reader, const google_firestore_v1_BatchGetDocumentsResponse& response) const; @@ -224,7 +246,29 @@ class Serializer { nanopb::Reader* reader, const google_firestore_v1_BatchGetDocumentsResponse& response) const; - std::string EncodeQueryPath(const model::ResourcePath& path) const; + pb_bytes_array_t* EncodeQueryPath(const model::ResourcePath& path) const; + + void ValidateDocumentKeyPath(nanopb::Reader* reader, + const model::ResourcePath& resource_name) const; + model::DocumentKey DecodeKey(nanopb::Reader* reader, + const model::ResourcePath& resource_name) const; + + model::FieldValue::Map::value_type DecodeFieldsEntry( + nanopb::Reader* reader, + const google_firestore_v1_Document_FieldsEntry& fields) const; + model::FieldValue::Map DecodeFields( + nanopb::Reader* reader, + size_t count, + const google_firestore_v1_Document_FieldsEntry* fields) const; + + model::FieldValue::Map DecodeMapValue( + nanopb::Reader* reader, + const google_firestore_v1_MapValue& map_value) const; + + model::DatabaseId DecodeDatabaseId( + nanopb::Reader* reader, const model::ResourcePath& resource_name) const; + model::FieldValue DecodeReference( + nanopb::Reader* reader, const pb_bytes_array_t* resource_name_raw) const; model::DatabaseId database_id_; const std::string database_name_; diff --git a/Firestore/core/src/firebase/firestore/remote/stream.h b/Firestore/core/src/firebase/firestore/remote/stream.h index b19c0ce6ab1..60ae4704ac0 100644 --- a/Firestore/core/src/firebase/firestore/remote/stream.h +++ b/Firestore/core/src/firebase/firestore/remote/stream.h @@ -28,8 +28,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_stream.h" #include "Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/strings/string_view.h" #include "grpcpp/support/byte_buffer.h" diff --git a/Firestore/core/src/firebase/firestore/remote/stream.mm b/Firestore/core/src/firebase/firestore/remote/stream.mm index c4dc1a09f3b..f9d9c205336 100644 --- a/Firestore/core/src/firebase/firestore/remote/stream.mm +++ b/Firestore/core/src/firebase/firestore/remote/stream.mm @@ -292,9 +292,8 @@ void Stream::OnStreamFinish(const Status& status) { EnsureOnQueue(); - // TODO(varconst): log error here? - LOG_DEBUG("%s Stream error", GetDebugDescription()); + LOG_DEBUG("%s Stream error: '%s'", GetDebugDescription(), status.ToString()); Close(status); } diff --git a/Firestore/core/src/firebase/firestore/remote/watch_change.h b/Firestore/core/src/firebase/firestore/remote/watch_change.h index f177889fcf0..02cad00161c 100644 --- a/Firestore/core/src/firebase/firestore/remote/watch_change.h +++ b/Firestore/core/src/firebase/firestore/remote/watch_change.h @@ -17,20 +17,13 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_WATCH_CHANGE_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_REMOTE_WATCH_CHANGE_H_ -#if !defined(__OBJC__) -// TODO(varconst): the only dependency is `NSData` -// (used to represent the resume token). -#error "This header only supports Objective-C++" -#endif // !defined(__OBJC__) - -#import - #include #include #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/maybe_document.h" #include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/nanopb/byte_string.h" #include "Firestore/core/src/firebase/firestore/remote/existence_filter.h" #include "Firestore/core/src/firebase/firestore/util/status.h" @@ -148,30 +141,31 @@ class WatchTargetChange : public WatchChange { public: WatchTargetChange(WatchTargetChangeState state, std::vector target_ids) - : WatchTargetChange{state, std::move(target_ids), [NSData data], + : WatchTargetChange{state, std::move(target_ids), nanopb::ByteString(), util::Status::OK()} { } WatchTargetChange(WatchTargetChangeState state, std::vector target_ids, - NSData* resume_token) - : WatchTargetChange{state, std::move(target_ids), resume_token, + nanopb::ByteString resume_token) + : WatchTargetChange{state, std::move(target_ids), std::move(resume_token), util::Status::OK()} { } WatchTargetChange(WatchTargetChangeState state, std::vector target_ids, util::Status cause) - : WatchTargetChange{state, std::move(target_ids), [NSData data], cause} { + : WatchTargetChange{state, std::move(target_ids), nanopb::ByteString(), + cause} { } WatchTargetChange(WatchTargetChangeState state, std::vector target_ids, - NSData* resume_token, + nanopb::ByteString resume_token, util::Status cause) : state_{state}, target_ids_{std::move(target_ids)}, - resume_token_{resume_token}, + resume_token_{std::move(resume_token)}, cause_{std::move(cause)} { } @@ -195,7 +189,7 @@ class WatchTargetChange : public WatchChange { * matches the query. The resume token essentially identifies a point in * time from which the server should resume sending results. */ - NSData* resume_token() const { + const nanopb::ByteString& resume_token() const { return resume_token_; } @@ -210,7 +204,7 @@ class WatchTargetChange : public WatchChange { private: WatchTargetChangeState state_; std::vector target_ids_; - NSData* resume_token_; + nanopb::ByteString resume_token_; util::Status cause_; }; diff --git a/Firestore/core/src/firebase/firestore/remote/watch_change.mm b/Firestore/core/src/firebase/firestore/remote/watch_change.mm index 0edb9239db3..753539aaf86 100644 --- a/Firestore/core/src/firebase/firestore/remote/watch_change.mm +++ b/Firestore/core/src/firebase/firestore/remote/watch_change.mm @@ -16,8 +16,6 @@ #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" -#include "Firestore/core/src/firebase/firestore/objc/objc_compatibility.h" - namespace firebase { namespace firestore { namespace remote { @@ -37,8 +35,7 @@ bool operator==(const WatchTargetChange& lhs, const WatchTargetChange& rhs) { return lhs.state() == rhs.state() && lhs.target_ids() == rhs.target_ids() && - objc::Equals(lhs.resume_token(), rhs.resume_token()) && - lhs.cause() == rhs.cause(); + lhs.resume_token() == rhs.resume_token() && lhs.cause() == rhs.cause(); } } // namespace remote diff --git a/Firestore/core/src/firebase/firestore/remote/watch_stream.h b/Firestore/core/src/firebase/firestore/remote/watch_stream.h index 2c9c31e10ab..85cbc637e0a 100644 --- a/Firestore/core/src/firebase/firestore/remote/watch_stream.h +++ b/Firestore/core/src/firebase/firestore/remote/watch_stream.h @@ -24,6 +24,7 @@ #include #include +#include "Firestore/core/src/firebase/firestore/local/query_data.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" #include "Firestore/core/src/firebase/firestore/model/types.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" @@ -31,12 +32,10 @@ #include "Firestore/core/src/firebase/firestore/remote/stream.h" #include "Firestore/core/src/firebase/firestore/remote/watch_change.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/strings/string_view.h" #include "grpcpp/support/byte_buffer.h" -#import "Firestore/Source/Core/FSTTypes.h" -#import "Firestore/Source/Local/FSTQueryData.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" namespace firebase { @@ -93,7 +92,8 @@ class WatchStream : public Stream { * query will be streamed back as WatchChange messages that reference the * target ID included in `query`. */ - virtual /*virtual for tests only*/ void WatchQuery(FSTQueryData* query); + virtual /*virtual for tests only*/ void WatchQuery( + const local::QueryData& query); /** * Unregisters interest in the results of the query associated with the given diff --git a/Firestore/core/src/firebase/firestore/remote/watch_stream.mm b/Firestore/core/src/firebase/firestore/remote/watch_stream.mm index 5a8081402ed..2d468b6ebd6 100644 --- a/Firestore/core/src/firebase/firestore/remote/watch_stream.mm +++ b/Firestore/core/src/firebase/firestore/remote/watch_stream.mm @@ -30,6 +30,7 @@ using auth::CredentialsProvider; using auth::Token; +using local::QueryData; using model::TargetId; using util::AsyncQueue; using util::TimerId; @@ -47,7 +48,7 @@ callback_{NOT_NULL(callback)} { } -void WatchStream::WatchQuery(FSTQueryData* query) { +void WatchStream::WatchQuery(const QueryData& query) { EnsureOnQueue(); GCFSListenRequest* request = serializer_bridge_.CreateWatchRequest(query); diff --git a/Firestore/core/src/firebase/firestore/remote/write_stream.h b/Firestore/core/src/firebase/firestore/remote/write_stream.h index 7357be9d187..4f8be832a59 100644 --- a/Firestore/core/src/firebase/firestore/remote/write_stream.h +++ b/Firestore/core/src/firebase/firestore/remote/write_stream.h @@ -31,11 +31,10 @@ #include "Firestore/core/src/firebase/firestore/remote/remote_objc_bridge.h" #include "Firestore/core/src/firebase/firestore/remote/stream.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/strings/string_view.h" #include "grpcpp/support/byte_buffer.h" -#import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" namespace firebase { @@ -102,7 +101,7 @@ class WriteStream : public Stream { GrpcConnection* grpc_connection, WriteStreamCallback* callback); - void SetLastStreamToken(NSData* token); + void SetLastStreamToken(const nanopb::ByteString& token); /** * The last received stream token from the server, used to acknowledge which * responses the client has processed. Stream tokens are opaque checkpoint @@ -111,7 +110,7 @@ class WriteStream : public Stream { * `WriteStream` manages propagating this value from responses to the * next request. */ - NSData* GetLastStreamToken() const; + nanopb::ByteString GetLastStreamToken() const; /** * Tracks whether or not a handshake has been successfully exchanged and diff --git a/Firestore/core/src/firebase/firestore/remote/write_stream.mm b/Firestore/core/src/firebase/firestore/remote/write_stream.mm index 2de26e7ba38..52ecb889724 100644 --- a/Firestore/core/src/firebase/firestore/remote/write_stream.mm +++ b/Firestore/core/src/firebase/firestore/remote/write_stream.mm @@ -31,6 +31,7 @@ using auth::CredentialsProvider; using auth::Token; using model::Mutation; +using nanopb::ByteString; using util::AsyncQueue; using util::TimerId; using util::Status; @@ -47,11 +48,11 @@ callback_{NOT_NULL(callback)} { } -void WriteStream::SetLastStreamToken(NSData* token) { +void WriteStream::SetLastStreamToken(const ByteString& token) { serializer_bridge_.SetLastStreamToken(token); } -NSData* WriteStream::GetLastStreamToken() const { +ByteString WriteStream::GetLastStreamToken() const { return serializer_bridge_.GetLastStreamToken(); } diff --git a/Firestore/core/src/firebase/firestore/timestamp.cc b/Firestore/core/src/firebase/firestore/timestamp.cc index 1eaeca49c35..2ba3d88e904 100644 --- a/Firestore/core/src/firebase/firestore/timestamp.cc +++ b/Firestore/core/src/firebase/firestore/timestamp.cc @@ -138,13 +138,3 @@ void Timestamp::ValidateBounds() const { } } // namespace firebase - -namespace std { -size_t hash::operator()( - const firebase::Timestamp& timestamp) const { - // Note: if sizeof(size_t) == 4, this discards high-order bits of seconds. - return 37 * static_cast(timestamp.seconds()) + - static_cast(timestamp.nanoseconds()); -} - -} // namespace std diff --git a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt index 6506a051ed0..733077f29be 100644 --- a/Firestore/core/src/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/util/CMakeLists.txt @@ -67,7 +67,7 @@ cc_select( cc_library( firebase_firestore_util_base_stdio SOURCES - hard_assert_stdio.cc + hard_assert.cc hard_assert.h log.h log_stdio.cc @@ -84,7 +84,9 @@ cc_library( cc_library( firebase_firestore_util_base_apple SOURCES + hard_assert.cc hard_assert.h + hard_assert_apple.h hard_assert_apple.mm log.h log_apple.mm @@ -205,11 +207,11 @@ cc_library( status.cc status.h status_apple.mm + status_fwd.h status_posix.cc status_win.cc statusor.cc statusor.h - statusor_callback.h statusor_internals.h strerror.cc strerror.h diff --git a/Firestore/core/src/firebase/firestore/util/async_queue.cc b/Firestore/core/src/firebase/firestore/util/async_queue.cc index d4e395ebbbf..23415f4c9f6 100644 --- a/Firestore/core/src/firebase/firestore/util/async_queue.cc +++ b/Firestore/core/src/firebase/firestore/util/async_queue.cc @@ -19,6 +19,7 @@ #include #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/algorithm/container.h" #include "absl/memory/memory.h" namespace firebase { @@ -98,22 +99,21 @@ void AsyncQueue::EnqueueRelaxed(const Operation& operation) { executor_->Execute(Wrap(operation)); } -DelayedOperation AsyncQueue::EnqueueAfterDelay(const Milliseconds delay, +DelayedOperation AsyncQueue::EnqueueAfterDelay(Milliseconds delay, const TimerId timer_id, const Operation& operation) { std::lock_guard lock{shut_down_mutex_}; 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. - HARD_ASSERT(!IsScheduled(timer_id), - "Attempted to schedule multiple operations with id %s", timer_id); - if (is_shutting_down_) { return DelayedOperation(); } + // Skip delays for timer_ids that have been overriden + if (absl::c_linear_search(timer_ids_to_skip_, timer_id)) { + delay = Milliseconds(0); + } + Executor::TaggedOperation tagged{static_cast(timer_id), Wrap(operation)}; return executor_->Schedule(delay, std::move(tagged)); } @@ -166,6 +166,10 @@ void AsyncQueue::RunScheduledOperationsUntil(const TimerId last_timer_id) { }); } +void AsyncQueue::SkipDelaysForTimerId(TimerId timer_id) { + timer_ids_to_skip_.push_back(timer_id); +} + } // 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 index d6f74b7bdf5..aaa87c5aa74 100644 --- a/Firestore/core/src/firebase/firestore/util/async_queue.h +++ b/Firestore/core/src/firebase/firestore/util/async_queue.h @@ -22,6 +22,7 @@ #include #include #include // NOLINT(build/c++11) +#include #include "Firestore/core/src/firebase/firestore/util/executor.h" @@ -55,10 +56,17 @@ enum class TimerId { * indefinitely for success or failure. */ OnlineStateTimeout, + /** * A timer used to periodically attempt LRU Garbage collection */ - GarbageCollectionDelay + GarbageCollectionDelay, + + /** + * A timer used to retry transactions. Since there can be multiple concurrent + * transactions, multiple of these may be in the queue at a given time. + */ + RetryTransaction }; // A serial queue that executes given operations asynchronously, one at a time. @@ -173,6 +181,10 @@ class AsyncQueue { // queue. void RunScheduledOperationsUntil(TimerId last_timer_id); + // For tests: Skip all subsequent delays for a specific TimerId. + // NOTE: This does not work with TimerId::All. + void SkipDelaysForTimerId(TimerId timer_id); + private: Operation Wrap(const Operation& operation); @@ -185,6 +197,8 @@ class AsyncQueue { bool is_shutting_down_ = false; mutable std::mutex shut_down_mutex_; + + std::vector timer_ids_to_skip_; }; } // namespace util diff --git a/Firestore/core/src/firebase/firestore/util/comparison.h b/Firestore/core/src/firebase/firestore/util/comparison.h index 8727e5bac92..67b81a90785 100644 --- a/Firestore/core/src/firebase/firestore/util/comparison.h +++ b/Firestore/core/src/firebase/firestore/util/comparison.h @@ -320,7 +320,7 @@ bool DoubleBitwiseEquals(double left, double right); /** * Computes a bitwise hash of a double, but normalizes NaN values, suitable for - * use when using FSTDoublesAreBitwiseEqual for equality. + * use when using DoubleBitwiseEquals for equality. */ size_t DoubleBitwiseHash(double d); diff --git a/Firestore/core/src/firebase/firestore/util/empty.h b/Firestore/core/src/firebase/firestore/util/empty.h new file mode 100644 index 00000000000..8e1abfdb28e --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/empty.h @@ -0,0 +1,37 @@ +/* + * Copyright 2019 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_EMPTY_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EMPTY_H_ + +namespace firebase { +namespace firestore { +namespace util { + +/** + * Used to represent an empty value. + */ +struct Empty { + friend bool operator==(Empty /* left */, Empty /* right */) { + return true; + } +}; + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_EMPTY_H_ diff --git a/Firestore/core/src/firebase/firestore/util/error_apple.h b/Firestore/core/src/firebase/firestore/util/error_apple.h index ae030e89509..ee900ad2b46 100644 --- a/Firestore/core/src/firebase/firestore/util/error_apple.h +++ b/Firestore/core/src/firebase/firestore/util/error_apple.h @@ -26,8 +26,7 @@ #include #include "Firestore/core/include/firebase/firestore/firestore_errors.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor_callback.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/strings/string_view.h" NS_ASSUME_NONNULL_BEGIN @@ -47,9 +46,7 @@ NSError* MakeNSError(int64_t error_code, const absl::string_view error_msg, NSError* cause = nil); -inline NSError* MakeNSError(const util::Status& status) { - return status.ToNSError(); -} +NSError* MakeNSError(const util::Status& status); Status MakeStatus(NSError* error); diff --git a/Firestore/core/src/firebase/firestore/util/error_apple.mm b/Firestore/core/src/firebase/firestore/util/error_apple.mm index 96a58d61486..0d80a03073c 100644 --- a/Firestore/core/src/firebase/firestore/util/error_apple.mm +++ b/Firestore/core/src/firebase/firestore/util/error_apple.mm @@ -18,6 +18,7 @@ #include "Firestore/core/include/firebase/firestore/firestore_errors.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" // NB: This is also declared in Firestore/Source/Public/FIRFirestoreErrors.h @@ -49,6 +50,10 @@ userInfo:user_info]; } +NSError* MakeNSError(const util::Status& status) { + return status.ToNSError(); +} + util::StatusCallback MakeCallback(VoidErrorBlock _Nullable block) { if (block) { return [block](Status status) { block(MakeNSError(status)); }; diff --git a/Firestore/core/src/firebase/firestore/util/executor.h b/Firestore/core/src/firebase/firestore/util/executor.h index 6299ae73613..c260a28a481 100644 --- a/Firestore/core/src/firebase/firestore/util/executor.h +++ b/Firestore/core/src/firebase/firestore/util/executor.h @@ -19,6 +19,7 @@ #include // NOLINT(build/c++11) #include +#include #include #include @@ -90,6 +91,12 @@ class Executor { Operation operation; }; + // Creates a new serial Executor of the platform-appropriate type, and gives + // it the given label, if the implementation supports it. + // + // Note that this method has multiple definitions, depending on the platform. + static std::unique_ptr CreateSerial(const char* label); + virtual ~Executor() { } diff --git a/Firestore/core/src/firebase/firestore/util/executor_libdispatch.mm b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.mm index 0b16827cf1e..e37f73e99d3 100644 --- a/Firestore/core/src/firebase/firestore/util/executor_libdispatch.mm +++ b/Firestore/core/src/firebase/firestore/util/executor_libdispatch.mm @@ -90,6 +90,8 @@ void RunSynchronized(const ExecutorLibdispatch* const executor, Work&& work) { } // namespace +// MARK: - TimeSlot + // Represents a "busy" time slot on the schedule. // // Since libdispatch doesn't provide a way to cancel a scheduled operation, once @@ -197,7 +199,7 @@ void MarkDone() { executor_->RemoveFromSchedule(this); } -// ExecutorLibdispatch +// MARK: - ExecutorLibdispatch ExecutorLibdispatch::ExecutorLibdispatch(const dispatch_queue_t dispatch_queue) : dispatch_queue_{dispatch_queue} { @@ -303,6 +305,13 @@ void MarkDone() { return result; } +// MARK: - Executor + +std::unique_ptr Executor::CreateSerial(const char* label) { + dispatch_queue_t queue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL); + return absl::make_unique(queue); +} + } // 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 index bfc47fed18e..0d03d4d948d 100644 --- a/Firestore/core/src/firebase/firestore/util/executor_std.cc +++ b/Firestore/core/src/firebase/firestore/util/executor_std.cc @@ -17,6 +17,7 @@ #include "Firestore/core/src/firebase/firestore/util/executor_std.h" #include // NOLINT(build/c++11) +#include #include namespace firebase { @@ -35,6 +36,8 @@ std::string ThreadIdToString(const std::thread::id thread_id) { } // namespace +// MARK: - ExecutorStd + ExecutorStd::ExecutorStd() { // Somewhat counter-intuitively, constructor of `std::atomic` assigns the // value non-atomically, so the atomic initialization must be provided here, @@ -147,6 +150,18 @@ absl::optional ExecutorStd::PopFromSchedule() { return {std::move(removed.value().tagged)}; } +// MARK: - Executor + +// Only defined on non-Apple platforms. On Apple platforms, see the alternative +// definition in executor_libdispatch.mm. +#if !__APPLE__ + +std::unique_ptr Executor::CreateSerial(const char*) { + return absl::make_unique(); +} + +#endif // !__APPLE__ + } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/filesystem.h b/Firestore/core/src/firebase/firestore/util/filesystem.h index 532c9544e41..98e3e47e0e7 100644 --- a/Firestore/core/src/firebase/firestore/util/filesystem.h +++ b/Firestore/core/src/firebase/firestore/util/filesystem.h @@ -22,7 +22,6 @@ #include "Firestore/core/src/firebase/firestore/util/path.h" #include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/util/filesystem_common.cc b/Firestore/core/src/firebase/firestore/util/filesystem_common.cc index f83fccfa05f..905447e02b0 100644 --- a/Firestore/core/src/firebase/firestore/util/filesystem_common.cc +++ b/Firestore/core/src/firebase/firestore/util/filesystem_common.cc @@ -20,6 +20,7 @@ #include #include "Firestore/core/src/firebase/firestore/util/filesystem.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "Firestore/core/src/firebase/firestore/util/string_format.h" using firebase::firestore::util::Path; diff --git a/Firestore/core/src/firebase/firestore/util/filesystem_detail.h b/Firestore/core/src/firebase/firestore/util/filesystem_detail.h index f2b344d5a2b..9242a457f9e 100644 --- a/Firestore/core/src/firebase/firestore/util/filesystem_detail.h +++ b/Firestore/core/src/firebase/firestore/util/filesystem_detail.h @@ -18,7 +18,7 @@ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_FILESYSTEM_DETAIL_H_ #include "Firestore/core/src/firebase/firestore/util/path.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" namespace firebase { namespace firestore { diff --git a/Firestore/core/src/firebase/firestore/util/filesystem_posix.cc b/Firestore/core/src/firebase/firestore/util/filesystem_posix.cc index 5b9b2c1733e..96ebf02ce10 100644 --- a/Firestore/core/src/firebase/firestore/util/filesystem_posix.cc +++ b/Firestore/core/src/firebase/firestore/util/filesystem_posix.cc @@ -27,6 +27,7 @@ #include "Firestore/core/src/firebase/firestore/util/filesystem_detail.h" #include "Firestore/core/src/firebase/firestore/util/path.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "Firestore/core/src/firebase/firestore/util/string_format.h" #include "absl/memory/memory.h" diff --git a/Firestore/core/src/firebase/firestore/util/hard_assert_stdio.cc b/Firestore/core/src/firebase/firestore/util/hard_assert.cc similarity index 70% rename from Firestore/core/src/firebase/firestore/util/hard_assert_stdio.cc rename to Firestore/core/src/firebase/firestore/util/hard_assert.cc index 6c50eb7221e..0b27f7e521c 100644 --- a/Firestore/core/src/firebase/firestore/util/hard_assert_stdio.cc +++ b/Firestore/core/src/firebase/firestore/util/hard_assert.cc @@ -26,12 +26,11 @@ namespace firebase { namespace firestore { namespace util { -namespace internal { -void Fail(const char* file, - const char* func, - const int line, - const std::string& message) { +ABSL_ATTRIBUTE_NORETURN void DefaultFailureHandler(const char* file, + const char* func, + const int line, + const std::string& message) { std::string failure = StringFormat("ASSERT: %s(%s) %s: %s", file, line, func, message); @@ -44,6 +43,29 @@ void Fail(const char* file, #endif } +namespace { +FailureHandler failure_handler = DefaultFailureHandler; +} // namespace + +FailureHandler SetFailureHandler(FailureHandler callback) { + FailureHandler previous = failure_handler; + failure_handler = callback; + return previous; +} + +namespace internal { + +void Fail(const char* file, + const char* func, + const int line, + const std::string& message) { + failure_handler(file, func, line, message); + + // It's expected that the failure handler above does not return. But if it + // does, just terminate. + std::terminate(); +} + void Fail(const char* file, const char* func, const int line, diff --git a/Firestore/core/src/firebase/firestore/util/hard_assert.h b/Firestore/core/src/firebase/firestore/util/hard_assert.h index d72b85ad0bf..eb104525875 100644 --- a/Firestore/core/src/firebase/firestore/util/hard_assert.h +++ b/Firestore/core/src/firebase/firestore/util/hard_assert.h @@ -101,6 +101,35 @@ namespace firebase { namespace firestore { namespace util { + +using FailureHandler = void (*)(const char* file, + const char* func, + const int line, + const std::string& failure_message); + +/** + * Overrides the default failure handler. + * + * The default essentially just calls std::terminate. While reasonable for C++, + * this isn't optimal for platforms that merely use the C++ core as their + * implementation and would otherwise be expected to throw a platform specific + * exception. + * + * @param callback A function that will handle the failure. This function is + * not expected to return. (If it does, std::terminate() will be called + * immediately after it does so.) + * @return A pointer to the previous failure handler. + */ +FailureHandler SetFailureHandler(FailureHandler callback); + +/** + * Default failure handler. This should typically not be called directly. + */ +ABSL_ATTRIBUTE_NORETURN void DefaultFailureHandler(const char* file, + const char* func, + const int line, + const std::string& message); + namespace internal { // A no-return helper function. To raise an assertion, use Macro instead. diff --git a/Firestore/core/src/firebase/firestore/util/hard_assert_apple.h b/Firestore/core/src/firebase/firestore/util/hard_assert_apple.h new file mode 100644 index 00000000000..1a16b3cfbec --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/hard_assert_apple.h @@ -0,0 +1,42 @@ +/* + * Copyright 2019 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_HARD_ASSERT_APPLE_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_HARD_ASSERT_APPLE_H_ + +#include + +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/base/attributes.h" + +namespace firebase { +namespace firestore { +namespace util { + +/** + * Default failure handler for ObjC/Swift. Typically shouldn't be used + * directly. + */ +ABSL_ATTRIBUTE_NORETURN void ObjcFailureHandler(const char* file, + const char* func, + const int line, + const std::string& message); + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_HARD_ASSERT_APPLE_H_ diff --git a/Firestore/core/src/firebase/firestore/util/hard_assert_apple.mm b/Firestore/core/src/firebase/firestore/util/hard_assert_apple.mm index 869a8a8b9f7..6ed55463075 100644 --- a/Firestore/core/src/firebase/firestore/util/hard_assert_apple.mm +++ b/Firestore/core/src/firebase/firestore/util/hard_assert_apple.mm @@ -14,23 +14,20 @@ * limitations under the License. */ -#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert_apple.h" #import -#include - #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace firebase { namespace firestore { namespace util { -namespace internal { -void Fail(const char* file, - const char* func, - const int line, - const std::string& message) { +ABSL_ATTRIBUTE_NORETURN void ObjcFailureHandler(const char* file, + const char* func, + const int line, + const std::string& message) { [[NSAssertionHandler currentHandler] handleFailureInFunction:MakeNSString(func) file:MakeNSString(file) @@ -40,21 +37,6 @@ void Fail(const char* file, abort(); } -void Fail(const char* file, - const char* func, - const int line, - const std::string& message, - const char* condition) { - std::string failure; - if (message.empty()) { - failure = condition; - } else { - failure = StringFormat("%s (expected %s)", message, condition); - } - Fail(file, func, line, failure); -} - -} // namespace internal } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/util/nullability.h b/Firestore/core/src/firebase/firestore/util/nullability.h new file mode 100644 index 00000000000..4313f26cd14 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/util/nullability.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 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_NULLABILITY_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_NULLABILITY_H_ + +namespace firebase { +namespace firestore { +namespace util { + +// Define NS_ASSUME_NONNULL_BEGIN for straight C++ so that everything gets the +// correct nullability specifier. +#if !defined(NS_ASSUME_NONNULL_BEGIN) +#if __clang__ +#define NS_ASSUME_NONNULL_BEGIN _Pragma("clang assume_nonnull begin") +#define NS_ASSUME_NONNULL_END _Pragma("clang assume_nonnull end") + +#else // !__clang__ +#define NS_ASSUME_NONNULL_BEGIN +#define NS_ASSUME_NONNULL_END +#define _Nonnull +#define _Nullable + +#endif // __clang__ +#endif // !defined(NS_ASSUME_NONNULL_BEGIN) + +} // namespace util +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_NULLABILITY_H_ diff --git a/Firestore/core/src/firebase/firestore/util/status.cc b/Firestore/core/src/firebase/firestore/util/status.cc index fa35e7713dd..b5860b18b24 100644 --- a/Firestore/core/src/firebase/firestore/util/status.cc +++ b/Firestore/core/src/firebase/firestore/util/status.cc @@ -28,9 +28,7 @@ namespace util { Status::Status(Error code, absl::string_view msg) { HARD_ASSERT(code != Error::Ok); - state_ = absl::make_unique(); - state_->code = code; - state_->msg = static_cast(msg); + state_ = State::MakePtr(code, static_cast(msg)); } void Status::Update(const Status& new_status) { @@ -40,19 +38,20 @@ void Status::Update(const Status& new_status) { } Status& Status::CausedBy(const Status& cause) { - if (cause.ok() || this == &cause) { + if (cause.ok() || this == &cause || cause.IsMovedFrom()) { return *this; } - if (ok()) { + if (ok() || IsMovedFrom()) { *this = cause; return *this; } + std::string new_message = error_message(); absl::StrAppend(&state_->msg, ": ", cause.error_message()); // If this Status has no accompanying PlatformError but the cause does, create - // an PlatformError for this Status ahead of time to preserve the causal chain + // a PlatformError for this Status ahead of time to preserve the causal chain // that Status doesn't otherwise support. if (state_->platform_error == nullptr && cause.state_->platform_error != nullptr) { @@ -65,15 +64,30 @@ Status& Status::CausedBy(const Status& cause) { Status& Status::WithPlatformError(std::unique_ptr error) { HARD_ASSERT(!ok(), "Platform errors should not be applied to Status::OK()"); + if (IsMovedFrom()) { + std::string message = moved_from_message(); + state_ = State::MakePtr(Error::Internal, std::move(message)); + } state_->platform_error = std::move(error); return *this; } +void Status::State::Deleter::operator()(const State* ptr) const { + if (ptr != State::MovedFromIndicator()) { + delete ptr; + } +} + +void Status::SetMovedFrom() { + // Set pointer value to `0x1` as the pointer is no longer useful. + state_ = State::StatePtr{State::MovedFromIndicator()}; +} + void Status::SlowCopyFrom(const State* src) { if (src == nullptr) { state_ = nullptr; } else { - state_ = absl::make_unique(*src); + state_ = State::MakePtr(*src); } } @@ -82,6 +96,11 @@ const std::string& Status::empty_string() { return *empty; } +const std::string& Status::moved_from_message() { + static std::string* message = new std::string("Status accessed after move."); + return *message; +} + std::string Status::ToString() const { if (state_ == nullptr) { return "OK"; @@ -141,7 +160,7 @@ std::string Status::ToString() const { break; } result += ": "; - result += state_->msg; + result += IsMovedFrom() ? moved_from_message() : state_->msg; return result; } } diff --git a/Firestore/core/src/firebase/firestore/util/status.h b/Firestore/core/src/firebase/firestore/util/status.h index d312a314d44..aae1804ebe3 100644 --- a/Firestore/core/src/firebase/firestore/util/status.h +++ b/Firestore/core/src/firebase/firestore/util/status.h @@ -25,9 +25,11 @@ #include #include #include +#include #include "Firestore/core/include/firebase/firestore/firestore_errors.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "absl/base/attributes.h" #include "absl/strings/string_view.h" @@ -52,6 +54,10 @@ class ABSL_MUST_USE_RESULT Status { Status(const Status& s); void operator=(const Status& s); + /// Move the specified status. + Status(Status&& s) noexcept; + void operator=(Status&& s) noexcept; + static Status OK() { return Status(); } @@ -71,15 +77,16 @@ class ABSL_MUST_USE_RESULT Status { /// Returns true iff the status indicates success. bool ok() const { - return (state_ == nullptr); + return state_ == nullptr; } Error code() const { - return ok() ? Error::Ok : state_->code; + return ok() ? Error::Ok : (IsMovedFrom() ? Error::Internal : state_->code); } const std::string& error_message() const { - return ok() ? empty_string() : state_->msg; + return ok() ? empty_string() + : (IsMovedFrom() ? moved_from_message() : state_->msg); } bool operator==(const Status& x) const; @@ -115,9 +122,29 @@ class ABSL_MUST_USE_RESULT Status { private: static const std::string& empty_string(); + static const std::string& moved_from_message(); + struct State { State() = default; State(const State& other); + State(Error code, std::string&& msg); + + struct Deleter { + void operator()(const State* ptr) const; + }; + // A `unique_ptr` with a custom deleter. If the pointer's value has been set + // to a special value (0x01) to indicate it is moved, invoking the custom + // deleter will be a no-op. + using StatePtr = std::unique_ptr; + + static State* MovedFromIndicator() { + return reinterpret_cast(0x01); + } + + template + static StatePtr MakePtr(Args&&... args) { + return StatePtr(new State(std::forward(args)...)); + } Error code; std::string msg; @@ -128,9 +155,20 @@ class ABSL_MUST_USE_RESULT Status { // NSError* to Status and back to NSError* losslessly. std::unique_ptr platform_error; }; - // OK status has a `nullptr` state_. Otherwise, `state_` points to - // a `State` structure containing the error code and message(s) - std::unique_ptr state_; + + // Asserts if `state_` is a valid pointer, should be used at all places where + // it is used as a pointer, instead of using `state_`. + bool IsMovedFrom() const { + return state_.get() == State::MovedFromIndicator(); + } + + // OK status has a `nullptr` `state_`. If this instance is moved, state_ has + // the value of `State::MovedFromIndicator()`. Otherwise `state_` points to + // a `State` structure containing the error code and message(s). + State::StatePtr state_; + + // Tags this instance as `moved-from`. + void SetMovedFrom(); void SlowCopyFrom(const State* src); }; @@ -151,7 +189,8 @@ class PlatformError { }; inline Status::Status(const Status& s) - : state_((s.state_ == nullptr) ? nullptr : new State(*s.state_)) { + : state_{s.state_ == nullptr ? State::StatePtr{} + : State::MakePtr(*s.state_)} { } inline Status::State::State(const State& s) @@ -161,6 +200,10 @@ inline Status::State::State(const State& s) : s.platform_error->Copy()) { } +inline Status::State::State(Error code, std::string&& msg) + : code(code), msg(std::move(msg)) { +} + inline void Status::operator=(const Status& s) { // The following condition catches both aliasing (when this == &s), // and the common case where both s and *this are ok. @@ -169,6 +212,18 @@ inline void Status::operator=(const Status& s) { } } +inline Status::Status(Status&& s) noexcept : state_(std::move(s.state_)) { + s.SetMovedFrom(); +} + +inline void Status::operator=(Status&& s) noexcept { + // Moving into self is a no-op. + if (this != &s) { + state_ = std::move(s.state_); + s.SetMovedFrom(); + } +} + inline bool Status::operator==(const Status& x) const { return (this->state_ == x.state_) || (ToString() == x.ToString()); } diff --git a/Firestore/core/src/firebase/firestore/util/status_apple.mm b/Firestore/core/src/firebase/firestore/util/status_apple.mm index fe98b72c772..87f21cfb9c3 100644 --- a/Firestore/core/src/firebase/firestore/util/status_apple.mm +++ b/Firestore/core/src/firebase/firestore/util/status_apple.mm @@ -110,6 +110,8 @@ Status FromFirestoreNSError(NSError* error) { NSError* Status::ToNSError() const { if (ok()) return nil; + // Early exit because `state_` is moved. + if (IsMovedFrom()) return MakeNSError(code(), error_message()); NSError* error = UnderlyingNSError::Recover(state_->platform_error); if (error) return error; diff --git a/Firestore/core/src/firebase/firestore/util/statusor_callback.h b/Firestore/core/src/firebase/firestore/util/status_fwd.h similarity index 70% rename from Firestore/core/src/firebase/firestore/util/statusor_callback.h rename to Firestore/core/src/firebase/firestore/util/status_fwd.h index ff39b5df2ae..65ee204edb7 100644 --- a/Firestore/core/src/firebase/firestore/util/statusor_callback.h +++ b/Firestore/core/src/firebase/firestore/util/status_fwd.h @@ -14,17 +14,24 @@ * limitations under the License. */ -#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STATUSOR_CALLBACK_H_ -#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STATUSOR_CALLBACK_H_ +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STATUS_FWD_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STATUS_FWD_H_ #include -#include "Firestore/core/src/firebase/firestore/util/statusor.h" - namespace firebase { namespace firestore { namespace util { +// Forward declarations for Status classes. + +class Status; + +using StatusCallback = std::function; + +template +class StatusOr; + template using StatusOrCallback = std::function)>; @@ -32,4 +39,4 @@ using StatusOrCallback = std::function)>; } // namespace firestore } // namespace firebase -#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STATUSOR_CALLBACK_H_ +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_UTIL_STATUS_FWD_H_ diff --git a/Firestore/core/test/firebase/firestore/core/CMakeLists.txt b/Firestore/core/test/firebase/firestore/core/CMakeLists.txt index 903be0ae6fc..7baa17ef900 100644 --- a/Firestore/core/test/firebase/firestore/core/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/core/CMakeLists.txt @@ -18,6 +18,7 @@ cc_test( database_info_test.cc target_id_generator_test.cc query_test.cc + view_test.cc DEPENDS GMock::GMock firebase_firestore_core diff --git a/Firestore/core/test/firebase/firestore/core/event_manager_test.mm b/Firestore/core/test/firebase/firestore/core/event_manager_test.mm new file mode 100644 index 00000000000..f5e32f71cd3 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/core/event_manager_test.mm @@ -0,0 +1,178 @@ +/* + * Copyright 2019 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 +#include +#include + +#include "Firestore/core/src/firebase/firestore/core/event_manager.h" +#include "Firestore/core/src/firebase/firestore/core/sync_engine.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace core { +namespace { + +using model::DocumentKeySet; +using model::DocumentSet; +using model::OnlineState; +using util::StatusOr; +using util::StatusOrCallback; +using testutil::Query; +using testing::_; +using testing::ElementsAre; +using testing::StrictMock; + +ViewSnapshot::Listener NoopViewSnapshotHandler() { + return EventListener::Create( + [](const StatusOr&) {}); +} + +std::shared_ptr NoopQueryListener(core::Query query) { + return QueryListener::Create(std::move(query), + ListenOptions::DefaultOptions(), + NoopViewSnapshotHandler()); +} + +class MockEventSource : public core::QueryEventSource { + public: + MOCK_METHOD1(SetCallback, void(core::SyncEngineCallback*)); + MOCK_METHOD1(Listen, model::TargetId(core::Query)); + MOCK_METHOD1(StopListening, void(const core::Query&)); +}; + +TEST(EventManagerTest, HandlesManyListnersPerQuery) { + core::Query query = Query("foo/bar"); + auto listener1 = NoopQueryListener(query); + auto listener2 = NoopQueryListener(query); + + StrictMock mock_event_source; + EXPECT_CALL(mock_event_source, SetCallback(_)); + EventManager event_manager(&mock_event_source); + + EXPECT_CALL(mock_event_source, Listen(query)); + event_manager.AddQueryListener(listener1); + + // Expecting no activity from mock_event_source. + event_manager.AddQueryListener(listener2); + event_manager.RemoveQueryListener(listener2); + + EXPECT_CALL(mock_event_source, StopListening(query)); + event_manager.RemoveQueryListener(listener1); +} + +TEST(EventManagerTest, HandlesUnlistenOnUnknownListenerGracefully) { + core::Query query = Query("foo/bar"); + auto listener = NoopQueryListener(query); + + MockEventSource mock_event_source; + EventManager event_manager(&mock_event_source); + + EXPECT_CALL(mock_event_source, StopListening(_)).Times(0); + event_manager.RemoveQueryListener(listener); +} + +ViewSnapshot make_empty_view_snapshot(const core::Query& query) { + DocumentSet empty_docs{query.Comparator()}; + // sync_state_changed has to be `true` to prevent an assertion about a + // meaningless view snapshot. + return ViewSnapshot{query, + empty_docs, + empty_docs, + {}, + DocumentKeySet{}, + false, + /*sync_state_changed=*/true, + false}; +} + +TEST(EventManagerTest, NotifiesListenersInTheRightOrder) { + core::Query query1 = Query("foo/bar"); + core::Query query2 = Query("bar/baz"); + std::vector event_order; + + auto listener1 = QueryListener::Create(query1, [&](StatusOr) { + event_order.push_back("listener1"); + }); + auto listener2 = QueryListener::Create(query2, [&](StatusOr) { + event_order.push_back("listener2"); + }); + auto listener3 = QueryListener::Create(query1, [&](StatusOr) { + event_order.push_back("listener3"); + }); + + MockEventSource mock_event_source; + EventManager event_manager(&mock_event_source); + + EXPECT_CALL(mock_event_source, Listen(query1)); + event_manager.AddQueryListener(listener1); + + EXPECT_CALL(mock_event_source, Listen(query2)); + event_manager.AddQueryListener(listener2); + + event_manager.AddQueryListener(listener3); + + ViewSnapshot snapshot1 = make_empty_view_snapshot(query1); + ViewSnapshot snapshot2 = make_empty_view_snapshot(query2); + event_manager.OnViewSnapshots({snapshot1, snapshot2}); + + ASSERT_THAT(event_order, ElementsAre("listener1", "listener3", "listener2")); +} + +TEST(EventManagerTest, WillForwardOnlineStateChanges) { + core::Query query = Query("foo/bar"); + + class FakeQueryListener : public QueryListener { + public: + explicit FakeQueryListener(core::Query query) + : QueryListener(std::move(query), + ListenOptions::DefaultOptions(), + NoopViewSnapshotHandler()) { + } + + bool OnOnlineStateChanged(OnlineState online_state) override { + events.push_back(online_state); + return false; + } + + std::vector events; + }; + + auto fake_listener = std::make_shared(query); + + MockEventSource mock_event_source; + EventManager event_manager(&mock_event_source); + + event_manager.AddQueryListener(fake_listener); + ASSERT_THAT(fake_listener->events, ElementsAre(OnlineState::Unknown)); + + event_manager.HandleOnlineStateChange(OnlineState::Online); + ASSERT_THAT(fake_listener->events, + ElementsAre(OnlineState::Unknown, OnlineState::Online)); +} + +} // namespace +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/core/query_listener_test.cc b/Firestore/core/test/firebase/firestore/core/query_listener_test.cc new file mode 100644 index 00000000000..c3d8b999681 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/core/query_listener_test.cc @@ -0,0 +1,518 @@ +/* + * 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. + */ + +#include // NOLINT(build/c++11) +#include +#include +#include + +#include "Firestore/core/include/firebase/firestore/firestore_errors.h" +#include "Firestore/core/src/firebase/firestore/core/event_listener.h" +#include "Firestore/core/src/firebase/firestore/core/listen_options.h" +#include "Firestore/core/src/firebase/firestore/core/query_listener.h" +#include "Firestore/core/src/firebase/firestore/core/view.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/model/types.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" +#include "Firestore/core/src/firebase/firestore/util/delayed_constructor.h" +#include "Firestore/core/src/firebase/firestore/util/executor.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/async_testing.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +#include "Firestore/core/test/firebase/firestore/testutil/view_testing.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace core { + +using model::Document; +using model::DocumentKeySet; +using model::DocumentSet; +using model::DocumentState; +using model::OnlineState; +using remote::TargetChange; +using util::DelayedConstructor; +using util::Executor; +using util::Status; +using util::StatusOr; + +using testing::ElementsAre; +using testing::IsEmpty; +using testutil::AckTarget; +using testutil::ApplyChanges; +using testutil::Doc; +using testutil::Expectation; +using testutil::Map; +using testutil::MarkCurrent; + +namespace { + +ViewSnapshot ExcludingMetadataChanges(const ViewSnapshot& snapshot) { + return ViewSnapshot{ + snapshot.query(), + snapshot.documents(), + snapshot.old_documents(), + snapshot.document_changes(), + snapshot.mutated_keys(), + snapshot.from_cache(), + snapshot.sync_state_changed(), + /*excludes_metadata_changes=*/true, + }; +} + +ViewSnapshot::Listener Accumulating(std::vector* values) { + return EventListener::Create( + [values](const StatusOr& maybe_snapshot) { + values->push_back(maybe_snapshot.ValueOrDie()); + }); +} + +} // namespace + +class QueryListenerTest : public testing::Test, public testutil::AsyncTest { + protected: + void SetUp() override { + _executor = testutil::ExecutorForTesting("worker"); + include_metadata_changes_ = ListenOptions::FromIncludeMetadataChanges(true); + } + + std::shared_ptr _executor; + ListenOptions include_metadata_changes_; +}; + +TEST_F(QueryListenerTest, RaisesCollectionEvents) { + std::vector accum; + std::vector other_accum; + + Query query = testutil::Query("rooms"); + Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros")); + Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); + Document doc2prime = + Doc("rooms/Hades", 3, Map("name", "Hades", "owner", "Jonny")); + + auto listener = QueryListener::Create(query, include_metadata_changes_, + Accumulating(&accum)); + auto other_listener = + QueryListener::Create(query, Accumulating(&other_accum)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {doc1, doc2}, absl::nullopt).value(); + ViewSnapshot snap2 = ApplyChanges(&view, {doc2prime}, absl::nullopt).value(); + + DocumentViewChange change1{doc1, DocumentViewChange::Type::Added}; + DocumentViewChange change2{doc2, DocumentViewChange::Type::Added}; + DocumentViewChange change3{doc2prime, DocumentViewChange::Type::Modified}; + DocumentViewChange change4{doc2prime, DocumentViewChange::Type::Added}; + + listener->OnViewSnapshot(snap1); + listener->OnViewSnapshot(snap2); + other_listener->OnViewSnapshot(snap2); + + ASSERT_THAT(accum, ElementsAre(snap1, snap2)); + ASSERT_THAT(accum[0].document_changes(), ElementsAre(change1, change2)); + ASSERT_THAT(accum[1].document_changes(), ElementsAre(change3)); + + ViewSnapshot expected_snap2{ + snap2.query(), + snap2.documents(), + /*old_documents=*/DocumentSet{snap2.query().Comparator()}, + /*document_changes=*/{change1, change4}, + snap2.mutated_keys(), + snap2.from_cache(), + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + ASSERT_THAT(other_accum, ElementsAre(expected_snap2)); +} + +TEST_F(QueryListenerTest, RaisesErrorEvent) { + __block std::vector accum; + Query query = testutil::Query("rooms/Eros"); + + auto listener = QueryListener::Create( + query, ^(const StatusOr& maybe_snapshot) { + accum.push_back(maybe_snapshot.status()); + }); + + Status test_error{Error::Unauthenticated, "Some info"}; + listener->OnError(test_error); + + ASSERT_THAT(accum, ElementsAre(test_error)); +} + +TEST_F(QueryListenerTest, RaisesEventForEmptyCollectionAfterSync) { + std::vector accum; + Query query = testutil::Query("rooms"); + + auto listener = QueryListener::Create(query, include_metadata_changes_, + Accumulating(&accum)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {}, absl::nullopt).value(); + ViewSnapshot snap2 = ApplyChanges(&view, {}, MarkCurrent()).value(); + + listener->OnViewSnapshot(snap1); + ASSERT_THAT(accum, IsEmpty()); + + listener->OnViewSnapshot(snap2); + ASSERT_THAT(accum, ElementsAre(snap2)); +} + +TEST_F(QueryListenerTest, MutingAsyncListenerPreventsAllSubsequentEvents) { + std::vector accum; + + Query query = testutil::Query("rooms/Eros"); + Document doc1 = Doc("rooms/Eros", 3, Map("name", "Eros")); + Document doc2 = Doc("rooms/Eros", 4, Map("name", "Eros2")); + + std::shared_ptr> listener = + AsyncEventListener::Create( + _executor, EventListener::Create( + [&accum, &listener]( + const StatusOr& maybe_snapshot) { + accum.push_back(maybe_snapshot.ValueOrDie()); + listener->Mute(); + })); + + View view(query, DocumentKeySet{}); + ViewSnapshot view_snapshot1 = + ApplyChanges(&view, {doc1}, absl::nullopt).value(); + ViewSnapshot view_snapshot2 = + ApplyChanges(&view, {doc2}, absl::nullopt).value(); + + listener->OnEvent(view_snapshot1); + listener->OnEvent(view_snapshot2); + + // Drain queue + Expectation drained; + _executor->Execute(drained.AsCallback()); + Await(drained); + + // We should get the first snapshot but not the second. + ASSERT_THAT(accum, ElementsAre(view_snapshot1)); +} + +TEST_F(QueryListenerTest, DoesNotRaiseEventsForMetadataChangesUnlessSpecified) { + std::vector filtered_accum; + std::vector full_accum; + + Query query = testutil::Query("rooms"); + Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros")); + Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); + + auto filtered_listener = + QueryListener::Create(query, Accumulating(&filtered_accum)); + auto full_listener = QueryListener::Create(query, include_metadata_changes_, + Accumulating(&full_accum)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {doc1}, absl::nullopt).value(); + + TargetChange ack_target = AckTarget({doc1}); + ViewSnapshot snap2 = ApplyChanges(&view, {}, ack_target).value(); + ViewSnapshot snap3 = ApplyChanges(&view, {doc2}, absl::nullopt).value(); + + filtered_listener->OnViewSnapshot(snap1); // local event + filtered_listener->OnViewSnapshot(snap2); // no event + filtered_listener->OnViewSnapshot(snap3); // doc2 update + + full_listener->OnViewSnapshot(snap1); // local event + full_listener->OnViewSnapshot(snap2); // state change event + full_listener->OnViewSnapshot(snap3); // doc2 update + + ASSERT_THAT(filtered_accum, ElementsAre(ExcludingMetadataChanges(snap1), + ExcludingMetadataChanges(snap3))); + ASSERT_THAT(full_accum, ElementsAre(snap1, snap2, snap3)); +} + +TEST_F(QueryListenerTest, RaisesDocumentMetadataEventsOnlyWhenSpecified) { + std::vector filtered_accum; + std::vector full_accum; + + Query query = testutil::Query("rooms"); + Document doc1 = + Doc("rooms/Eros", 1, Map("name", "Eros"), DocumentState::kLocalMutations); + Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); + Document doc1_prime = Doc("rooms/Eros", 1, Map("name", "Eros")); + Document doc3 = Doc("rooms/Other", 3, Map("name", "Other")); + + ListenOptions options( + /*include_query_metadata_changes=*/false, + /*include_document_metadata_changes=*/true, + /*wait_for_sync_when_online=*/false); + + auto filtered_listener = + QueryListener::Create(query, Accumulating(&filtered_accum)); + auto full_listener = + QueryListener::Create(query, options, Accumulating(&full_accum)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {doc1, doc2}, absl::nullopt).value(); + ViewSnapshot snap2 = ApplyChanges(&view, {doc1_prime}, absl::nullopt).value(); + ViewSnapshot snap3 = ApplyChanges(&view, {doc3}, absl::nullopt).value(); + + DocumentViewChange change1{doc1, DocumentViewChange::Type::Added}; + DocumentViewChange change2{doc2, DocumentViewChange::Type::Added}; + DocumentViewChange change3{doc1_prime, DocumentViewChange::Type::Metadata}; + DocumentViewChange change4{doc3, DocumentViewChange::Type::Added}; + + filtered_listener->OnViewSnapshot(snap1); + filtered_listener->OnViewSnapshot(snap2); + filtered_listener->OnViewSnapshot(snap3); + full_listener->OnViewSnapshot(snap1); + full_listener->OnViewSnapshot(snap2); + full_listener->OnViewSnapshot(snap3); + + ASSERT_THAT(filtered_accum, ElementsAre(ExcludingMetadataChanges(snap1), + ExcludingMetadataChanges(snap3))); + ASSERT_THAT(filtered_accum[0].document_changes(), + ElementsAre(change1, change2)); + ASSERT_THAT(filtered_accum[1].document_changes(), ElementsAre(change4)); + + ASSERT_THAT(full_accum, ElementsAre(snap1, snap2, snap3)); + ASSERT_THAT(full_accum[0].document_changes(), ElementsAre(change1, change2)); + ASSERT_THAT(full_accum[1].document_changes(), ElementsAre(change3)); + ASSERT_THAT(full_accum[2].document_changes(), ElementsAre(change4)); +} + +TEST_F(QueryListenerTest, + RaisesQueryMetadataEventsOnlyWhenHasPendingWritesOnTheQueryChanges) { + std::vector full_accum; + + Query query = testutil::Query("rooms"); + Document doc1 = + Doc("rooms/Eros", 1, Map("name", "Eros"), DocumentState::kLocalMutations); + Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades"), + DocumentState::kLocalMutations); + Document doc1_prime = Doc("rooms/Eros", 1, Map("name", "Eros")); + Document doc2_prime = Doc("rooms/Hades", 2, Map("name", "Hades")); + Document doc3 = Doc("rooms/Other", 3, Map("name", "Other")); + + ListenOptions options( + /*include_query_metadata_changes=*/true, + /*include_document_metadata_changes=*/false, + /*wait_for_sync_when_online=*/false); + auto full_listener = + QueryListener::Create(query, options, Accumulating(&full_accum)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {doc1, doc2}, absl::nullopt).value(); + ViewSnapshot snap2 = ApplyChanges(&view, {doc1_prime}, absl::nullopt).value(); + ViewSnapshot snap3 = ApplyChanges(&view, {doc3}, absl::nullopt).value(); + ViewSnapshot snap4 = ApplyChanges(&view, {doc2_prime}, absl::nullopt).value(); + + full_listener->OnViewSnapshot(snap1); + full_listener->OnViewSnapshot(snap2); // Emits no events. + full_listener->OnViewSnapshot(snap3); + full_listener->OnViewSnapshot(snap4); // Metadata change event. + + ViewSnapshot expected_snap4{ + snap4.query(), + snap4.documents(), + snap3.documents(), + /*document_changes=*/{}, + snap4.mutated_keys(), + snap4.from_cache(), + snap4.sync_state_changed(), + /*excludes_metadata_changes=*/true // This test excludes document + // metadata changes + }; + + ASSERT_THAT(full_accum, + ElementsAre(ExcludingMetadataChanges(snap1), + ExcludingMetadataChanges(snap3), expected_snap4)); +} + +TEST_F(QueryListenerTest, + TestMetadataOnlyDocChangesAreRemovedWhenIncludeMetadataChangesIsFalse) { + std::vector filtered_accum; + + Query query = testutil::Query("rooms"); + Document doc1 = + Doc("rooms/Eros", 1, Map("name", "Eros"), DocumentState::kLocalMutations); + Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); + Document doc1_prime = Doc("rooms/Eros", 1, Map("name", "Eros")); + Document doc3 = Doc("rooms/Other", 3, Map("name", "Other")); + + auto filtered_listener = + QueryListener::Create(query, Accumulating(&filtered_accum)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {doc1, doc2}, absl::nullopt).value(); + ViewSnapshot snap2 = + ApplyChanges(&view, {doc1_prime, doc3}, absl::nullopt).value(); + + DocumentViewChange change3{doc3, DocumentViewChange::Type::Added}; + + filtered_listener->OnViewSnapshot(snap1); + filtered_listener->OnViewSnapshot(snap2); + + ViewSnapshot expected_snap2{snap2.query(), + snap2.documents(), + snap1.documents(), + /*document_changes=*/{change3}, + snap2.mutated_keys(), + snap2.from_cache(), + snap2.sync_state_changed(), + /*excludes_metadata_changes=*/true}; + ASSERT_THAT(filtered_accum, + ElementsAre(ExcludingMetadataChanges(snap1), expected_snap2)); +} + +TEST_F(QueryListenerTest, WillWaitForSyncIfOnline) { + std::vector events; + + Query query = testutil::Query("rooms"); + Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros")); + Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); + + ListenOptions options( + /*include_query_metadata_changes=*/false, + /*include_document_metadata_changes=*/false, + /*wait_for_sync_when_online=*/true); + auto listener = QueryListener::Create(query, options, Accumulating(&events)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {doc1}, absl::nullopt).value(); + ViewSnapshot snap2 = ApplyChanges(&view, {doc2}, absl::nullopt).value(); + ViewSnapshot snap3 = ApplyChanges(&view, {}, AckTarget({doc1, doc2})).value(); + + listener->OnOnlineStateChanged(OnlineState::Online); // no event + listener->OnViewSnapshot(snap1); + listener->OnOnlineStateChanged(OnlineState::Unknown); + listener->OnOnlineStateChanged(OnlineState::Online); + listener->OnViewSnapshot(snap2); + listener->OnViewSnapshot(snap3); + + DocumentViewChange change1{doc1, DocumentViewChange::Type::Added}; + DocumentViewChange change2{doc2, DocumentViewChange::Type::Added}; + ViewSnapshot expected_snap{ + snap3.query(), + snap3.documents(), + /*old_documents=*/DocumentSet{snap3.query().Comparator()}, + /*document_changes=*/{change1, change2}, + snap3.mutated_keys(), + /*from_cache=*/false, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + ASSERT_THAT(events, ElementsAre(expected_snap)); +} + +TEST_F(QueryListenerTest, WillRaiseInitialEventWhenGoingOffline) { + std::vector events; + + Query query = testutil::Query("rooms"); + Document doc1 = Doc("rooms/Eros", 1, Map("name", "Eros")); + Document doc2 = Doc("rooms/Hades", 2, Map("name", "Hades")); + + ListenOptions options( + /*include_query_metadata_changes=*/false, + /*include_document_metadata_changes=*/false, + /*wait_for_sync_when_online=*/true); + + auto listener = QueryListener::Create(query, options, Accumulating(&events)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {doc1}, absl::nullopt).value(); + ViewSnapshot snap2 = ApplyChanges(&view, {doc2}, absl::nullopt).value(); + + listener->OnOnlineStateChanged(OnlineState::Online); // no event + listener->OnViewSnapshot(snap1); // no event + listener->OnOnlineStateChanged(OnlineState::Offline); // event + listener->OnOnlineStateChanged(OnlineState::Unknown); // no event + listener->OnOnlineStateChanged(OnlineState::Offline); // no event + listener->OnViewSnapshot(snap2); // another event + + DocumentViewChange change1{doc1, DocumentViewChange::Type::Added}; + DocumentViewChange change2{doc2, DocumentViewChange::Type::Added}; + ViewSnapshot expected_snap1{ + query, + /*documents=*/snap1.documents(), + /*old_documents=*/DocumentSet{snap1.query().Comparator()}, + /*document_changes=*/{change1}, + snap1.mutated_keys(), + /*from_cache=*/true, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + + ViewSnapshot expected_snap2{query, + /*documents=*/snap2.documents(), + /*old_documents=*/snap1.documents(), + /*document_changes=*/{change2}, + snap2.mutated_keys(), + /*from_cache=*/true, + /*sync_state_changed=*/false, + /*excludes_metadata_changes=*/true}; + ASSERT_THAT(events, ElementsAre(expected_snap1, expected_snap2)); +} + +TEST_F(QueryListenerTest, + WillRaiseInitialEventWhenGoingOfflineAndThereAreNoDocs) { + std::vector events; + + Query query = testutil::Query("rooms"); + auto listener = QueryListener::Create(query, Accumulating(&events)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {}, absl::nullopt).value(); + + listener->OnOnlineStateChanged(OnlineState::Online); // no event + listener->OnViewSnapshot(snap1); // no event + listener->OnOnlineStateChanged(OnlineState::Offline); // event + + ViewSnapshot expected_snap{ + query, + /*documents=*/snap1.documents(), + /*old_documents=*/DocumentSet{snap1.query().Comparator()}, + /*document_changes=*/{}, + snap1.mutated_keys(), + /*from_cache=*/true, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + ASSERT_THAT(events, ElementsAre(expected_snap)); +} + +TEST_F(QueryListenerTest, + WillRaiseInitialEventWhenStartingOfflineAndThereAreNoDocs) { + std::vector events; + + Query query = testutil::Query("rooms"); + auto listener = QueryListener::Create(query, Accumulating(&events)); + + View view(query, DocumentKeySet{}); + ViewSnapshot snap1 = ApplyChanges(&view, {}, absl::nullopt).value(); + + listener->OnOnlineStateChanged(OnlineState::Offline); // no event + listener->OnViewSnapshot(snap1); // event + + ViewSnapshot expected_snap{ + query, + /*documents=*/snap1.documents(), + /*old_documents=*/DocumentSet{snap1.query().Comparator()}, + /*document_changes=*/{}, + snap1.mutated_keys(), + /*from_cache=*/true, + /*sync_state_changed=*/true, + /*excludes_metadata_changes=*/true}; + ASSERT_THAT(events, ElementsAre(expected_snap)); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/core/view_test.cc b/Firestore/core/test/firebase/firestore/core/view_test.cc new file mode 100644 index 00000000000..c27aef9c6d6 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/core/view_test.cc @@ -0,0 +1,676 @@ +/* + * 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. + */ + +#include +#include +#include + +#include "Firestore/core/src/firebase/firestore/core/field_filter.h" +#include "Firestore/core/src/firebase/firestore/core/filter.h" +#include "Firestore/core/src/firebase/firestore/core/view.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_set.h" +#include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/test/firebase/firestore/testutil/testutil.h" +#include "Firestore/core/test/firebase/firestore/testutil/view_testing.h" +#include "absl/types/optional.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace core { + +using model::Document; +using model::DocumentKeySet; +using model::DocumentSet; +using model::DocumentState; +using model::FieldValue; +using model::ResourcePath; + +using testing::ElementsAre; +using testutil::AckTarget; +using testutil::ApplyChanges; +using testutil::DeletedDoc; +using testutil::Doc; +using testutil::DocUpdates; +using testutil::Field; +using testutil::Filter; +using testutil::Map; +using testutil::MarkCurrent; +using testutil::OrderBy; + +/** + * A custom matcher that verifies that the subject has the same keys as the + * given documents without verifying that the contents are the same. + */ +MATCHER_P(ContainsDocs, expected, "") { + if (expected.size() != arg.size()) { + return false; + } + for (const Document& doc : expected) { + if (!arg.ContainsKey(doc.key())) { + return false; + } + } + return true; +} + +/** Constructs `ContainsDocs` instances with an initializer list. */ +inline ContainsDocsMatcherP> ContainsDocs( + std::vector docs) { + return ContainsDocsMatcherP>(std::move(docs)); +} + +/** Returns a new empty query to use for testing. */ +inline Query QueryForMessages() { + return testutil::Query("rooms/eros/messages"); +} + +TEST(ViewTest, AddsDocumentsBasedOnQuery) { + Query query = QueryForMessages(); + View view(query, DocumentKeySet{}); + + Document doc1 = Doc("rooms/eros/messages/1", 0, Map("text", "msg1")); + Document doc2 = Doc("rooms/eros/messages/2", 0, Map("text", "msg2")); + Document doc3 = Doc("rooms/other/messages/1", 0, Map("text", "msg3")); + + absl::optional maybe_snapshot = + ApplyChanges(&view, {doc1, doc2, doc3}, AckTarget({doc1, doc2, doc3})); + ASSERT_TRUE(maybe_snapshot.has_value()); + ViewSnapshot snapshot = std::move(maybe_snapshot).value(); + + ASSERT_EQ(snapshot.query(), query); + + ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc2)); + + ASSERT_TRUE((snapshot.document_changes() == + std::vector{ + DocumentViewChange{doc1, DocumentViewChange::Type::Added}, + DocumentViewChange{doc2, DocumentViewChange::Type::Added}})); + + ASSERT_FALSE(snapshot.from_cache()); + ASSERT_FALSE(snapshot.has_pending_writes()); + ASSERT_TRUE(snapshot.sync_state_changed()); +} + +TEST(ViewTest, RemovesDocuments) { + Query query = QueryForMessages(); + View view(query, DocumentKeySet{}); + + Document doc1 = Doc("rooms/eros/messages/1", 0, Map("text", "msg1")); + Document doc2 = Doc("rooms/eros/messages/2", 0, Map("text", "msg2")); + Document doc3 = Doc("rooms/eros/messages/3", 0, Map("text", "msg3")); + + // initial state + ApplyChanges(&view, {doc1, doc2}, absl::nullopt); + + // delete doc2, add doc3 + absl::optional maybe_snapshot = + ApplyChanges(&view, {DeletedDoc("rooms/eros/messages/2"), doc3}, + AckTarget({doc1, doc3})); + ASSERT_TRUE(maybe_snapshot.has_value()); + ViewSnapshot snapshot = std::move(maybe_snapshot).value(); + + ASSERT_EQ(snapshot.query(), query); + + ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc3)); + + ASSERT_TRUE((snapshot.document_changes() == + std::vector{ + DocumentViewChange{doc2, DocumentViewChange::Type::Removed}, + DocumentViewChange{doc3, DocumentViewChange::Type::Added}})); + + ASSERT_FALSE(snapshot.from_cache()); + ASSERT_TRUE(snapshot.sync_state_changed()); +} + +TEST(ViewTest, ReturnsNilIfThereAreNoChanges) { + Query query = QueryForMessages(); + View view(query, DocumentKeySet{}); + + Document doc1 = Doc("rooms/eros/messages/1", 0, Map("text", "msg1")); + Document doc2 = Doc("rooms/eros/messages/2", 0, Map("text", "msg2")); + + // initial state + ApplyChanges(&view, {doc1, doc2}, absl::nullopt); + + // reapply same docs, no changes + absl::optional snapshot = + ApplyChanges(&view, {doc1, doc2}, absl::nullopt); + ASSERT_FALSE(snapshot.has_value()); +} + +TEST(ViewTest, DoesNotReturnNilForFirstChanges) { + Query query = QueryForMessages(); + View view(query, DocumentKeySet{}); + + absl::optional snapshot = + ApplyChanges(&view, {}, absl::nullopt); + ASSERT_TRUE(snapshot.has_value()); +} + +TEST(ViewTest, FiltersDocumentsBasedOnQueryWithFilter) { + Query query = QueryForMessages().AddingFilter(Filter("sort", "<=", 2)); + + View view(query, DocumentKeySet{}); + Document doc1 = Doc("rooms/eros/messages/1", 0, Map("sort", 1)); + Document doc2 = Doc("rooms/eros/messages/2", 0, Map("sort", 2)); + Document doc3 = Doc("rooms/eros/messages/3", 0, Map("sort", 3)); + Document doc4 = Doc("rooms/eros/messages/4", 0, Map()); // no sort, no match + Document doc5 = Doc("rooms/eros/messages/5", 0, Map("sort", 1)); + + absl::optional maybe_snapshot = + ApplyChanges(&view, {doc1, doc2, doc3, doc4, doc5}, absl::nullopt); + ASSERT_TRUE(maybe_snapshot.has_value()); + ViewSnapshot snapshot = std::move(maybe_snapshot).value(); + + ASSERT_EQ(snapshot.query(), query); + + ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc5, doc2)); + + ASSERT_TRUE((snapshot.document_changes() == + std::vector{ + DocumentViewChange{doc1, DocumentViewChange::Type::Added}, + DocumentViewChange{doc5, DocumentViewChange::Type::Added}, + DocumentViewChange{doc2, DocumentViewChange::Type::Added}})); + + ASSERT_TRUE(snapshot.from_cache()); + ASSERT_TRUE(snapshot.sync_state_changed()); +} + +TEST(ViewTest, UpdatesDocumentsBasedOnQueryWithFilter) { + Query query = QueryForMessages().AddingFilter(Filter("sort", "<=", 2)); + + View view(query, DocumentKeySet{}); + Document doc1 = Doc("rooms/eros/messages/1", 0, Map("sort", 1)); + Document doc2 = Doc("rooms/eros/messages/2", 0, Map("sort", 3)); + Document doc3 = Doc("rooms/eros/messages/3", 0, Map("sort", 2)); + Document doc4 = Doc("rooms/eros/messages/4", 0, Map()); + + ViewSnapshot snapshot = + ApplyChanges(&view, {doc1, doc2, doc3, doc4}, absl::nullopt).value(); + + ASSERT_EQ(snapshot.query(), query); + + ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc3)); + + Document new_doc2 = Doc("rooms/eros/messages/2", 1, Map("sort", 2)); + Document new_doc3 = Doc("rooms/eros/messages/3", 1, Map("sort", 3)); + Document new_doc4 = Doc("rooms/eros/messages/4", 1, Map("sort", 0)); + + snapshot = ApplyChanges(&view, {new_doc2, new_doc3, new_doc4}, absl::nullopt) + .value(); + + ASSERT_EQ(snapshot.query(), query); + + ASSERT_THAT(snapshot.documents(), ElementsAre(new_doc4, doc1, new_doc2)); + + ASSERT_THAT( + snapshot.document_changes(), + ElementsAre( + DocumentViewChange{doc3, DocumentViewChange::Type::Removed}, + DocumentViewChange{new_doc4, DocumentViewChange::Type::Added}, + DocumentViewChange{new_doc2, DocumentViewChange::Type::Added})); + + ASSERT_TRUE(snapshot.from_cache()); + ASSERT_FALSE(snapshot.sync_state_changed()); +} + +TEST(ViewTest, RemovesDocumentsForQueryWithLimit) { + Query query = QueryForMessages().WithLimit(2); + View view(query, DocumentKeySet{}); + + Document doc1 = Doc("rooms/eros/messages/1", 0, Map("text", "msg1")); + Document doc2 = Doc("rooms/eros/messages/2", 0, Map("text", "msg2")); + Document doc3 = Doc("rooms/eros/messages/3", 0, Map("text", "msg3")); + + // initial state + ApplyChanges(&view, {doc1, doc3}, absl::nullopt); + + // add doc2, which should push out doc3 + ViewSnapshot snapshot = + ApplyChanges(&view, {doc2}, AckTarget({doc1, doc2, doc3})).value(); + + ASSERT_EQ(snapshot.query(), query); + + ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc2)); + + ASSERT_TRUE((snapshot.document_changes() == + std::vector{ + DocumentViewChange{doc3, DocumentViewChange::Type::Removed}, + DocumentViewChange{doc2, DocumentViewChange::Type::Added}})); + + ASSERT_FALSE(snapshot.from_cache()); + ASSERT_TRUE(snapshot.sync_state_changed()); +} + +TEST(ViewTest, DoesntReportChangesForDocumentBeyondLimitOfQuery) { + Query query = QueryForMessages().AddingOrderBy(OrderBy("num")).WithLimit(2); + View view(query, DocumentKeySet{}); + + Document doc1 = Doc("rooms/eros/messages/1", 0, Map("num", 1)); + Document doc2 = Doc("rooms/eros/messages/2", 0, Map("num", 2)); + Document doc3 = Doc("rooms/eros/messages/3", 0, Map("num", 3)); + Document doc4 = Doc("rooms/eros/messages/4", 0, Map("num", 4)); + + // initial state + ApplyChanges(&view, {doc1, doc2}, absl::nullopt); + + // change doc2 to 5, and add doc3 and doc4. + // doc2 will be modified + removed = removed + // doc3 will be added + // doc4 will be added + removed = nothing + doc2 = Doc("rooms/eros/messages/2", 1, Map("num", 5)); + ViewDocumentChanges view_doc_changes = + view.ComputeDocumentChanges(DocUpdates({doc2, doc3, doc4})); + ASSERT_TRUE(view_doc_changes.needs_refill()); + // Verify that all the docs still match. + view_doc_changes = view.ComputeDocumentChanges( + DocUpdates({doc1, doc2, doc3, doc4}), view_doc_changes); + absl::optional maybe_snapshot = + view.ApplyChanges(view_doc_changes, AckTarget({doc1, doc2, doc3, doc4})) + .snapshot(); + ASSERT_TRUE(maybe_snapshot.has_value()); + ViewSnapshot snapshot = std::move(maybe_snapshot).value(); + + ASSERT_EQ(snapshot.query(), query); + + ASSERT_THAT(snapshot.documents(), ElementsAre(doc1, doc3)); + + ASSERT_THAT( + snapshot.document_changes(), + ElementsAre(DocumentViewChange{doc2, DocumentViewChange::Type::Removed}, + DocumentViewChange{doc3, DocumentViewChange::Type::Added})); + + ASSERT_FALSE(snapshot.from_cache()); + ASSERT_TRUE(snapshot.sync_state_changed()); +} + +TEST(ViewTest, KeepsTrackOfLimboDocuments) { + Query query = QueryForMessages(); + View view(query, DocumentKeySet{}); + + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); + Document doc3 = Doc("rooms/eros/messages/2", 0, Map()); + + ViewChange change = + view.ApplyChanges(view.ComputeDocumentChanges(DocUpdates({doc1}))); + ASSERT_THAT(change.limbo_changes(), ElementsAre()); + + change = view.ApplyChanges(view.ComputeDocumentChanges(DocUpdates({})), + MarkCurrent()); + ASSERT_THAT(change.limbo_changes(), + ElementsAre(LimboDocumentChange::Added(doc1.key()))); + + change = view.ApplyChanges(view.ComputeDocumentChanges(DocUpdates({})), + AckTarget({doc1})); + ASSERT_THAT(change.limbo_changes(), + ElementsAre(LimboDocumentChange::Removed(doc1.key()))); + + change = view.ApplyChanges(view.ComputeDocumentChanges(DocUpdates({doc2})), + AckTarget({doc2})); + ASSERT_THAT(change.limbo_changes(), ElementsAre()); + + change = view.ApplyChanges(view.ComputeDocumentChanges(DocUpdates({doc3}))); + ASSERT_THAT(change.limbo_changes(), + ElementsAre(LimboDocumentChange::Added(doc3.key()))); + + change = view.ApplyChanges(view.ComputeDocumentChanges( + DocUpdates({DeletedDoc("rooms/eros/messages/2")}))); // remove + ASSERT_THAT(change.limbo_changes(), + ElementsAre(LimboDocumentChange::Removed(doc3.key()))); +} + +TEST(ViewTest, ResumingQueryCreatesNoLimbos) { + Query query = QueryForMessages(); + + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); + + // 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. + View view(query, DocumentKeySet{doc1.key(), doc2.key()}); + + ViewDocumentChanges changes = view.ComputeDocumentChanges(DocUpdates({})); + ViewChange change = view.ApplyChanges(changes, MarkCurrent()); + ASSERT_THAT(change.limbo_changes(), ElementsAre()); +} + +TEST(ViewTest, ReturnsNeedsRefillOnDeleteInLimitQuery) { + Query query = QueryForMessages().WithLimit(2); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(2, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); + + // Remove one of the docs. + changes = view.ComputeDocumentChanges( + DocUpdates({DeletedDoc("rooms/eros/messages/0")})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc2})); + ASSERT_TRUE(changes.needs_refill()); + ASSERT_EQ(1, changes.change_set().GetChanges().size()); + // Refill it with just the one doc remaining. + changes = view.ComputeDocumentChanges(DocUpdates({doc2}), changes); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc2})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(1, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); +} + +TEST(ViewTest, ReturnsNeedsRefillOnReorderInLimitQuery) { + Query query = QueryForMessages().AddingOrderBy(OrderBy("order")).WithLimit(2); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map("order", 1)); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map("order", 2)); + Document doc3 = Doc("rooms/eros/messages/2", 0, Map("order", 3)); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2, doc3})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(2, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); + + // Move one of the docs. + doc2 = Doc("rooms/eros/messages/1", 1, Map("order", 2000)); + changes = view.ComputeDocumentChanges(DocUpdates({doc2})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2})); + ASSERT_TRUE(changes.needs_refill()); + ASSERT_EQ(1, changes.change_set().GetChanges().size()); + // Refill it with all three current docs. + changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2, doc3}), changes); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc3})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(2, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); +} + +TEST(ViewTest, DoesntNeedRefillOnReorderWithinLimit) { + Query query = QueryForMessages().AddingOrderBy(OrderBy("order")).WithLimit(3); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map("order", 1)); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map("order", 2)); + Document doc3 = Doc("rooms/eros/messages/2", 0, Map("order", 3)); + Document doc4 = Doc("rooms/eros/messages/3", 0, Map("order", 4)); + Document doc5 = Doc("rooms/eros/messages/4", 0, Map("order", 5)); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2, doc3, doc4, doc5})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2, doc3})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(3, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); + + // Move one of the docs. + doc1 = Doc("rooms/eros/messages/0", 1, Map("order", 3)); + changes = view.ComputeDocumentChanges(DocUpdates({doc1})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc2, doc3, doc1})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(1, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); +} + +TEST(ViewTest, DoesntNeedRefillOnReorderAfterLimitQuery) { + Query query = QueryForMessages().AddingOrderBy(OrderBy("order")).WithLimit(3); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map("order", 1)); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map("order", 2)); + Document doc3 = Doc("rooms/eros/messages/2", 0, Map("order", 3)); + Document doc4 = Doc("rooms/eros/messages/3", 0, Map("order", 4)); + Document doc5 = Doc("rooms/eros/messages/4", 0, Map("order", 5)); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2, doc3, doc4, doc5})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2, doc3})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(3, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); + + // Move one of the docs. + doc4 = Doc("rooms/eros/messages/3", 1, Map("order", 6)); + changes = view.ComputeDocumentChanges(DocUpdates({doc4})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2, doc3})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(0, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); +} + +TEST(ViewTest, DoesntNeedRefillForAdditionAfterTheLimit) { + Query query = QueryForMessages().WithLimit(2); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(2, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); + + // Add a doc that is past the limit. + Document doc3 = Doc("rooms/eros/messages/2", 1, Map()); + changes = view.ComputeDocumentChanges(DocUpdates({doc3})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(0, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); +} + +TEST(ViewTest, DoesntNeedRefillForDeletionsWhenNotNearTheLimit) { + Query query = QueryForMessages().WithLimit(20); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); + View view(query, DocumentKeySet{}); + + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(2, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); + + // Remove one of the docs. + changes = view.ComputeDocumentChanges( + DocUpdates({DeletedDoc("rooms/eros/messages/1")})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(1, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); +} + +TEST(ViewTest, HandlesApplyingIrrelevantDocs) { + Query query = QueryForMessages().WithLimit(2); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(2, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); + + // Remove a doc that isn't even in the results. + changes = view.ComputeDocumentChanges( + DocUpdates({DeletedDoc("rooms/eros/messages/2")})); + ASSERT_THAT(changes.document_set(), ContainsDocs({doc1, doc2})); + ASSERT_FALSE(changes.needs_refill()); + ASSERT_EQ(0, changes.change_set().GetChanges().size()); + view.ApplyChanges(changes); +} + +TEST(ViewTest, ComputesMutatedKeys) { + Query query = QueryForMessages(); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = Doc("rooms/eros/messages/1", 0, Map()); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2})); + view.ApplyChanges(changes); + ASSERT_EQ(changes.mutated_keys(), DocumentKeySet{}); + + Document doc3 = + Doc("rooms/eros/messages/2", 0, Map(), DocumentState::kLocalMutations); + changes = view.ComputeDocumentChanges(DocUpdates({doc3})); + ASSERT_EQ(changes.mutated_keys(), DocumentKeySet{doc3.key()}); +} + +TEST(ViewTest, RemovesKeysFromMutatedKeysWhenNewDocHasNoLocalChanges) { + Query query = QueryForMessages(); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = + Doc("rooms/eros/messages/1", 0, Map(), DocumentState::kLocalMutations); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2})); + view.ApplyChanges(changes); + ASSERT_EQ(changes.mutated_keys(), (DocumentKeySet{doc2.key()})); + + Document doc2_prime = Doc("rooms/eros/messages/1", 0, Map()); + changes = view.ComputeDocumentChanges(DocUpdates({doc2_prime})); + view.ApplyChanges(changes); + ASSERT_EQ(changes.mutated_keys(), DocumentKeySet{}); +} + +TEST(ViewTest, RemembersLocalMutationsFromPreviousSnapshot) { + Query query = QueryForMessages(); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = + Doc("rooms/eros/messages/1", 0, Map(), DocumentState::kLocalMutations); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2})); + view.ApplyChanges(changes); + ASSERT_EQ(changes.mutated_keys(), (DocumentKeySet{doc2.key()})); + + Document doc3 = Doc("rooms/eros/messages/2", 0, Map()); + changes = view.ComputeDocumentChanges(DocUpdates({doc3})); + view.ApplyChanges(changes); + ASSERT_EQ(changes.mutated_keys(), (DocumentKeySet{doc2.key()})); +} + +TEST(ViewTest, + RemembersLocalMutationsFromPreviousCallToComputeDocumentChanges) { + Query query = QueryForMessages(); + Document doc1 = Doc("rooms/eros/messages/0", 0, Map()); + Document doc2 = + Doc("rooms/eros/messages/1", 0, Map(), DocumentState::kLocalMutations); + View view(query, DocumentKeySet{}); + + // Start with a full view. + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2})); + ASSERT_EQ(changes.mutated_keys(), (DocumentKeySet{doc2.key()})); + + Document doc3 = Doc("rooms/eros/messages/2", 0, Map()); + changes = view.ComputeDocumentChanges(DocUpdates({doc3}), changes); + ASSERT_EQ(changes.mutated_keys(), (DocumentKeySet{doc2.key()})); +} + +TEST(ViewTest, RaisesHasPendingWritesForPendingMutationsInInitialSnapshot) { + Query query = QueryForMessages(); + Document doc1 = + Doc("rooms/eros/messages/1", 0, Map(), DocumentState::kLocalMutations); + View view(query, DocumentKeySet{}); + ViewDocumentChanges changes = view.ComputeDocumentChanges(DocUpdates({doc1})); + ViewChange view_change = view.ApplyChanges(changes); + ASSERT_TRUE(view_change.snapshot()->has_pending_writes()); +} + +TEST(ViewTest, + DoesntRaiseHasPendingWritesForCommittedMutationsInInitialSnapshot) { + Query query = QueryForMessages(); + Document doc1 = Doc("rooms/eros/messages/1", 0, Map(), + DocumentState::kCommittedMutations); + View view(query, DocumentKeySet{}); + ViewDocumentChanges changes = view.ComputeDocumentChanges(DocUpdates({doc1})); + ViewChange view_change = view.ApplyChanges(changes); + ASSERT_FALSE(view_change.snapshot()->has_pending_writes()); +} + +TEST(ViewTest, SuppressesWriteAcknowledgementIfWatchHasNotCaughtUp) { + // This test verifies that we don't get three events for an FSTServerTimestamp + // mutation. We suppress the event generated by the write acknowledgement and + // instead wait for Watch to catch up. + + Query query = QueryForMessages(); + Document doc1 = Doc("rooms/eros/messages/1", 1, Map("time", 1), + DocumentState::kLocalMutations); + Document doc1_committed = Doc("rooms/eros/messages/1", 2, Map("time", 2), + DocumentState::kCommittedMutations); + Document doc1_acknowledged = Doc("rooms/eros/messages/1", 2, Map("time", 2)); + Document doc2 = Doc("rooms/eros/messages/2", 1, Map("time", 1), + DocumentState::kLocalMutations); + Document doc2_modified = Doc("rooms/eros/messages/2", 2, Map("time", 3), + DocumentState::kLocalMutations); + Document doc2_acknowledged = Doc("rooms/eros/messages/2", 2, Map("time", 3)); + View view(query, DocumentKeySet{}); + ViewDocumentChanges changes = + view.ComputeDocumentChanges(DocUpdates({doc1, doc2})); + ViewChange view_change = view.ApplyChanges(changes); + + ASSERT_THAT( + view_change.snapshot()->document_changes(), + ElementsAre(DocumentViewChange{doc1, DocumentViewChange::Type::Added}, + DocumentViewChange{doc2, DocumentViewChange::Type::Added})); + + changes = + view.ComputeDocumentChanges(DocUpdates({doc1_committed, doc2_modified})); + view_change = view.ApplyChanges(changes); + // The 'doc1_committed' update is suppressed + ASSERT_THAT(view_change.snapshot()->document_changes(), + ElementsAre(DocumentViewChange{ + doc2_modified, DocumentViewChange::Type::Modified})); + + changes = view.ComputeDocumentChanges( + DocUpdates({doc1_acknowledged, doc2_acknowledged})); + view_change = view.ApplyChanges(changes); + ASSERT_THAT( + view_change.snapshot()->document_changes(), + ElementsAre(DocumentViewChange{doc1_acknowledged, + DocumentViewChange::Type::Modified}, + DocumentViewChange{doc2_acknowledged, + DocumentViewChange::Type::Metadata})); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/local/index_manager_test.h b/Firestore/core/test/firebase/firestore/local/index_manager_test.h index bcea21c79a7..cdaaaf16057 100644 --- a/Firestore/core/test/firebase/firestore/local/index_manager_test.h +++ b/Firestore/core/test/firebase/firestore/local/index_manager_test.h @@ -28,15 +28,15 @@ #include "Firestore/core/src/firebase/firestore/local/index_manager.h" #include "gtest/gtest.h" -#import "Firestore/Source/Local/FSTPersistence.h" - NS_ASSUME_NONNULL_BEGIN namespace firebase { namespace firestore { namespace local { -using FactoryFunc = id _Nonnull (*)(); +class Persistence; + +using FactoryFunc = std::unique_ptr (*)(); class IndexManagerTest : public ::testing::TestWithParam { public: @@ -44,7 +44,7 @@ class IndexManagerTest : public ::testing::TestWithParam { IndexManagerTest() : persistence{GetParam()()} { } - id persistence; + std::unique_ptr persistence; virtual ~IndexManagerTest(); diff --git a/Firestore/core/test/firebase/firestore/local/index_manager_test.mm b/Firestore/core/test/firebase/firestore/local/index_manager_test.mm index a2680a44bd3..aa8cee25e43 100644 --- a/Firestore/core/test/firebase/firestore/local/index_manager_test.mm +++ b/Firestore/core/test/firebase/firestore/local/index_manager_test.mm @@ -22,6 +22,7 @@ #include "Firestore/core/test/firebase/firestore/local/index_manager_test.h" #include "Firestore/core/src/firebase/firestore/local/index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/persistence.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "gtest/gtest.h" @@ -33,7 +34,7 @@ void IndexManagerTest::AssertParents(const std::string& collection_id, std::vector expected) { - IndexManager* index_manager = persistence.indexManager; + IndexManager* index_manager = persistence->index_manager(); std::vector actual_paths = index_manager->GetCollectionParents(collection_id); std::vector actual; @@ -48,12 +49,12 @@ } IndexManagerTest::~IndexManagerTest() { - [persistence shutdown]; + persistence->Shutdown(); } TEST_P(IndexManagerTest, AddAndReadCollectionParentIndexEntries) { - IndexManager* index_manager = persistence.indexManager; - persistence.run("AddAndReadCollectionParentIndexEntries", [&]() { + IndexManager* index_manager = persistence->index_manager(); + persistence->Run("AddAndReadCollectionParentIndexEntries", [&]() { index_manager->AddToCollectionParentIndex(ResourcePath{"messages"}); index_manager->AddToCollectionParentIndex(ResourcePath{"messages"}); index_manager->AddToCollectionParentIndex( diff --git a/Firestore/core/test/firebase/firestore/local/leveldb_index_manager_test.mm b/Firestore/core/test/firebase/firestore/local/leveldb_index_manager_test.mm index a68581b17b1..92c067187e8 100644 --- a/Firestore/core/test/firebase/firestore/local/leveldb_index_manager_test.mm +++ b/Firestore/core/test/firebase/firestore/local/leveldb_index_manager_test.mm @@ -17,9 +17,9 @@ #include "Firestore/core/test/firebase/firestore/local/index_manager_test.h" #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Source/Local/FSTPersistence.h" #include "Firestore/core/src/firebase/firestore/local/leveldb_index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/leveldb_persistence.h" #include "absl/memory/memory.h" #include "gtest/gtest.h" @@ -31,9 +31,8 @@ namespace { -id PersistenceFactory() { - return static_cast>( - [FSTPersistenceTestHelpers levelDBPersistence]); +std::unique_ptr PersistenceFactory() { + return [FSTPersistenceTestHelpers levelDBPersistence]; } } // namespace diff --git a/Firestore/core/test/firebase/firestore/local/leveldb_key_test.cc b/Firestore/core/test/firebase/firestore/local/leveldb_key_test.cc index c6843f76943..18322ed21c1 100644 --- a/Firestore/core/test/firebase/firestore/local/leveldb_key_test.cc +++ b/Firestore/core/test/firebase/firestore/local/leveldb_key_test.cc @@ -61,7 +61,7 @@ std::string DocTargetKey(absl::string_view key, TargetId target_id) { * description. * * @param key A StringView of a textual key - * @param key An string that `Describe(key)` is expected to produce. + * @param key A string that `Describe(key)` is expected to produce. */ #define AssertExpectedKeyDescription(expected_description, key) \ ASSERT_EQ((expected_description), DescribeKey(key)) diff --git a/Firestore/core/test/firebase/firestore/local/leveldb_util_test.cc b/Firestore/core/test/firebase/firestore/local/leveldb_util_test.cc index b4964f00385..f9cf9e475d9 100644 --- a/Firestore/core/test/firebase/firestore/local/leveldb_util_test.cc +++ b/Firestore/core/test/firebase/firestore/local/leveldb_util_test.cc @@ -15,6 +15,7 @@ */ #include "Firestore/core/src/firebase/firestore/local/leveldb_util.h" +#include "Firestore/core/src/firebase/firestore/util/status.h" #include "gtest/gtest.h" diff --git a/Firestore/core/test/firebase/firestore/local/local_serializer_test.cc b/Firestore/core/test/firebase/firestore/local/local_serializer_test.cc index dbec523b106..6b652fe8eba 100644 --- a/Firestore/core/test/firebase/firestore/local/local_serializer_test.cc +++ b/Firestore/core/test/firebase/firestore/local/local_serializer_test.cc @@ -169,7 +169,7 @@ class LocalSerializerTest : public ::testing::Test { ByteString EncodeQueryData(local::LocalSerializer* serializer, const QueryData& query_data) { - EXPECT_EQ(query_data.purpose(), QueryPurpose::kListen); + EXPECT_EQ(query_data.purpose(), QueryPurpose::Listen); ByteStringWriter writer; firestore_client_Target proto = serializer->EncodeQueryData(query_data); writer.WriteNanopbMessage(firestore_client_Target_fields, &proto); @@ -216,6 +216,10 @@ class LocalSerializerTest : public ::testing::Test { }; TEST_F(LocalSerializerTest, EncodesMutationBatch) { + Mutation base = + PatchMutation(Key("bar/baz"), WrapObject("a", "b"), FieldMask{Field("a")}, + Precondition::Exists(true)); + Mutation set = testutil::SetMutation("foo/bar", Map("a", "b", "num", 1)); Mutation patch = PatchMutation(Key("bar/baz"), WrapObject("a", "b", "num", 1), @@ -223,17 +227,20 @@ TEST_F(LocalSerializerTest, EncodesMutationBatch) { Mutation del = testutil::DeleteMutation("baz/quux"); Timestamp write_time = Timestamp::Now(); - std::vector mutations; - mutations.push_back(std::move(set)); - mutations.push_back(std::move(patch)); - mutations.push_back(std::move(del)); - MutationBatch model(42, write_time, std::move(mutations)); + MutationBatch model(42, write_time, {base}, {set, patch, del}); v1::Value b_value{}; *b_value.mutable_string_value() = "b"; v1::Value one_value{}; one_value.set_integer_value(1); + v1::Write base_proto{}; + *base_proto.mutable_update()->mutable_name() = + "projects/p/databases/d/documents/bar/baz"; + (*base_proto.mutable_update()->mutable_fields())["a"] = b_value; + base_proto.mutable_update_mask()->add_field_paths("a"); + base_proto.mutable_current_document()->set_exists(true); + v1::Write set_proto{}; *set_proto.mutable_update()->mutable_name() = "projects/p/databases/d/documents/foo/bar"; @@ -257,6 +264,7 @@ TEST_F(LocalSerializerTest, EncodesMutationBatch) { ::firestore::client::WriteBatch batch_proto{}; batch_proto.set_batch_id(42); + *batch_proto.add_base_writes() = base_proto; *batch_proto.add_writes() = set_proto; assert(batch_proto.writes(0).update().name() == "projects/p/databases/d/documents/foo/bar"); @@ -316,7 +324,7 @@ TEST_F(LocalSerializerTest, EncodesQueryData) { ByteString resume_token = testutil::ResumeToken(1039); QueryData query_data(core::Query(query), target_id, sequence_number, - QueryPurpose::kListen, SnapshotVersion(version), + QueryPurpose::Listen, SnapshotVersion(version), ByteString(resume_token)); // Let the RPC serializer test various permutations of query serialization. diff --git a/Firestore/core/test/firebase/firestore/local/memory_index_manager_test.mm b/Firestore/core/test/firebase/firestore/local/memory_index_manager_test.mm index 28039b37243..d72bb87d85d 100644 --- a/Firestore/core/test/firebase/firestore/local/memory_index_manager_test.mm +++ b/Firestore/core/test/firebase/firestore/local/memory_index_manager_test.mm @@ -17,11 +17,12 @@ #include "Firestore/core/test/firebase/firestore/local/index_manager_test.h" #include "Firestore/core/src/firebase/firestore/local/memory_index_manager.h" +#include "Firestore/core/src/firebase/firestore/local/memory_persistence.h" +#include "Firestore/core/src/firebase/firestore/local/reference_delegate.h" #include "absl/memory/memory.h" #include "gtest/gtest.h" #import "Firestore/Example/Tests/Local/FSTPersistenceTestHelpers.h" -#import "Firestore/Source/Local/FSTPersistence.h" NS_ASSUME_NONNULL_BEGIN @@ -31,9 +32,8 @@ namespace { -id PersistenceFactory() { - return static_cast>( - [FSTPersistenceTestHelpers lruMemoryPersistence]); +std::unique_ptr PersistenceFactory() { + return [FSTPersistenceTestHelpers lruMemoryPersistence]; } } // namespace diff --git a/Firestore/core/test/firebase/firestore/model/field_value_test.cc b/Firestore/core/test/firebase/firestore/model/field_value_test.cc index da5bde48164..d7896f01827 100644 --- a/Firestore/core/test/firebase/firestore/model/field_value_test.cc +++ b/Firestore/core/test/firebase/firestore/model/field_value_test.cc @@ -370,7 +370,7 @@ TEST(FieldValue, ToString) { // Bytes escaped as hex auto blob = FieldValue::FromBlob(ByteString("HI")); - EXPECT_EQ("<4849>", blob.ToString()); + EXPECT_EQ("HI", blob.ToString()); auto ref = FieldValue::FromReference(DatabaseId("p", "d"), Key("foo/bar")); EXPECT_EQ("Reference(key=foo/bar)", ref.ToString()); diff --git a/Firestore/core/test/firebase/firestore/nanopb/byte_string_test.cc b/Firestore/core/test/firebase/firestore/nanopb/byte_string_test.cc index 65242eb7f19..548ebaef708 100644 --- a/Firestore/core/test/firebase/firestore/nanopb/byte_string_test.cc +++ b/Firestore/core/test/firebase/firestore/nanopb/byte_string_test.cc @@ -156,6 +156,13 @@ TEST(ByteStringTest, Comparison) { EXPECT_TRUE(abc2 >= abc); } +TEST(ByteStringTest, ToString) { + EXPECT_EQ(ByteString{""}.ToString(), ""); + EXPECT_EQ(ByteString{"abc"}.ToString(), "abc"); + EXPECT_EQ(ByteString{"abc\ndef"}.ToString(), "abc\\ndef"); + EXPECT_EQ(ByteString{"abc\002"}.ToString(), "abc\\002"); +} + } // namespace nanopb } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/nanopb/nanopb_testing.h b/Firestore/core/test/firebase/firestore/nanopb/nanopb_testing.h index 2e29d753080..2660cca0ede 100644 --- a/Firestore/core/test/firebase/firestore/nanopb/nanopb_testing.h +++ b/Firestore/core/test/firebase/firestore/nanopb/nanopb_testing.h @@ -23,8 +23,7 @@ #include "Firestore/core/src/firebase/firestore/nanopb/byte_string.h" #include "Firestore/core/src/firebase/firestore/nanopb/nanopb_util.h" #include "Firestore/core/src/firebase/firestore/nanopb/writer.h" -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "google/protobuf/message.h" #include "gtest/gtest.h" diff --git a/Firestore/core/test/firebase/firestore/remote/CMakeLists.txt b/Firestore/core/test/firebase/firestore/remote/CMakeLists.txt index 291d550b5eb..a372f33a822 100644 --- a/Firestore/core/test/firebase/firestore/remote/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/remote/CMakeLists.txt @@ -28,5 +28,6 @@ cc_test( firebase_firestore_core firebase_firestore_remote firebase_firestore_remote_test_util + firebase_firestore_testutil firebase_firestore_util_async_std ) diff --git a/Firestore/core/test/firebase/firestore/remote/datastore_test.mm b/Firestore/core/test/firebase/firestore/remote/datastore_test.mm index 2a66e8f178a..8eb8d3e884d 100644 --- a/Firestore/core/test/firebase/firestore/remote/datastore_test.mm +++ b/Firestore/core/test/firebase/firestore/remote/datastore_test.mm @@ -20,9 +20,10 @@ #include "Firestore/core/src/firebase/firestore/remote/datastore.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" #include "Firestore/core/test/firebase/firestore/util/fake_credentials_provider.h" #include "Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h" #include "absl/memory/memory.h" @@ -47,10 +48,9 @@ using util::GrpcStreamTester; using util::FakeCredentialsProvider; using util::FakeGrpcQueue; -using util::ExecutorLibdispatch; +using util::Executor; using util::CompletionResult::Error; using util::CompletionResult::Ok; -using util::ExecutorStd; using util::Status; using Type = GrpcCompletion::Type; @@ -104,9 +104,7 @@ void CancelLastCall() { class DatastoreTest : public testing::Test { public: DatastoreTest() - : worker_queue{std::make_shared( - absl::make_unique(dispatch_queue_create( - "datastore_test", DISPATCH_QUEUE_SERIAL)))}, + : worker_queue{testutil::AsyncQueueForTesting()}, database_info{DatabaseId{"p", "d"}, "", "localhost", false}, datastore{CreateDatastore(database_info, worker_queue, credentials)}, fake_grpc_queue{datastore->queue()} { diff --git a/Firestore/core/test/firebase/firestore/remote/exponential_backoff_test.cc b/Firestore/core/test/firebase/firestore/remote/exponential_backoff_test.cc index 7297d7eebb7..958a7969b37 100644 --- a/Firestore/core/test/firebase/firestore/remote/exponential_backoff_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/exponential_backoff_test.cc @@ -18,27 +18,27 @@ #include "Firestore/core/src/firebase/firestore/remote/exponential_backoff.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_std.h" -#include "Firestore/core/test/firebase/firestore/util/async_tests_util.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" #include "absl/memory/memory.h" #include "gtest/gtest.h" -using firebase::firestore::util::AsyncQueue; -using firebase::firestore::util::ExecutorStd; -using firebase::firestore::util::TestWithTimeoutMixin; -using firebase::firestore::util::TimerId; - namespace chr = std::chrono; namespace firebase { namespace firestore { namespace remote { -class ExponentialBackoffTest : public TestWithTimeoutMixin, - public testing::Test { +using testutil::Expectation; +using util::AsyncQueue; +using util::Executor; +using util::TimerId; + +class ExponentialBackoffTest : public testing::Test, + public testutil::AsyncTest { public: ExponentialBackoffTest() - : queue{std::make_shared(absl::make_unique())}, + : queue{testutil::AsyncQueueForTesting()}, backoff{queue, timer_id, 1.5, chr::seconds{5}, chr::seconds{30}} { } @@ -50,12 +50,13 @@ class ExponentialBackoffTest : public TestWithTimeoutMixin, TEST_F(ExponentialBackoffTest, CanScheduleOperations) { EXPECT_FALSE(queue->IsScheduled(timer_id)); + Expectation finished; queue->EnqueueBlocking([&] { - backoff.BackoffAndRun([&] { signal_finished(); }); + backoff.BackoffAndRun(finished.AsCallback()); EXPECT_TRUE(queue->IsScheduled(timer_id)); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(finished); EXPECT_FALSE(queue->IsScheduled(timer_id)); } @@ -74,16 +75,17 @@ TEST_F(ExponentialBackoffTest, CanCancelOperations) { } TEST_F(ExponentialBackoffTest, SequentialCallsToBackoffAndRun) { + Expectation finished; queue->EnqueueBlocking([&] { backoff.BackoffAndRun([] {}); backoff.BackoffAndRun([] {}); - backoff.BackoffAndRun([&] { signal_finished(); }); + backoff.BackoffAndRun(finished.AsCallback()); }); // The chosen value of initial_delay is large enough that it shouldn't be // realistically possible for backoff to finish already. queue->RunScheduledOperationsUntil(timer_id); - EXPECT_TRUE(WaitForTestToFinish()); + Await(finished); } } // namespace remote diff --git a/Firestore/core/test/firebase/firestore/remote/grpc_connection_test.cc b/Firestore/core/test/firebase/firestore/remote/grpc_connection_test.cc index 0a11098ba67..8fae9d8806f 100644 --- a/Firestore/core/test/firebase/firestore/remote/grpc_connection_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/grpc_connection_test.cc @@ -23,7 +23,9 @@ #include "Firestore/core/src/firebase/firestore/remote/connectivity_monitor.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_connection.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_std.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/async_testing.h" #include "Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h" #include "absl/memory/memory.h" #include "gtest/gtest.h" @@ -36,10 +38,10 @@ using auth::Token; using auth::User; using core::DatabaseInfo; using util::AsyncQueue; -using util::ExecutorStd; using util::GrpcStreamTester; using util::Status; using util::StatusOr; + using NetworkStatus = ConnectivityMonitor::NetworkStatus; namespace { @@ -84,8 +86,7 @@ class ConnectivityObserver : public GrpcStreamObserver { class GrpcConnectionTest : public testing::Test { public: GrpcConnectionTest() - : worker_queue{std::make_shared( - absl::make_unique())}, + : worker_queue{testutil::AsyncQueueForTesting()}, connectivity_monitor{ absl::make_unique(worker_queue)}, tester{worker_queue, connectivity_monitor.get()} { diff --git a/Firestore/core/test/firebase/firestore/remote/grpc_stream_test.cc b/Firestore/core/test/firebase/firestore/remote/grpc_stream_test.cc index 425a2655905..bc3afb663a0 100644 --- a/Firestore/core/test/firebase/firestore/remote/grpc_stream_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/grpc_stream_test.cc @@ -26,9 +26,10 @@ #include "Firestore/core/src/firebase/firestore/remote/connectivity_monitor.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_completion.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_std.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" #include "Firestore/core/src/firebase/firestore/util/status.h" #include "Firestore/core/src/firebase/firestore/util/string_format.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" #include "Firestore/core/test/firebase/firestore/util/create_noop_connectivity_monitor.h" #include "Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h" #include "absl/memory/memory.h" @@ -44,7 +45,7 @@ using util::ByteBufferToString; using util::CompletionEndState; using util::CompletionResult; using util::CreateNoOpConnectivityMonitor; -using util::ExecutorStd; +using util::Executor; using util::GetFirestoreErrorName; using util::GetGrpcErrorCodeName; using util::GrpcStreamTester; @@ -109,8 +110,7 @@ class DestroyingObserver : public GrpcStreamObserver { class GrpcStreamTest : public testing::Test { public: GrpcStreamTest() - : worker_queue{std::make_shared( - absl::make_unique())}, + : worker_queue{testutil::AsyncQueueForTesting()}, connectivity_monitor{CreateNoOpConnectivityMonitor()}, tester{worker_queue, connectivity_monitor.get()}, observer{absl::make_unique()}, diff --git a/Firestore/core/test/firebase/firestore/remote/grpc_streaming_reader_test.cc b/Firestore/core/test/firebase/firestore/remote/grpc_streaming_reader_test.cc index 0a11e3e810a..9f4082cb829 100644 --- a/Firestore/core/test/firebase/firestore/remote/grpc_streaming_reader_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/grpc_streaming_reader_test.cc @@ -20,7 +20,9 @@ #include #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_std.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/async_testing.h" #include "Firestore/core/test/firebase/firestore/util/create_noop_connectivity_monitor.h" #include "Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h" #include "absl/types/optional.h" @@ -36,7 +38,6 @@ using util::ByteBufferToString; using util::CompletionEndState; using util::CompletionResult; using util::CreateNoOpConnectivityMonitor; -using util::ExecutorStd; using util::GetFirestoreErrorName; using util::GetGrpcErrorCodeName; using util::GrpcStreamTester; @@ -49,8 +50,7 @@ using Type = GrpcCompletion::Type; class GrpcStreamingReaderTest : public testing::Test { public: GrpcStreamingReaderTest() - : worker_queue{std::make_shared( - absl::make_unique())}, + : worker_queue{testutil::AsyncQueueForTesting()}, connectivity_monitor{CreateNoOpConnectivityMonitor()}, tester{worker_queue, connectivity_monitor.get()}, reader{tester.CreateStreamingReader()} { diff --git a/Firestore/core/test/firebase/firestore/remote/grpc_unary_call_test.cc b/Firestore/core/test/firebase/firestore/remote/grpc_unary_call_test.cc index 485accbbd6d..44cf51b90d8 100644 --- a/Firestore/core/test/firebase/firestore/remote/grpc_unary_call_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/grpc_unary_call_test.cc @@ -20,9 +20,9 @@ #include "Firestore/core/src/firebase/firestore/remote/connectivity_monitor.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_std.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/async_testing.h" #include "Firestore/core/test/firebase/firestore/util/create_noop_connectivity_monitor.h" #include "Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h" #include "absl/types/optional.h" @@ -38,7 +38,6 @@ using util::ByteBufferToString; using util::CompletionEndState; using util::CompletionResult; using util::CreateNoOpConnectivityMonitor; -using util::ExecutorStd; using util::GrpcStreamTester; using util::MakeByteBuffer; using util::Status; @@ -48,8 +47,7 @@ using Type = GrpcCompletion::Type; class GrpcUnaryCallTest : public testing::Test { public: GrpcUnaryCallTest() - : worker_queue{std::make_shared( - absl::make_unique())}, + : worker_queue{testutil::AsyncQueueForTesting()}, connectivity_monitor{CreateNoOpConnectivityMonitor()}, tester{worker_queue, connectivity_monitor.get()}, call{tester.CreateUnaryCall()} { diff --git a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc index 49769c42681..03f004c4b04 100644 --- a/Firestore/core/test/firebase/firestore/remote/serializer_test.cc +++ b/Firestore/core/test/firebase/firestore/remote/serializer_test.cc @@ -57,6 +57,8 @@ namespace firebase { namespace firestore { namespace remote { +namespace { + namespace v1 = google::firestore::v1; using google::protobuf::util::MessageDifferencer; using model::DatabaseId; @@ -81,6 +83,21 @@ using testutil::Map; using util::Status; using util::StatusOr; +const char* const kProjectId = "p"; +const char* const kDatabaseId = "d"; + +// These helper functions are just shorter aliases to reduce verbosity. +ByteString ToBytes(const std::string& str) { + return ByteString{Serializer::EncodeString(str)}; +} + +std::string FromBytes(pb_bytes_array_t*&& ptr) { + auto byte_string = ByteString::Take(ptr); + return Serializer::DecodeString(byte_string.get()); +} + +} // namespace + 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 @@ -92,7 +109,7 @@ TEST(Serializer, CanLinkToNanopb) { // Fixture for running serializer tests. class SerializerTest : public ::testing::Test { public: - SerializerTest() : serializer(DatabaseId("p", "d")) { + SerializerTest() : serializer(DatabaseId(kProjectId, kDatabaseId)) { msg_diff.ReportDifferencesToString(&message_differences); } @@ -149,26 +166,27 @@ class SerializerTest : public ::testing::Test { EXPECT_EQ(status.code(), reader.status().code()); } - v1::Value ValueProto(std::nullptr_t) { - ByteString bytes = EncodeFieldValue(&serializer, FieldValue::Null()); - return ProtobufParse(bytes); - } - - ByteString EncodeFieldValue(Serializer* serializer, const FieldValue& fv) { + ByteString EncodeFieldValue(const FieldValue& fv) { ByteStringWriter writer; - google_firestore_v1_Value proto = serializer->EncodeFieldValue(fv); + google_firestore_v1_Value proto = serializer.EncodeFieldValue(fv); writer.WriteNanopbMessage(google_firestore_v1_Value_fields, &proto); - serializer->FreeNanopbMessage(google_firestore_v1_Value_fields, &proto); + serializer.FreeNanopbMessage(google_firestore_v1_Value_fields, &proto); return writer.Release(); } - ByteString EncodeDocument(Serializer* serializer, - const DocumentKey& key, - const ObjectValue& value) { + ByteString EncodeDocument(const DocumentKey& key, const ObjectValue& value) { ByteStringWriter writer; - google_firestore_v1_Document proto = serializer->EncodeDocument(key, value); + google_firestore_v1_Document proto = serializer.EncodeDocument(key, value); writer.WriteNanopbMessage(google_firestore_v1_Document_fields, &proto); - serializer->FreeNanopbMessage(google_firestore_v1_Document_fields, &proto); + serializer.FreeNanopbMessage(google_firestore_v1_Document_fields, &proto); + return writer.Release(); + } + + ByteString EncodeMutation(const Mutation& mutation) { + ByteStringWriter writer; + google_firestore_v1_Write proto = serializer.EncodeMutation(mutation); + writer.WriteNanopbMessage(google_firestore_v1_Write_fields, &proto); + serializer.FreeNanopbMessage(google_firestore_v1_Write_fields, &proto); return writer.Release(); } @@ -187,53 +205,64 @@ class SerializerTest : public ::testing::Test { *byte = new_value; } + v1::Value ValueProto(std::nullptr_t) { + ByteString bytes = EncodeFieldValue(FieldValue::Null()); + return ProtobufParse(bytes); + } + v1::Value ValueProto(bool b) { - ByteString bytes = - EncodeFieldValue(&serializer, FieldValue::FromBoolean(b)); + ByteString bytes = EncodeFieldValue(FieldValue::FromBoolean(b)); return ProtobufParse(bytes); } v1::Value ValueProto(int64_t i) { - ByteString bytes = - EncodeFieldValue(&serializer, FieldValue::FromInteger(i)); + ByteString bytes = EncodeFieldValue(FieldValue::FromInteger(i)); return ProtobufParse(bytes); } v1::Value ValueProto(double d) { - ByteString bytes = EncodeFieldValue(&serializer, FieldValue::FromDouble(d)); + ByteString bytes = EncodeFieldValue(FieldValue::FromDouble(d)); return ProtobufParse(bytes); } + // int64_t and double are equally good overloads for integer literals so this + // avoids ambiguity + v1::Value ValueProto(int i) { + return ValueProto(static_cast(i)); + } + v1::Value ValueProto(const char* s) { return ValueProto(std::string(s)); } v1::Value ValueProto(const std::string& s) { - ByteString bytes = EncodeFieldValue(&serializer, FieldValue::FromString(s)); + ByteString bytes = EncodeFieldValue(FieldValue::FromString(s)); return ProtobufParse(bytes); } v1::Value ValueProto(const Timestamp& ts) { - ByteString bytes = - EncodeFieldValue(&serializer, FieldValue::FromTimestamp(ts)); + ByteString bytes = EncodeFieldValue(FieldValue::FromTimestamp(ts)); return ProtobufParse(bytes); } v1::Value ValueProto(const ByteString& blob) { - ByteString bytes = - EncodeFieldValue(&serializer, FieldValue::FromBlob(blob)); + ByteString bytes = EncodeFieldValue(FieldValue::FromBlob(blob)); + return ProtobufParse(bytes); + } + + v1::Value ValueProto(const FieldValue::Reference& ref) { + ByteString bytes = EncodeFieldValue( + FieldValue::FromReference(ref.database_id(), ref.key())); return ProtobufParse(bytes); } v1::Value ValueProto(const GeoPoint& geo_point) { - ByteString bytes = - EncodeFieldValue(&serializer, FieldValue::FromGeoPoint(geo_point)); + ByteString bytes = EncodeFieldValue(FieldValue::FromGeoPoint(geo_point)); return ProtobufParse(bytes); } v1::Value ValueProto(const std::vector& array) { - ByteString bytes = - EncodeFieldValue(&serializer, FieldValue::FromArray(array)); + ByteString bytes = EncodeFieldValue(FieldValue::FromArray(array)); return ProtobufParse(bytes); } @@ -266,7 +295,7 @@ class SerializerTest : public ::testing::Test { const v1::Value& proto, FieldValue::Type type) { EXPECT_EQ(type, model.type()); - ByteString bytes = EncodeFieldValue(&serializer, model); + ByteString bytes = EncodeFieldValue(model); auto actual_proto = ProtobufParse(bytes); EXPECT_TRUE(msg_diff.Compare(proto, actual_proto)) << message_differences; @@ -294,7 +323,7 @@ class SerializerTest : public ::testing::Test { const ObjectValue& value, const SnapshotVersion& update_time, const v1::BatchGetDocumentsResponse& proto) { - ByteString bytes = EncodeDocument(&serializer, key, value); + ByteString bytes = EncodeDocument(key, value); auto actual_proto = ProtobufParse(bytes); // Note that the client can only serialize Documents (and cannot serialize @@ -504,6 +533,19 @@ TEST_F(SerializerTest, EncodesNullBlobs) { EXPECT_EQ(actual, ""); } +TEST_F(SerializerTest, EncodesReferences) { + std::vector cases{ + {DatabaseId{kProjectId, kDatabaseId}, + DocumentKey::FromPathString("baz/a")}, + }; + + for (const auto& ref_value : cases) { + FieldValue model = + FieldValue::FromReference(ref_value.database_id(), ref_value.key()); + ExpectRoundTrip(model, ValueProto(ref_value), FieldValue::Type::Reference); + } +} + TEST_F(SerializerTest, EncodesGeoPoint) { std::vector cases{ {1.23, 4.56}, @@ -662,8 +704,7 @@ TEST_F(SerializerTest, EncodesFieldValuesWithRepeatedEntries) { } TEST_F(SerializerTest, BadNullValue) { - std::vector bytes = - MakeVector(EncodeFieldValue(&serializer, FieldValue::Null())); + std::vector bytes = MakeVector(EncodeFieldValue(FieldValue::Null())); // Alter the null value from 0 to 1. Mutate(&bytes[1], /*expected_initial_value=*/0, /*new_value=*/1); @@ -674,7 +715,7 @@ TEST_F(SerializerTest, BadNullValue) { TEST_F(SerializerTest, BadBoolValueInterpretedAsTrue) { std::vector bytes = - MakeVector(EncodeFieldValue(&serializer, FieldValue::FromBoolean(true))); + MakeVector(EncodeFieldValue(FieldValue::FromBoolean(true))); // Alter the bool value from 1 to 2. (Value values are 0,1) Mutate(&bytes[1], /*expected_initial_value=*/1, /*new_value=*/2); @@ -692,8 +733,7 @@ TEST_F(SerializerTest, BadBoolValueInterpretedAsTrue) { TEST_F(SerializerTest, BadIntegerValue) { // Encode 'maxint'. This should result in 9 0xff bytes, followed by a 1. auto max_int = FieldValue::FromInteger(std::numeric_limits::max()); - std::vector bytes = - MakeVector(EncodeFieldValue(&serializer, max_int)); + std::vector bytes = MakeVector(EncodeFieldValue(max_int)); ASSERT_EQ(11u, bytes.size()); for (size_t i = 1; i < bytes.size() - 1; i++) { ASSERT_EQ(0xff, bytes[i]); @@ -710,7 +750,7 @@ TEST_F(SerializerTest, BadIntegerValue) { TEST_F(SerializerTest, BadStringValue) { std::vector bytes = - MakeVector(EncodeFieldValue(&serializer, FieldValue::FromString("a"))); + MakeVector(EncodeFieldValue(FieldValue::FromString("a"))); // Claim that the string length is 5 instead of 1. (The first two bytes are // used by the encoded tag.) @@ -722,8 +762,7 @@ TEST_F(SerializerTest, BadStringValue) { TEST_F(SerializerTest, BadTimestampValue_TooLarge) { auto max_ts = FieldValue::FromTimestamp(TimestampInternal::Max()); - std::vector bytes = - MakeVector(EncodeFieldValue(&serializer, max_ts)); + std::vector bytes = MakeVector(EncodeFieldValue(max_ts)); // Add some time, which should push us above the maximum allowed timestamp. Mutate(&bytes[4], 0x82, 0x83); @@ -734,8 +773,7 @@ TEST_F(SerializerTest, BadTimestampValue_TooLarge) { TEST_F(SerializerTest, BadTimestampValue_TooSmall) { auto min_ts = FieldValue::FromTimestamp(TimestampInternal::Min()); - std::vector bytes = - MakeVector(EncodeFieldValue(&serializer, min_ts)); + std::vector bytes = MakeVector(EncodeFieldValue(min_ts)); // Remove some time, which should push us below the minimum allowed timestamp. Mutate(&bytes[4], 0x92, 0x91); @@ -750,8 +788,7 @@ TEST_F(SerializerTest, BadFieldValueTagAndNoOtherTagPresent) { // assume some sort of default type in this situation, we've decided to fail // the deserialization process in this case instead. - std::vector bytes = - MakeVector(EncodeFieldValue(&serializer, FieldValue::Null())); + std::vector bytes = MakeVector(EncodeFieldValue(FieldValue::Null())); // The v1::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 @@ -813,8 +850,7 @@ TEST_F(SerializerTest, BadFieldValueTagWithOtherValidTagsPresent) { } TEST_F(SerializerTest, IncompleteFieldValue) { - std::vector bytes = - MakeVector(EncodeFieldValue(&serializer, FieldValue::Null())); + std::vector bytes = MakeVector(EncodeFieldValue(FieldValue::Null())); ASSERT_EQ(2u, bytes.size()); // Remove the (null) payload @@ -841,24 +877,28 @@ TEST_F(SerializerTest, FailOnInvalidInputBytes) { } TEST_F(SerializerTest, EncodesKey) { - EXPECT_EQ("projects/p/databases/d/documents", serializer.EncodeKey(Key(""))); + EXPECT_EQ("projects/p/databases/d/documents", + FromBytes(serializer.EncodeKey(Key("")))); EXPECT_EQ("projects/p/databases/d/documents/one/two/three/four", - serializer.EncodeKey(Key("one/two/three/four"))); + FromBytes(serializer.EncodeKey(Key("one/two/three/four")))); } TEST_F(SerializerTest, DecodesKey) { Reader reader(nullptr, 0); EXPECT_EQ(Key(""), - serializer.DecodeKey(&reader, "projects/p/databases/d/documents")); - EXPECT_EQ( - Key("one/two/three/four"), - serializer.DecodeKey( - &reader, "projects/p/databases/d/documents/one/two/three/four")); + serializer.DecodeKey( + &reader, ToBytes("projects/p/databases/d/documents").get())); + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + &reader, + ToBytes("projects/p/databases/d/documents/one/two/three/four") + .get())); // Same, but with a leading slash - EXPECT_EQ( - Key("one/two/three/four"), - serializer.DecodeKey( - &reader, "/projects/p/databases/d/documents/one/two/three/four")); + EXPECT_EQ(Key("one/two/three/four"), + serializer.DecodeKey( + &reader, + ToBytes("/projects/p/databases/d/documents/one/two/three/four") + .get())); EXPECT_OK(reader.status()); } @@ -877,7 +917,7 @@ TEST_F(SerializerTest, BadKey) { for (const std::string& bad_key : bad_cases) { Reader reader(nullptr, 0); - serializer.DecodeKey(&reader, bad_key); + serializer.DecodeKey(&reader, ToBytes(bad_key).get()); EXPECT_NOT_OK(reader.status()); } } @@ -889,7 +929,7 @@ TEST_F(SerializerTest, EncodesEmptyDocument) { v1::BatchGetDocumentsResponse proto; v1::Document* doc_proto = proto.mutable_found(); - doc_proto->set_name(serializer.EncodeKey(key)); + doc_proto->set_name(FromBytes(serializer.EncodeKey(key))); doc_proto->mutable_fields(); google::protobuf::Timestamp* update_time_proto = @@ -920,7 +960,7 @@ TEST_F(SerializerTest, EncodesNonEmptyDocument) { v1::BatchGetDocumentsResponse proto; v1::Document* doc_proto = proto.mutable_found(); - doc_proto->set_name(serializer.EncodeKey(key)); + doc_proto->set_name(FromBytes(serializer.EncodeKey(key))); google::protobuf::Map& m = *doc_proto->mutable_fields(); m["foo"] = ValueProto("bar"); @@ -948,7 +988,7 @@ TEST_F(SerializerTest, DecodesNoDocument) { SnapshotVersion{{/*seconds=*/1234, /*nanoseconds=*/5678}}; v1::BatchGetDocumentsResponse proto; - proto.set_missing(serializer.EncodeKey(key)); + proto.set_missing(FromBytes(serializer.EncodeKey(key))); google::protobuf::Timestamp* read_time_proto = proto.mutable_read_time(); read_time_proto->set_seconds(read_time.timestamp().seconds()); read_time_proto->set_nanos(read_time.timestamp().nanoseconds()); diff --git a/Firestore/core/test/firebase/firestore/remote/stream_test.mm b/Firestore/core/test/firebase/firestore/remote/stream_test.mm index 11997245706..3588a8dcc75 100644 --- a/Firestore/core/test/firebase/firestore/remote/stream_test.mm +++ b/Firestore/core/test/firebase/firestore/remote/stream_test.mm @@ -25,7 +25,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_stream.h" #include "Firestore/core/src/firebase/firestore/remote/stream.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_std.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" #include "Firestore/core/test/firebase/firestore/util/create_noop_connectivity_monitor.h" #include "Firestore/core/test/firebase/firestore/util/fake_credentials_provider.h" #include "Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h" @@ -56,7 +56,6 @@ using util::MakeByteBuffer; using util::StringFormat; using util::TimerId; -using util::ExecutorStd; using Type = GrpcCompletion::Type; namespace { @@ -144,8 +143,7 @@ void NotifyStreamClose(const util::Status& status) override { class StreamTest : public testing::Test { public: StreamTest() - : worker_queue{std::make_shared( - absl::make_unique())}, + : worker_queue{testutil::AsyncQueueForTesting()}, connectivity_monitor{CreateNoOpConnectivityMonitor()}, tester{worker_queue, connectivity_monitor.get()}, credentials{std::make_shared()}, diff --git a/Firestore/core/test/firebase/firestore/testutil/CMakeLists.txt b/Firestore/core/test/firebase/firestore/testutil/CMakeLists.txt index ac2422db77e..fb6017999f2 100644 --- a/Firestore/core/test/firebase/firestore/testutil/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/testutil/CMakeLists.txt @@ -31,16 +31,23 @@ endif() cc_library( firebase_firestore_testutil SOURCES + async_testing.cc + async_testing.h equals_tester.h testutil.cc testutil.h time_testing.cc time_testing.h + view_testing.cc + view_testing.h DEPENDS # TODO(b/111328563) Force nanopb first to work around ODR violations firebase_firestore_nanopb ${TESTUTIL_DEPENDS} + GTest::GTest + absl_time firebase_firestore_core firebase_firestore_model + firebase_firestore_util ) diff --git a/Firestore/core/test/firebase/firestore/testutil/async_testing.cc b/Firestore/core/test/firebase/firestore/testutil/async_testing.cc new file mode 100644 index 00000000000..6803e3a7f5d --- /dev/null +++ b/Firestore/core/test/firebase/firestore/testutil/async_testing.cc @@ -0,0 +1,104 @@ +/* + * Copyright 2019 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/testutil/async_testing.h" + +#include +#include + +#include "Firestore/core/src/firebase/firestore/util/async_queue.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" +#include "absl/strings/str_cat.h" +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace testutil { + +using util::AsyncQueue; +using util::Executor; + +std::unique_ptr ExecutorForTesting(const char* name) { + std::string label = absl::StrCat("firestore.testing.", name); + return Executor::CreateSerial(label.c_str()); +} + +std::shared_ptr AsyncQueueForTesting() { + return std::make_shared(ExecutorForTesting("worker")); +} + +// MARK: - Expectation + +Expectation::Expectation() + : promise_(std::make_shared>()), + future_(promise_->get_future().share()) { +} + +void Expectation::Fulfill() { + promise_->set_value(); +} + +std::function Expectation::AsCallback() const { + // Additional copy required because putting `promise_` in the capture list + // is not allowed. Generalized capture in C++14 would allow direct + // initialization. + auto promise = promise_; + return [promise] { promise->set_value(); }; +} + +// MARK: - AsyncTest + +std::future AsyncTest::Async(std::function action) const { + std::packaged_task task(std::move(action)); + auto future = task.get_future(); + + std::thread thread(std::move(task)); + thread.detach(); + return future; +} + +void AsyncTest::Await(const std::future& future, + std::chrono::milliseconds timeout) const { + std::future_status result = future.wait_for(timeout); + if (result == std::future_status::ready) { + return; + } + + ADD_FAILURE() << "Test timed out after " << timeout.count() << " ms"; +} + +void AsyncTest::Await(const std::shared_future& future, + std::chrono::milliseconds timeout) const { + std::future_status result = future.wait_for(timeout); + if (result == std::future_status::ready) { + return; + } + + ADD_FAILURE() << "Test timed out after " << timeout.count() << " ms"; +} + +void AsyncTest::Await(Expectation& expectation, + std::chrono::milliseconds timeout) const { + return Await(expectation.get_future(), timeout); +} + +void AsyncTest::SleepFor(int millis) const { + std::this_thread::sleep_for(std::chrono::milliseconds(millis)); +} + +} // namespace testutil +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/testutil/async_testing.h b/Firestore/core/test/firebase/firestore/testutil/async_testing.h new file mode 100644 index 00000000000..384f9209659 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/testutil/async_testing.h @@ -0,0 +1,142 @@ +/* + * Copyright 2019 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_TESTUTIL_ASYNC_TESTING_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_ASYNC_TESTING_H_ + +#include // NOLINT(build/c++11) +#include +#include // NOLINT(build/c++11) +#include +#include // NOLINT(build/c++11) + +#include "gtest/gtest.h" + +namespace firebase { +namespace firestore { +namespace util { + +class AsyncQueue; +class Executor; + +} // namespace util + +namespace testutil { + +/** + * Creates an AsyncQueue suitable for testing, based on the default executor + * for the current platform. + * + * @param name A simple name for the kind of executor this is (e.g. "user" for + * executors that emulate delivery of user events or "worker" for executors + * that back AsyncQueues.) + */ +std::unique_ptr ExecutorForTesting(const char* name); + +/** + * Creates an AsyncQueue suitable for testing, based on the default executor + * for the current platform. + */ +std::shared_ptr AsyncQueueForTesting(); + +constexpr auto kTimeout = std::chrono::seconds(5); + +/** + * An expected outcome of an asynchronous test. + */ +class Expectation { + public: + Expectation(); + + /** + * Marks this expectation as fulfilled. + * + * Only a single call to `Fulfill` is allowed for any given `Expectaction`. An + * exception is thrown if `Fulfill` is called more than once. + */ + void Fulfill(); + + /** + * Returns a callback function, that when invoked, fullfills the expectation. + * + * The returned function has a lifetime that's independent of the Expectation + * that created it. + */ + std::function AsCallback() const; + + /** + * Returns a `shared_future` that represents the completion of this + * Expectation. + */ + const std::shared_future& get_future() const { + return future_; + } + + private: + std::shared_ptr> promise_; + std::shared_future future_; +}; + +/** + * A mixin that supplies utilities for safely writing asynchronous tests. + */ +class AsyncTest { + public: + AsyncTest() = default; + + std::future Async(std::function action) const; + + /** + * Waits for the future to become ready. + * + * Fails the current test if the timeout occurs. + */ + void Await(const std::future& future, + std::chrono::milliseconds timeout = kTimeout) const; + + /** + * Waits for the shared future to become ready. + * + * Fails the current test if the timeout occurs. + */ + void Await(const std::shared_future& future, + std::chrono::milliseconds timeout = kTimeout) const; + + /** + * Waits for the expectation to become fulfilled. + * + * Fails the current test if the timeout occurs. + */ + void Await(Expectation& expectation, // NOLINT(runtime/references) + std::chrono::milliseconds timeout = kTimeout) const; + + /** + * Sleeps the current thread for the given number of milliseconds. + */ + void SleepFor(int millis) const; + + private: + testing::ScopedTrace trace_{ + "Test case name", 1, + testing::Message() + << testing::UnitTest::GetInstance()->current_test_info()->name()}; +}; + +} // namespace testutil +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_ASYNC_TESTING_H_ diff --git a/Firestore/core/test/firebase/firestore/testutil/testutil.h b/Firestore/core/test/firebase/firestore/testutil/testutil.h index 188668b84a2..7a85d960f97 100644 --- a/Firestore/core/test/firebase/firestore/testutil/testutil.h +++ b/Firestore/core/test/firebase/firestore/testutil/testutil.h @@ -66,6 +66,11 @@ constexpr const char* kDeleteSentinel = ""; // Convenience methods for creating instances for tests. +template +nanopb::ByteString Bytes(Ints... octets) { + return nanopb::ByteString{static_cast(octets)...}; +} + inline model::FieldValue Value(std::nullptr_t) { return model::FieldValue::Null(); } @@ -107,7 +112,7 @@ EnableForExactlyBool Value(T bool_value) { * * @tparam T Any integral type (but not bool). Types larger than int64_t will * likely generate a warning. - * @param int_value An integer value. + * @param value An integer value. */ template EnableForInts Value(T value) { @@ -317,7 +322,7 @@ inline model::UnknownDocument UnknownDoc(absl::string_view key, #if __APPLE__ /** - * Creates an DocumentComparator that will compare Documents by the given + * Creates a DocumentComparator that will compare Documents by the given * fieldPath string then by key. */ model::DocumentComparator DocComparator(absl::string_view field_path); diff --git a/Firestore/core/test/firebase/firestore/testutil/time_testing.cc b/Firestore/core/test/firebase/firestore/testutil/time_testing.cc index 6d2790cd37f..eee59a02196 100644 --- a/Firestore/core/test/firebase/firestore/testutil/time_testing.cc +++ b/Firestore/core/test/firebase/firestore/testutil/time_testing.cc @@ -16,35 +16,28 @@ #include "Firestore/core/test/firebase/firestore/testutil/time_testing.h" -#include +#include "Firestore/core/src/firebase/firestore/util/warnings.h" + +SUPPRESS_COMMA_WARNINGS_BEGIN() +#include "absl/time/clock.h" +#include "absl/time/time.h" +SUPPRESS_END() namespace firebase { namespace firestore { namespace testutil { -#if defined(_WIN32) -#define timegm _mkgmtime - -#elif defined(__ANDROID__) -// time.h doesn't necessarily define this properly, even though it's present -// from API 12 and up. -extern time_t timegm(struct tm*); -#endif +std::chrono::time_point +Now() { + return std::chrono::time_point_cast( + std::chrono::steady_clock::now()); +} time_point MakeTimePoint( int year, int month, int day, int hour, int minute, int second) { - struct std::tm tm {}; - tm.tm_year = year - 1900; // counts from 1900 - tm.tm_mon = month - 1; // counts from 0 - tm.tm_mday = day; // counts from 1 - tm.tm_hour = hour; - tm.tm_min = minute; - tm.tm_sec = second; - - // std::mktime produces a time value in local time, and conversion to GMT is - // not defined. timegm is nonstandard but widespread enough for our purposes. - time_t t = timegm(&tm); - return time_point::clock::from_time_t(t); + absl::CivilSecond ct(year, month, day, hour, minute, second); + absl::Time time = absl::FromCivil(ct, absl::UTCTimeZone()); + return absl::ToChronoTime(time); } Timestamp MakeTimestamp( diff --git a/Firestore/core/test/firebase/firestore/testutil/time_testing.h b/Firestore/core/test/firebase/firestore/testutil/time_testing.h index 7bb63495a32..f63bad74519 100644 --- a/Firestore/core/test/firebase/firestore/testutil/time_testing.h +++ b/Firestore/core/test/firebase/firestore/testutil/time_testing.h @@ -27,6 +27,10 @@ namespace testutil { using time_point = std::chrono::system_clock::time_point; +/** Returns the current time, in milliseconds. */ +std::chrono::time_point +Now(); + /** * Makes a TimePoint from the given date components, given in UTC. */ diff --git a/Firestore/core/test/firebase/firestore/testutil/view_testing.cc b/Firestore/core/test/firebase/firestore/testutil/view_testing.cc new file mode 100644 index 00000000000..859b5b65261 --- /dev/null +++ b/Firestore/core/test/firebase/firestore/testutil/view_testing.cc @@ -0,0 +1,83 @@ +/* + * Copyright 2019 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/testutil/view_testing.h" + +#include + +#include "Firestore/core/src/firebase/firestore/core/view.h" +#include "Firestore/core/src/firebase/firestore/core/view_snapshot.h" +#include "Firestore/core/src/firebase/firestore/remote/remote_event.h" + +namespace firebase { +namespace firestore { +namespace testutil { + +using core::View; +using core::ViewChange; +using core::ViewSnapshot; +using model::Document; +using model::DocumentKeySet; +using model::MaybeDocument; +using model::MaybeDocumentMap; +using nanopb::ByteString; +using remote::TargetChange; + +model::MaybeDocumentMap DocUpdates( + const std::vector& docs) { + MaybeDocumentMap updates; + for (const MaybeDocument& doc : docs) { + updates = updates.insert(doc.key(), doc); + } + return updates; +} + +absl::optional ApplyChanges( + View* view, + const std::vector& docs, + const absl::optional& targetChange) { + ViewChange change = view->ApplyChanges( + view->ComputeDocumentChanges(DocUpdates(docs)), targetChange); + return change.snapshot(); +} + +TargetChange AckTarget(DocumentKeySet docs) { + return {ByteString(), + /*current=*/true, + /*added_documents*/ std::move(docs), + /*modified_documents*/ DocumentKeySet{}, + /*removed_documents*/ DocumentKeySet{}}; +} + +TargetChange AckTarget(std::initializer_list docs) { + DocumentKeySet keys; + for (const auto& doc : docs) { + keys = keys.insert(doc.key()); + } + return AckTarget(keys); +} + +TargetChange MarkCurrent() { + return {ByteString(), + /*current=*/true, + /*added_documents=*/DocumentKeySet{}, + /*modified_documents=*/DocumentKeySet{}, + /*removed_documents=*/DocumentKeySet{}}; +} + +} // namespace testutil +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/testutil/view_testing.h b/Firestore/core/test/firebase/firestore/testutil/view_testing.h new file mode 100644 index 00000000000..933a59e5c5e --- /dev/null +++ b/Firestore/core/test/firebase/firestore/testutil/view_testing.h @@ -0,0 +1,82 @@ +/* + * Copyright 2019 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_TESTUTIL_VIEW_TESTING_H_ +#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_VIEW_TESTING_H_ + +#include +#include + +#include "Firestore/core/src/firebase/firestore/model/document_key_set.h" +#include "Firestore/core/src/firebase/firestore/model/document_map.h" + +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace core { + +class View; +class ViewSnapshot; + +} // namespace core + +namespace model { + +class Document; +class MaybeDocument; +class TransformMutation; +class TransformOperation; + +} // namespace model + +namespace remote { + +class TargetChange; + +} // namespace remote + +namespace testutil { + +/** Converts a list of documents to a sorted map. */ +model::MaybeDocumentMap DocUpdates( + const std::vector& docs); + +/** + * Computes changes to the view with the docs and then applies them and returns + * the snapshot. + */ +absl::optional ApplyChanges( + core::View* view, + const std::vector& docs, + const absl::optional& targetChange); + +/** + * Creates a test target change that acks all 'docs' and marks the target as + * CURRENT. + */ +remote::TargetChange AckTarget(model::DocumentKeySet docs); + +remote::TargetChange AckTarget(std::initializer_list docs); + +/** Creates a test target change that marks the target as CURRENT */ +remote::TargetChange MarkCurrent(); + +} // namespace testutil +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_VIEW_TESTING_H_ diff --git a/Firestore/core/test/firebase/firestore/testutil/xcgmock.h b/Firestore/core/test/firebase/firestore/testutil/xcgmock.h index 6c03ee7c367..90e193df90e 100644 --- a/Firestore/core/test/firebase/firestore/testutil/xcgmock.h +++ b/Firestore/core/test/firebase/firestore/testutil/xcgmock.h @@ -141,38 +141,15 @@ OBJC_PRINT_TO(FIRTransaction); OBJC_PRINT_TO(FIRWriteBatch); OBJC_PRINT_TO(FSTArrayRemoveFieldValue); OBJC_PRINT_TO(FSTArrayUnionFieldValue); -OBJC_PRINT_TO(FSTArrayValue); -OBJC_PRINT_TO(FSTDelegateValue); OBJC_PRINT_TO(FSTDeleteFieldValue); OBJC_PRINT_TO(FSTDocumentKeyReference); -OBJC_PRINT_TO(FSTDocumentSet); -OBJC_PRINT_TO(FSTEventManager); -OBJC_PRINT_TO(FSTFirestoreClient); OBJC_PRINT_TO(FSTFirestoreComponent); -OBJC_PRINT_TO(FSTLRUGarbageCollector); -OBJC_PRINT_TO(FSTLevelDB); -OBJC_PRINT_TO(FSTLevelDBLRUDelegate); -OBJC_PRINT_TO(FSTLimboDocumentChange); OBJC_PRINT_TO(FSTListenerRegistration); -OBJC_PRINT_TO(FSTLocalDocumentsView); OBJC_PRINT_TO(FSTLocalSerializer); OBJC_PRINT_TO(FSTLocalStore); -OBJC_PRINT_TO(FSTLocalViewChanges); -OBJC_PRINT_TO(FSTLocalWriteResult); -OBJC_PRINT_TO(FSTMemoryEagerReferenceDelegate); -OBJC_PRINT_TO(FSTMemoryLRUReferenceDelegate); -OBJC_PRINT_TO(FSTMemoryPersistence); -OBJC_PRINT_TO(FSTMutationBatch); -OBJC_PRINT_TO(FSTMutationBatchResult); OBJC_PRINT_TO(FSTNumericIncrementFieldValue); -OBJC_PRINT_TO(FSTQueryData); OBJC_PRINT_TO(FSTSerializerBeta); OBJC_PRINT_TO(FSTServerTimestampFieldValue); -OBJC_PRINT_TO(FSTStringValue); -OBJC_PRINT_TO(FSTSyncEngine); OBJC_PRINT_TO(FSTUserDataConverter); -OBJC_PRINT_TO(FSTView); -OBJC_PRINT_TO(FSTViewChange); -OBJC_PRINT_TO(FSTViewDocumentChanges); #endif // FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_TESTUTIL_XCGMOCK_H_ diff --git a/Firestore/core/test/firebase/firestore/timestamp_test.cc b/Firestore/core/test/firebase/firestore/timestamp_test.cc index 732fb3c5aa2..de152ce8ce3 100644 --- a/Firestore/core/test/firebase/firestore/timestamp_test.cc +++ b/Firestore/core/test/firebase/firestore/timestamp_test.cc @@ -274,18 +274,6 @@ TEST(Timestamp, Comparison) { EXPECT_NE(Timestamp(123, 123456789), Timestamp(123, 123456780)); } -TEST(Timestamp, Hash) { - const Timestamp foo1{123, 456000000}; - const Timestamp foo2 = foo1; - const Timestamp foo3 = - Timestamp::FromTimePoint(TimePoint{Sec(123) + Ms(456)}); - EXPECT_EQ(std::hash()(foo1), std::hash()(foo2)); - EXPECT_EQ(std::hash()(foo2), std::hash()(foo3)); - - const Timestamp bar{123, 456}; - EXPECT_NE(std::hash()(foo1), std::hash()(bar)); -} - TEST(Timestamp, InvalidArguments) { // Negative nanoseconds. ASSERT_ANY_THROW(Timestamp(0, -1)); diff --git a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt index 2df92fe425a..33102dcf0b6 100644 --- a/Firestore/core/test/firebase/firestore/util/CMakeLists.txt +++ b/Firestore/core/test/firebase/firestore/util/CMakeLists.txt @@ -28,11 +28,11 @@ cc_test( async_queue_std_test.cc async_queue_test.cc async_queue_test.h - async_tests_util.h executor_std_test.cc executor_test.cc executor_test.h DEPENDS + firebase_firestore_testutil firebase_firestore_util_async_std ) @@ -43,11 +43,11 @@ if(HAVE_LIBDISPATCH) async_queue_libdispatch_test.mm async_queue_test.cc async_queue_test.h - async_tests_util.h executor_libdispatch_test.mm executor_test.cc executor_test.h DEPENDS + firebase_firestore_testutil firebase_firestore_util_async_libdispatch ) endif() @@ -187,6 +187,7 @@ cc_library( firebase_firestore_auth firebase_firestore_core firebase_firestore_remote + firebase_firestore_testutil firebase_firestore_util grpc++ ) 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 index 590d6976eea..105e7bfa1f8 100644 --- a/Firestore/core/test/firebase/firestore/util/async_queue_libdispatch_test.mm +++ b/Firestore/core/test/firebase/firestore/util/async_queue_libdispatch_test.mm @@ -25,9 +25,10 @@ namespace firebase { namespace firestore { namespace util { - namespace { +using testutil::Expectation; + dispatch_queue_t CreateDispatchQueue() { return dispatch_queue_create("AsyncQueueTests", DISPATCH_QUEUE_SERIAL); } @@ -47,8 +48,8 @@ dispatch_queue_t CreateDispatchQueue() { AsyncQueueTest, ::testing::Values(CreateExecutorLibdispatch)); -class AsyncQueueTestLibdispatchOnly : public TestWithTimeoutMixin, - public ::testing::Test { +class AsyncQueueTestLibdispatchOnly : public ::testing::Test, + public testutil::AsyncTest { public: AsyncQueueTestLibdispatchOnly() : underlying_queue{CreateDispatchQueue()}, @@ -63,10 +64,10 @@ dispatch_queue_t CreateDispatchQueue() { // interacts with raw usage of libdispatch. TEST_F(AsyncQueueTestLibdispatchOnly, SameQueueIsAllowedForUnownedActions) { - internal::DispatchAsync(underlying_queue, [this] { - queue.Enqueue([this] { signal_finished(); }); - }); - EXPECT_TRUE(WaitForTestToFinish()); + Expectation ran; + internal::DispatchAsync(underlying_queue, + [&] { queue.Enqueue(ran.AsCallback()); }); + Await(ran); } TEST_F(AsyncQueueTestLibdispatchOnly, diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_test.cc b/Firestore/core/test/firebase/firestore/util/async_queue_test.cc index 97fe5b1799b..d19a72223e6 100644 --- a/Firestore/core/test/firebase/firestore/util/async_queue_test.cc +++ b/Firestore/core/test/firebase/firestore/util/async_queue_test.cc @@ -27,9 +27,10 @@ namespace firebase { namespace firestore { namespace util { - namespace { +using testutil::Expectation; + // In these generic tests the specific timer ids don't matter. const TimerId kTimerId1 = TimerId::ListenStreamConnectionBackoff; const TimerId kTimerId2 = TimerId::ListenStreamIdle; @@ -38,27 +39,32 @@ const TimerId kTimerId3 = TimerId::WriteStreamConnectionBackoff; } // namespace TEST_P(AsyncQueueTest, Enqueue) { - queue.Enqueue([&] { signal_finished(); }); - EXPECT_TRUE(WaitForTestToFinish()); + Expectation ran; + queue.Enqueue(ran.AsCallback()); + Await(ran); } TEST_P(AsyncQueueTest, EnqueueDisallowsNesting) { - queue.Enqueue([&] { // clang-format off - // clang-format on + Expectation ran; + // clang-format off + queue.Enqueue([&] { EXPECT_ANY_THROW(queue.Enqueue([] {})); - signal_finished(); + ran.Fulfill(); }); + // clang-format on - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); } TEST_P(AsyncQueueTest, EnqueueRelaxedWorksFromWithinEnqueue) { - queue.Enqueue([&] { // clang-format off - queue.EnqueueRelaxed([&] { signal_finished(); }); - // clang-format on + Expectation ran; + // clang-format off + queue.Enqueue([&] { + queue.EnqueueRelaxed(ran.AsCallback()); }); + // clang-format on - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); } TEST_P(AsyncQueueTest, EnqueueBlocking) { @@ -68,10 +74,11 @@ TEST_P(AsyncQueueTest, EnqueueBlocking) { } TEST_P(AsyncQueueTest, EnqueueBlockingDisallowsNesting) { - queue.EnqueueBlocking([&] { // clang-format off + // clang-format off + queue.EnqueueBlocking([&] { EXPECT_ANY_THROW(queue.EnqueueBlocking([] {});); - // clang-format on }); + // clang-format on } TEST_P(AsyncQueueTest, ExecuteBlockingDisallowsNesting) { @@ -88,24 +95,26 @@ TEST_P(AsyncQueueTest, VerifyIsCurrentQueueWorksWithOperationInProgress) { // 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) { + Expectation ran; std::string steps; queue.Enqueue([&steps] { steps += '1'; }); queue.Enqueue([&] { queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(20), kTimerId1, [&] { steps += '4'; - signal_finished(); + ran.Fulfill(); }); queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(10), kTimerId2, [&steps] { steps += '3'; }); queue.EnqueueRelaxed([&steps] { steps += '2'; }); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); EXPECT_EQ(steps, "1234"); } TEST_P(AsyncQueueTest, CanCancelDelayedOperations) { + Expectation ran; std::string steps; queue.Enqueue([&] { @@ -119,7 +128,7 @@ TEST_P(AsyncQueueTest, CanCancelDelayedOperations) { queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(5), kTimerId2, [&] { steps += '3'; - signal_finished(); + ran.Fulfill(); }); EXPECT_TRUE(queue.IsScheduled(kTimerId1)); @@ -127,25 +136,28 @@ TEST_P(AsyncQueueTest, CanCancelDelayedOperations) { EXPECT_FALSE(queue.IsScheduled(kTimerId1)); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); EXPECT_EQ(steps, "13"); EXPECT_FALSE(queue.IsScheduled(kTimerId1)); } TEST_P(AsyncQueueTest, CanCallCancelOnDelayedOperationAfterTheOperationHasRun) { + Expectation ran; + DelayedOperation delayed_operation; queue.Enqueue([&] { - delayed_operation = queue.EnqueueAfterDelay( - AsyncQueue::Milliseconds(10), kTimerId1, [&] { signal_finished(); }); + delayed_operation = queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(10), + kTimerId1, ran.AsCallback()); EXPECT_TRUE(queue.IsScheduled(kTimerId1)); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); EXPECT_FALSE(queue.IsScheduled(kTimerId1)); EXPECT_NO_THROW(delayed_operation.Cancel()); } TEST_P(AsyncQueueTest, CanManuallyDrainAllDelayedOperationsForTesting) { + Expectation ran; std::string steps; queue.Enqueue([&] { @@ -155,15 +167,16 @@ TEST_P(AsyncQueueTest, CanManuallyDrainAllDelayedOperationsForTesting) { queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(10000), kTimerId2, [&steps] { steps += '3'; }); queue.EnqueueRelaxed([&steps] { steps += '2'; }); - signal_finished(); + ran.Fulfill(); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); queue.RunScheduledOperationsUntil(TimerId::All); EXPECT_EQ(steps, "1234"); } TEST_P(AsyncQueueTest, CanManuallyDrainSpecificDelayedOperationsForTesting) { + Expectation ran; std::string steps; queue.Enqueue([&] { @@ -175,24 +188,25 @@ TEST_P(AsyncQueueTest, CanManuallyDrainSpecificDelayedOperationsForTesting) { queue.EnqueueAfterDelay(AsyncQueue::Milliseconds(15000), kTimerId3, [&steps] { steps += '4'; }); queue.EnqueueRelaxed([&] { steps += '2'; }); - signal_finished(); + ran.Fulfill(); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); queue.RunScheduledOperationsUntil(kTimerId3); EXPECT_EQ(steps, "1234"); } TEST_P(AsyncQueueTest, CanScheduleOprationsWithRespectsToShutdownState) { + Expectation ran; std::string steps; queue.Enqueue([&] { steps += '1'; }); queue.EnqueueAndInitiateShutdown([&] { steps += '2'; }); queue.Enqueue([&] { steps += '3'; }); queue.EnqueueEvenAfterShutdown([&] { steps += '4'; }); - queue.EnqueueEvenAfterShutdown([&] { signal_finished(); }); + queue.EnqueueEvenAfterShutdown(ran.AsCallback()); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); EXPECT_EQ(steps, "124"); } diff --git a/Firestore/core/test/firebase/firestore/util/async_queue_test.h b/Firestore/core/test/firebase/firestore/util/async_queue_test.h index b0e5c7e3016..29a472745e2 100644 --- a/Firestore/core/test/firebase/firestore/util/async_queue_test.h +++ b/Firestore/core/test/firebase/firestore/util/async_queue_test.h @@ -22,7 +22,7 @@ #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" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" namespace firebase { namespace firestore { @@ -30,8 +30,8 @@ namespace util { using FactoryFunc = std::unique_ptr (*)(); -class AsyncQueueTest : public TestWithTimeoutMixin, - public ::testing::TestWithParam { +class AsyncQueueTest : public ::testing::TestWithParam, + public testutil::AsyncTest { public: // `GetParam()` must return a factory function. AsyncQueueTest() : queue{GetParam()()} { diff --git a/Firestore/core/test/firebase/firestore/util/async_tests_util.h b/Firestore/core/test/firebase/firestore/util/async_tests_util.h deleted file mode 100644 index f953d667825..00000000000 --- a/Firestore/core/test/firebase/firestore/util/async_tests_util.h +++ /dev/null @@ -1,90 +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. - */ - -#ifndef FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_TESTS_UTIL_H_ -#define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_ASYNC_TESTS_UTIL_H_ - -#include // NOLINT(build/c++11) -#include -#include // NOLINT(build/c++11) - -#include "gtest/gtest.h" - -namespace firebase { -namespace firestore { -namespace util { - -inline std::chrono::time_point -now() { - return std::chrono::time_point_cast( - 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& 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& 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 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 index 939521df003..ffbd37a40c1 100644 --- a/Firestore/core/test/firebase/firestore/util/executor_libdispatch_test.mm +++ b/Firestore/core/test/firebase/firestore/util/executor_libdispatch_test.mm @@ -19,15 +19,17 @@ #include #include "Firestore/core/src/firebase/firestore/util/executor_libdispatch.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" #include "absl/memory/memory.h" #include "gtest/gtest.h" namespace firebase { namespace firestore { namespace util { - namespace { +using testutil::Expectation; + std::unique_ptr ExecutorFactory() { return absl::make_unique( dispatch_queue_create("ExecutorLibdispatchTests", DISPATCH_QUEUE_SERIAL)); @@ -42,8 +44,8 @@ ::testing::Values(ExecutorFactory)); namespace internal { -class ExecutorLibdispatchOnlyTests : public TestWithTimeoutMixin, - public ::testing::Test { +class ExecutorLibdispatchOnlyTests : public ::testing::Test, + public testutil::AsyncTest { public: ExecutorLibdispatchOnlyTests() : executor{ExecutorFactory()} { } @@ -52,22 +54,24 @@ }; TEST_F(ExecutorLibdispatchOnlyTests, NameReturnsLabelOfTheQueue) { + Expectation ran; EXPECT_EQ(executor->Name(), "ExecutorLibdispatchTests"); executor->Execute([&] { EXPECT_EQ(executor->CurrentExecutorName(), "ExecutorLibdispatchTests"); - signal_finished(); + ran.Fulfill(); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); } TEST_F(ExecutorLibdispatchOnlyTests, ExecuteBlockingOnTheCurrentQueueIsNotAllowed) { + Expectation ran; EXPECT_NO_THROW(executor->ExecuteBlocking([] {})); executor->Execute([&] { EXPECT_ANY_THROW(executor->ExecuteBlocking([] {})); - signal_finished(); + ran.Fulfill(); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); } TEST_F(ExecutorLibdispatchOnlyTests, ScheduledOperationOutlivesExecutor) { diff --git a/Firestore/core/test/firebase/firestore/util/executor_std_test.cc b/Firestore/core/test/firebase/firestore/util/executor_std_test.cc index 84c0495f950..abbeff00fb9 100644 --- a/Firestore/core/test/firebase/firestore/util/executor_std_test.cc +++ b/Firestore/core/test/firebase/firestore/util/executor_std_test.cc @@ -23,7 +23,8 @@ #include // 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 "Firestore/core/test/firebase/firestore/testutil/async_testing.h" +#include "Firestore/core/test/firebase/firestore/testutil/time_testing.h" #include "absl/memory/memory.h" #include "gtest/gtest.h" @@ -32,11 +33,14 @@ namespace firestore { namespace util { namespace chr = std::chrono; + using async::Schedule; +using testutil::kTimeout; +using testutil::Now; -class ScheduleTest : public ::testing::Test { +class ScheduleTest : public ::testing::Test, public testutil::AsyncTest { public: - ScheduleTest() : start_time{now()} { + ScheduleTest() : start_time{Now()} { } using ScheduleT = Schedule; @@ -83,13 +87,13 @@ TEST_F(ScheduleTest, PopBlocking) { EXPECT_FALSE(schedule.PopIfDue().has_value()); EXPECT_EQ(schedule.PopBlocking(), 1); - EXPECT_GE(now(), start_time + chr::milliseconds(3)); + 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)); + schedule.Push(2, Now() + chr::minutes(1)); auto maybe_removed = schedule.RemoveIf([](const int v) { return v == 1; }); EXPECT_TRUE(maybe_removed.has_value()); @@ -128,44 +132,44 @@ TEST_F(ScheduleTest, Ordering) { } TEST_F(ScheduleTest, AddingEntryUnblocksEmptyQueue) { - const auto future = std::async(std::launch::async, [&] { + const auto future = 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); + Await(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, [&] { + const auto future = 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); + Await(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, [&] { + const auto future = 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); + 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); + Await(future); } TEST_F(ScheduleTest, PopBlockingCanReadjustTimeIfSeveralElementsAreAdded) { @@ -173,21 +177,21 @@ TEST_F(ScheduleTest, PopBlockingCanReadjustTimeIfSeveralElementsAreAdded) { const auto very_far_away = start_time + chr::seconds(10); schedule.Push(3, very_far_away); - const auto future = std::async(std::launch::async, [&] { + const auto future = Async([&] { ASSERT_FALSE(schedule.PopIfDue().has_value()); EXPECT_EQ(schedule.PopBlocking(), 1); - EXPECT_LT(now(), far_away); + EXPECT_LT(Now(), far_away); }); - std::this_thread::sleep_for(chr::milliseconds(5)); + SleepFor(5); schedule.Push(2, far_away); - std::this_thread::sleep_for(chr::milliseconds(1)); + SleepFor(1); schedule.Push(1, start_time + chr::milliseconds(100)); - ABORT_ON_TIMEOUT(future); + Await(future); } TEST_F(ScheduleTest, PopBlockingNoticesRemovals) { - const auto future = std::async(std::launch::async, [&] { + const auto future = Async([&] { schedule.Push(1, start_time + chr::milliseconds(50)); schedule.Push(2, start_time + chr::milliseconds(100)); ASSERT_FALSE(schedule.PopIfDue().has_value()); @@ -195,16 +199,16 @@ TEST_F(ScheduleTest, PopBlockingNoticesRemovals) { }); while (schedule.empty()) { - std::this_thread::sleep_for(chr::milliseconds(1)); + SleepFor(1); } const auto maybe_removed = schedule.RemoveIf([](const int v) { return v == 1; }); EXPECT_EQ(maybe_removed.value(), 1); - ABORT_ON_TIMEOUT(future); + Await(future); } TEST_F(ScheduleTest, PopBlockingIsNotAffectedByIrrelevantRemovals) { - const auto future = std::async(std::launch::async, [&] { + const auto future = Async([&] { schedule.Push(1, start_time + chr::milliseconds(50)); schedule.Push(2, start_time + chr::seconds(10)); ASSERT_FALSE(schedule.PopIfDue().has_value()); @@ -213,8 +217,8 @@ TEST_F(ScheduleTest, PopBlockingIsNotAffectedByIrrelevantRemovals) { // Wait (with timeout) for both values to appear in the schedule. while (schedule.size() != 2) { - if (now() - start_time >= kTimeout) { - Abort(); + if (Now() - start_time >= kTimeout) { + FAIL() << "Timed out."; } std::this_thread::sleep_for(chr::milliseconds(1)); } @@ -222,7 +226,7 @@ TEST_F(ScheduleTest, PopBlockingIsNotAffectedByIrrelevantRemovals) { schedule.RemoveIf([](const int v) { return v == 2; }); ASSERT_TRUE(maybe_removed.has_value()); EXPECT_EQ(maybe_removed.value(), 2); - ABORT_ON_TIMEOUT(future); + Await(future); } // ExecutorStd tests diff --git a/Firestore/core/test/firebase/firestore/util/executor_test.cc b/Firestore/core/test/firebase/firestore/util/executor_test.cc index 98ae677c628..6b890e91c47 100644 --- a/Firestore/core/test/firebase/firestore/util/executor_test.cc +++ b/Firestore/core/test/firebase/firestore/util/executor_test.cc @@ -28,10 +28,11 @@ namespace firebase { namespace firestore { namespace util { +namespace { namespace chr = std::chrono; -namespace { +using testutil::Expectation; DelayedOperation Schedule(Executor* const executor, const Executor::Milliseconds delay, @@ -44,8 +45,9 @@ DelayedOperation Schedule(Executor* const executor, } // namespace TEST_P(ExecutorTest, Execute) { - executor->Execute([&] { signal_finished(); }); - EXPECT_TRUE(WaitForTestToFinish()); + Expectation ran; + executor->Execute(ran.AsCallback()); + Await(ran); } TEST_P(ExecutorTest, ExecuteBlocking) { @@ -55,14 +57,14 @@ TEST_P(ExecutorTest, ExecuteBlocking) { } TEST_P(ExecutorTest, DestructorDoesNotBlockIfThereArePendingTasks) { - const auto future = std::async(std::launch::async, [&] { + const auto future = 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); + Await(future); } // TODO(varconst): this test is inherently flaky because it can't be guaranteed @@ -71,23 +73,24 @@ TEST_P(ExecutorTest, DestructorDoesNotBlockIfThereArePendingTasks) { // test is unlikely to fail in practice. Need to revisit this. TEST_P(ExecutorTest, CanScheduleOperationsInTheFuture) { std::string steps; - + Expectation ran; executor->Execute([&steps] { steps += '1'; }); Schedule(executor.get(), Executor::Milliseconds(20), [&] { steps += '4'; - signal_finished(); + ran.Fulfill(); }); Schedule(executor.get(), Executor::Milliseconds(10), [&steps] { steps += '3'; }); executor->Execute([&steps] { steps += '2'; }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); EXPECT_EQ(steps, "1234"); } TEST_P(ExecutorTest, CanCancelDelayedOperations) { std::string steps; + Expectation ran; executor->Execute([&] { executor->Execute([&steps] { steps += '1'; }); @@ -96,21 +99,23 @@ TEST_P(ExecutorTest, CanCancelDelayedOperations) { Schedule(executor.get(), Executor::Milliseconds(5), [&] { steps += '3'; - signal_finished(); + ran.Fulfill(); }); delayed_operation.Cancel(); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); EXPECT_EQ(steps, "13"); } TEST_P(ExecutorTest, DelayedOperationIsValidAfterTheOperationHasRun) { - DelayedOperation delayed_operation = Schedule( - executor.get(), Executor::Milliseconds(1), [&] { signal_finished(); }); + Expectation ran; + + DelayedOperation delayed_operation = + Schedule(executor.get(), Executor::Milliseconds(1), ran.AsCallback()); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); EXPECT_NO_THROW(delayed_operation.Cancel()); } @@ -122,19 +127,20 @@ TEST_P(ExecutorTest, CancelingEmptyDelayedOperationIsValid) { TEST_P(ExecutorTest, DoubleCancelingDelayedOperationIsValid) { std::string steps; + Expectation ran; executor->Execute([&] { DelayedOperation delayed_operation = Schedule( executor.get(), Executor::Milliseconds(1), [&steps] { steps += '1'; }); Schedule(executor.get(), Executor::Milliseconds(5), [&] { steps += '2'; - signal_finished(); + ran.Fulfill(); }); delayed_operation.Cancel(); delayed_operation.Cancel(); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); EXPECT_EQ(steps, "2"); } @@ -152,13 +158,14 @@ TEST_P(ExecutorTest, IsCurrentExecutor) { EXPECT_EQ(executor->Name(), executor->CurrentExecutorName()); }); + Expectation ran; Schedule(executor.get(), Executor::Milliseconds(1), [&] { EXPECT_TRUE(executor->IsCurrentExecutor()); EXPECT_EQ(executor->Name(), executor->CurrentExecutorName()); - signal_finished(); + ran.Fulfill(); }); - EXPECT_TRUE(WaitForTestToFinish()); + Await(ran); } TEST_P(ExecutorTest, OperationsCanBeRemovedFromScheduleBeforeTheyRun) { diff --git a/Firestore/core/test/firebase/firestore/util/executor_test.h b/Firestore/core/test/firebase/firestore/util/executor_test.h index 88b5cb02052..8bb008ca59f 100644 --- a/Firestore/core/test/firebase/firestore/util/executor_test.h +++ b/Firestore/core/test/firebase/firestore/util/executor_test.h @@ -22,7 +22,7 @@ #include "gtest/gtest.h" #include "Firestore/core/src/firebase/firestore/util/executor.h" -#include "Firestore/core/test/firebase/firestore/util/async_tests_util.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" namespace firebase { namespace firestore { @@ -30,8 +30,8 @@ namespace util { using FactoryFunc = std::unique_ptr (*)(); -class ExecutorTest : public TestWithTimeoutMixin, - public ::testing::TestWithParam { +class ExecutorTest : public ::testing::TestWithParam, + public testutil::AsyncTest { public: // `GetParam()` must return a factory function. ExecutorTest() : executor{GetParam()()} { diff --git a/Firestore/core/test/firebase/firestore/util/filesystem_test.cc b/Firestore/core/test/firebase/firestore/util/filesystem_test.cc index 871b6411e8f..c5f06f44925 100644 --- a/Firestore/core/test/firebase/firestore/util/filesystem_test.cc +++ b/Firestore/core/test/firebase/firestore/util/filesystem_test.cc @@ -24,6 +24,7 @@ #include "Firestore/core/src/firebase/firestore/util/autoid.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/path.h" +#include "Firestore/core/src/firebase/firestore/util/statusor.h" #include "Firestore/core/src/firebase/firestore/util/string_win.h" #include "Firestore/core/test/firebase/firestore/util/status_testing.h" #include "absl/strings/match.h" diff --git a/Firestore/core/test/firebase/firestore/util/grpc_stream_tester.cc b/Firestore/core/test/firebase/firestore/util/grpc_stream_tester.cc index 57864d0f1ac..8656a4ae892 100644 --- a/Firestore/core/test/firebase/firestore/util/grpc_stream_tester.cc +++ b/Firestore/core/test/firebase/firestore/util/grpc_stream_tester.cc @@ -23,6 +23,7 @@ #include #include "Firestore/core/src/firebase/firestore/util/string_format.h" +#include "Firestore/core/test/firebase/firestore/testutil/async_testing.h" #include "absl/memory/memory.h" namespace firebase { @@ -37,6 +38,7 @@ using remote::GrpcCompletion; using remote::GrpcStream; using remote::GrpcStreamingReader; using remote::GrpcStreamObserver; +using testutil::ExecutorForTesting; using util::CompletionEndState; // Misc @@ -127,8 +129,7 @@ void CompletionEndState::Apply(GrpcCompletion* completion) { // FakeGrpcQueue FakeGrpcQueue::FakeGrpcQueue(grpc::CompletionQueue* grpc_queue) - : dedicated_executor_{absl::make_unique()}, - grpc_queue_{grpc_queue} { + : dedicated_executor_{ExecutorForTesting("rpc")}, grpc_queue_{grpc_queue} { } void FakeGrpcQueue::Shutdown() { diff --git a/Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h b/Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h index 0535c9192d1..9ab1ef6d1aa 100644 --- a/Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h +++ b/Firestore/core/test/firebase/firestore/util/grpc_stream_tester.h @@ -32,7 +32,7 @@ #include "Firestore/core/src/firebase/firestore/remote/grpc_streaming_reader.h" #include "Firestore/core/src/firebase/firestore/remote/grpc_unary_call.h" #include "Firestore/core/src/firebase/firestore/util/async_queue.h" -#include "Firestore/core/src/firebase/firestore/util/executor_std.h" +#include "Firestore/core/src/firebase/firestore/util/executor.h" #include "absl/types/optional.h" #include "grpcpp/client_context.h" #include "grpcpp/completion_queue.h" @@ -118,7 +118,7 @@ class FakeGrpcQueue { private: remote::GrpcCompletion* ExtractCompletion(); - std::unique_ptr dedicated_executor_; + std::unique_ptr dedicated_executor_; grpc::CompletionQueue* grpc_queue_; bool is_shut_down_ = false; diff --git a/Firestore/core/test/firebase/firestore/util/hard_assert_test.cc b/Firestore/core/test/firebase/firestore/util/hard_assert_test.cc index fab64757904..24c89ca0182 100644 --- a/Firestore/core/test/firebase/firestore/util/hard_assert_test.cc +++ b/Firestore/core/test/firebase/firestore/util/hard_assert_test.cc @@ -47,6 +47,29 @@ TEST(HardAssertTest, WithMessage) { EXPECT_ANY_THROW(AssertWithMessage(false)); } +TEST(HardAssertTest, NonDefaultFailureHandler) { + // Used to ensure the original failure handler is restored. + class FailureHandlerRestorer { + public: + explicit FailureHandlerRestorer(FailureHandler orig) : orig_(orig) { + } + ~FailureHandlerRestorer() { + SetFailureHandler(orig_); + } + + private: + FailureHandler orig_; + }; + + struct FakeException {}; + FailureHandler prev = + SetFailureHandler([](const char*, const char*, const int, + const std::string&) { throw FakeException(); }); + FailureHandlerRestorer _(prev); + + EXPECT_THROW(Assert(false), FakeException); +} + } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/status_apple_test.mm b/Firestore/core/test/firebase/firestore/util/status_apple_test.mm index 70880e3bdd0..d631dc25ba8 100644 --- a/Firestore/core/test/firebase/firestore/util/status_apple_test.mm +++ b/Firestore/core/test/firebase/firestore/util/status_apple_test.mm @@ -86,6 +86,14 @@ EXPECT_TRUE([not_found_nserror isEqual:cause]); } +TEST(Status, MovedFromToNSError) { + Status not_found(Error::NotFound, "Some file not found"); + Status unused = std::move(not_found); + + EXPECT_EQ(not_found.ToNSError().domain, FIRFirestoreErrorDomain); + EXPECT_EQ(not_found.ToNSError().code, Internal); +} + } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/status_test.cc b/Firestore/core/test/firebase/firestore/util/status_test.cc index 24b948c7644..d41350c5834 100644 --- a/Firestore/core/test/firebase/firestore/util/status_test.cc +++ b/Firestore/core/test/firebase/firestore/util/status_test.cc @@ -55,6 +55,20 @@ TEST(Status, Copy) { ASSERT_EQ(a.ToString(), b.ToString()); } +TEST(Status, Move) { + Status s(Status(Error::InvalidArgument, "Invalid")); + ASSERT_EQ(Error::InvalidArgument, s.code()); + + Status new_s = std::move(s); + ASSERT_EQ(Error::InvalidArgument, new_s.code()); + + Status ok = Status::OK(); + Status new_ok = std::move(ok); + ASSERT_TRUE(new_ok.ok()); + // Moved OK is not OK anymore. + ASSERT_FALSE(ok.ok()); +} + TEST(Status, Assign) { Status a(Error::InvalidArgument, "Invalid"); Status b; @@ -62,6 +76,34 @@ TEST(Status, Assign) { ASSERT_EQ(a.ToString(), b.ToString()); } +TEST(Status, MoveAssign) { + Status ok; + Status reassigned{Error::InvalidArgument, "Foo"}; + reassigned = std::move(ok); + ASSERT_EQ(reassigned, Status::OK()); + + Status bad{Error::InvalidArgument, "Foo"}; + reassigned = std::move(bad); + ASSERT_EQ(reassigned, Status(Error::InvalidArgument, "Foo")); +} + +TEST(Status, CanAccessMovedFrom) { + Status ok = Status::OK(); + Status assigned = std::move(ok); + + ASSERT_FALSE(ok.ok()); + ASSERT_EQ(ok.error_message(), "Status accessed after move."); + ASSERT_EQ(ok.code(), Error::Internal); +} + +TEST(Status, CanAssignToMovedFromStatus) { + Status a(Error::InvalidArgument, "Invalid"); + Status b = std::move(a); + + EXPECT_NO_THROW({ a = Status(Error::Internal, "Internal"); }); + ASSERT_EQ(a.ToString(), "Internal: Internal"); +} + TEST(Status, Update) { Status s; s.Update(Status::OK()); @@ -77,6 +119,14 @@ TEST(Status, Update) { ASSERT_FALSE(s.ok()); } +TEST(Status, CanUpdateMovedFrom) { + Status a(Error::InvalidArgument, "Invalid"); + Status b = std::move(a); + + a.Update(b); + ASSERT_EQ(a.ToString(), "Internal: Status accessed after move."); +} + TEST(Status, EqualsOK) { ASSERT_EQ(Status::OK(), Status()); } @@ -105,6 +155,17 @@ TEST(Status, EqualsDifferentMessage) { ASSERT_NE(a, b); } +TEST(Status, EqualsApplyToMovedFrom) { + Status a(Error::InvalidArgument, "message"); + Status unused = std::move(a); + Status b(Error::InvalidArgument, "message"); + + ASSERT_NE(a, b); + + unused = std::move(b); + ASSERT_EQ(a, b); +} + TEST(Status, FromErrno) { Status a = Status::FromErrno(EEXIST, "Cannot write file"); ASSERT_THAT( @@ -159,6 +220,15 @@ TEST(Status, CauseBy_Self) { EXPECT_EQ(not_found, result); } +TEST(Status, CauseBy_OnMovedFrom) { + Status not_ready(Error::FailedPrecondition, "DB not ready"); + Status not_found(Error::NotFound, "file not found"); + Status unused = std::move(not_ready); + + Status result = not_ready.CausedBy(not_found); + EXPECT_EQ(not_found, result); +} + } // namespace util } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/util/status_testing.h b/Firestore/core/test/firebase/firestore/util/status_testing.h index 562894da18d..e901378463c 100644 --- a/Firestore/core/test/firebase/firestore/util/status_testing.h +++ b/Firestore/core/test/firebase/firestore/util/status_testing.h @@ -17,8 +17,7 @@ #ifndef FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_STATUS_TESTING_H_ #define FIRESTORE_CORE_TEST_FIREBASE_FIRESTORE_UTIL_STATUS_TESTING_H_ -#include "Firestore/core/src/firebase/firestore/util/status.h" -#include "Firestore/core/src/firebase/firestore/util/statusor.h" +#include "Firestore/core/src/firebase/firestore/util/status_fwd.h" #include "gtest/gtest.h" namespace firebase { diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift b/Firestore/third_party/FirestoreEncoder/FirestoreDecoder.swift similarity index 96% rename from Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift rename to Firestore/third_party/FirestoreEncoder/FirestoreDecoder.swift index 53dbf3a82d1..bc9cd2b781e 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift +++ b/Firestore/third_party/FirestoreEncoder/FirestoreDecoder.swift @@ -18,6 +18,8 @@ import Foundation extension Firestore { public struct Decoder { + fileprivate static let documentRefUserInfoKey = CodingUserInfoKey(rawValue: "DocumentRefUserInfoKey") + public init() {} /// Returns an instance of specified type from a Firestore document. /// @@ -31,9 +33,17 @@ extension Firestore { /// - Parameters: /// - A type to decode a document to. /// - container: A Map keyed of String representing a Firestore document. + /// - document: A reference to the Firestore Document that is being + /// decoded. /// - Returns: An instance of specified type by the first parameter. - public func decode(_: T.Type, from container: [String: Any]) throws -> T { + public func decode(_: T.Type, + from container: [String: Any], + in document: DocumentReference? = nil) throws -> T { let decoder = _FirestoreDecoder(referencing: container) + if let doc = document { + decoder.userInfo[Firestore.Decoder.documentRefUserInfoKey!] = doc + } + guard let value = try decoder.unbox(container, as: T.self) else { throw DecodingError.valueNotFound( T.self, @@ -323,6 +333,24 @@ private struct _FirestoreKeyedDecodingContainer: KeyedDecodingCont } public func decode(_: T.Type, forKey key: Key) throws -> T { + if T.self == SelfDocumentID.self { + let docRef = decoder.userInfo[ + Firestore.Decoder.documentRefUserInfoKey! + ] as! DocumentReference? + + if contains(key) { + let docPath = (docRef != nil) ? docRef!.path : "nil" + var codingPathCopy = codingPath.map { key in key.stringValue } + codingPathCopy.append(key.stringValue) + + throw FirestoreDecodingError.fieldNameConfict("Field name " + + "\(codingPathCopy) was found from document \"\(docPath)\", " + + "cannot assign the document reference to this field.") + } + + return SelfDocumentID(from: docRef) as! T + } + let entry = try require(key: key) decoder.codingPath.append(key) diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift b/Firestore/third_party/FirestoreEncoder/FirestoreEncoder.swift similarity index 98% rename from Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift rename to Firestore/third_party/FirestoreEncoder/FirestoreEncoder.swift index 71840deee0f..71ab4ec3b74 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift +++ b/Firestore/third_party/FirestoreEncoder/FirestoreEncoder.swift @@ -36,8 +36,11 @@ extension Firestore { /// - Returns: A Map keyed by String representing a document Firestore /// API can work with. public func encode(_ value: T) throws -> [String: Any] { - // DocumentReference and FieldValue cannot be encoded directly. - guard T.self != DocumentReference.self, T.self != FieldValue.self else { + // SelfDocumentID, DocumentReference and FieldValue cannot be + // encoded directly. + guard T.self != SelfDocumentID.self, + T.self != DocumentReference.self, + T.self != FieldValue.self else { throw FirestoreEncodingError.encodingIsNotSupported } guard let topLevel = try _FirestoreEncoder().box_(value) else { @@ -212,6 +215,11 @@ private struct _FirestoreKeyedEncodingContainer: KeyedEncodingCont public mutating func encode(_ value: Double, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } public mutating func encode(_ value: T, forKey key: Key) throws { + // `SelfDocumentID` is ignored during encoding. + if T.self == SelfDocumentID.self { + return + } + encoder.codingPath.append(key) defer { encoder.codingPath.removeLast() diff --git a/Firestore/third_party/FirestoreEncoder/LICENSE b/Firestore/third_party/FirestoreEncoder/LICENSE new file mode 100644 index 00000000000..61b0c78195f --- /dev/null +++ b/Firestore/third_party/FirestoreEncoder/LICENSE @@ -0,0 +1,211 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. diff --git a/Firestore/third_party/FirestoreEncoder/METADATA b/Firestore/third_party/FirestoreEncoder/METADATA new file mode 100644 index 00000000000..9735fc0c291 --- /dev/null +++ b/Firestore/third_party/FirestoreEncoder/METADATA @@ -0,0 +1,19 @@ +name: "Swift stdlib" +description: + "This is the FirestoreEncoder and FirestoreDecoder, which are heavily " + "modified derivatives of the JSONEncoder and JSONDecoder from the Swift " + "stdlib's Foundation framework." + +third_party { + url { + type: HOMEPAGE + value: "https://swift.org/" + } + url { + type: GIT + value: "https://github.com/apple/swift" + } + version: "swift-DEVELOPMENT-SNAPSHOT-2019-09-04-a" + last_upgrade_date { year: 2019 month: 9 day: 6 } + license_type: NOTICE +} diff --git a/Gemfile b/Gemfile index 22d09535466..4f47658aa98 100644 --- a/Gemfile +++ b/Gemfile @@ -2,5 +2,5 @@ # commit Gemfile and Gemfile.lock. source 'https://rubygems.org' -gem 'cocoapods', "1.7.5" -gem 'cocoapods-generate', "1.5.0" +gem 'cocoapods', "1.8.1" +gem 'cocoapods-generate', '1.6.0' diff --git a/Gemfile.lock b/Gemfile.lock index 4cef9352a06..f8d4db918b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,24 +1,27 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.0) + CFPropertyList (3.0.1) activesupport (4.2.11.1) i18n (~> 0.7) minitest (~> 5.1) thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) + algoliasearch (1.27.1) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) atomos (0.1.3) - claide (1.0.2) - cocoapods (1.7.5) + claide (1.0.3) + cocoapods (1.8.1) activesupport (>= 4.0.2, < 5) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.7.5) + cocoapods-core (= 1.8.1) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 1.2.2, < 2.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-stats (>= 1.0.0, < 2.0) - cocoapods-trunk (>= 1.3.1, < 2.0) + cocoapods-trunk (>= 1.4.0, < 2.0) cocoapods-try (>= 1.1.0, < 2.0) colored2 (~> 3.1) escape (~> 0.0.4) @@ -27,21 +30,23 @@ GEM molinillo (~> 0.6.6) nap (~> 1.0) ruby-macho (~> 1.4) - xcodeproj (>= 1.10.0, < 2.0) - cocoapods-core (1.7.5) + xcodeproj (>= 1.11.1, < 2.0) + cocoapods-core (1.8.1) activesupport (>= 4.0.2, < 6) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) fuzzy_match (~> 2.0.4) nap (~> 1.0) cocoapods-deintegrate (1.0.4) cocoapods-disable-podfile-validations (0.1.1) cocoapods-downloader (1.2.2) - cocoapods-generate (1.5.0) + cocoapods-generate (1.6.0) cocoapods-disable-podfile-validations (~> 0.1.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.0) cocoapods-stats (1.1.0) - cocoapods-trunk (1.3.1) + cocoapods-trunk (1.4.1) nap (>= 0.8, < 2.0) netrc (~> 0.11) cocoapods-try (1.1.0) @@ -51,9 +56,11 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) + httpclient (2.8.3) i18n (0.9.5) concurrent-ruby (~> 1.0) - minitest (5.11.3) + json (2.2.0) + minitest (5.12.0) molinillo (0.6.6) nanaimo (0.2.6) nap (1.1.0) @@ -62,7 +69,7 @@ GEM thread_safe (0.3.6) tzinfo (1.2.5) thread_safe (~> 0.1) - xcodeproj (1.11.0) + xcodeproj (1.12.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) @@ -73,8 +80,8 @@ PLATFORMS ruby DEPENDENCIES - cocoapods (= 1.7.5) - cocoapods-generate (= 1.5.0) + cocoapods (= 1.8.1) + cocoapods-generate (= 1.6.0) BUNDLED WITH 1.17.3 diff --git a/GoogleDataTransport.podspec b/GoogleDataTransport.podspec index eaa36544a0f..a71ce6c167b 100644 --- a/GoogleDataTransport.podspec +++ b/GoogleDataTransport.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleDataTransport' - s.version = '1.1.1' + s.version = '2.0.0' s.summary = 'Google iOS SDK data transport.' s.description = <<-DESC @@ -19,15 +19,15 @@ Shared library for iOS SDK data transport needs. s.osx.deployment_target = '10.11' s.tvos.deployment_target = '10.0' - # To develop or run the tests, >= 1.6.0 must be installed. + # To develop or run the tests, >= 1.8.0.beta.1 must be installed. s.cocoapods_version = '>= 1.4.0' s.static_framework = true s.prefix_header_file = false - s.source_files = 'GoogleDataTransport/GDTLibrary/**/*' - s.public_header_files = 'GoogleDataTransport/GDTLibrary/Public/*.h' - s.private_header_files = 'GoogleDataTransport/GDTLibrary/Private/*.h' + s.source_files = 'GoogleDataTransport/GDTCORLibrary/**/*' + s.public_header_files = 'GoogleDataTransport/GDTCORLibrary/Public/*.h' + s.private_header_files = 'GoogleDataTransport/GDTCORLibrary/Private/*.h' header_search_paths = { 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}/GoogleDataTransport/"' @@ -39,26 +39,55 @@ Shared library for iOS SDK data transport needs. 'CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY' => 'YES' }.merge(header_search_paths) - common_test_sources = ['GoogleDataTransport/GDTTests/Common/**/*.{h,m}'] + common_test_sources = ['GoogleDataTransport/GDTCORTests/Common/**/*.{h,m}'] + + # Test app specs + if ENV['GDT_DEV'] && ENV['GDT_DEV'] == '1' then + s.app_spec 'TestApp' do |app_spec| + app_spec.source_files = 'GoogleDataTransport/GDTTestApp/*.swift' + app_spec.ios.resources = ['GoogleDataTransport/GDTTestApp/ios/*.storyboard'] + app_spec.macos.resources = ['GoogleDataTransport/GDTTestApp/macos/*.storyboard'] + app_spec.tvos.resources = ['GoogleDataTransport/GDTTestApp/tvos/*.storyboard'] + app_spec.info_plist = { + 'UILaunchStoryboardName' => 'Main', + 'UIMainStoryboardFile' => 'Main', + 'NSMainStoryboardFile' => 'Main' + } + end + end # Unit test specs s.test_spec 'Tests-Unit' do |test_spec| test_spec.requires_app_host = false - test_spec.source_files = ['GoogleDataTransport/GDTTests/Unit/**/*.{h,m}'] + common_test_sources + test_spec.source_files = ['GoogleDataTransport/GDTCORTests/Unit/**/*.{h,m}'] + common_test_sources test_spec.pod_target_xcconfig = header_search_paths end s.test_spec 'Tests-Lifecycle' do |test_spec| test_spec.requires_app_host = false - test_spec.source_files = ['GoogleDataTransport/GDTTests/Lifecycle/**/*.{h,m}'] + common_test_sources + test_spec.source_files = ['GoogleDataTransport/GDTCORTests/Lifecycle/**/*.{h,m}'] + common_test_sources test_spec.pod_target_xcconfig = header_search_paths end # Integration test specs s.test_spec 'Tests-Integration' do |test_spec| test_spec.requires_app_host = false - test_spec.source_files = ['GoogleDataTransport/GDTTests/Integration/**/*.{h,m}'] + common_test_sources + test_spec.source_files = ['GoogleDataTransport/GDTCORTests/Integration/**/*.{h,m}'] + common_test_sources test_spec.pod_target_xcconfig = header_search_paths test_spec.dependency 'GCDWebServer' end + + # Monkey test specs TODO(mikehaney24): Uncomment when travis is running >= cocoapods-1.8.0 + if ENV['GDT_DEV'] && ENV['GDT_DEV'] == '1' then + s.test_spec 'Tests-Monkey' do |test_spec| + test_spec.requires_app_host = true + test_spec.app_host_name = 'GoogleDataTransport/TestApp' + test_spec.dependency 'GoogleDataTransport/TestApp' + test_spec.source_files = ['GoogleDataTransport/GDTTests/Monkey/**/*.{swift}'] + test_spec.info_plist = { + 'GDT_MONKEYTEST' => '1' + } + end + end + end diff --git a/GoogleDataTransport/CHANGELOG.md b/GoogleDataTransport/CHANGELOG.md index 45fe4d70a45..a75a2a68f49 100644 --- a/GoogleDataTransport/CHANGELOG.md +++ b/GoogleDataTransport/CHANGELOG.md @@ -1,9 +1,19 @@ +# v2.0.0 +- Change/rename all classes and references from GDT to GDTCOR. (#3729) + +# v1.2.0 +- Removes all NSAsserts in favor of custom asserts. (#3747) + +# v1.1.3 +- Wrap decoding in GDTCORUploadCoordinator in a try catch. (#3676) + # v1.1.2 - Add initial support for iOS 13. - Add initial support for Catalyst. +- Backgrounding in GDTCORStorage is fixed. (#3623 and #3625) # v1.1.1 -- Fixes a crash in GDTUploadPackage and GDTStorage. (#3547) +- Fixes a crash in GDTCORUploadPackage and GDTCORStorage. (#3547) # v1.1.0 - Remove almost all NSAsserts and NSCAsserts for a better development diff --git a/GoogleDataTransport/GDTLibrary/GDTAssert.m b/GoogleDataTransport/GDTCORLibrary/GDTCORAssert.m similarity index 66% rename from GoogleDataTransport/GDTLibrary/GDTAssert.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORAssert.m index a6bedd2410b..3e5f57b5783 100644 --- a/GoogleDataTransport/GDTLibrary/GDTAssert.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORAssert.m @@ -14,18 +14,18 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTAssert.h" +#import "GDTCORLibrary/Public/GDTCORAssert.h" -GDTAssertionBlock GDTAssertionBlockToRunInsteadOfNSAssert(void) { +GDTCORAssertionBlock GDTCORAssertionBlockToRunInstead(void) { // This class is only compiled in by unit tests, and this should fail quickly in optimized builds. - Class GDTAssertClass = NSClassFromString(@"GDTAssertHelper"); - if (__builtin_expect(!!GDTAssertClass, 0)) { + Class GDTCORAssertClass = NSClassFromString(@"GDTCORAssertHelper"); + if (__builtin_expect(!!GDTCORAssertClass, 0)) { SEL assertionBlockSEL = NSSelectorFromString(@"assertionBlock"); if (assertionBlockSEL) { - IMP assertionBlockIMP = [GDTAssertClass methodForSelector:assertionBlockSEL]; + IMP assertionBlockIMP = [GDTCORAssertClass methodForSelector:assertionBlockSEL]; if (assertionBlockIMP) { - GDTAssertionBlock assertionBlock = - ((GDTAssertionBlock(*)(id, SEL))assertionBlockIMP)(GDTAssertClass, assertionBlockSEL); + GDTCORAssertionBlock assertionBlock = ((GDTCORAssertionBlock(*)(id, SEL))assertionBlockIMP)( + GDTCORAssertClass, assertionBlockSEL); if (assertionBlock) { return assertionBlock; } diff --git a/GoogleDataTransport/GDTLibrary/GDTClock.m b/GoogleDataTransport/GDTCORLibrary/GDTCORClock.m similarity index 81% rename from GoogleDataTransport/GDTLibrary/GDTClock.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORClock.m index 2cc3d1c7fd4..f0ea8ab606c 100644 --- a/GoogleDataTransport/GDTLibrary/GDTClock.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORClock.m @@ -14,7 +14,7 @@ * limitations under the License. */ -#import "GDTLibrary/Public/GDTClock.h" +#import "GDTCORLibrary/Public/GDTCORClock.h" #import @@ -74,7 +74,7 @@ static int64_t UptimeInNanoseconds() { } // TODO: Consider adding a 'trustedTime' property that can be populated by the response from a BE. -@implementation GDTClock +@implementation GDTCORClock - (instancetype)init { self = [super init]; @@ -90,17 +90,17 @@ - (instancetype)init { return self; } -+ (GDTClock *)snapshot { - return [[GDTClock alloc] init]; ++ (GDTCORClock *)snapshot { + return [[GDTCORClock alloc] init]; } + (instancetype)clockSnapshotInTheFuture:(uint64_t)millisInTheFuture { - GDTClock *snapshot = [self snapshot]; + GDTCORClock *snapshot = [self snapshot]; snapshot->_timeMillis += millisInTheFuture; return snapshot; } -- (BOOL)isAfter:(GDTClock *)otherClock { +- (BOOL)isAfter:(GDTCORClock *)otherClock { // These clocks are trivially comparable when they share a kernel boot time. if (_kernelBootTime == otherClock->_kernelBootTime) { int64_t timeDiff = (_timeMillis + _timezoneOffsetSeconds) - @@ -126,16 +126,16 @@ - (BOOL)isEqual:(id)object { #pragma mark - NSSecureCoding /** NSKeyedCoder key for timeMillis property. */ -static NSString *const kGDTClockTimeMillisKey = @"GDTClockTimeMillis"; +static NSString *const kGDTCORClockTimeMillisKey = @"GDTCORClockTimeMillis"; /** NSKeyedCoder key for timezoneOffsetMillis property. */ -static NSString *const kGDTClockTimezoneOffsetSeconds = @"GDTClockTimezoneOffsetSeconds"; +static NSString *const kGDTCORClockTimezoneOffsetSeconds = @"GDTCORClockTimezoneOffsetSeconds"; /** NSKeyedCoder key for _kernelBootTime ivar. */ -static NSString *const kGDTClockKernelBootTime = @"GDTClockKernelBootTime"; +static NSString *const kGDTCORClockKernelBootTime = @"GDTCORClockKernelBootTime"; /** NSKeyedCoder key for _uptime ivar. */ -static NSString *const kGDTClockUptime = @"GDTClockUptime"; +static NSString *const kGDTCORClockUptime = @"GDTCORClockUptime"; + (BOOL)supportsSecureCoding { return YES; @@ -146,19 +146,19 @@ - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self) { // TODO: If the kernelBootTime is more recent, we need to change the kernel boot time and // uptimeMillis ivars - _timeMillis = [aDecoder decodeInt64ForKey:kGDTClockTimeMillisKey]; - _timezoneOffsetSeconds = [aDecoder decodeInt64ForKey:kGDTClockTimezoneOffsetSeconds]; - _kernelBootTime = [aDecoder decodeInt64ForKey:kGDTClockKernelBootTime]; - _uptime = [aDecoder decodeInt64ForKey:kGDTClockUptime]; + _timeMillis = [aDecoder decodeInt64ForKey:kGDTCORClockTimeMillisKey]; + _timezoneOffsetSeconds = [aDecoder decodeInt64ForKey:kGDTCORClockTimezoneOffsetSeconds]; + _kernelBootTime = [aDecoder decodeInt64ForKey:kGDTCORClockKernelBootTime]; + _uptime = [aDecoder decodeInt64ForKey:kGDTCORClockUptime]; } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { - [aCoder encodeInt64:_timeMillis forKey:kGDTClockTimeMillisKey]; - [aCoder encodeInt64:_timezoneOffsetSeconds forKey:kGDTClockTimezoneOffsetSeconds]; - [aCoder encodeInt64:_kernelBootTime forKey:kGDTClockKernelBootTime]; - [aCoder encodeInt64:_uptime forKey:kGDTClockUptime]; + [aCoder encodeInt64:_timeMillis forKey:kGDTCORClockTimeMillisKey]; + [aCoder encodeInt64:_timezoneOffsetSeconds forKey:kGDTCORClockTimezoneOffsetSeconds]; + [aCoder encodeInt64:_kernelBootTime forKey:kGDTCORClockKernelBootTime]; + [aCoder encodeInt64:_uptime forKey:kGDTCORClockUptime]; } @end diff --git a/GoogleDataTransport/GDTLibrary/GDTConsoleLogger.m b/GoogleDataTransport/GDTCORLibrary/GDTCORConsoleLogger.m similarity index 63% rename from GoogleDataTransport/GDTLibrary/GDTConsoleLogger.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORConsoleLogger.m index 2c391dcf1aa..1626b6f0c4e 100644 --- a/GoogleDataTransport/GDTLibrary/GDTConsoleLogger.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORConsoleLogger.m @@ -14,20 +14,20 @@ * limitations under the License. */ -#import "GDTLibrary/Public/GDTConsoleLogger.h" +#import "GDTCORLibrary/Public/GDTCORConsoleLogger.h" /** The console logger prefix. */ -static NSString *kGDTConsoleLogger = @"[GoogleDataTransport]"; +static NSString *kGDTCORConsoleLogger = @"[GoogleDataTransport]"; -NSString *GDTMessageCodeEnumToString(GDTMessageCode code) { - return [[NSString alloc] initWithFormat:@"I-GDT%06ld", (long)code]; +NSString *GDTCORMessageCodeEnumToString(GDTCORMessageCode code) { + return [[NSString alloc] initWithFormat:@"I-GDTCOR%06ld", (long)code]; } -void GDTLog(GDTMessageCode code, NSString *format, ...) { +void GDTCORLog(GDTCORMessageCode code, NSString *format, ...) { // Don't log anything in not debug builds. #ifndef NDEBUG - NSString *logFormat = [NSString - stringWithFormat:@"%@[%@] %@", kGDTConsoleLogger, GDTMessageCodeEnumToString(code), format]; + NSString *logFormat = [NSString stringWithFormat:@"%@[%@] %@", kGDTCORConsoleLogger, + GDTCORMessageCodeEnumToString(code), format]; va_list args; va_start(args, format); NSLogv(logFormat, args); diff --git a/GoogleDataTransport/GDTLibrary/GDTDataFuture.m b/GoogleDataTransport/GDTCORLibrary/GDTCORDataFuture.m similarity index 76% rename from GoogleDataTransport/GDTLibrary/GDTDataFuture.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORDataFuture.m index 61dda60b691..33c3f1521e2 100644 --- a/GoogleDataTransport/GDTLibrary/GDTDataFuture.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORDataFuture.m @@ -14,9 +14,9 @@ * limitations under the License. */ -#import +#import -@implementation GDTDataFuture +@implementation GDTCORDataFuture - (instancetype)initWithFileURL:(NSURL *)fileURL { self = [super init]; @@ -38,25 +38,25 @@ - (NSUInteger)hash { #pragma mark - NSSecureCoding /** Coding key for _fileURL ivar. */ -static NSString *kGDTDataFutureFileURLKey = @"GDTDataFutureFileURLKey"; +static NSString *kGDTCORDataFutureFileURLKey = @"GDTCORDataFutureFileURLKey"; /** Coding key for _data ivar. */ -static NSString *kGDTDataFutureDataKey = @"GDTDataFutureDataKey"; +static NSString *kGDTCORDataFutureDataKey = @"GDTCORDataFutureDataKey"; + (BOOL)supportsSecureCoding { return YES; } - (void)encodeWithCoder:(nonnull NSCoder *)aCoder { - [aCoder encodeObject:_fileURL forKey:kGDTDataFutureFileURLKey]; - [aCoder encodeObject:_originalData forKey:kGDTDataFutureDataKey]; + [aCoder encodeObject:_fileURL forKey:kGDTCORDataFutureFileURLKey]; + [aCoder encodeObject:_originalData forKey:kGDTCORDataFutureDataKey]; } - (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder { self = [self init]; if (self) { - _fileURL = [aDecoder decodeObjectOfClass:[NSURL class] forKey:kGDTDataFutureFileURLKey]; - _originalData = [aDecoder decodeObjectOfClass:[NSData class] forKey:kGDTDataFutureDataKey]; + _fileURL = [aDecoder decodeObjectOfClass:[NSURL class] forKey:kGDTCORDataFutureFileURLKey]; + _originalData = [aDecoder decodeObjectOfClass:[NSData class] forKey:kGDTCORDataFutureDataKey]; } return self; } diff --git a/GoogleDataTransport/GDTLibrary/GDTEvent.m b/GoogleDataTransport/GDTCORLibrary/GDTCOREvent.m similarity index 76% rename from GoogleDataTransport/GDTLibrary/GDTEvent.m rename to GoogleDataTransport/GDTCORLibrary/GDTCOREvent.m index 3ea9425d1cc..0b781d66dda 100644 --- a/GoogleDataTransport/GDTLibrary/GDTEvent.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCOREvent.m @@ -14,29 +14,32 @@ * limitations under the License. */ -#import +#import -#import +#import +#import -#import "GDTLibrary/Private/GDTAssert.h" -#import "GDTLibrary/Private/GDTEvent_Private.h" +#import "GDTCORLibrary/Private/GDTCOREvent_Private.h" -@implementation GDTEvent +@implementation GDTCOREvent -- (instancetype)initWithMappingID:(NSString *)mappingID target:(NSInteger)target { - GDTAssert(mappingID.length > 0, @"Please give a valid mapping ID"); - GDTAssert(target > 0, @"A target cannot be negative or 0"); +- (nullable instancetype)initWithMappingID:(NSString *)mappingID target:(NSInteger)target { + GDTCORAssert(mappingID.length > 0, @"Please give a valid mapping ID"); + GDTCORAssert(target > 0, @"A target cannot be negative or 0"); + if (mappingID == nil || mappingID.length == 0 || target <= 0) { + return nil; + } self = [super init]; if (self) { _mappingID = mappingID; _target = target; - _qosTier = GDTEventQosDefault; + _qosTier = GDTCOREventQosDefault; } return self; } - (instancetype)copy { - GDTEvent *copy = [[GDTEvent alloc] initWithMappingID:_mappingID target:_target]; + GDTCOREvent *copy = [[GDTCOREvent alloc] initWithMappingID:_mappingID target:_target]; copy.dataObject = _dataObject; copy.dataObjectTransportBytes = _dataObjectTransportBytes; copy.qosTier = _qosTier; @@ -57,7 +60,7 @@ - (BOOL)isEqual:(id)object { return [self hash] == [object hash]; } -- (void)setDataObject:(id)dataObject { +- (void)setDataObject:(id)dataObject { // If you're looking here because of a performance issue in -transportBytes slowing the assignment // of -dataObject, one way to address this is to add a queue to this class, // dispatch_(barrier_ if concurrent)async here, and implement the getter with a dispatch_sync. @@ -67,8 +70,8 @@ - (void)setDataObject:(id)dataObject { } } -- (GDTStoredEvent *)storedEventWithDataFuture:(GDTDataFuture *)dataFuture { - return [[GDTStoredEvent alloc] initWithEvent:self dataFuture:dataFuture]; +- (GDTCORStoredEvent *)storedEventWithDataFuture:(GDTCORDataFuture *)dataFuture { + return [[GDTCORStoredEvent alloc] initWithEvent:self dataFuture:dataFuture]; } #pragma mark - NSSecureCoding and NSCoding Protocols @@ -100,7 +103,7 @@ - (id)initWithCoder:(NSCoder *)aDecoder { _dataObjectTransportBytes = [aDecoder decodeObjectOfClass:[NSData class] forKey:dataObjectTransportBytesKey]; _qosTier = [aDecoder decodeIntegerForKey:qosTierKey]; - _clockSnapshot = [aDecoder decodeObjectOfClass:[GDTClock class] forKey:clockSnapshotKey]; + _clockSnapshot = [aDecoder decodeObjectOfClass:[GDTCORClock class] forKey:clockSnapshotKey]; } return self; } diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORLifecycle.m b/GoogleDataTransport/GDTCORLibrary/GDTCORLifecycle.m new file mode 100644 index 00000000000..dab1f50dcb2 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORLifecycle.m @@ -0,0 +1,119 @@ +/* + * Copyright 2019 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 "GDTCORLibrary/Public/GDTCORLifecycle.h" + +#import + +#import "GDTCORLibrary/Private/GDTCORRegistrar_Private.h" +#import "GDTCORLibrary/Private/GDTCORStorage_Private.h" +#import "GDTCORLibrary/Private/GDTCORTransformer_Private.h" +#import "GDTCORLibrary/Private/GDTCORUploadCoordinator.h" + +@implementation GDTCORLifecycle + ++ (void)load { + [self sharedInstance]; +} + +/** Creates/returns the singleton instance of this class. + * + * @return The singleton instance of this class. + */ ++ (instancetype)sharedInstance { + static GDTCORLifecycle *sharedInstance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[GDTCORLifecycle alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter addObserver:self + selector:@selector(applicationDidEnterBackground:) + name:kGDTCORApplicationDidEnterBackgroundNotification + object:nil]; + [notificationCenter addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:kGDTCORApplicationWillEnterForegroundNotification + object:nil]; + + NSString *name = kGDTCORApplicationWillTerminateNotification; + [notificationCenter addObserver:self + selector:@selector(applicationWillTerminate:) + name:name + object:nil]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)applicationDidEnterBackground:(NSNotification *)notification { + GDTCORApplication *application = [GDTCORApplication sharedApplication]; + if ([[GDTCORTransformer sharedInstance] respondsToSelector:@selector(appWillBackground:)]) { + [[GDTCORTransformer sharedInstance] appWillBackground:application]; + } + if ([[GDTCORStorage sharedInstance] respondsToSelector:@selector(appWillBackground:)]) { + [[GDTCORStorage sharedInstance] appWillBackground:application]; + } + if ([[GDTCORUploadCoordinator sharedInstance] respondsToSelector:@selector(appWillBackground:)]) { + [[GDTCORUploadCoordinator sharedInstance] appWillBackground:application]; + } + if ([[GDTCORRegistrar sharedInstance] respondsToSelector:@selector(appWillBackground:)]) { + [[GDTCORRegistrar sharedInstance] appWillBackground:application]; + } +} + +- (void)applicationWillEnterForeground:(NSNotification *)notification { + GDTCORApplication *application = [GDTCORApplication sharedApplication]; + if ([[GDTCORTransformer sharedInstance] respondsToSelector:@selector(appWillForeground:)]) { + [[GDTCORTransformer sharedInstance] appWillForeground:application]; + } + if ([[GDTCORStorage sharedInstance] respondsToSelector:@selector(appWillForeground:)]) { + [[GDTCORStorage sharedInstance] appWillForeground:application]; + } + if ([[GDTCORUploadCoordinator sharedInstance] respondsToSelector:@selector(appWillForeground:)]) { + [[GDTCORUploadCoordinator sharedInstance] appWillForeground:application]; + } + if ([[GDTCORRegistrar sharedInstance] respondsToSelector:@selector(appWillForeground:)]) { + [[GDTCORRegistrar sharedInstance] appWillForeground:application]; + } +} + +- (void)applicationWillTerminate:(NSNotification *)notification { + GDTCORApplication *application = [GDTCORApplication sharedApplication]; + if ([[GDTCORTransformer sharedInstance] respondsToSelector:@selector(appWillTerminate:)]) { + [[GDTCORTransformer sharedInstance] appWillTerminate:application]; + } + if ([[GDTCORStorage sharedInstance] respondsToSelector:@selector(appWillTerminate:)]) { + [[GDTCORStorage sharedInstance] appWillTerminate:application]; + } + if ([[GDTCORUploadCoordinator sharedInstance] respondsToSelector:@selector(appWillTerminate:)]) { + [[GDTCORUploadCoordinator sharedInstance] appWillTerminate:application]; + } + if ([[GDTCORRegistrar sharedInstance] respondsToSelector:@selector(appWillTerminate:)]) { + [[GDTCORRegistrar sharedInstance] appWillTerminate:application]; + } +} + +@end diff --git a/GoogleDataTransport/GDTLibrary/GDTPlatform.m b/GoogleDataTransport/GDTCORLibrary/GDTCORPlatform.m similarity index 75% rename from GoogleDataTransport/GDTLibrary/GDTPlatform.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORPlatform.m index 21301f6ddae..cd304569258 100644 --- a/GoogleDataTransport/GDTLibrary/GDTPlatform.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORPlatform.m @@ -14,20 +14,22 @@ * limitations under the License. */ -#import +#import -const GDTBackgroundIdentifier GDTBackgroundIdentifierInvalid = 0; +#import -NSString *const kGDTApplicationDidEnterBackgroundNotification = - @"GDTApplicationDidEnterBackgroundNotification"; +const GDTCORBackgroundIdentifier GDTCORBackgroundIdentifierInvalid = 0; -NSString *const kGDTApplicationWillEnterForegroundNotification = - @"GDTApplicationWillEnterForegroundNotification"; +NSString *const kGDTCORApplicationDidEnterBackgroundNotification = + @"GDTCORApplicationDidEnterBackgroundNotification"; -NSString *const kGDTApplicationWillTerminateNotification = - @"GDTApplicationWillTerminateNotification"; +NSString *const kGDTCORApplicationWillEnterForegroundNotification = + @"GDTCORApplicationWillEnterForegroundNotification"; -BOOL GDTReachabilityFlagsContainWWAN(SCNetworkReachabilityFlags flags) { +NSString *const kGDTCORApplicationWillTerminateNotification = + @"GDTCORApplicationWillTerminateNotification"; + +BOOL GDTCORReachabilityFlagsContainWWAN(SCNetworkReachabilityFlags flags) { #if TARGET_OS_IOS return (flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN; #else @@ -35,22 +37,23 @@ BOOL GDTReachabilityFlagsContainWWAN(SCNetworkReachabilityFlags flags) { #endif // TARGET_OS_IOS } -@implementation GDTApplication +@implementation GDTCORApplication + (void)load { #if TARGET_OS_IOS || TARGET_OS_TV // If this asserts, please file a bug at https://github.com/firebase/firebase-ios-sdk/issues. - NSAssert(GDTBackgroundIdentifierInvalid == UIBackgroundTaskInvalid, - @"GDTBackgroundIdentifierInvalid and UIBackgroundTaskInvalid should be the same."); + GDTCORFatalAssert( + GDTCORBackgroundIdentifierInvalid == UIBackgroundTaskInvalid, + @"GDTCORBackgroundIdentifierInvalid and UIBackgroundTaskInvalid should be the same."); #endif [self sharedApplication]; } -+ (nullable GDTApplication *)sharedApplication { - static GDTApplication *application; ++ (nullable GDTCORApplication *)sharedApplication { + static GDTCORApplication *application; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - application = [[GDTApplication alloc] init]; + application = [[GDTCORApplication alloc] init]; }); return application; } @@ -99,13 +102,15 @@ - (instancetype)init { return self; } -- (GDTBackgroundIdentifier)beginBackgroundTaskWithExpirationHandler:(void (^)(void))handler { +- (GDTCORBackgroundIdentifier)beginBackgroundTaskWithExpirationHandler:(void (^)(void))handler { return [[self sharedApplicationForBackgroundTask] beginBackgroundTaskWithExpirationHandler:handler]; } -- (void)endBackgroundTask:(GDTBackgroundIdentifier)bgID { - [[self sharedApplicationForBackgroundTask] endBackgroundTask:bgID]; +- (void)endBackgroundTask:(GDTCORBackgroundIdentifier)bgID { + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [[self sharedApplicationForBackgroundTask] endBackgroundTask:bgID]; + } } #pragma mark - App environment helpers @@ -145,17 +150,17 @@ - (nullable id)sharedApplicationForBackgroundTask { #if TARGET_OS_IOS || TARGET_OS_TV - (void)iOSApplicationDidEnterBackground:(NSNotification *)notif { NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; - [notifCenter postNotificationName:kGDTApplicationDidEnterBackgroundNotification object:nil]; + [notifCenter postNotificationName:kGDTCORApplicationDidEnterBackgroundNotification object:nil]; } - (void)iOSApplicationWillEnterForeground:(NSNotification *)notif { NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; - [notifCenter postNotificationName:kGDTApplicationWillEnterForegroundNotification object:nil]; + [notifCenter postNotificationName:kGDTCORApplicationWillEnterForegroundNotification object:nil]; } - (void)iOSApplicationWillTerminate:(NSNotification *)notif { NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; - [notifCenter postNotificationName:kGDTApplicationWillTerminateNotification object:nil]; + [notifCenter postNotificationName:kGDTCORApplicationWillTerminateNotification object:nil]; } #endif // TARGET_OS_IOS || TARGET_OS_TV @@ -164,7 +169,7 @@ - (void)iOSApplicationWillTerminate:(NSNotification *)notif { #if TARGET_OS_OSX - (void)macOSApplicationWillTerminate:(NSNotification *)notif { NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; - [notifCenter postNotificationName:kGDTApplicationWillTerminateNotification object:nil]; + [notifCenter postNotificationName:kGDTCORApplicationWillTerminateNotification object:nil]; } #endif // TARGET_OS_OSX diff --git a/GoogleDataTransport/GDTLibrary/GDTReachability.m b/GoogleDataTransport/GDTCORLibrary/GDTCORReachability.m similarity index 65% rename from GoogleDataTransport/GDTLibrary/GDTReachability.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORReachability.m index 2da6bbd7d5d..0796ead957c 100644 --- a/GoogleDataTransport/GDTLibrary/GDTReachability.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORReachability.m @@ -14,10 +14,10 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTReachability.h" -#import "GDTLibrary/Private/GDTReachability_Private.h" +#import "GDTCORLibrary/Private/GDTCORReachability.h" +#import "GDTCORLibrary/Private/GDTCORReachability_Private.h" -#import +#import #import @@ -27,11 +27,11 @@ * @param flags The new flag values. * @param info Any data that might be passed in by the callback. */ -static void GDTReachabilityCallback(SCNetworkReachabilityRef reachability, - SCNetworkReachabilityFlags flags, - void *info); +static void GDTCORReachabilityCallback(SCNetworkReachabilityRef reachability, + SCNetworkReachabilityFlags flags, + void *info); -@implementation GDTReachability { +@implementation GDTCORReachability { /** The reachability object. */ SCNetworkReachabilityRef _reachabilityRef; @@ -47,18 +47,18 @@ + (void)load { } + (instancetype)sharedInstance { - static GDTReachability *sharedInstance; + static GDTCORReachability *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - sharedInstance = [[GDTReachability alloc] init]; + sharedInstance = [[GDTCORReachability alloc] init]; }); return sharedInstance; } + (SCNetworkReachabilityFlags)currentFlags { __block SCNetworkReachabilityFlags currentFlags; - dispatch_sync([GDTReachability sharedInstance] -> _reachabilityQueue, ^{ - GDTReachability *reachability = [GDTReachability sharedInstance]; + dispatch_sync([GDTCORReachability sharedInstance] -> _reachabilityQueue, ^{ + GDTCORReachability *reachability = [GDTCORReachability sharedInstance]; currentFlags = reachability->_flags ? reachability->_flags : reachability->_callbackFlags; }); return currentFlags; @@ -72,16 +72,18 @@ - (instancetype)init { zeroAddress.sin_len = sizeof(zeroAddress); zeroAddress.sin_family = AF_INET; - _reachabilityQueue = dispatch_queue_create("com.google.GDTReachability", DISPATCH_QUEUE_SERIAL); + _reachabilityQueue = + dispatch_queue_create("com.google.GDTCORReachability", DISPATCH_QUEUE_SERIAL); _reachabilityRef = SCNetworkReachabilityCreateWithAddress( kCFAllocatorDefault, (const struct sockaddr *)&zeroAddress); Boolean success = SCNetworkReachabilitySetDispatchQueue(_reachabilityRef, _reachabilityQueue); if (!success) { - GDTLogWarning(GDTMCWReachabilityFailed, @"%@", @"The reachability queue wasn't set."); + GDTCORLogWarning(GDTCORMCWReachabilityFailed, @"%@", @"The reachability queue wasn't set."); } - success = SCNetworkReachabilitySetCallback(_reachabilityRef, GDTReachabilityCallback, NULL); + success = SCNetworkReachabilitySetCallback(_reachabilityRef, GDTCORReachabilityCallback, NULL); if (!success) { - GDTLogWarning(GDTMCWReachabilityFailed, @"%@", @"The reachability callback wasn't set."); + GDTCORLogWarning(GDTCORMCWReachabilityFailed, @"%@", + @"The reachability callback wasn't set."); } // Get the initial set of flags. @@ -103,8 +105,8 @@ - (void)setCallbackFlags:(SCNetworkReachabilityFlags)flags { @end -static void GDTReachabilityCallback(SCNetworkReachabilityRef reachability, - SCNetworkReachabilityFlags flags, - void *info) { - [[GDTReachability sharedInstance] setCallbackFlags:flags]; +static void GDTCORReachabilityCallback(SCNetworkReachabilityRef reachability, + SCNetworkReachabilityFlags flags, + void *info) { + [[GDTCORReachability sharedInstance] setCallbackFlags:flags]; } diff --git a/GoogleDataTransport/GDTLibrary/GDTRegistrar.m b/GoogleDataTransport/GDTCORLibrary/GDTCORRegistrar.m similarity index 55% rename from GoogleDataTransport/GDTLibrary/GDTRegistrar.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORRegistrar.m index 500f9cc68df..65335c5a922 100644 --- a/GoogleDataTransport/GDTLibrary/GDTRegistrar.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORRegistrar.m @@ -14,23 +14,23 @@ * limitations under the License. */ -#import "GDTLibrary/Public/GDTRegistrar.h" +#import "GDTCORLibrary/Public/GDTCORRegistrar.h" -#import "GDTLibrary/Private/GDTRegistrar_Private.h" +#import "GDTCORLibrary/Private/GDTCORRegistrar_Private.h" -@implementation GDTRegistrar { +@implementation GDTCORRegistrar { /** Backing ivar for targetToUploader property. */ - NSMutableDictionary> *_targetToUploader; + NSMutableDictionary> *_targetToUploader; /** Backing ivar for targetToPrioritizer property. */ - NSMutableDictionary> *_targetToPrioritizer; + NSMutableDictionary> *_targetToPrioritizer; } + (instancetype)sharedInstance { - static GDTRegistrar *sharedInstance; + static GDTCORRegistrar *sharedInstance; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - sharedInstance = [[GDTRegistrar alloc] init]; + sharedInstance = [[GDTCORRegistrar alloc] init]; }); return sharedInstance; } @@ -38,38 +38,39 @@ + (instancetype)sharedInstance { - (instancetype)init { self = [super init]; if (self) { - _registrarQueue = dispatch_queue_create("com.google.GDTRegistrar", DISPATCH_QUEUE_CONCURRENT); + _registrarQueue = + dispatch_queue_create("com.google.GDTCORRegistrar", DISPATCH_QUEUE_CONCURRENT); _targetToPrioritizer = [[NSMutableDictionary alloc] init]; _targetToUploader = [[NSMutableDictionary alloc] init]; } return self; } -- (void)registerUploader:(id)backend target:(GDTTarget)target { - __weak GDTRegistrar *weakSelf = self; +- (void)registerUploader:(id)backend target:(GDTCORTarget)target { + __weak GDTCORRegistrar *weakSelf = self; dispatch_barrier_async(_registrarQueue, ^{ - GDTRegistrar *strongSelf = weakSelf; + GDTCORRegistrar *strongSelf = weakSelf; if (strongSelf) { strongSelf->_targetToUploader[@(target)] = backend; } }); } -- (void)registerPrioritizer:(id)prioritizer target:(GDTTarget)target { - __weak GDTRegistrar *weakSelf = self; +- (void)registerPrioritizer:(id)prioritizer target:(GDTCORTarget)target { + __weak GDTCORRegistrar *weakSelf = self; dispatch_barrier_async(_registrarQueue, ^{ - GDTRegistrar *strongSelf = weakSelf; + GDTCORRegistrar *strongSelf = weakSelf; if (strongSelf) { strongSelf->_targetToPrioritizer[@(target)] = prioritizer; } }); } -- (NSMutableDictionary> *)targetToUploader { - __block NSMutableDictionary> *targetToUploader; - __weak GDTRegistrar *weakSelf = self; +- (NSMutableDictionary> *)targetToUploader { + __block NSMutableDictionary> *targetToUploader; + __weak GDTCORRegistrar *weakSelf = self; dispatch_sync(_registrarQueue, ^{ - GDTRegistrar *strongSelf = weakSelf; + GDTCORRegistrar *strongSelf = weakSelf; if (strongSelf) { targetToUploader = strongSelf->_targetToUploader; } @@ -77,11 +78,11 @@ - (void)registerPrioritizer:(id)prioritizer target:(GDTTarget)ta return targetToUploader; } -- (NSMutableDictionary> *)targetToPrioritizer { - __block NSMutableDictionary> *targetToPrioritizer; - __weak GDTRegistrar *weakSelf = self; +- (NSMutableDictionary> *)targetToPrioritizer { + __block NSMutableDictionary> *targetToPrioritizer; + __weak GDTCORRegistrar *weakSelf = self; dispatch_sync(_registrarQueue, ^{ - GDTRegistrar *strongSelf = weakSelf; + GDTCORRegistrar *strongSelf = weakSelf; if (strongSelf) { targetToPrioritizer = strongSelf->_targetToPrioritizer; } @@ -89,16 +90,16 @@ - (void)registerPrioritizer:(id)prioritizer target:(GDTTarget)ta return targetToPrioritizer; } -#pragma mark - GDTLifecycleProtocol +#pragma mark - GDTCORLifecycleProtocol -- (void)appWillBackground:(nonnull GDTApplication *)app { +- (void)appWillBackground:(nonnull GDTCORApplication *)app { dispatch_async(_registrarQueue, ^{ - for (id uploader in [self->_targetToUploader allValues]) { + for (id uploader in [self->_targetToUploader allValues]) { if ([uploader respondsToSelector:@selector(appWillBackground:)]) { [uploader appWillBackground:app]; } } - for (id prioritizer in [self->_targetToPrioritizer allValues]) { + for (id prioritizer in [self->_targetToPrioritizer allValues]) { if ([prioritizer respondsToSelector:@selector(appWillBackground:)]) { [prioritizer appWillBackground:app]; } @@ -106,14 +107,14 @@ - (void)appWillBackground:(nonnull GDTApplication *)app { }); } -- (void)appWillForeground:(nonnull GDTApplication *)app { +- (void)appWillForeground:(nonnull GDTCORApplication *)app { dispatch_async(_registrarQueue, ^{ - for (id uploader in [self->_targetToUploader allValues]) { + for (id uploader in [self->_targetToUploader allValues]) { if ([uploader respondsToSelector:@selector(appWillForeground:)]) { [uploader appWillForeground:app]; } } - for (id prioritizer in [self->_targetToPrioritizer allValues]) { + for (id prioritizer in [self->_targetToPrioritizer allValues]) { if ([prioritizer respondsToSelector:@selector(appWillForeground:)]) { [prioritizer appWillForeground:app]; } @@ -121,14 +122,14 @@ - (void)appWillForeground:(nonnull GDTApplication *)app { }); } -- (void)appWillTerminate:(nonnull GDTApplication *)app { +- (void)appWillTerminate:(nonnull GDTCORApplication *)app { dispatch_sync(_registrarQueue, ^{ - for (id uploader in [self->_targetToUploader allValues]) { + for (id uploader in [self->_targetToUploader allValues]) { if ([uploader respondsToSelector:@selector(appWillTerminate:)]) { [uploader appWillTerminate:app]; } } - for (id prioritizer in [self->_targetToPrioritizer allValues]) { + for (id prioritizer in [self->_targetToPrioritizer allValues]) { if ([prioritizer respondsToSelector:@selector(appWillTerminate:)]) { [prioritizer appWillTerminate:app]; } diff --git a/GoogleDataTransport/GDTLibrary/GDTStorage.m b/GoogleDataTransport/GDTCORLibrary/GDTCORStorage.m similarity index 50% rename from GoogleDataTransport/GDTLibrary/GDTStorage.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORStorage.m index 89931134796..a8102cdcd30 100644 --- a/GoogleDataTransport/GDTLibrary/GDTStorage.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORStorage.m @@ -14,24 +14,24 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTStorage.h" -#import "GDTLibrary/Private/GDTStorage_Private.h" +#import "GDTCORLibrary/Private/GDTCORStorage.h" +#import "GDTCORLibrary/Private/GDTCORStorage_Private.h" -#import -#import -#import -#import +#import +#import +#import +#import +#import -#import "GDTLibrary/Private/GDTAssert.h" -#import "GDTLibrary/Private/GDTEvent_Private.h" -#import "GDTLibrary/Private/GDTRegistrar_Private.h" -#import "GDTLibrary/Private/GDTUploadCoordinator.h" +#import "GDTCORLibrary/Private/GDTCOREvent_Private.h" +#import "GDTCORLibrary/Private/GDTCORRegistrar_Private.h" +#import "GDTCORLibrary/Private/GDTCORUploadCoordinator.h" /** Creates and/or returns a singleton NSString that is the shared storage path. * * @return The SDK event storage path. */ -static NSString *GDTStoragePath() { +static NSString *GDTCORStoragePath() { static NSString *storagePath; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -42,22 +42,22 @@ return storagePath; } -@implementation GDTStorage +@implementation GDTCORStorage + (NSString *)archivePath { static NSString *archivePath; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - archivePath = [GDTStoragePath() stringByAppendingPathComponent:@"GDTStorageArchive"]; + archivePath = [GDTCORStoragePath() stringByAppendingPathComponent:@"GDTCORStorageArchive"]; }); return archivePath; } + (instancetype)sharedInstance { - static GDTStorage *sharedStorage; + static GDTCORStorage *sharedStorage; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - sharedStorage = [[GDTStorage alloc] init]; + sharedStorage = [[GDTCORStorage alloc] init]; }); return sharedStorage; } @@ -65,21 +65,28 @@ + (instancetype)sharedInstance { - (instancetype)init { self = [super init]; if (self) { - _storageQueue = dispatch_queue_create("com.google.GDTStorage", DISPATCH_QUEUE_SERIAL); + _storageQueue = dispatch_queue_create("com.google.GDTCORStorage", DISPATCH_QUEUE_SERIAL); _targetToEventSet = [[NSMutableDictionary alloc] init]; _storedEvents = [[NSMutableOrderedSet alloc] init]; - _uploadCoordinator = [GDTUploadCoordinator sharedInstance]; + _uploadCoordinator = [GDTCORUploadCoordinator sharedInstance]; } return self; } -- (void)storeEvent:(GDTEvent *)event { +- (void)storeEvent:(GDTCOREvent *)event { + if (event == nil) { + return; + } + [self createEventDirectoryIfNotExists]; - __block GDTBackgroundIdentifier bgID = GDTBackgroundIdentifierInvalid; + __block GDTCORBackgroundIdentifier bgID = GDTCORBackgroundIdentifierInvalid; if (_runningInBackground) { - bgID = [[GDTApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ - [[GDTApplication sharedApplication] endBackgroundTask:bgID]; + bgID = [[GDTCORApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; + } }]; } @@ -88,15 +95,16 @@ - (void)storeEvent:(GDTEvent *)event { NSInteger target = event.target; // Check that a prioritizer is available for this target. - id prioritizer = [GDTRegistrar sharedInstance].targetToPrioritizer[@(target)]; - GDTAssert(prioritizer, @"There's no prioritizer registered for the given target."); + id prioritizer = + [GDTCORRegistrar sharedInstance].targetToPrioritizer[@(target)]; + GDTCORAssert(prioritizer, @"There's no prioritizer registered for the given target."); // Write the transport bytes to disk, get a filename. - GDTAssert(event.dataObjectTransportBytes, @"The event should have been serialized to bytes"); + GDTCORAssert(event.dataObjectTransportBytes, @"The event should have been serialized to bytes"); NSURL *eventFile = [self saveEventBytesToDisk:event.dataObjectTransportBytes eventHash:event.hash]; - GDTDataFuture *dataFuture = [[GDTDataFuture alloc] initWithFileURL:eventFile]; - GDTStoredEvent *storedEvent = [event storedEventWithDataFuture:dataFuture]; + GDTCORDataFuture *dataFuture = [[GDTCORDataFuture alloc] initWithFileURL:eventFile]; + GDTCORStoredEvent *storedEvent = [event storedEventWithDataFuture:dataFuture]; // Add event to tracking collections. [self addEventToTrackingCollections:storedEvent]; @@ -105,37 +113,43 @@ - (void)storeEvent:(GDTEvent *)event { [prioritizer prioritizeEvent:storedEvent]; // Check the QoS, if it's high priority, notify the target that it has a high priority event. - if (event.qosTier == GDTEventQoSFast) { + if (event.qosTier == GDTCOREventQoSFast) { [self.uploadCoordinator forceUploadForTarget:target]; } - // If running in the background, save state to disk and end the associated background task. - if (bgID != GDTBackgroundIdentifierInvalid) { + // Write state to disk. + if (self->_runningInBackground) { if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { + NSError *error; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self requiringSecureCoding:YES - error:nil]; - [data writeToFile:[GDTStorage archivePath] atomically:YES]; + error:&error]; + [data writeToFile:[GDTCORStorage archivePath] atomically:YES]; } else { #if !defined(TARGET_OS_MACCATALYST) - [NSKeyedArchiver archiveRootObject:self toFile:[GDTStorage archivePath]]; + [NSKeyedArchiver archiveRootObject:self toFile:[GDTCORStorage archivePath]]; #endif } - [[GDTApplication sharedApplication] endBackgroundTask:bgID]; + } + + // If running in the background, save state to disk and end the associated background task. + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; } }); } -- (void)removeEvents:(NSSet *)events { - NSSet *eventsToRemove = [events copy]; +- (void)removeEvents:(NSSet *)events { + NSSet *eventsToRemove = [events copy]; dispatch_async(_storageQueue, ^{ - for (GDTStoredEvent *event in eventsToRemove) { + for (GDTCORStoredEvent *event in eventsToRemove) { // Remove from disk, first and foremost. NSError *error; if (event.dataFuture.fileURL) { NSURL *fileURL = event.dataFuture.fileURL; [[NSFileManager defaultManager] removeItemAtURL:fileURL error:&error]; - GDTAssert(error == nil, @"There was an error removing an event file: %@", error); + GDTCORAssert(error == nil, @"There was an error removing an event file: %@", error); } // Remove from the tracking collections. @@ -150,12 +164,12 @@ - (void)removeEvents:(NSSet *)events { /** Creates the storage directory if it does not exist. */ - (void)createEventDirectoryIfNotExists { NSError *error; - BOOL result = [[NSFileManager defaultManager] createDirectoryAtPath:GDTStoragePath() + BOOL result = [[NSFileManager defaultManager] createDirectoryAtPath:GDTCORStoragePath() withIntermediateDirectories:YES attributes:0 error:&error]; if (!result || error) { - GDTLogError(GDTMCEDirectoryCreationError, @"Error creating the directory: %@", error); + GDTCORLogError(GDTCORMCEDirectoryCreationError, @"Error creating the directory: %@", error); } } @@ -169,16 +183,17 @@ - (void)createEventDirectoryIfNotExists { * @return The filename */ - (NSURL *)saveEventBytesToDisk:(NSData *)transportBytes eventHash:(NSUInteger)eventHash { - NSString *storagePath = GDTStoragePath(); + NSString *storagePath = GDTCORStoragePath(); NSString *event = [NSString stringWithFormat:@"event-%lu", (unsigned long)eventHash]; NSURL *eventFilePath = [NSURL fileURLWithPath:[storagePath stringByAppendingPathComponent:event]]; - GDTAssert(![[NSFileManager defaultManager] fileExistsAtPath:eventFilePath.path], - @"An event shouldn't already exist at this path: %@", eventFilePath.path); + GDTCORAssert(![[NSFileManager defaultManager] fileExistsAtPath:eventFilePath.path], + @"An event shouldn't already exist at this path: %@", eventFilePath.path); BOOL writingSuccess = [transportBytes writeToURL:eventFilePath atomically:YES]; if (!writingSuccess) { - GDTLogError(GDTMCEFileWriteError, @"An event file could not be written: %@", eventFilePath); + GDTCORLogError(GDTCORMCEFileWriteError, @"An event file could not be written: %@", + eventFilePath); } return eventFilePath; @@ -191,58 +206,69 @@ - (NSURL *)saveEventBytesToDisk:(NSData *)transportBytes eventHash:(NSUInteger)e * * @param event The event to track. */ -- (void)addEventToTrackingCollections:(GDTStoredEvent *)event { +- (void)addEventToTrackingCollections:(GDTCORStoredEvent *)event { [_storedEvents addObject:event]; - NSMutableSet *events = self.targetToEventSet[event.target]; + NSMutableSet *events = self.targetToEventSet[event.target]; events = events ? events : [[NSMutableSet alloc] init]; [events addObject:event]; _targetToEventSet[event.target] = events; } -#pragma mark - GDTLifecycleProtocol +#pragma mark - GDTCORLifecycleProtocol -- (void)appWillForeground:(GDTApplication *)app { +- (void)appWillForeground:(GDTCORApplication *)app { if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { - NSData *data = [NSData dataWithContentsOfFile:[GDTStorage archivePath]]; - [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTStorage class] fromData:data error:nil]; + NSError *error; + NSData *data = [NSData dataWithContentsOfFile:[GDTCORStorage archivePath]]; + [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTCORStorage class] fromData:data error:&error]; } else { #if !defined(TARGET_OS_MACCATALYST) - [NSKeyedUnarchiver unarchiveObjectWithFile:[GDTStorage archivePath]]; + [NSKeyedUnarchiver unarchiveObjectWithFile:[GDTCORStorage archivePath]]; #endif } - self->_runningInBackground = NO; } -- (void)appWillBackground:(GDTApplication *)app { +- (void)appWillBackground:(GDTCORApplication *)app { self->_runningInBackground = YES; - if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { - NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self - requiringSecureCoding:YES - error:nil]; - [data writeToFile:[GDTStorage archivePath] atomically:YES]; - } else { + dispatch_async(_storageQueue, ^{ + if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { + NSError *error; + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self + requiringSecureCoding:YES + error:&error]; + [data writeToFile:[GDTCORStorage archivePath] atomically:YES]; + } else { #if !defined(TARGET_OS_MACCATALYST) - [NSKeyedArchiver archiveRootObject:self toFile:[GDTStorage archivePath]]; + [NSKeyedArchiver archiveRootObject:self toFile:[GDTCORStorage archivePath]]; #endif - } + } + }); + // Create an immediate background task to run until the end of the current queue of work. - __block GDTBackgroundIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ - [app endBackgroundTask:bgID]; + __block GDTCORBackgroundIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [app endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; + } }]; dispatch_async(_storageQueue, ^{ - [app endBackgroundTask:bgID]; + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [app endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; + } }); } -- (void)appWillTerminate:(GDTApplication *)application { +- (void)appWillTerminate:(GDTCORApplication *)application { if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { + NSError *error; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self requiringSecureCoding:YES - error:nil]; - [data writeToFile:[GDTStorage archivePath] atomically:YES]; + error:&error]; + [data writeToFile:[GDTCORStorage archivePath] atomically:YES]; } else { #if !defined(TARGET_OS_MACCATALYST) - [NSKeyedArchiver archiveRootObject:self toFile:[GDTStorage archivePath]]; + [NSKeyedArchiver archiveRootObject:self toFile:[GDTCORStorage archivePath]]; #endif } } @@ -250,13 +276,13 @@ - (void)appWillTerminate:(GDTApplication *)application { #pragma mark - NSSecureCoding /** The NSKeyedCoder key for the storedEvents property. */ -static NSString *const kGDTStorageStoredEventsKey = @"GDTStorageStoredEventsKey"; +static NSString *const kGDTCORStorageStoredEventsKey = @"GDTCORStorageStoredEventsKey"; /** The NSKeyedCoder key for the targetToEventSet property. */ -static NSString *const kGDTStorageTargetToEventSetKey = @"GDTStorageTargetToEventSetKey"; +static NSString *const kGDTCORStorageTargetToEventSetKey = @"GDTCORStorageTargetToEventSetKey"; /** The NSKeyedCoder key for the uploadCoordinator property. */ -static NSString *const kGDTStorageUploadCoordinatorKey = @"GDTStorageUploadCoordinatorKey"; +static NSString *const kGDTCORStorageUploadCoordinatorKey = @"GDTCORStorageUploadCoordinatorKey"; + (BOOL)supportsSecureCoding { return YES; @@ -264,44 +290,38 @@ + (BOOL)supportsSecureCoding { - (instancetype)initWithCoder:(NSCoder *)aDecoder { // Create the singleton and populate its ivars. - GDTStorage *sharedInstance = [self.class sharedInstance]; + GDTCORStorage *sharedInstance = [self.class sharedInstance]; dispatch_sync(sharedInstance.storageQueue, ^{ NSSet *classes = - [NSSet setWithObjects:[NSMutableOrderedSet class], [GDTStoredEvent class], nil]; + [NSSet setWithObjects:[NSMutableOrderedSet class], [GDTCORStoredEvent class], nil]; sharedInstance->_storedEvents = [aDecoder decodeObjectOfClasses:classes - forKey:kGDTStorageStoredEventsKey]; + forKey:kGDTCORStorageStoredEventsKey]; classes = [NSSet setWithObjects:[NSMutableDictionary class], [NSMutableSet class], - [GDTStoredEvent class], nil]; + [GDTCORStoredEvent class], nil]; sharedInstance->_targetToEventSet = - [aDecoder decodeObjectOfClasses:classes forKey:kGDTStorageTargetToEventSetKey]; + [aDecoder decodeObjectOfClasses:classes forKey:kGDTCORStorageTargetToEventSetKey]; sharedInstance->_uploadCoordinator = - [aDecoder decodeObjectOfClass:[GDTUploadCoordinator class] - forKey:kGDTStorageUploadCoordinatorKey]; + [aDecoder decodeObjectOfClass:[GDTCORUploadCoordinator class] + forKey:kGDTCORStorageUploadCoordinatorKey]; }); return sharedInstance; } - (void)encodeWithCoder:(NSCoder *)aCoder { - GDTStorage *sharedInstance = [self.class sharedInstance]; - dispatch_queue_t storageQueue = sharedInstance.storageQueue; - if (!storageQueue) { - return; + GDTCORStorage *sharedInstance = [self.class sharedInstance]; + NSMutableOrderedSet *storedEvents = sharedInstance->_storedEvents; + if (storedEvents) { + [aCoder encodeObject:storedEvents forKey:kGDTCORStorageStoredEventsKey]; + } + NSMutableDictionary *> *targetToEventSet = + sharedInstance->_targetToEventSet; + if (targetToEventSet) { + [aCoder encodeObject:targetToEventSet forKey:kGDTCORStorageTargetToEventSetKey]; + } + GDTCORUploadCoordinator *uploadCoordinator = sharedInstance->_uploadCoordinator; + if (uploadCoordinator) { + [aCoder encodeObject:uploadCoordinator forKey:kGDTCORStorageUploadCoordinatorKey]; } - dispatch_sync(storageQueue, ^{ - NSMutableOrderedSet *storedEvents = sharedInstance->_storedEvents; - if (storedEvents) { - [aCoder encodeObject:storedEvents forKey:kGDTStorageStoredEventsKey]; - } - NSMutableDictionary *> *targetToEventSet = - sharedInstance->_targetToEventSet; - if (targetToEventSet) { - [aCoder encodeObject:targetToEventSet forKey:kGDTStorageTargetToEventSetKey]; - } - GDTUploadCoordinator *uploadCoordinator = sharedInstance->_uploadCoordinator; - if (uploadCoordinator) { - [aCoder encodeObject:uploadCoordinator forKey:kGDTStorageUploadCoordinatorKey]; - } - }); } @end diff --git a/GoogleDataTransport/GDTLibrary/GDTStoredEvent.m b/GoogleDataTransport/GDTCORLibrary/GDTCORStoredEvent.m similarity index 71% rename from GoogleDataTransport/GDTLibrary/GDTStoredEvent.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORStoredEvent.m index 6217f139a41..a2433947204 100644 --- a/GoogleDataTransport/GDTLibrary/GDTStoredEvent.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORStoredEvent.m @@ -14,15 +14,16 @@ * limitations under the License. */ -#import +#import -#import +#import -#import "GDTLibrary/Private/GDTStorage_Private.h" +#import "GDTCORLibrary/Private/GDTCORStorage_Private.h" -@implementation GDTStoredEvent +@implementation GDTCORStoredEvent -- (instancetype)initWithEvent:(GDTEvent *)event dataFuture:(nonnull GDTDataFuture *)dataFuture { +- (instancetype)initWithEvent:(GDTCOREvent *)event + dataFuture:(nonnull GDTCORDataFuture *)dataFuture { self = [super init]; if (self) { _dataFuture = dataFuture; @@ -38,22 +39,22 @@ - (instancetype)initWithEvent:(GDTEvent *)event dataFuture:(nonnull GDTDataFutur #pragma mark - NSSecureCoding /** Coding key for the dataFuture ivar. */ -static NSString *kDataFutureKey = @"GDTStoredEventDataFutureKey"; +static NSString *kDataFutureKey = @"GDTCORStoredEventDataFutureKey"; /** Coding key for mappingID ivar. */ -static NSString *kMappingIDKey = @"GDTStoredEventMappingIDKey"; +static NSString *kMappingIDKey = @"GDTCORStoredEventMappingIDKey"; /** Coding key for target ivar. */ -static NSString *kTargetKey = @"GDTStoredEventTargetKey"; +static NSString *kTargetKey = @"GDTCORStoredEventTargetKey"; /** Coding key for qosTier ivar. */ -static NSString *kQosTierKey = @"GDTStoredEventQosTierKey"; +static NSString *kQosTierKey = @"GDTCORStoredEventQosTierKey"; /** Coding key for clockSnapshot ivar. */ -static NSString *kClockSnapshotKey = @"GDTStoredEventClockSnapshotKey"; +static NSString *kClockSnapshotKey = @"GDTCORStoredEventClockSnapshotKey"; /** Coding key for customPrioritizationParams ivar. */ -static NSString *kCustomPrioritizationParamsKey = @"GDTStoredEventcustomPrioritizationParamsKey"; +static NSString *kCustomPrioritizationParamsKey = @"GDTCORStoredEventcustomPrioritizationParamsKey"; + (BOOL)supportsSecureCoding { return YES; @@ -71,19 +72,19 @@ - (void)encodeWithCoder:(nonnull NSCoder *)aCoder { - (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder { self = [self init]; if (self) { - _dataFuture = [aDecoder decodeObjectOfClass:[GDTDataFuture class] forKey:kDataFutureKey]; + _dataFuture = [aDecoder decodeObjectOfClass:[GDTCORDataFuture class] forKey:kDataFutureKey]; _mappingID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kMappingIDKey]; _target = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:kTargetKey]; NSNumber *qosTier = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:kQosTierKey]; _qosTier = [qosTier intValue]; - _clockSnapshot = [aDecoder decodeObjectOfClass:[GDTClock class] forKey:kClockSnapshotKey]; + _clockSnapshot = [aDecoder decodeObjectOfClass:[GDTCORClock class] forKey:kClockSnapshotKey]; _customPrioritizationParams = [aDecoder decodeObjectOfClass:[NSDictionary class] forKey:kCustomPrioritizationParamsKey]; } return self; } -- (BOOL)isEqual:(GDTStoredEvent *)other { +- (BOOL)isEqual:(GDTCORStoredEvent *)other { return [self hash] == [other hash]; } diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORTransformer.m b/GoogleDataTransport/GDTCORLibrary/GDTCORTransformer.m new file mode 100644 index 00000000000..0a5277f2a28 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORTransformer.m @@ -0,0 +1,113 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GDTCORLibrary/Private/GDTCORTransformer.h" +#import "GDTCORLibrary/Private/GDTCORTransformer_Private.h" + +#import +#import +#import +#import + +#import "GDTCORLibrary/Private/GDTCORStorage.h" + +@implementation GDTCORTransformer + ++ (instancetype)sharedInstance { + static GDTCORTransformer *eventTransformer; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + eventTransformer = [[self alloc] init]; + }); + return eventTransformer; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _eventWritingQueue = + dispatch_queue_create("com.google.GDTCORTransformer", DISPATCH_QUEUE_SERIAL); + _storageInstance = [GDTCORStorage sharedInstance]; + } + return self; +} + +- (void)transformEvent:(GDTCOREvent *)event + withTransformers:(NSArray> *)transformers { + GDTCORAssert(event, @"You can't write a nil event"); + + __block GDTCORBackgroundIdentifier bgID = GDTCORBackgroundIdentifierInvalid; + if (_runningInBackground) { + bgID = [[GDTCORApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; + } + }]; + } + dispatch_async(_eventWritingQueue, ^{ + GDTCOREvent *transformedEvent = event; + for (id transformer in transformers) { + if ([transformer respondsToSelector:@selector(transform:)]) { + transformedEvent = [transformer transform:transformedEvent]; + if (!transformedEvent) { + return; + } + } else { + GDTCORLogError(GDTCORMCETransformerDoesntImplementTransform, + @"Transformer doesn't implement transform: %@", transformer); + return; + } + } + [self.storageInstance storeEvent:transformedEvent]; + if (self->_runningInBackground) { + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; + } + }); +} + +#pragma mark - GDTCORLifecycleProtocol + +- (void)appWillForeground:(GDTCORApplication *)app { + dispatch_async(_eventWritingQueue, ^{ + self->_runningInBackground = NO; + }); +} + +- (void)appWillBackground:(GDTCORApplication *)app { + // Create an immediate background task to run until the end of the current queue of work. + __block GDTCORBackgroundIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [app endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; + } + }]; + dispatch_async(_eventWritingQueue, ^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [app endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; + } + }); +} + +- (void)appWillTerminate:(GDTCORApplication *)application { + // Flush the queue immediately. + dispatch_sync(_eventWritingQueue, ^{ + }); +} + +@end diff --git a/GoogleDataTransport/GDTCORLibrary/GDTCORTransport.m b/GoogleDataTransport/GDTCORLibrary/GDTCORTransport.m new file mode 100644 index 00000000000..d9d9995637c --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORTransport.m @@ -0,0 +1,69 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import "GDTCORLibrary/Private/GDTCORTransport_Private.h" + +#import +#import +#import + +#import "GDTCORLibrary/Private/GDTCORTransformer.h" + +@implementation GDTCORTransport + +- (nullable instancetype)initWithMappingID:(NSString *)mappingID + transformers: + (nullable NSArray> *)transformers + target:(NSInteger)target { + GDTCORAssert(mappingID.length > 0, @"A mapping ID cannot be nil or empty"); + GDTCORAssert(target > 0, @"A target cannot be negative or 0"); + if (mappingID == nil || mappingID.length == 0 || target <= 0) { + return nil; + } + self = [super init]; + if (self) { + _mappingID = mappingID; + _transformers = transformers; + _target = target; + _transformerInstance = [GDTCORTransformer sharedInstance]; + } + return self; +} + +- (void)sendTelemetryEvent:(GDTCOREvent *)event { + // TODO: Determine if sending an event before registration is allowed. + GDTCORAssert(event, @"You can't send a nil event"); + GDTCOREvent *copiedEvent = [event copy]; + copiedEvent.qosTier = GDTCOREventQoSTelemetry; + copiedEvent.clockSnapshot = [GDTCORClock snapshot]; + [self.transformerInstance transformEvent:copiedEvent withTransformers:_transformers]; +} + +- (void)sendDataEvent:(GDTCOREvent *)event { + // TODO: Determine if sending an event before registration is allowed. + GDTCORAssert(event, @"You can't send a nil event"); + GDTCORAssert(event.qosTier != GDTCOREventQoSTelemetry, @"Use -sendTelemetryEvent, please."); + GDTCOREvent *copiedEvent = [event copy]; + copiedEvent.clockSnapshot = [GDTCORClock snapshot]; + [self.transformerInstance transformEvent:copiedEvent withTransformers:_transformers]; +} + +- (GDTCOREvent *)eventForTransport { + return [[GDTCOREvent alloc] initWithMappingID:_mappingID target:_target]; +} + +@end diff --git a/GoogleDataTransport/GDTLibrary/GDTUploadCoordinator.m b/GoogleDataTransport/GDTCORLibrary/GDTCORUploadCoordinator.m similarity index 59% rename from GoogleDataTransport/GDTLibrary/GDTUploadCoordinator.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORUploadCoordinator.m index 8f3df2dda72..45ec3c2ea2c 100644 --- a/GoogleDataTransport/GDTLibrary/GDTUploadCoordinator.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORUploadCoordinator.m @@ -14,23 +14,23 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTUploadCoordinator.h" +#import "GDTCORLibrary/Private/GDTCORUploadCoordinator.h" -#import -#import +#import +#import +#import -#import "GDTLibrary/Private/GDTAssert.h" -#import "GDTLibrary/Private/GDTReachability.h" -#import "GDTLibrary/Private/GDTRegistrar_Private.h" -#import "GDTLibrary/Private/GDTStorage.h" +#import "GDTCORLibrary/Private/GDTCORReachability.h" +#import "GDTCORLibrary/Private/GDTCORRegistrar_Private.h" +#import "GDTCORLibrary/Private/GDTCORStorage.h" -@implementation GDTUploadCoordinator +@implementation GDTCORUploadCoordinator + (instancetype)sharedInstance { - static GDTUploadCoordinator *sharedUploader; + static GDTCORUploadCoordinator *sharedUploader; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - sharedUploader = [[GDTUploadCoordinator alloc] init]; + sharedUploader = [[GDTCORUploadCoordinator alloc] init]; [sharedUploader startTimer]; }); return sharedUploader; @@ -40,8 +40,8 @@ - (instancetype)init { self = [super init]; if (self) { _coordinationQueue = - dispatch_queue_create("com.google.GDTUploadCoordinator", DISPATCH_QUEUE_SERIAL); - _registrar = [GDTRegistrar sharedInstance]; + dispatch_queue_create("com.google.GDTCORUploadCoordinator", DISPATCH_QUEUE_SERIAL); + _registrar = [GDTCORRegistrar sharedInstance]; _timerInterval = 30 * NSEC_PER_SEC; _timerLeeway = 5 * NSEC_PER_SEC; _targetToInFlightPackages = [[NSMutableDictionary alloc] init]; @@ -49,21 +49,21 @@ - (instancetype)init { return self; } -- (void)forceUploadForTarget:(GDTTarget)target { +- (void)forceUploadForTarget:(GDTCORTarget)target { dispatch_async(_coordinationQueue, ^{ - GDTUploadConditions conditions = [self uploadConditions]; - conditions |= GDTUploadConditionHighPriority; + GDTCORUploadConditions conditions = [self uploadConditions]; + conditions |= GDTCORUploadConditionHighPriority; [self uploadTargets:@[ @(target) ] conditions:conditions]; }); } #pragma mark - Property overrides -// GDTStorage and GDTUploadCoordinator +sharedInstance methods call each other, so this breaks +// GDTCORStorage and GDTCORUploadCoordinator +sharedInstance methods call each other, so this breaks // the loop. -- (GDTStorage *)storage { +- (GDTCORStorage *)storage { if (!_storage) { - _storage = [GDTStorage sharedInstance]; + _storage = [GDTCORStorage sharedInstance]; } return _storage; } @@ -81,7 +81,7 @@ - (void)startTimer { self->_timerLeeway); dispatch_source_set_event_handler(self->_timer, ^{ if (!self->_runningInBackground) { - GDTUploadConditions conditions = [self uploadConditions]; + GDTCORUploadConditions conditions = [self uploadConditions]; [self uploadTargets:[self.registrar.targetToUploader allKeys] conditions:conditions]; } }); @@ -101,9 +101,9 @@ - (void)stopTimer { * @param targets An array of targets to trigger. * @param conditions The set of upload conditions. */ -- (void)uploadTargets:(NSArray *)targets conditions:(GDTUploadConditions)conditions { +- (void)uploadTargets:(NSArray *)targets conditions:(GDTCORUploadConditions)conditions { dispatch_async(_coordinationQueue, ^{ - if ((conditions & GDTUploadConditionNoNetwork) == GDTUploadConditionNoNetwork) { + if ((conditions & GDTCORUploadConditionNoNetwork) == GDTCORUploadConditionNoNetwork) { return; } for (NSNumber *target in targets) { @@ -112,10 +112,10 @@ - (void)uploadTargets:(NSArray *)targets conditions:(GDTUploadCondit continue; } // Ask the uploader if they can upload and do so, if it can. - id uploader = self.registrar.targetToUploader[target]; + id uploader = self.registrar.targetToUploader[target]; if ([uploader readyToUploadWithConditions:conditions]) { - id prioritizer = self.registrar.targetToPrioritizer[target]; - GDTUploadPackage *package = [prioritizer uploadPackageWithConditions:conditions]; + id prioritizer = self.registrar.targetToPrioritizer[target]; + GDTCORUploadPackage *package = [prioritizer uploadPackageWithConditions:conditions]; if (package.events.count) { self->_targetToInFlightPackages[target] = package; [uploader uploadPackage:package]; @@ -131,8 +131,8 @@ - (void)uploadTargets:(NSArray *)targets conditions:(GDTUploadCondit * * @return The current upload conditions. */ -- (GDTUploadConditions)uploadConditions { - SCNetworkReachabilityFlags currentFlags = [GDTReachability currentFlags]; +- (GDTCORUploadConditions)uploadConditions { + SCNetworkReachabilityFlags currentFlags = [GDTCORReachability currentFlags]; BOOL reachable = (currentFlags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable; BOOL connectionRequired = (currentFlags & kSCNetworkReachabilityFlagsConnectionRequired) == @@ -140,14 +140,14 @@ - (GDTUploadConditions)uploadConditions { BOOL networkConnected = reachable && !connectionRequired; if (!networkConnected) { - return GDTUploadConditionNoNetwork; + return GDTCORUploadConditionNoNetwork; } - BOOL isWWAN = GDTReachabilityFlagsContainWWAN(currentFlags); + BOOL isWWAN = GDTCORReachabilityFlagsContainWWAN(currentFlags); if (isWWAN) { - return GDTUploadConditionMobileData; + return GDTCORUploadConditionMobileData; } else { - return GDTUploadConditionWifiData; + return GDTCORUploadConditionWifiData; } } @@ -155,35 +155,42 @@ - (GDTUploadConditions)uploadConditions { /** The NSKeyedCoder key for the targetToInFlightPackages property. */ static NSString *const ktargetToInFlightPackagesKey = - @"GDTUploadCoordinatortargetToInFlightPackages"; + @"GDTCORUploadCoordinatortargetToInFlightPackages"; + (BOOL)supportsSecureCoding { return YES; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { - GDTUploadCoordinator *sharedCoordinator = [GDTUploadCoordinator sharedInstance]; - sharedCoordinator->_targetToInFlightPackages = - [aDecoder decodeObjectOfClass:[NSMutableDictionary class] - forKey:ktargetToInFlightPackagesKey]; + GDTCORUploadCoordinator *sharedCoordinator = [GDTCORUploadCoordinator sharedInstance]; + @try { + sharedCoordinator->_targetToInFlightPackages = + [aDecoder decodeObjectOfClass:[NSMutableDictionary class] + forKey:ktargetToInFlightPackagesKey]; + + } @catch (NSException *exception) { + sharedCoordinator->_targetToInFlightPackages = [NSMutableDictionary dictionary]; + } return sharedCoordinator; } - (void)encodeWithCoder:(NSCoder *)aCoder { // All packages that have been given to uploaders need to be tracked so that their expiration // timers can be called. - [aCoder encodeObject:_targetToInFlightPackages forKey:ktargetToInFlightPackagesKey]; + if (_targetToInFlightPackages.count > 0) { + [aCoder encodeObject:_targetToInFlightPackages forKey:ktargetToInFlightPackagesKey]; + } } -#pragma mark - GDTLifecycleProtocol +#pragma mark - GDTCORLifecycleProtocol -- (void)appWillForeground:(GDTApplication *)app { +- (void)appWillForeground:(GDTCORApplication *)app { // Not entirely thread-safe, but it should be fine. self->_runningInBackground = NO; [self startTimer]; } -- (void)appWillBackground:(GDTApplication *)app { +- (void)appWillBackground:(GDTCORApplication *)app { // Not entirely thread-safe, but it should be fine. self->_runningInBackground = YES; @@ -191,63 +198,71 @@ - (void)appWillBackground:(GDTApplication *)app { [self stopTimer]; // Create an immediate background task to run until the end of the current queue of work. - __block GDTBackgroundIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ - [app endBackgroundTask:bgID]; + __block GDTCORBackgroundIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [app endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; + } }]; dispatch_async(_coordinationQueue, ^{ - [app endBackgroundTask:bgID]; + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [app endBackgroundTask:bgID]; + bgID = GDTCORBackgroundIdentifierInvalid; + } }); } -- (void)appWillTerminate:(GDTApplication *)application { +- (void)appWillTerminate:(GDTCORApplication *)application { dispatch_sync(_coordinationQueue, ^{ [self stopTimer]; }); } -#pragma mark - GDTUploadPackageProtocol +#pragma mark - GDTCORUploadPackageProtocol -- (void)packageDelivered:(GDTUploadPackage *)package successful:(BOOL)successful { +- (void)packageDelivered:(GDTCORUploadPackage *)package successful:(BOOL)successful { if (!_coordinationQueue) { return; } dispatch_async(_coordinationQueue, ^{ NSNumber *targetNumber = @(package.target); - NSMutableDictionary *targetToInFlightPackages = + NSMutableDictionary *targetToInFlightPackages = self->_targetToInFlightPackages; - GDTRegistrar *registrar = self->_registrar; + GDTCORRegistrar *registrar = self->_registrar; if (targetToInFlightPackages) { [targetToInFlightPackages removeObjectForKey:targetNumber]; } if (registrar) { - id prioritizer = registrar.targetToPrioritizer[targetNumber]; + id prioritizer = registrar.targetToPrioritizer[targetNumber]; if (!prioritizer) { - GDTLogError(GDTMCEPrioritizerError, - @"A prioritizer should be registered for this target: %@", targetNumber); + GDTCORLogError(GDTCORMCEPrioritizerError, + @"A prioritizer should be registered for this target: %@", targetNumber); } if ([prioritizer respondsToSelector:@selector(packageDelivered:successful:)]) { [prioritizer packageDelivered:package successful:successful]; } } - [self.storage removeEvents:package.events]; + if (package.events != nil) { + [self.storage removeEvents:package.events]; + } }); } -- (void)packageExpired:(GDTUploadPackage *)package { +- (void)packageExpired:(GDTCORUploadPackage *)package { if (!_coordinationQueue) { return; } dispatch_async(_coordinationQueue, ^{ NSNumber *targetNumber = @(package.target); - NSMutableDictionary *targetToInFlightPackages = + NSMutableDictionary *targetToInFlightPackages = self->_targetToInFlightPackages; - GDTRegistrar *registrar = self->_registrar; + GDTCORRegistrar *registrar = self->_registrar; if (targetToInFlightPackages) { [targetToInFlightPackages removeObjectForKey:targetNumber]; } if (registrar) { - id prioritizer = registrar.targetToPrioritizer[targetNumber]; - id uploader = registrar.targetToUploader[targetNumber]; + id prioritizer = registrar.targetToPrioritizer[targetNumber]; + id uploader = registrar.targetToUploader[targetNumber]; if ([prioritizer respondsToSelector:@selector(packageExpired:)]) { [prioritizer packageExpired:package]; } diff --git a/GoogleDataTransport/GDTLibrary/GDTUploadPackage.m b/GoogleDataTransport/GDTCORLibrary/GDTCORUploadPackage.m similarity index 69% rename from GoogleDataTransport/GDTLibrary/GDTUploadPackage.m rename to GoogleDataTransport/GDTCORLibrary/GDTCORUploadPackage.m index 24d3b6e5441..e549718d0ec 100644 --- a/GoogleDataTransport/GDTLibrary/GDTUploadPackage.m +++ b/GoogleDataTransport/GDTCORLibrary/GDTCORUploadPackage.m @@ -14,17 +14,17 @@ * limitations under the License. */ -#import +#import -#import -#import -#import +#import +#import +#import -#import "GDTLibrary/Private/GDTStorage_Private.h" -#import "GDTLibrary/Private/GDTUploadCoordinator.h" -#import "GDTLibrary/Private/GDTUploadPackage_Private.h" +#import "GDTCORLibrary/Private/GDTCORStorage_Private.h" +#import "GDTCORLibrary/Private/GDTCORUploadCoordinator.h" +#import "GDTCORLibrary/Private/GDTCORUploadPackage_Private.h" -@implementation GDTUploadPackage { +@implementation GDTCORUploadPackage { /** If YES, the package's -completeDelivery method has been called. */ BOOL _isDelivered; @@ -35,13 +35,13 @@ @implementation GDTUploadPackage { NSTimer *_expirationTimer; } -- (instancetype)initWithTarget:(GDTTarget)target { +- (instancetype)initWithTarget:(GDTCORTarget)target { self = [super init]; if (self) { _target = target; - _storage = [GDTStorage sharedInstance]; - _deliverByTime = [GDTClock clockSnapshotInTheFuture:180000]; - _handler = [GDTUploadCoordinator sharedInstance]; + _storage = [GDTCORStorage sharedInstance]; + _deliverByTime = [GDTCORClock clockSnapshotInTheFuture:180000]; + _handler = [GDTCORUploadCoordinator sharedInstance]; _expirationTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(checkIfPackageIsExpired:) @@ -52,7 +52,7 @@ - (instancetype)initWithTarget:(GDTTarget)target { } - (instancetype)copy { - GDTUploadPackage *newPackage = [[GDTUploadPackage alloc] initWithTarget:_target]; + GDTCORUploadPackage *newPackage = [[GDTCORUploadPackage alloc] initWithTarget:_target]; newPackage->_events = [_events copy]; return newPackage; } @@ -69,7 +69,7 @@ - (void)dealloc { [_expirationTimer invalidate]; } -- (void)setStorage:(GDTStorage *)storage { +- (void)setStorage:(GDTCORStorage *)storage { if (storage != _storage) { _storage = storage; } @@ -77,8 +77,8 @@ - (void)setStorage:(GDTStorage *)storage { - (void)completeDelivery { if (_isDelivered) { - GDTLogError(GDTMCEDeliverTwice, @"%@", - @"It's an API violation to call -completeDelivery twice."); + GDTCORLogError(GDTCORMCEDeliverTwice, @"%@", + @"It's an API violation to call -completeDelivery twice."); } _isDelivered = YES; if (!_isHandled && _handler && @@ -99,7 +99,7 @@ - (void)retryDeliveryInTheFuture { } - (void)checkIfPackageIsExpired:(NSTimer *)timer { - if ([[GDTClock snapshot] isAfter:_deliverByTime]) { + if ([[GDTCORClock snapshot] isAfter:_deliverByTime]) { if (_handler && [_handler respondsToSelector:@selector(packageExpired:)]) { _isHandled = YES; [_expirationTimer invalidate]; @@ -111,19 +111,19 @@ - (void)checkIfPackageIsExpired:(NSTimer *)timer { #pragma mark - NSSecureCoding /** The keyed archiver key for the events property. */ -static NSString *const kEventsKey = @"GDTUploadPackageEventsKey"; +static NSString *const kEventsKey = @"GDTCORUploadPackageEventsKey"; /** The keyed archiver key for the _isHandled property. */ -static NSString *const kDeliverByTimeKey = @"GDTUploadPackageDeliveryByTimeKey"; +static NSString *const kDeliverByTimeKey = @"GDTCORUploadPackageDeliveryByTimeKey"; /** The keyed archiver key for the _isHandled ivar. */ -static NSString *const kIsHandledKey = @"GDTUploadPackageIsHandledKey"; +static NSString *const kIsHandledKey = @"GDTCORUploadPackageIsHandledKey"; /** The keyed archiver key for the handler property. */ -static NSString *const kHandlerKey = @"GDTUploadPackageHandlerKey"; +static NSString *const kHandlerKey = @"GDTCORUploadPackageHandlerKey"; /** The keyed archiver key for the target property. */ -static NSString *const kTargetKey = @"GDTUploadPackageTargetKey"; +static NSString *const kTargetKey = @"GDTCORUploadPackageTargetKey"; + (BOOL)supportsSecureCoding { return YES; @@ -138,12 +138,12 @@ - (void)encodeWithCoder:(nonnull NSCoder *)aCoder { } - (nullable instancetype)initWithCoder:(nonnull NSCoder *)aDecoder { - GDTTarget target = [aDecoder decodeIntegerForKey:kTargetKey]; + GDTCORTarget target = [aDecoder decodeIntegerForKey:kTargetKey]; self = [self initWithTarget:target]; if (self) { - NSSet *classes = [NSSet setWithObjects:[NSSet class], [GDTStoredEvent class], nil]; + NSSet *classes = [NSSet setWithObjects:[NSSet class], [GDTCORStoredEvent class], nil]; _events = [aDecoder decodeObjectOfClasses:classes forKey:kEventsKey]; - _deliverByTime = [aDecoder decodeObjectOfClass:[GDTClock class] forKey:kDeliverByTimeKey]; + _deliverByTime = [aDecoder decodeObjectOfClass:[GDTCORClock class] forKey:kDeliverByTimeKey]; _isHandled = [aDecoder decodeBoolForKey:kIsHandledKey]; // _handler isn't technically NSSecureCoding, because we don't know the class of this object. // but it gets decoded anyway. diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTEvent_Private.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCOREvent_Private.h similarity index 86% rename from GoogleDataTransport/GDTLibrary/Private/GDTEvent_Private.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCOREvent_Private.h index a7be1ff5b08..f7f8a28b71a 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTEvent_Private.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCOREvent_Private.h @@ -14,13 +14,13 @@ * limitations under the License. */ -#import +#import -#import +#import NS_ASSUME_NONNULL_BEGIN -@interface GDTEvent () +@interface GDTCOREvent () /** The serialized bytes of the event data object. */ @property(nonatomic) NSData *dataObjectTransportBytes; diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTReachability.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORReachability.h similarity index 95% rename from GoogleDataTransport/GDTLibrary/Private/GDTReachability.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORReachability.h index 27f0feb808a..0f58ab31823 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTReachability.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORReachability.h @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN /** This class helps determine upload conditions by determining connectivity. */ -@interface GDTReachability : NSObject +@interface GDTCORReachability : NSObject /** The current set flags indicating network conditions */ + (SCNetworkReachabilityFlags)currentFlags; diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTReachability_Private.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORReachability_Private.h similarity index 91% rename from GoogleDataTransport/GDTLibrary/Private/GDTReachability_Private.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORReachability_Private.h index 1842949acfa..88f64a072ab 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTReachability_Private.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORReachability_Private.h @@ -14,9 +14,9 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTReachability.h" +#import "GDTCORLibrary/Private/GDTCORReachability.h" -@interface GDTReachability () +@interface GDTCORReachability () /** Allows manually setting the flags for testing purposes. */ @property(nonatomic, readwrite) SCNetworkReachabilityFlags flags; diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTRegistrar_Private.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h similarity index 83% rename from GoogleDataTransport/GDTLibrary/Private/GDTRegistrar_Private.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h index 90bdcea1758..074fc1148c2 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTRegistrar_Private.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORRegistrar_Private.h @@ -14,9 +14,9 @@ * limitations under the License. */ -#import +#import -@interface GDTRegistrar () +@interface GDTCORRegistrar () NS_ASSUME_NONNULL_BEGIN @@ -24,11 +24,11 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, readonly) dispatch_queue_t registrarQueue; /** A map of targets to backend implementations. */ -@property(atomic, readonly) NSMutableDictionary> *targetToUploader; +@property(atomic, readonly) NSMutableDictionary> *targetToUploader; /** A map of targets to prioritizer implementations. */ @property(atomic, readonly) - NSMutableDictionary> *targetToPrioritizer; + NSMutableDictionary> *targetToPrioritizer; @end diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTStorage.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorage.h similarity index 78% rename from GoogleDataTransport/GDTLibrary/Private/GDTStorage.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorage.h index 6e3b98b64ae..008b1d93c46 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTStorage.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorage.h @@ -16,15 +16,15 @@ #import -#import +#import -@class GDTEvent; -@class GDTStoredEvent; +@class GDTCOREvent; +@class GDTCORStoredEvent; NS_ASSUME_NONNULL_BEGIN /** Manages the storage of events. This class is thread-safe. */ -@interface GDTStorage : NSObject +@interface GDTCORStorage : NSObject /** Creates and/or returns the storage singleton. * @@ -33,17 +33,17 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)sharedInstance; /** Stores event.dataObjectTransportBytes into a shared on-device folder and tracks the event via - * a GDTStoredEvent instance. + * a GDTCORStoredEvent instance. * * @param event The event to store. */ -- (void)storeEvent:(GDTEvent *)event; +- (void)storeEvent:(GDTCOREvent *)event; /** Removes a set of events from storage specified by their hash. * * @param events The set of stored events to remove. */ -- (void)removeEvents:(NSSet *)events; +- (void)removeEvents:(NSSet *)events; @end diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTStorage_Private.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorage_Private.h similarity index 76% rename from GoogleDataTransport/GDTLibrary/Private/GDTStorage_Private.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorage_Private.h index 7cff7441887..0f3f7fc76ad 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTStorage_Private.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORStorage_Private.h @@ -14,30 +14,30 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTStorage.h" +#import "GDTCORLibrary/Private/GDTCORStorage.h" -@class GDTUploadCoordinator; +@class GDTCORUploadCoordinator; NS_ASSUME_NONNULL_BEGIN -@interface GDTStorage () +@interface GDTCORStorage () /** The queue on which all storage work will occur. */ @property(nonatomic) dispatch_queue_t storageQueue; /** A map of targets to a set of stored events. */ @property(nonatomic) - NSMutableDictionary *> *targetToEventSet; + NSMutableDictionary *> *targetToEventSet; /** All the events that have been stored. */ -@property(readonly, nonatomic) NSMutableOrderedSet *storedEvents; +@property(readonly, nonatomic) NSMutableOrderedSet *storedEvents; /** The upload coordinator instance used by this storage instance. */ -@property(nonatomic) GDTUploadCoordinator *uploadCoordinator; +@property(nonatomic) GDTCORUploadCoordinator *uploadCoordinator; /** If YES, every call to -storeLog results in background task and serializes the singleton to disk. */ -@property(nonatomic, readonly) BOOL runningInBackground; +@property(nonatomic) BOOL runningInBackground; /** Returns the path to the keyed archive of the singleton. This is where the singleton is saved * to disk during certain app lifecycle events. diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTTransformer.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer.h similarity index 84% rename from GoogleDataTransport/GDTLibrary/Private/GDTTransformer.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer.h index 9b1ba3e70e0..9f4ec39cab8 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTTransformer.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer.h @@ -16,11 +16,11 @@ #import -#import +#import -@class GDTEvent; +@class GDTCOREvent; -@protocol GDTEventTransformer; +@protocol GDTCOREventTransformer; NS_ASSUME_NONNULL_BEGIN @@ -30,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN * maintain state (or at least, there's nothing to stop them from doing that) and the same instances * may be used across multiple instances. */ -@interface GDTTransformer : NSObject +@interface GDTCORTransformer : NSObject /** Instantiates or returns the event transformer singleton. * @@ -46,8 +46,8 @@ NS_ASSUME_NONNULL_BEGIN * @param event The event to apply transformers on. * @param transformers The list of transformers to apply. */ -- (void)transformEvent:(GDTEvent *)event - withTransformers:(nullable NSArray> *)transformers; +- (void)transformEvent:(GDTCOREvent *)event + withTransformers:(nullable NSArray> *)transformers; @end diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTTransformer_Private.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer_Private.h similarity index 86% rename from GoogleDataTransport/GDTLibrary/Private/GDTTransformer_Private.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer_Private.h index 5ebfee271f3..c94790ce163 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTTransformer_Private.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransformer_Private.h @@ -14,19 +14,19 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTTransformer.h" +#import "GDTCORLibrary/Private/GDTCORTransformer.h" -@class GDTStorage; +@class GDTCORStorage; NS_ASSUME_NONNULL_BEGIN -@interface GDTTransformer () +@interface GDTCORTransformer () /** The queue on which all work will occur. */ @property(nonatomic) dispatch_queue_t eventWritingQueue; /** The storage instance used to store events. Should only be used to inject a testing fake. */ -@property(nonatomic) GDTStorage *storageInstance; +@property(nonatomic) GDTCORStorage *storageInstance; /** If YES, every call to -transformEvent will result in a background task. */ @property(nonatomic, readonly) BOOL runningInBackground; diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTTransport_Private.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransport_Private.h similarity index 81% rename from GoogleDataTransport/GDTLibrary/Private/GDTTransport_Private.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransport_Private.h index c9e4c907b47..71f73a6f43f 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTTransport_Private.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORTransport_Private.h @@ -14,25 +14,25 @@ * limitations under the License. */ -#import +#import -@class GDTTransformer; +@class GDTCORTransformer; NS_ASSUME_NONNULL_BEGIN -@interface GDTTransport () +@interface GDTCORTransport () /** The mapping identifier that the target backend will use to map the transport bytes to proto. */ @property(nonatomic) NSString *mappingID; /** The transformers that will operate on events sent by this transport. */ -@property(nonatomic) NSArray> *transformers; +@property(nonatomic) NSArray> *transformers; /** The target backend of this transport. */ @property(nonatomic) NSInteger target; /** The transformer instance to used to transform events. Allows injecting a fake during testing. */ -@property(nonatomic) GDTTransformer *transformerInstance; +@property(nonatomic) GDTCORTransformer *transformerInstance; @end diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTUploadCoordinator.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadCoordinator.h similarity index 80% rename from GoogleDataTransport/GDTLibrary/Private/GDTUploadCoordinator.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadCoordinator.h index 30f3c2dc7ad..31364e11a77 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTUploadCoordinator.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadCoordinator.h @@ -16,21 +16,21 @@ #import -#import -#import +#import +#import -#import "GDTLibrary/Private/GDTUploadPackage_Private.h" +#import "GDTCORLibrary/Private/GDTCORUploadPackage_Private.h" -@class GDTClock; -@class GDTStorage; +@class GDTCORClock; +@class GDTCORStorage; NS_ASSUME_NONNULL_BEGIN /** This class connects storage and uploader implementations, providing events to an uploader * and informing the storage what events were successfully uploaded or not. */ -@interface GDTUploadCoordinator - : NSObject +@interface GDTCORUploadCoordinator + : NSObject /** The queue on which all upload coordination will occur. Also used by a dispatch timer. */ /** Creates and/or returrns the singleton. @@ -51,13 +51,13 @@ NS_ASSUME_NONNULL_BEGIN /** The map of targets to in-flight packages. */ @property(nonatomic, readonly) - NSMutableDictionary *targetToInFlightPackages; + NSMutableDictionary *targetToInFlightPackages; /** The storage object the coordinator will use. Generally used for testing. */ -@property(nonatomic) GDTStorage *storage; +@property(nonatomic) GDTCORStorage *storage; /** The registrar object the coordinator will use. Generally used for testing. */ -@property(nonatomic) GDTRegistrar *registrar; +@property(nonatomic) GDTCORRegistrar *registrar; /** If YES, completion and other operations will result in serializing the singleton to disk. */ @property(nonatomic, readonly) BOOL runningInBackground; @@ -67,7 +67,7 @@ NS_ASSUME_NONNULL_BEGIN * * @param target The target that should force an upload. */ -- (void)forceUploadForTarget:(GDTTarget)target; +- (void)forceUploadForTarget:(GDTCORTarget)target; /** Starts the upload timer. */ - (void)startTimer; diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTUploadPackage_Private.h b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadPackage_Private.h similarity index 76% rename from GoogleDataTransport/GDTLibrary/Private/GDTUploadPackage_Private.h rename to GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadPackage_Private.h index ae174f3f2fa..1eb58d4c7a3 100644 --- a/GoogleDataTransport/GDTLibrary/Private/GDTUploadPackage_Private.h +++ b/GoogleDataTransport/GDTCORLibrary/Private/GDTCORUploadPackage_Private.h @@ -14,16 +14,16 @@ * limitations under the License. */ -#import +#import -@class GDTStorage; +@class GDTCORStorage; -@interface GDTUploadPackage () +@interface GDTCORUploadPackage () /** The storage object this upload package will use to resolve event hashes to files. */ -@property(nonatomic) GDTStorage *storage; +@property(nonatomic) GDTCORStorage *storage; /** A handler that will receive callbacks for certain events. */ -@property(nonatomic) id handler; +@property(nonatomic) id handler; @end diff --git a/GoogleDataTransport/GDTCORLibrary/Public/GDTCORAssert.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORAssert.h new file mode 100644 index 00000000000..941e412e210 --- /dev/null +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORAssert.h @@ -0,0 +1,91 @@ +/* + * Copyright 2019 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 + +#import + +/** A block type that could be run instead of normal assertion logging. No return type, no params. + */ +typedef void (^GDTCORAssertionBlock)(void); + +/** Returns the result of executing a soft-linked method present in unit tests that allows a block + * to be run instead of normal assertion logging. This helps ameliorate issues with catching + * exceptions that occur on a dispatch_queue. + * + * @return A block that can be run instead of normal assert printing. + */ +FOUNDATION_EXPORT GDTCORAssertionBlock _Nullable GDTCORAssertionBlockToRunInstead(void); + +#if defined(NS_BLOCK_ASSERTIONS) + +#define GDTCORAssert(condition, ...) \ + do { \ + } while (0); + +#define GDTCORFatalAssert(condition, ...) \ + do { \ + } while (0); + +#else // defined(NS_BLOCK_ASSERTIONS) + +/** Asserts using a console log, unless a block was specified to be run instead. + * + * @param condition The condition you'd expect to be YES. + */ +#define GDTCORAssert(condition, ...) \ + do { \ + if (__builtin_expect(!(condition), 0)) { \ + GDTCORAssertionBlock assertionBlock = GDTCORAssertionBlockToRunInstead(); \ + if (assertionBlock) { \ + assertionBlock(); \ + } else { \ + __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \ + NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \ + __assert_file__ = __assert_file__ ? __assert_file__ : @""; \ + GDTCORLogError(GDTCORMCEGeneralError, @"Assertion failed (%@:%d): %s,", __assert_file__, \ + __LINE__, ##__VA_ARGS__); \ + __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \ + } \ + } \ + } while (0); + +/** Asserts by logging to the console and throwing an exception if NS_BLOCK_ASSERTIONS is not + * defined. + * + * @param condition The condition you'd expect to be YES. + */ +#define GDTCORFatalAssert(condition, ...) \ + do { \ + __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \ + if (__builtin_expect(!(condition), 0)) { \ + NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \ + __assert_file__ = __assert_file__ ? __assert_file__ : @""; \ + GDTCORLogError(GDTCORMCEFatalAssertion, \ + @"Fatal assertion encountered, please open an issue at " \ + "https://github.com/firebase/firebase-ios-sdk/issues " \ + "(%@:%d): %s,", \ + __assert_file__, __LINE__, ##__VA_ARGS__); \ + [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \ + object:self \ + file:__assert_file__ \ + lineNumber:__LINE__ \ + description:@"%@", ##__VA_ARGS__]; \ + } \ + __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \ + } while (0); + +#endif // defined(NS_BLOCK_ASSERTIONS) diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTClock.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORClock.h similarity index 83% rename from GoogleDataTransport/GDTLibrary/Public/GDTClock.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORClock.h index 4c6666e0659..01de21ae092 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTClock.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORClock.h @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN /** This class manages the device clock and produces snapshots of the current time. */ -@interface GDTClock : NSObject +@interface GDTCORClock : NSObject /** The wallclock time, UTC, in milliseconds. */ @property(nonatomic, readonly) int64_t timeMillis; @@ -33,13 +33,13 @@ NS_ASSUME_NONNULL_BEGIN /** The device uptime when this clock was created. */ @property(nonatomic, readonly) int64_t uptime; -/** Creates a GDTClock object using the current time and offsets. +/** Creates a GDTCORClock object using the current time and offsets. * - * @return A new GDTClock object representing the current time state. + * @return A new GDTCORClock object representing the current time state. */ + (instancetype)snapshot; -/** Creates a GDTClock object representing a time in the future, relative to now. +/** Creates a GDTCORClock object representing a time in the future, relative to now. * * @param millisInTheFuture The millis in the future from now this clock should represent. * @return An instance representing a future time. @@ -50,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return YES if the calling clock's time is after the given clock's time. */ -- (BOOL)isAfter:(GDTClock *)otherClock; +- (BOOL)isAfter:(GDTCORClock *)otherClock; @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTConsoleLogger.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORConsoleLogger.h similarity index 66% rename from GoogleDataTransport/GDTLibrary/Public/GDTConsoleLogger.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORConsoleLogger.h index 6035f3da76e..b7e7818e1c9 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTConsoleLogger.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORConsoleLogger.h @@ -23,57 +23,62 @@ * - MCW => MessageCodeWarning * - MCE => MessageCodeError */ -typedef NS_ENUM(NSInteger, GDTMessageCode) { +typedef NS_ENUM(NSInteger, GDTCORMessageCode) { /** For warning messages concerning transportBytes: not being implemented by a data object. */ - GDTMCWDataObjectMissingBytesImpl = 1, + GDTCORMCWDataObjectMissingBytesImpl = 1, /** For warning messages concerning a failed event upload. */ - GDTMCWUploadFailed = 2, + GDTCORMCWUploadFailed = 2, /** For warning messages concerning a forced event upload. */ - GDTMCWForcedUpload = 3, + GDTCORMCWForcedUpload = 3, /** For warning messages concerning a failed reachability call. */ - GDTMCWReachabilityFailed = 4, + GDTCORMCWReachabilityFailed = 4, /** For error messages concerning transform: not being implemented by an event transformer. */ - GDTMCETransformerDoesntImplementTransform = 1000, + GDTCORMCETransformerDoesntImplementTransform = 1000, /** For error messages concerning the creation of a directory failing. */ - GDTMCEDirectoryCreationError = 1001, + GDTCORMCEDirectoryCreationError = 1001, /** For error messages concerning the writing of a event file. */ - GDTMCEFileWriteError = 1002, + GDTCORMCEFileWriteError = 1002, /** For error messages concerning the lack of a prioritizer for a given backend. */ - GDTMCEPrioritizerError = 1003, + GDTCORMCEPrioritizerError = 1003, /** For error messages concerning a package delivery API violation. */ - GDTMCEDeliverTwice = 1004, + GDTCORMCEDeliverTwice = 1004, /** For error messages concerning an error in an implementation of -transportBytes. */ - GDTMCETransportBytesError = 1005, + GDTCORMCETransportBytesError = 1005, /** For general purpose error messages in a dependency. */ - GDTMCEGeneralError = 1006 + GDTCORMCEGeneralError = 1006, + + /** For fatal errors. Please go to https://github.com/firebase/firebase-ios-sdk/issues and open + * an issue if you encounter an error with this code. + */ + GDTCORMCEFatalAssertion = 1007 }; /** */ FOUNDATION_EXPORT -void GDTLog(GDTMessageCode code, NSString *_Nonnull format, ...); +void GDTCORLog(GDTCORMessageCode code, NSString *_Nonnull format, ...); /** Returns the string that represents some message code. * * @param code The code to convert to a string. * @return The string representing the message code. */ -FOUNDATION_EXPORT NSString *_Nonnull GDTMessageCodeEnumToString(GDTMessageCode code); +FOUNDATION_EXPORT NSString *_Nonnull GDTCORMessageCodeEnumToString(GDTCORMessageCode code); // A define to wrap GULLogWarning with slightly more convenient usage. -#define GDTLogWarning(MESSAGE_CODE, MESSAGE_FORMAT, ...) \ - GDTLog(MESSAGE_CODE, MESSAGE_FORMAT, __VA_ARGS__); +#define GDTCORLogWarning(MESSAGE_CODE, MESSAGE_FORMAT, ...) \ + GDTCORLog(MESSAGE_CODE, MESSAGE_FORMAT, __VA_ARGS__); // A define to wrap GULLogError with slightly more convenient usage and a failing assert. -#define GDTLogError(MESSAGE_CODE, MESSAGE_FORMAT, ...) \ - GDTLog(MESSAGE_CODE, MESSAGE_FORMAT, __VA_ARGS__); +#define GDTCORLogError(MESSAGE_CODE, MESSAGE_FORMAT, ...) \ + GDTCORLog(MESSAGE_CODE, MESSAGE_FORMAT, __VA_ARGS__); diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTDataFuture.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORDataFuture.h similarity index 96% rename from GoogleDataTransport/GDTLibrary/Public/GDTDataFuture.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORDataFuture.h index 114db4e0ac8..07f428fc942 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTDataFuture.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORDataFuture.h @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN /** This class represents a future data object, determined at instantiation time. */ -@interface GDTDataFuture : NSObject +@interface GDTCORDataFuture : NSObject /** The data, computed on-demand, depending on the initializer. */ @property(nullable, readonly, nonatomic) NSData *data; diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTEvent.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCOREvent.h similarity index 72% rename from GoogleDataTransport/GDTLibrary/Public/GDTEvent.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCOREvent.h index ebe4d8a5a9e..1ab55de8f19 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTEvent.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCOREvent.h @@ -16,36 +16,36 @@ #import -#import +#import -@class GDTClock; -@class GDTDataFuture; -@class GDTStoredEvent; +@class GDTCORClock; +@class GDTCORDataFuture; +@class GDTCORStoredEvent; NS_ASSUME_NONNULL_BEGIN /** The different possible quality of service specifiers. High values indicate high priority. */ -typedef NS_ENUM(NSInteger, GDTEventQoS) { +typedef NS_ENUM(NSInteger, GDTCOREventQoS) { /** The QoS tier wasn't set, and won't ever be sent. */ - GDTEventQoSUnknown = 0, + GDTCOREventQoSUnknown = 0, /** This event is internal telemetry data that should not be sent on its own if possible. */ - GDTEventQoSTelemetry = 1, + GDTCOREventQoSTelemetry = 1, /** This event should be sent, but in a batch only roughly once per day. */ - GDTEventQoSDaily = 2, + GDTCOREventQoSDaily = 2, /** This event should be sent when requested by the uploader. */ - GDTEventQosDefault = 3, + GDTCOREventQosDefault = 3, /** This event should be sent immediately along with any other data that can be batched. */ - GDTEventQoSFast = 4, + GDTCOREventQoSFast = 4, /** This event should only be uploaded on wifi. */ - GDTEventQoSWifiOnly = 5, + GDTCOREventQoSWifiOnly = 5, }; -@interface GDTEvent : NSObject +@interface GDTCOREvent : NSObject /** The mapping identifier, to allow backends to map the transport bytes to a proto. */ @property(readonly, nonatomic) NSString *mappingID; @@ -54,14 +54,14 @@ typedef NS_ENUM(NSInteger, GDTEventQoS) { @property(readonly, nonatomic) NSInteger target; /** The data object encapsulated in the transport of your choice, as long as it implements - * the GDTEventDataObject protocol. */ -@property(nullable, nonatomic) id dataObject; + * the GDTCOREventDataObject protocol. */ +@property(nullable, nonatomic) id dataObject; /** The quality of service tier this event belongs to. */ -@property(nonatomic) GDTEventQoS qosTier; +@property(nonatomic) GDTCOREventQoS qosTier; /** The clock snapshot at the time of the event. */ -@property(nonatomic) GDTClock *clockSnapshot; +@property(nonatomic) GDTCORClock *clockSnapshot; /** A dictionary provided to aid prioritizers by allowing the passing of arbitrary data. It will be * retained by a copy in -copy, but not used for -hash. @@ -79,15 +79,15 @@ typedef NS_ENUM(NSInteger, GDTEventQoS) { * @param target The event's target identifier. * @return An instance of this class. */ -- (instancetype)initWithMappingID:(NSString *)mappingID - target:(NSInteger)target NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithMappingID:(NSString *)mappingID + target:(NSInteger)target NS_DESIGNATED_INITIALIZER; -/** Returns the GDTStoredEvent equivalent of self. +/** Returns the GDTCORStoredEvent equivalent of self. * * @param dataFuture The data future representing the transport bytes of the original event. - * @return An equivalent GDTStoredEvent. + * @return An equivalent GDTCORStoredEvent. */ -- (GDTStoredEvent *)storedEventWithDataFuture:(GDTDataFuture *)dataFuture; +- (GDTCORStoredEvent *)storedEventWithDataFuture:(GDTCORDataFuture *)dataFuture; @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTEventDataObject.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCOREventDataObject.h similarity index 96% rename from GoogleDataTransport/GDTLibrary/Public/GDTEventDataObject.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCOREventDataObject.h index f9031cbe835..34ef62424d8 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTEventDataObject.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCOREventDataObject.h @@ -21,7 +21,7 @@ NS_ASSUME_NONNULL_BEGIN /** This protocol defines the common interface that event protos should implement regardless of the * underlying transport technology (protobuf, nanopb, etc). */ -@protocol GDTEventDataObject +@protocol GDTCOREventDataObject @required diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTEventTransformer.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCOREventTransformer.h similarity index 90% rename from GoogleDataTransport/GDTLibrary/Public/GDTEventTransformer.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCOREventTransformer.h index d7ba9341abd..c26d0c21a27 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTEventTransformer.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCOREventTransformer.h @@ -16,12 +16,12 @@ #import -@class GDTEvent; +@class GDTCOREvent; NS_ASSUME_NONNULL_BEGIN /** Defines the API that event transformers must adopt. */ -@protocol GDTEventTransformer +@protocol GDTCOREventTransformer @required @@ -31,7 +31,7 @@ NS_ASSUME_NONNULL_BEGIN * @param event The event to transform. * @return A transformed event, or nil if the transformation dropped the event. */ -- (GDTEvent *)transform:(GDTEvent *)event; +- (GDTCOREvent *)transform:(GDTCOREvent *)event; @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTLifecycle.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORLifecycle.h similarity index 69% rename from GoogleDataTransport/GDTLibrary/Public/GDTLifecycle.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORLifecycle.h index 088d81d9091..4d61a21312a 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTLifecycle.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORLifecycle.h @@ -16,36 +16,36 @@ #import -#import +#import -@class GDTEvent; +@class GDTCOREvent; NS_ASSUME_NONNULL_BEGIN /** A protocol defining the lifecycle events objects in the library must respond to immediately. */ -@protocol GDTLifecycleProtocol +@protocol GDTCORLifecycleProtocol @optional /** Indicates an imminent app termination in the rare occurrence when -applicationWillTerminate: has * been called. * - * @param app The GDTApplication instance. + * @param app The GDTCORApplication instance. */ -- (void)appWillTerminate:(GDTApplication *)app; +- (void)appWillTerminate:(GDTCORApplication *)app; /** Indicates that the app is moving to background and eventual suspension or the current UIScene is * deactivating. * - * @param app The GDTApplication instance. + * @param app The GDTCORApplication instance. */ -- (void)appWillBackground:(GDTApplication *)app; +- (void)appWillBackground:(GDTCORApplication *)app; /** Indicates that the app is resuming operation or a UIScene is activating. * - * @param app The GDTApplication instance. + * @param app The GDTCORApplication instance. */ -- (void)appWillForeground:(GDTApplication *)app; +- (void)appWillForeground:(GDTCORApplication *)app; @end @@ -53,10 +53,10 @@ NS_ASSUME_NONNULL_BEGIN * * When backgrounding, the library doesn't stop processing events, it's just that several background * tasks will end up being created for every event that's sent, and the stateful objects of the - * library (GDTStorage and GDTUploadCoordinator singletons) will deserialize themselves from and to - * disk before and after every operation, respectively. + * library (GDTCORStorage and GDTCORUploadCoordinator singletons) will deserialize themselves from + * and to disk before and after every operation, respectively. */ -@interface GDTLifecycle : NSObject +@interface GDTCORLifecycle : NSObject @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTPlatform.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORPlatform.h similarity index 67% rename from GoogleDataTransport/GDTLibrary/Public/GDTPlatform.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORPlatform.h index 5773700c091..a39fd180092 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTPlatform.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORPlatform.h @@ -26,61 +26,61 @@ NS_ASSUME_NONNULL_BEGIN /** A notification sent out if the app is backgrounding. */ -FOUNDATION_EXPORT NSString *const kGDTApplicationDidEnterBackgroundNotification; +FOUNDATION_EXPORT NSString *const kGDTCORApplicationDidEnterBackgroundNotification; /** A notification sent out if the app is foregrounding. */ -FOUNDATION_EXPORT NSString *const kGDTApplicationWillEnterForegroundNotification; +FOUNDATION_EXPORT NSString *const kGDTCORApplicationWillEnterForegroundNotification; /** A notification sent out if the app is terminating. */ -FOUNDATION_EXPORT NSString *const kGDTApplicationWillTerminateNotification; +FOUNDATION_EXPORT NSString *const kGDTCORApplicationWillTerminateNotification; /** Compares flags with the WWAN reachability flag, if available, and returns YES if present. * * @param flags The set of reachability flags. * @return YES if the WWAN flag is set, NO otherwise. */ -BOOL GDTReachabilityFlagsContainWWAN(SCNetworkReachabilityFlags flags); +BOOL GDTCORReachabilityFlagsContainWWAN(SCNetworkReachabilityFlags flags); /** A typedef identify background identifiers. */ -typedef NSUInteger GDTBackgroundIdentifier; +typedef volatile NSUInteger GDTCORBackgroundIdentifier; /** A background task's invalid sentinel value. */ -FOUNDATION_EXPORT const GDTBackgroundIdentifier GDTBackgroundIdentifierInvalid; +FOUNDATION_EXPORT const GDTCORBackgroundIdentifier GDTCORBackgroundIdentifierInvalid; #if TARGET_OS_IOS || TARGET_OS_TV /** A protocol that wraps UIApplicationDelegate or NSObject protocol, depending on the platform. */ -@protocol GDTApplicationDelegate +@protocol GDTCORApplicationDelegate #elif TARGET_OS_OSX -@protocol GDTApplicationDelegate +@protocol GDTCORApplicationDelegate #else -@protocol GDTApplicationDelegate +@protocol GDTCORApplicationDelegate #endif // TARGET_OS_IOS || TARGET_OS_TV @end /** A cross-platform application class. */ -@interface GDTApplication : NSObject +@interface GDTCORApplication : NSObject /** Creates and/or returns the shared application instance. * * @return The shared application instance. */ -+ (nullable GDTApplication *)sharedApplication; ++ (nullable GDTCORApplication *)sharedApplication; /** Creates a background task with the returned identifier if on a suitable platform. * * @param handler The handler block that is called if the background task expires. - * @return An identifier for the background task, or GDTBackgroundIdentifierInvalid if one couldn't - * be created. + * @return An identifier for the background task, or GDTCORBackgroundIdentifierInvalid if one + * couldn't be created. */ -- (GDTBackgroundIdentifier)beginBackgroundTaskWithExpirationHandler: +- (GDTCORBackgroundIdentifier)beginBackgroundTaskWithExpirationHandler: (void (^__nullable)(void))handler; /** Ends the background task if the identifier is valid. * * @param bgID The background task to end. */ -- (void)endBackgroundTask:(GDTBackgroundIdentifier)bgID; +- (void)endBackgroundTask:(GDTCORBackgroundIdentifier)bgID; @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTPrioritizer.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORPrioritizer.h similarity index 74% rename from GoogleDataTransport/GDTLibrary/Public/GDTPrioritizer.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORPrioritizer.h index 85ed70b42ad..3c0c3c63ddb 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTPrioritizer.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORPrioritizer.h @@ -16,39 +16,39 @@ #import -#import -#import +#import +#import -@class GDTStoredEvent; +@class GDTCORStoredEvent; NS_ASSUME_NONNULL_BEGIN /** Options that define a set of upload conditions. This is used to help minimize end user data * consumption impact. */ -typedef NS_OPTIONS(NSInteger, GDTUploadConditions) { +typedef NS_OPTIONS(NSInteger, GDTCORUploadConditions) { /** An upload shouldn't be attempted, because there's no network. */ - GDTUploadConditionNoNetwork = 1 << 0, + GDTCORUploadConditionNoNetwork = 1 << 0, /** An upload would likely use mobile data. */ - GDTUploadConditionMobileData = 1 << 1, + GDTCORUploadConditionMobileData = 1 << 1, /** An upload would likely use wifi data. */ - GDTUploadConditionWifiData = 1 << 2, + GDTCORUploadConditionWifiData = 1 << 2, /** An upload uses some sort of network connection, but it's unclear which. */ - GDTUploadConditionUnclearConnection = 1 << 3, + GDTCORUploadConditionUnclearConnection = 1 << 3, /** A high priority event has occurred. */ - GDTUploadConditionHighPriority = 1 << 4, + GDTCORUploadConditionHighPriority = 1 << 4, }; /** This protocol defines the common interface of event prioritization. Prioritizers are * stateful objects that prioritize events upon insertion into storage and remain prepared to return * a set of filenames to the storage system. */ -@protocol GDTPrioritizer +@protocol GDTCORPrioritizer @required @@ -58,7 +58,7 @@ typedef NS_OPTIONS(NSInteger, GDTUploadConditions) { * * @param event The event to prioritize. */ -- (void)prioritizeEvent:(GDTStoredEvent *)event; +- (void)prioritizeEvent:(GDTCORStoredEvent *)event; /** Returns a set of events to upload given a set of conditions. * @@ -66,7 +66,7 @@ typedef NS_OPTIONS(NSInteger, GDTUploadConditions) { * @return An object to be used by the uploader to determine file URLs to upload with respect to the * current conditions. */ -- (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)conditions; +- (GDTCORUploadPackage *)uploadPackageWithConditions:(GDTCORUploadConditions)conditions; @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTRegistrar.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORRegistrar.h similarity index 77% rename from GoogleDataTransport/GDTLibrary/Public/GDTRegistrar.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORRegistrar.h index 8d532b3ee48..0a8fbb0a91e 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTRegistrar.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORRegistrar.h @@ -16,14 +16,14 @@ #import -#import -#import -#import +#import +#import +#import NS_ASSUME_NONNULL_BEGIN /** Manages the registration of targets with the transport SDK. */ -@interface GDTRegistrar : NSObject +@interface GDTCORRegistrar : NSObject /** Creates and/or returns the singleton instance. * @@ -36,14 +36,14 @@ NS_ASSUME_NONNULL_BEGIN * @param backend The backend object to register. * @param target The target this backend object will be responsible for. */ -- (void)registerUploader:(id)backend target:(GDTTarget)target; +- (void)registerUploader:(id)backend target:(GDTCORTarget)target; /** Registers a event prioritizer implementation with the GoogleDataTransport infrastructure. * * @param prioritizer The prioritizer object to register. * @param target The target this prioritizer object will be responsible for. */ -- (void)registerPrioritizer:(id)prioritizer target:(GDTTarget)target; +- (void)registerPrioritizer:(id)prioritizer target:(GDTCORTarget)target; @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTStoredEvent.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORStoredEvent.h similarity index 78% rename from GoogleDataTransport/GDTLibrary/Public/GDTStoredEvent.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORStoredEvent.h index 09f480c5557..647b22088fc 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTStoredEvent.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORStoredEvent.h @@ -15,17 +15,17 @@ */ #import -#import -#import +#import +#import -@class GDTEvent; +@class GDTCOREvent; NS_ASSUME_NONNULL_BEGIN -@interface GDTStoredEvent : NSObject +@interface GDTCORStoredEvent : NSObject /** The data future representing the original event's transport bytes. */ -@property(readonly, nonatomic) GDTDataFuture *dataFuture; +@property(readonly, nonatomic) GDTCORDataFuture *dataFuture; /** The mapping identifier, to allow backends to map the transport bytes to a proto. */ @property(readonly, nonatomic) NSString *mappingID; @@ -34,10 +34,10 @@ NS_ASSUME_NONNULL_BEGIN @property(readonly, nonatomic) NSNumber *target; /** The quality of service tier this event belongs to. */ -@property(readonly, nonatomic) GDTEventQoS qosTier; +@property(readonly, nonatomic) GDTCOREventQoS qosTier; /** The clock snapshot at the time of the event. */ -@property(readonly, nonatomic) GDTClock *clockSnapshot; +@property(readonly, nonatomic) GDTCORClock *clockSnapshot; /** A dictionary provided to aid prioritizers by allowing the passing of arbitrary data. * @@ -51,7 +51,7 @@ NS_ASSUME_NONNULL_BEGIN * @param dataFuture The dataFuture this event represents. * @return An instance of this class. */ -- (instancetype)initWithEvent:(GDTEvent *)event dataFuture:(GDTDataFuture *)dataFuture; +- (instancetype)initWithEvent:(GDTCOREvent *)event dataFuture:(GDTCORDataFuture *)dataFuture; @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTTargets.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORTargets.h similarity index 84% rename from GoogleDataTransport/GDTLibrary/Public/GDTTargets.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORTargets.h index fc79da50048..ebd36d11b13 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTTargets.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORTargets.h @@ -19,11 +19,14 @@ /** The list of targets supported by the shared transport infrastructure. If adding a new target, * please use the previous value +1. */ -typedef NS_ENUM(NSInteger, GDTTarget) { +typedef NS_ENUM(NSInteger, GDTCORTarget) { /** A target only used in testing. */ - kGDTTargetTest = 999, + kGDTCORTargetTest = 999, /** The CCT target. */ - kGDTTargetCCT = 1000, + kGDTCORTargetCCT = 1000, + + /** The FLL target. */ + kGDTCORTargetFLL = 1001, }; diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTTransport.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORTransport.h similarity index 77% rename from GoogleDataTransport/GDTLibrary/Public/GDTTransport.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORTransport.h index caf3c679c6f..a9522403625 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTTransport.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORTransport.h @@ -16,13 +16,13 @@ #import -#import +#import -@class GDTEvent; +@class GDTCOREvent; NS_ASSUME_NONNULL_BEGIN -@interface GDTTransport : NSObject +@interface GDTCORTransport : NSObject // Please use the designated initializer. - (instancetype)init NS_UNAVAILABLE; @@ -35,9 +35,10 @@ NS_ASSUME_NONNULL_BEGIN * @param target The target backend of this transport. * @return A transport that will send events. */ -- (instancetype)initWithMappingID:(NSString *)mappingID - transformers:(nullable NSArray> *)transformers - target:(NSInteger)target NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithMappingID:(NSString *)mappingID + transformers: + (nullable NSArray> *)transformers + target:(NSInteger)target NS_DESIGNATED_INITIALIZER; /** Copies and sends an internal telemetry event. Events sent using this API are lower in priority, * and sometimes won't be sent on their own. @@ -46,7 +47,7 @@ NS_ASSUME_NONNULL_BEGIN * * @param event The event to send. */ -- (void)sendTelemetryEvent:(GDTEvent *)event; +- (void)sendTelemetryEvent:(GDTCOREvent *)event; /** Copies and sends an SDK service data event. Events send using this API are higher in priority, * and will cause a network request at some point in the relative near future. @@ -55,13 +56,13 @@ NS_ASSUME_NONNULL_BEGIN * * @param event The event to send. */ -- (void)sendDataEvent:(GDTEvent *)event; +- (void)sendDataEvent:(GDTCOREvent *)event; /** Creates an event for use by this transport. * * @return An event that is suited for use by this transport. */ -- (GDTEvent *)eventForTransport; +- (GDTCOREvent *)eventForTransport; @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTUploadPackage.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORUploadPackage.h similarity index 71% rename from GoogleDataTransport/GDTLibrary/Public/GDTUploadPackage.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORUploadPackage.h index f9e1584dadf..46a676b9d15 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTUploadPackage.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORUploadPackage.h @@ -16,14 +16,14 @@ #import -#import +#import -@class GDTClock; -@class GDTStoredEvent; -@class GDTUploadPackage; +@class GDTCORClock; +@class GDTCORStoredEvent; +@class GDTCORUploadPackage; /** A protocol that allows a handler to respond to package lifecycle events. */ -@protocol GDTUploadPackageProtocol +@protocol GDTCORUploadPackageProtocol @optional @@ -33,37 +33,37 @@ * * @param package The package that has expired. */ -- (void)packageExpired:(GDTUploadPackage *)package; +- (void)packageExpired:(GDTCORUploadPackage *)package; /** Indicates that the package was successfully delivered. * * @param package The package that was delivered. */ -- (void)packageDelivered:(GDTUploadPackage *)package successful:(BOOL)successful; +- (void)packageDelivered:(GDTCORUploadPackage *)package successful:(BOOL)successful; @end /** This class is a container that's handed off to uploaders. */ -@interface GDTUploadPackage : NSObject +@interface GDTCORUploadPackage : NSObject /** The set of stored events in this upload package. */ -@property(nonatomic) NSSet *events; +@property(nonatomic) NSSet *events; -/** The expiration time. If [[GDTClock snapshot] isAfter:deliverByTime] this package has expired. +/** The expiration time. If [[GDTCORClock snapshot] isAfter:deliverByTime] this package has expired. * * @note By default, the expiration time will be 3 minutes from creation. */ -@property(nonatomic) GDTClock *deliverByTime; +@property(nonatomic) GDTCORClock *deliverByTime; /** The target of this package. */ -@property(nonatomic, readonly) GDTTarget target; +@property(nonatomic, readonly) GDTCORTarget target; /** Initializes a package instance. * * @param target The target/destination of this package. * @return An instance of this class. */ -- (instancetype)initWithTarget:(GDTTarget)target NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithTarget:(GDTCORTarget)target NS_DESIGNATED_INITIALIZER; // Please use the designated initializer. - (instancetype)init NS_UNAVAILABLE; diff --git a/GoogleDataTransport/GDTLibrary/Public/GDTUploader.h b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORUploader.h similarity index 72% rename from GoogleDataTransport/GDTLibrary/Public/GDTUploader.h rename to GoogleDataTransport/GDTCORLibrary/Public/GDTCORUploader.h index 9461a874d37..a34f8b2acb1 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GDTUploader.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GDTCORUploader.h @@ -16,16 +16,16 @@ #import -#import -#import -#import -#import -#import +#import +#import +#import +#import +#import NS_ASSUME_NONNULL_BEGIN /** This protocol defines the common interface for uploader implementations. */ -@protocol GDTUploader +@protocol GDTCORUploader @required @@ -34,13 +34,13 @@ NS_ASSUME_NONNULL_BEGIN * @param conditions The conditions that the upload attempt is likely to occur under. * @return YES if the uploader can make an upload attempt, NO otherwise. */ -- (BOOL)readyToUploadWithConditions:(GDTUploadConditions)conditions; +- (BOOL)readyToUploadWithConditions:(GDTCORUploadConditions)conditions; /** Uploads events to the backend using this specific backend's chosen format. * * @param package The event package to upload. Make sure to call -completeDelivery. */ -- (void)uploadPackage:(GDTUploadPackage *)package; +- (void)uploadPackage:(GDTCORUploadPackage *)package; @end diff --git a/GoogleDataTransport/GDTLibrary/Public/GoogleDataTransport.h b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport.h similarity index 59% rename from GoogleDataTransport/GDTLibrary/Public/GoogleDataTransport.h rename to GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport.h index c9eb22b0120..e46a385bfc1 100644 --- a/GoogleDataTransport/GDTLibrary/Public/GoogleDataTransport.h +++ b/GoogleDataTransport/GDTCORLibrary/Public/GoogleDataTransport.h @@ -14,17 +14,17 @@ * limitations under the License. */ -#import "GDTClock.h" -#import "GDTConsoleLogger.h" -#import "GDTDataFuture.h" -#import "GDTEvent.h" -#import "GDTEventDataObject.h" -#import "GDTEventTransformer.h" -#import "GDTLifecycle.h" -#import "GDTPrioritizer.h" -#import "GDTRegistrar.h" -#import "GDTStoredEvent.h" -#import "GDTTargets.h" -#import "GDTTransport.h" -#import "GDTUploadPackage.h" -#import "GDTUploader.h" +#import "GDTCORClock.h" +#import "GDTCORConsoleLogger.h" +#import "GDTCORDataFuture.h" +#import "GDTCOREvent.h" +#import "GDTCOREventDataObject.h" +#import "GDTCOREventTransformer.h" +#import "GDTCORLifecycle.h" +#import "GDTCORPrioritizer.h" +#import "GDTCORRegistrar.h" +#import "GDTCORStoredEvent.h" +#import "GDTCORTargets.h" +#import "GDTCORTransport.h" +#import "GDTCORUploadPackage.h" +#import "GDTCORUploader.h" diff --git a/GoogleDataTransport/GDTTests/Common/Categories/GDTRegistrar+Testing.h b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.h similarity index 79% rename from GoogleDataTransport/GDTTests/Common/Categories/GDTRegistrar+Testing.h rename to GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.h index 0088e885cb5..5824a51facf 100644 --- a/GoogleDataTransport/GDTTests/Common/Categories/GDTRegistrar+Testing.h +++ b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.h @@ -14,13 +14,13 @@ * limitations under the License. */ -#import -#import "GDTLibrary/Private/GDTRegistrar_Private.h" +#import +#import "GDTCORLibrary/Private/GDTCORRegistrar_Private.h" NS_ASSUME_NONNULL_BEGIN -/** Testing-only methods for GDTRegistrar. */ -@interface GDTRegistrar (Testing) +/** Testing-only methods for GDTCORRegistrar. */ +@interface GDTCORRegistrar (Testing) /** Resets the properties of the singleon, but does not reallocate a new singleton. */ - (void)reset; diff --git a/GoogleDataTransport/GDTTests/Common/Categories/GDTRegistrar+Testing.m b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.m similarity index 82% rename from GoogleDataTransport/GDTTests/Common/Categories/GDTRegistrar+Testing.m rename to GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.m index a5b4fdd5c6e..05617eee48c 100644 --- a/GoogleDataTransport/GDTTests/Common/Categories/GDTRegistrar+Testing.m +++ b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.m @@ -14,11 +14,11 @@ * limitations under the License. */ -#import "GDTTests/Common/Categories/GDTRegistrar+Testing.h" +#import "GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.h" -#import "GDTLibrary/Private/GDTRegistrar_Private.h" +#import "GDTCORLibrary/Private/GDTCORRegistrar_Private.h" -@implementation GDTRegistrar (Testing) +@implementation GDTCORRegistrar (Testing) - (void)reset { dispatch_sync(self.registrarQueue, ^{ diff --git a/GoogleDataTransport/GDTTests/Common/Categories/GDTStorage+Testing.h b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORStorage+Testing.h similarity index 81% rename from GoogleDataTransport/GDTTests/Common/Categories/GDTStorage+Testing.h rename to GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORStorage+Testing.h index 2a3231ebfca..d3bf56e5e22 100644 --- a/GoogleDataTransport/GDTTests/Common/Categories/GDTStorage+Testing.h +++ b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORStorage+Testing.h @@ -16,13 +16,13 @@ #import -#import "GDTLibrary/Private/GDTStorage.h" -#import "GDTLibrary/Private/GDTStorage_Private.h" +#import "GDTCORLibrary/Private/GDTCORStorage.h" +#import "GDTCORLibrary/Private/GDTCORStorage_Private.h" NS_ASSUME_NONNULL_BEGIN -/** Testing-only methods for GDTStorage. */ -@interface GDTStorage (Testing) +/** Testing-only methods for GDTCORStorage. */ +@interface GDTCORStorage (Testing) /** Resets the properties of the singleon, but does not reallocate a new singleton. This also * doesn't remove stored files from disk. diff --git a/GoogleDataTransport/GDTTests/Common/Categories/GDTStorage+Testing.m b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORStorage+Testing.m similarity index 75% rename from GoogleDataTransport/GDTTests/Common/Categories/GDTStorage+Testing.m rename to GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORStorage+Testing.m index a7c79c71ba4..f0c0e49653a 100644 --- a/GoogleDataTransport/GDTTests/Common/Categories/GDTStorage+Testing.m +++ b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORStorage+Testing.m @@ -14,18 +14,18 @@ * limitations under the License. */ -#import "GDTTests/Common/Categories/GDTStorage+Testing.h" +#import "GDTCORTests/Common/Categories/GDTCORStorage+Testing.h" -#import "GDTLibrary/Private/GDTStorage_Private.h" +#import "GDTCORLibrary/Private/GDTCORStorage_Private.h" -@implementation GDTStorage (Testing) +@implementation GDTCORStorage (Testing) - (void)reset { dispatch_sync(self.storageQueue, ^{ [self.targetToEventSet removeAllObjects]; [self.storedEvents removeAllObjects]; NSError *error; - [[NSFileManager defaultManager] removeItemAtPath:[GDTStorage archivePath] error:&error]; + [[NSFileManager defaultManager] removeItemAtPath:[GDTCORStorage archivePath] error:&error]; }); } diff --git a/GoogleDataTransport/GDTTests/Common/Categories/GDTUploadCoordinator+Testing.h b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.h similarity index 90% rename from GoogleDataTransport/GDTTests/Common/Categories/GDTUploadCoordinator+Testing.h rename to GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.h index 5298afabca3..fbc2b9e1d92 100644 --- a/GoogleDataTransport/GDTTests/Common/Categories/GDTUploadCoordinator+Testing.h +++ b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.h @@ -14,11 +14,11 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTUploadCoordinator.h" +#import "GDTCORLibrary/Private/GDTCORUploadCoordinator.h" NS_ASSUME_NONNULL_BEGIN -@interface GDTUploadCoordinator (Testing) +@interface GDTCORUploadCoordinator (Testing) /** Resets the properties of the singleton, but does not reallocate a new singleton. */ - (void)reset; diff --git a/GoogleDataTransport/GDTTests/Common/Categories/GDTUploadCoordinator+Testing.m b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.m similarity index 77% rename from GoogleDataTransport/GDTTests/Common/Categories/GDTUploadCoordinator+Testing.m rename to GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.m index d1bc7a02c3c..08b3dcd0139 100644 --- a/GoogleDataTransport/GDTTests/Common/Categories/GDTUploadCoordinator+Testing.m +++ b/GoogleDataTransport/GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.m @@ -14,19 +14,19 @@ * limitations under the License. */ -#import "GDTTests/Common/Categories/GDTUploadCoordinator+Testing.h" +#import "GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.h" #import -#import "GDTLibrary/Private/GDTStorage.h" -#import "GDTLibrary/Public/GDTRegistrar.h" +#import "GDTCORLibrary/Private/GDTCORStorage.h" +#import "GDTCORLibrary/Public/GDTCORRegistrar.h" -@implementation GDTUploadCoordinator (Testing) +@implementation GDTCORUploadCoordinator (Testing) - (void)reset { dispatch_sync(self.coordinationQueue, ^{ - self.storage = [GDTStorage sharedInstance]; - self.registrar = [GDTRegistrar sharedInstance]; + self.storage = [GDTCORStorage sharedInstance]; + self.registrar = [GDTCORRegistrar sharedInstance]; [self.targetToInFlightPackages removeAllObjects]; }); } diff --git a/GoogleDataTransport/GDTTests/Common/Fakes/GDTTransformerFake.h b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h similarity index 88% rename from GoogleDataTransport/GDTTests/Common/Fakes/GDTTransformerFake.h rename to GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h index 56549faca5f..156404d41b6 100644 --- a/GoogleDataTransport/GDTTests/Common/Fakes/GDTTransformerFake.h +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.h @@ -14,12 +14,12 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTTransformer.h" +#import "GDTCORLibrary/Private/GDTCORStorage.h" NS_ASSUME_NONNULL_BEGIN /** A functionless fake that can be injected into classes that need it. */ -@interface GDTTransformerFake : GDTTransformer +@interface GDTCORStorageFake : GDTCORStorage @end diff --git a/GoogleDataTransport/GDTTests/Common/Fakes/GDTStorageFake.m b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.m similarity index 76% rename from GoogleDataTransport/GDTTests/Common/Fakes/GDTStorageFake.m rename to GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.m index 2adf213b780..a13f13caaa7 100644 --- a/GoogleDataTransport/GDTTests/Common/Fakes/GDTStorageFake.m +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORStorageFake.m @@ -14,14 +14,14 @@ * limitations under the License. */ -#import "GDTTests/Common/Fakes/GDTStorageFake.h" +#import "GDTCORTests/Common/Fakes/GDTCORStorageFake.h" -@implementation GDTStorageFake +@implementation GDTCORStorageFake -- (void)storeEvent:(GDTEvent *)event { +- (void)storeEvent:(GDTCOREvent *)event { } -- (void)removeEvents:(NSSet *)events { +- (void)removeEvents:(NSSet *)events { } @end diff --git a/GoogleDataTransport/GDTTests/Common/Fakes/GDTStorageFake.h b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransformerFake.h similarity index 87% rename from GoogleDataTransport/GDTTests/Common/Fakes/GDTStorageFake.h rename to GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransformerFake.h index 5da6e851444..347a938d779 100644 --- a/GoogleDataTransport/GDTTests/Common/Fakes/GDTStorageFake.h +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransformerFake.h @@ -14,12 +14,12 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTStorage.h" +#import "GDTCORLibrary/Private/GDTCORTransformer.h" NS_ASSUME_NONNULL_BEGIN /** A functionless fake that can be injected into classes that need it. */ -@interface GDTStorageFake : GDTStorage +@interface GDTCORTransformerFake : GDTCORTransformer @end diff --git a/GoogleDataTransport/GDTTests/Common/Fakes/GDTTransformerFake.m b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransformerFake.m similarity index 73% rename from GoogleDataTransport/GDTTests/Common/Fakes/GDTTransformerFake.m rename to GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransformerFake.m index a7d35fe1cda..40c94abbb85 100644 --- a/GoogleDataTransport/GDTTests/Common/Fakes/GDTTransformerFake.m +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransformerFake.m @@ -14,12 +14,12 @@ * limitations under the License. */ -#import "GDTTests/Common/Fakes/GDTTransformerFake.h" +#import "GDTCORTests/Common/Fakes/GDTCORTransformerFake.h" -@implementation GDTTransformerFake +@implementation GDTCORTransformerFake -- (void)transformEvent:(GDTEvent *)event - withTransformers:(NSArray> *)transformers { +- (void)transformEvent:(GDTCOREvent *)event + withTransformers:(NSArray> *)transformers { } @end diff --git a/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransportFake.h b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransportFake.h new file mode 100644 index 00000000000..b6a1e4d3910 --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransportFake.h @@ -0,0 +1,32 @@ +/* + * Copyright 2019 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 + +NS_ASSUME_NONNULL_BEGIN + +/** A fake for GDTCORTransport that can be used in tests. */ +@interface GDTCORTransportFake : GDTCORTransport + +/** All log events that have been logged. */ +@property(nonatomic, readonly) NSArray *logEvents; + +/** All logged events are cleared from memory, resetting the fake. */ +- (void)reset; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransportFake.m b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransportFake.m new file mode 100644 index 00000000000..2718472bcf5 --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORTransportFake.m @@ -0,0 +1,60 @@ +/* + * Copyright 2019 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 "GDTCORTests/Common/Fakes/GDTCORTransportFake.h" + +@interface GDTCORTransportFake () + +/** + * Internal array that stores all log events that have been logged. + * All access to this array is protected by @synchronized. + */ +@property(nonatomic, readonly) NSMutableArray *events; + +@end + +@implementation GDTCORTransportFake + +- (instancetype)initWithMappingID:(NSString *)mappingID + transformers:(nullable NSArray> *)transformers + target:(NSInteger)target { + self = [super initWithMappingID:mappingID transformers:transformers target:target]; + + if (self) { + _events = [NSMutableArray array]; + } + return self; +} + +- (void)sendDataEvent:(GDTCOREvent *)event { + @synchronized(self.events) { + [self.events addObject:event]; + } +} + +- (NSArray *)logEvents { + @synchronized(self.events) { + return [self.events copy]; + } +} + +- (void)reset { + @synchronized(self.events) { + [self.events removeAllObjects]; + } +} + +@end diff --git a/GoogleDataTransport/GDTTests/Common/Fakes/GDTUploadCoordinatorFake.h b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORUploadCoordinatorFake.h similarity index 84% rename from GoogleDataTransport/GDTTests/Common/Fakes/GDTUploadCoordinatorFake.h rename to GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORUploadCoordinatorFake.h index 4933bbbc743..cf21c14ae3c 100644 --- a/GoogleDataTransport/GDTTests/Common/Fakes/GDTUploadCoordinatorFake.h +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORUploadCoordinatorFake.h @@ -14,11 +14,11 @@ * limitations under the License. */ -#import "GDTLibrary/Private/GDTUploadCoordinator.h" +#import "GDTCORLibrary/Private/GDTCORUploadCoordinator.h" NS_ASSUME_NONNULL_BEGIN -@interface GDTUploadCoordinatorFake : GDTUploadCoordinator +@interface GDTCORUploadCoordinatorFake : GDTCORUploadCoordinator @property(nonatomic) BOOL forceUploadCalled; diff --git a/GoogleDataTransport/GDTTests/Common/Fakes/GDTUploadCoordinatorFake.m b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORUploadCoordinatorFake.m similarity index 79% rename from GoogleDataTransport/GDTTests/Common/Fakes/GDTUploadCoordinatorFake.m rename to GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORUploadCoordinatorFake.m index 878c4c541e5..54e7c82b2d0 100644 --- a/GoogleDataTransport/GDTTests/Common/Fakes/GDTUploadCoordinatorFake.m +++ b/GoogleDataTransport/GDTCORTests/Common/Fakes/GDTCORUploadCoordinatorFake.m @@ -14,11 +14,11 @@ * limitations under the License. */ -#import "GDTTests/Common/Fakes/GDTUploadCoordinatorFake.h" +#import "GDTCORTests/Common/Fakes/GDTCORUploadCoordinatorFake.h" -@implementation GDTUploadCoordinatorFake +@implementation GDTCORUploadCoordinatorFake -- (void)forceUploadForTarget:(GDTTarget)target { +- (void)forceUploadForTarget:(GDTCORTarget)target { self.forceUploadCalled = YES; } diff --git a/GoogleDataTransport/GDTTests/Integration/GDTIntegrationTest.m b/GoogleDataTransport/GDTCORTests/Integration/GDTCORIntegrationTest.m similarity index 52% rename from GoogleDataTransport/GDTTests/Integration/GDTIntegrationTest.m rename to GoogleDataTransport/GDTCORTests/Integration/GDTCORIntegrationTest.m index d89f3634b15..02aa7aca1be 100644 --- a/GoogleDataTransport/GDTTests/Integration/GDTIntegrationTest.m +++ b/GoogleDataTransport/GDTCORTests/Integration/GDTCORIntegrationTest.m @@ -16,27 +16,27 @@ #import -#import -#import -#import -#import +#import +#import +#import +#import -#import "GDTTests/Common/Categories/GDTUploadCoordinator+Testing.h" +#import "GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.h" -#import "GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.h" -#import "GDTTests/Integration/Helpers/GDTIntegrationTestUploader.h" -#import "GDTTests/Integration/TestServer/GDTTestServer.h" +#import "GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.h" +#import "GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploader.h" +#import "GDTCORTests/Integration/TestServer/GDTCORTestServer.h" -#import "GDTLibrary/Private/GDTReachability_Private.h" -#import "GDTLibrary/Private/GDTStorage_Private.h" -#import "GDTLibrary/Private/GDTTransformer_Private.h" +#import "GDTCORLibrary/Private/GDTCORReachability_Private.h" +#import "GDTCORLibrary/Private/GDTCORStorage_Private.h" +#import "GDTCORLibrary/Private/GDTCORTransformer_Private.h" /** A test-only event data object used in this integration test. */ -@interface GDTIntegrationTestEvent : NSObject +@interface GDTCORIntegrationTestEvent : NSObject @end -@implementation GDTIntegrationTestEvent +@implementation GDTCORIntegrationTestEvent - (NSData *)transportBytes { // In real usage, protobuf's -data method or a custom implementation using nanopb are used. @@ -46,13 +46,13 @@ - (NSData *)transportBytes { @end /** A test-only event transformer. */ -@interface GDTIntegrationTestTransformer : NSObject +@interface GDTCORIntegrationTestTransformer : NSObject @end -@implementation GDTIntegrationTestTransformer +@implementation GDTCORIntegrationTestTransformer -- (GDTEvent *)transform:(GDTEvent *)event { +- (GDTCOREvent *)transform:(GDTCOREvent *)event { // drop half the events during transforming. if (arc4random_uniform(2) == 0) { event = nil; @@ -62,27 +62,27 @@ - (GDTEvent *)transform:(GDTEvent *)event { @end -@interface GDTIntegrationTest : XCTestCase +@interface GDTCORIntegrationTest : XCTestCase /** A test prioritizer. */ -@property(nonatomic) GDTIntegrationTestPrioritizer *prioritizer; +@property(nonatomic) GDTCORIntegrationTestPrioritizer *prioritizer; /** A test uploader. */ -@property(nonatomic) GDTIntegrationTestUploader *uploader; +@property(nonatomic) GDTCORIntegrationTestUploader *uploader; /** The first test transport. */ -@property(nonatomic) GDTTransport *transport1; +@property(nonatomic) GDTCORTransport *transport1; /** The second test transport. */ -@property(nonatomic) GDTTransport *transport2; +@property(nonatomic) GDTCORTransport *transport2; @end -@implementation GDTIntegrationTest +@implementation GDTCORIntegrationTest - (void)tearDown { - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 0); }); } @@ -91,10 +91,10 @@ - (void)testEndToEndEvent { expectation.assertForOverFulfill = NO; // Manually set the reachability flag. - [GDTReachability sharedInstance].flags = kSCNetworkReachabilityFlagsReachable; + [GDTCORReachability sharedInstance].flags = kSCNetworkReachabilityFlagsReachable; // Create the server. - GDTTestServer *testServer = [[GDTTestServer alloc] init]; + GDTCORTestServer *testServer = [[GDTCORTestServer alloc] init]; [testServer setResponseCompletedBlock:^(GCDWebServerRequest *_Nonnull request, GCDWebServerResponse *_Nonnull response) { [expectation fulfill]; @@ -103,38 +103,38 @@ - (void)testEndToEndEvent { [testServer start]; // Create eventgers. - self.transport1 = [[GDTTransport alloc] initWithMappingID:@"eventMap1" - transformers:nil - target:kGDTIntegrationTestTarget]; + self.transport1 = [[GDTCORTransport alloc] initWithMappingID:@"eventMap1" + transformers:nil + target:kGDTCORIntegrationTestTarget]; - self.transport2 = - [[GDTTransport alloc] initWithMappingID:@"eventMap2" - transformers:@[ [[GDTIntegrationTestTransformer alloc] init] ] - target:kGDTIntegrationTestTarget]; + self.transport2 = [[GDTCORTransport alloc] + initWithMappingID:@"eventMap2" + transformers:@[ [[GDTCORIntegrationTestTransformer alloc] init] ] + target:kGDTCORIntegrationTestTarget]; // Create a prioritizer and uploader. - self.prioritizer = [[GDTIntegrationTestPrioritizer alloc] init]; - self.uploader = [[GDTIntegrationTestUploader alloc] initWithServerURL:testServer.serverURL]; + self.prioritizer = [[GDTCORIntegrationTestPrioritizer alloc] init]; + self.uploader = [[GDTCORIntegrationTestUploader alloc] initWithServerURL:testServer.serverURL]; // Set the interval to be much shorter than the standard timer. - [GDTUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC * 0.1; - [GDTUploadCoordinator sharedInstance].timerLeeway = NSEC_PER_SEC * 0.01; + [GDTCORUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC * 0.1; + [GDTCORUploadCoordinator sharedInstance].timerLeeway = NSEC_PER_SEC * 0.01; // Confirm no events are in disk. - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); - XCTAssertEqual([GDTStorage sharedInstance].targetToEventSet.count, 0); + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 0); + XCTAssertEqual([GDTCORStorage sharedInstance].targetToEventSet.count, 0); // Generate some events data. [self generateEvents]; // Flush the transformer queue. - dispatch_sync([GDTTransformer sharedInstance].eventWritingQueue, ^{ + dispatch_sync([GDTCORTransformer sharedInstance].eventWritingQueue, ^{ }); // Confirm events are on disk. - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertGreaterThan([GDTStorage sharedInstance].storedEvents.count, 0); - XCTAssertGreaterThan([GDTStorage sharedInstance].targetToEventSet.count, 0); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertGreaterThan([GDTCORStorage sharedInstance].storedEvents.count, 0); + XCTAssertGreaterThan([GDTCORStorage sharedInstance].targetToEventSet.count, 0); }); // Confirm events were sent and received. @@ -142,8 +142,8 @@ - (void)testEndToEndEvent { // Generate events for a bit. NSUInteger lengthOfTestToRunInSeconds = 30; - [GDTUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC * 5; - [GDTUploadCoordinator sharedInstance].timerLeeway = NSEC_PER_SEC * 1; + [GDTCORUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC * 5; + [GDTCORUploadCoordinator sharedInstance].timerLeeway = NSEC_PER_SEC * 1; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); @@ -169,12 +169,12 @@ - (void)testEndToEndEvent { - (void)generateEvents { for (int i = 0; i < arc4random_uniform(10) + 1; i++) { // Choose a random transport, and randomly choose if it's a telemetry event. - GDTTransport *transport = arc4random_uniform(2) ? self.transport1 : self.transport2; + GDTCORTransport *transport = arc4random_uniform(2) ? self.transport1 : self.transport2; BOOL isTelemetryEvent = arc4random_uniform(2); // Create an event. - GDTEvent *event = [transport eventForTransport]; - event.dataObject = [[GDTIntegrationTestEvent alloc] init]; + GDTCOREvent *event = [transport eventForTransport]; + event.dataObject = [[GDTCORIntegrationTestEvent alloc] init]; if (isTelemetryEvent) { [transport sendTelemetryEvent:event]; diff --git a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.h b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.h similarity index 74% rename from GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.h rename to GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.h index a8f108db3d3..604882ada87 100644 --- a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.h +++ b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.h @@ -16,13 +16,13 @@ #import -#import -#import +#import +#import -/** The integration test target. Normally, you should use a value in GDTTargets.h. */ -static GDTTarget kGDTIntegrationTestTarget = 100; +/** The integration test target. Normally, you should use a value in GDTCORTargets.h. */ +static GDTCORTarget kGDTCORIntegrationTestTarget = 100; /** An integration test prioritization class. */ -@interface GDTIntegrationTestPrioritizer : NSObject +@interface GDTCORIntegrationTestPrioritizer : NSObject @end diff --git a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.m b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.m similarity index 54% rename from GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.m rename to GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.m index e757633d8f5..fa2edbf8947 100644 --- a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.m +++ b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.m @@ -14,43 +14,43 @@ * limitations under the License. */ -#import "GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.h" +#import "GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.h" -#import -#import +#import +#import -#import "GDTTests/Integration/Helpers/GDTIntegrationTestUploadPackage.h" +#import "GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploadPackage.h" -@interface GDTIntegrationTestPrioritizer () +@interface GDTCORIntegrationTestPrioritizer () /** Events that are only supposed to be uploaded whilst on wifi. */ -@property(nonatomic) NSMutableSet *wifiOnlyEvents; +@property(nonatomic) NSMutableSet *wifiOnlyEvents; /** Events that can be uploaded on any type of connection. */ -@property(nonatomic) NSMutableSet *nonWifiEvents; +@property(nonatomic) NSMutableSet *nonWifiEvents; /** The queue on which this prioritizer operates. */ @property(nonatomic) dispatch_queue_t queue; @end -@implementation GDTIntegrationTestPrioritizer +@implementation GDTCORIntegrationTestPrioritizer - (instancetype)init { self = [super init]; if (self) { _queue = - dispatch_queue_create("com.google.GDTIntegrationTestPrioritizer", DISPATCH_QUEUE_SERIAL); + dispatch_queue_create("com.google.GDTCORIntegrationTestPrioritizer", DISPATCH_QUEUE_SERIAL); _wifiOnlyEvents = [[NSMutableSet alloc] init]; _nonWifiEvents = [[NSMutableSet alloc] init]; - [[GDTRegistrar sharedInstance] registerPrioritizer:self target:kGDTIntegrationTestTarget]; + [[GDTCORRegistrar sharedInstance] registerPrioritizer:self target:kGDTCORIntegrationTestTarget]; } return self; } -- (void)prioritizeEvent:(GDTStoredEvent *)event { +- (void)prioritizeEvent:(GDTCORStoredEvent *)event { dispatch_async(_queue, ^{ - if (event.qosTier == GDTEventQoSWifiOnly) { + if (event.qosTier == GDTCOREventQoSWifiOnly) { [self.wifiOnlyEvents addObject:event]; } else { [self.nonWifiEvents addObject:event]; @@ -58,11 +58,11 @@ - (void)prioritizeEvent:(GDTStoredEvent *)event { }); } -- (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)conditions { - __block GDTIntegrationTestUploadPackage *uploadPackage = - [[GDTIntegrationTestUploadPackage alloc] initWithTarget:kGDTIntegrationTestTarget]; +- (GDTCORUploadPackage *)uploadPackageWithConditions:(GDTCORUploadConditions)conditions { + __block GDTCORIntegrationTestUploadPackage *uploadPackage = + [[GDTCORIntegrationTestUploadPackage alloc] initWithTarget:kGDTCORIntegrationTestTarget]; dispatch_sync(_queue, ^{ - if ((conditions & GDTUploadConditionWifiData) == GDTUploadConditionWifiData) { + if ((conditions & GDTCORUploadConditionWifiData) == GDTCORUploadConditionWifiData) { uploadPackage.events = [self.wifiOnlyEvents setByAddingObjectsFromSet:self.nonWifiEvents]; } else { uploadPackage.events = self.nonWifiEvents; @@ -71,9 +71,9 @@ - (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)condition return uploadPackage; } -- (void)packageDelivered:(GDTUploadPackage *)package successful:(BOOL)successful { +- (void)packageDelivered:(GDTCORUploadPackage *)package successful:(BOOL)successful { dispatch_async(_queue, ^{ - for (GDTStoredEvent *event in package.events) { + for (GDTCORStoredEvent *event in package.events) { [self.wifiOnlyEvents removeObject:event]; [self.nonWifiEvents removeObject:event]; } diff --git a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploadPackage.h b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploadPackage.h similarity index 83% rename from GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploadPackage.h rename to GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploadPackage.h index 8271f2739d3..15af3680e1d 100644 --- a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploadPackage.h +++ b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploadPackage.h @@ -14,9 +14,9 @@ * limitations under the License. */ -#import "GDTTests/Unit/Helpers/GDTTestPrioritizer.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h" /** An upload package used in testing. */ -@interface GDTIntegrationTestUploadPackage : GDTUploadPackage +@interface GDTCORIntegrationTestUploadPackage : GDTCORUploadPackage @end diff --git a/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploadPackage.m b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploadPackage.m new file mode 100644 index 00000000000..5496e774f32 --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploadPackage.m @@ -0,0 +1,21 @@ +/* + * Copyright 2019 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 "GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploadPackage.h" + +@implementation GDTCORIntegrationTestUploadPackage + +@end diff --git a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploader.h b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploader.h similarity index 82% rename from GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploader.h rename to GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploader.h index 64933e8b524..d1b10ded688 100644 --- a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploader.h +++ b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploader.h @@ -16,12 +16,12 @@ #import -#import +#import -#import "GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.h" +#import "GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.h" /** An integration test uploader. */ -@interface GDTIntegrationTestUploader : NSObject +@interface GDTCORIntegrationTestUploader : NSObject /** Instantiates an instance of this uploader with the given server URL. * diff --git a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploader.m b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploader.m similarity index 52% rename from GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploader.m rename to GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploader.m index 279ac2aadde..55d08ab8e8b 100644 --- a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploader.m +++ b/GoogleDataTransport/GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploader.m @@ -14,16 +14,17 @@ * limitations under the License. */ -#import "GDTTests/Integration/Helpers/GDTIntegrationTestUploader.h" +#import "GDTCORTests/Integration/Helpers/GDTCORIntegrationTestUploader.h" -#import -#import +#import +#import +#import -#import "GDTTests/Integration/Helpers/GDTIntegrationTestPrioritizer.h" +#import "GDTCORTests/Integration/Helpers/GDTCORIntegrationTestPrioritizer.h" -#import "GDTTests/Integration/TestServer/GDTTestServer.h" +#import "GDTCORTests/Integration/TestServer/GDTCORTestServer.h" -@implementation GDTIntegrationTestUploader { +@implementation GDTCORIntegrationTestUploader { /** The current upload task. */ NSURLSessionUploadTask *_currentUploadTask; @@ -35,13 +36,14 @@ - (instancetype)initWithServerURL:(NSURL *)serverURL { self = [super init]; if (self) { _serverURL = serverURL; - [[GDTRegistrar sharedInstance] registerUploader:self target:kGDTIntegrationTestTarget]; + [[GDTCORRegistrar sharedInstance] registerUploader:self target:kGDTCORIntegrationTestTarget]; } return self; } -- (void)uploadPackage:(GDTUploadPackage *)package { - NSAssert(!_currentUploadTask, @"An upload shouldn't be initiated with another in progress."); +- (void)uploadPackage:(GDTCORUploadPackage *)package { + GDTCORFatalAssert(!_currentUploadTask, + @"An upload shouldn't be initiated with another in progress."); NSURL *serverURL = arc4random_uniform(2) ? [_serverURL URLByAppendingPathComponent:@"log"] : [_serverURL URLByAppendingPathComponent:@"logBatch"]; NSURLSession *session = [NSURLSession sharedSession]; @@ -52,30 +54,30 @@ - (void)uploadPackage:(GDTUploadPackage *)package { NSLog(@"Uploading batch of %lu events: ", (unsigned long)[package events].count); // In real usage, you'd create an instance of whatever request proto your server needs. - for (GDTStoredEvent *event in package.events) { + for (GDTCORStoredEvent *event in package.events) { NSData *fileData = [NSData dataWithContentsOfURL:event.dataFuture.fileURL]; - NSAssert(fileData, @"An event file shouldn't be empty"); + GDTCORFatalAssert(fileData, @"An event file shouldn't be empty"); [uploadData appendData:fileData]; } - _currentUploadTask = - [session uploadTaskWithRequest:request - fromData:uploadData - completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, - NSError *_Nullable error) { - NSLog(@"Batch upload complete."); - // Remove from the prioritizer if there were no errors. - NSAssert(!error, @"There should be no errors uploading events: %@", error); - if (error) { - [package retryDeliveryInTheFuture]; - } else { - [package completeDelivery]; - } - self->_currentUploadTask = nil; - }]; + _currentUploadTask = [session + uploadTaskWithRequest:request + fromData:uploadData + completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response, + NSError *_Nullable error) { + NSLog(@"Batch upload complete."); + // Remove from the prioritizer if there were no errors. + GDTCORFatalAssert(!error, @"There should be no errors uploading events: %@", error); + if (error) { + [package retryDeliveryInTheFuture]; + } else { + [package completeDelivery]; + } + self->_currentUploadTask = nil; + }]; [_currentUploadTask resume]; } -- (BOOL)readyToUploadWithConditions:(GDTUploadConditions)conditions { +- (BOOL)readyToUploadWithConditions:(GDTCORUploadConditions)conditions { return _currentUploadTask ? NO : YES; } diff --git a/GoogleDataTransport/GDTTests/Integration/TestServer/GDTTestServer.h b/GoogleDataTransport/GDTCORTests/Integration/TestServer/GDTCORTestServer.h similarity index 97% rename from GoogleDataTransport/GDTTests/Integration/TestServer/GDTTestServer.h rename to GoogleDataTransport/GDTCORTests/Integration/TestServer/GDTCORTestServer.h index 59f680750a5..9501bdab268 100644 --- a/GoogleDataTransport/GDTTests/Integration/TestServer/GDTTestServer.h +++ b/GoogleDataTransport/GDTCORTests/Integration/TestServer/GDTCORTestServer.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @class GCDWebServerResponse; /** This class provides a hermetic test service that runs on the test device/simulator. */ -@interface GDTTestServer : NSObject +@interface GDTCORTestServer : NSObject /** The URL of the server. */ @property(nonatomic, readonly) NSURL *serverURL; diff --git a/GoogleDataTransport/GDTTests/Integration/TestServer/GDTTestServer.m b/GoogleDataTransport/GDTCORTests/Integration/TestServer/GDTCORTestServer.m similarity index 96% rename from GoogleDataTransport/GDTTests/Integration/TestServer/GDTTestServer.m rename to GoogleDataTransport/GDTCORTests/Integration/TestServer/GDTCORTestServer.m index 57c4ae58cd0..490cba2813a 100644 --- a/GoogleDataTransport/GDTTests/Integration/TestServer/GDTTestServer.m +++ b/GoogleDataTransport/GDTCORTests/Integration/TestServer/GDTCORTestServer.m @@ -14,9 +14,9 @@ * limitations under the License. */ -#import "GDTTests/Integration/TestServer/GDTTestServer.h" +#import "GDTCORTests/Integration/TestServer/GDTCORTestServer.h" -@interface GDTTestServer () +@interface GDTCORTestServer () /** The server object. */ @property(nonatomic) GCDWebServer *server; @@ -26,7 +26,7 @@ @interface GDTTestServer () @end -@implementation GDTTestServer +@implementation GDTCORTestServer - (instancetype)init { self = [super init]; diff --git a/GoogleDataTransport/GDTCORTests/Lifecycle/GDTCORLifecycleTest.m b/GoogleDataTransport/GDTCORTests/Lifecycle/GDTCORLifecycleTest.m new file mode 100644 index 00000000000..c4589c96e4d --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Lifecycle/GDTCORLifecycleTest.m @@ -0,0 +1,179 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import +#import +#import + +#import "GDTCORLibrary/Private/GDTCORStorage_Private.h" +#import "GDTCORLibrary/Private/GDTCORTransformer_Private.h" +#import "GDTCORLibrary/Private/GDTCORUploadCoordinator.h" + +#import "GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestPrioritizer.h" +#import "GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestUploader.h" + +#import "GDTCORTests/Common/Categories/GDTCORStorage+Testing.h" +#import "GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.h" + +/** Waits for the result of waitBlock to be YES, or times out and fails. + * + * @param waitBlock The block to periodically execute. + * @param timeInterval The timeout. + */ +#define GDTCORWaitForBlock(waitBlock, timeInterval) \ + { \ + NSPredicate *pred = \ + [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, \ + NSDictionary *_Nullable bindings) { \ + return waitBlock(); \ + }]; \ + XCTestExpectation *expectation = [self expectationForPredicate:pred \ + evaluatedWithObject:[[NSObject alloc] init] \ + handler:^BOOL { \ + return YES; \ + }]; \ + [self waitForExpectations:@[ expectation ] timeout:timeInterval]; \ + } + +/** A test-only event data object used in this integration test. */ +@interface GDTCORLifecycleTestEvent : NSObject + +@end + +@implementation GDTCORLifecycleTestEvent + +- (NSData *)transportBytes { + // In real usage, protobuf's -data method or a custom implementation using nanopb are used. + return [[NSString stringWithFormat:@"%@", [NSDate date]] dataUsingEncoding:NSUTF8StringEncoding]; +} + +@end + +@interface GDTCORLifecycleTest : XCTestCase + +/** The test prioritizer. */ +@property(nonatomic) GDTCORLifecycleTestPrioritizer *prioritizer; + +/** The test uploader. */ +@property(nonatomic) GDTCORLifecycleTestUploader *uploader; + +@end + +@implementation GDTCORLifecycleTest + +- (void)setUp { + [super setUp]; + // Don't check the error, because it'll be populated in cases where the file doesn't exist. + NSError *error; + [[NSFileManager defaultManager] removeItemAtPath:[GDTCORStorage archivePath] error:&error]; + self.uploader = [[GDTCORLifecycleTestUploader alloc] init]; + [[GDTCORRegistrar sharedInstance] registerUploader:self.uploader target:kGDTCORTargetTest]; + + self.prioritizer = [[GDTCORLifecycleTestPrioritizer alloc] init]; + [[GDTCORRegistrar sharedInstance] registerPrioritizer:self.prioritizer target:kGDTCORTargetTest]; + [[GDTCORStorage sharedInstance] reset]; + [[GDTCORUploadCoordinator sharedInstance] reset]; +} + +/** Tests that the library serializes itself to disk when the app backgrounds. */ +- (void)testBackgrounding { + GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"test" + transformers:nil + target:kGDTCORTargetTest]; + GDTCOREvent *event = [transport eventForTransport]; + event.dataObject = [[GDTCORLifecycleTestEvent alloc] init]; + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 0); + [transport sendDataEvent:event]; + GDTCORWaitForBlock( + ^BOOL { + return [GDTCORStorage sharedInstance].storedEvents.count > 0; + }, + 5.0); + + NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; + [notifCenter postNotificationName:kGDTCORApplicationDidEnterBackgroundNotification object:nil]; + XCTAssertTrue([GDTCORUploadCoordinator sharedInstance].runningInBackground); + GDTCORWaitForBlock( + ^BOOL { + NSFileManager *fm = [NSFileManager defaultManager]; + return [fm fileExistsAtPath:[GDTCORStorage archivePath] isDirectory:NULL]; + }, + 5.0); +} + +/** Tests that the library deserializes itself from disk when the app foregrounds. */ +- (void)testForegrounding { + GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"test" + transformers:nil + target:kGDTCORTargetTest]; + GDTCOREvent *event = [transport eventForTransport]; + event.dataObject = [[GDTCORLifecycleTestEvent alloc] init]; + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 0); + [transport sendDataEvent:event]; + GDTCORWaitForBlock( + ^BOOL { + return [GDTCORStorage sharedInstance].storedEvents.count > 0; + }, + 5.0); + + NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; + [notifCenter postNotificationName:kGDTCORApplicationDidEnterBackgroundNotification object:nil]; + + GDTCORWaitForBlock( + ^BOOL { + NSFileManager *fm = [NSFileManager defaultManager]; + return [fm fileExistsAtPath:[GDTCORStorage archivePath] isDirectory:NULL]; + }, + 5.0); + + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + [notifCenter postNotificationName:kGDTCORApplicationWillEnterForegroundNotification object:nil]; + XCTAssertFalse([GDTCORUploadCoordinator sharedInstance].runningInBackground); + GDTCORWaitForBlock( + ^BOOL { + return [GDTCORStorage sharedInstance].storedEvents.count > 0; + }, + 5.0); +} + +/** Tests that the library gracefully stops doing stuff when terminating. */ +- (void)testTermination { + GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"test" + transformers:nil + target:kGDTCORTargetTest]; + GDTCOREvent *event = [transport eventForTransport]; + event.dataObject = [[GDTCORLifecycleTestEvent alloc] init]; + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 0); + [transport sendDataEvent:event]; + GDTCORWaitForBlock( + ^BOOL { + return [GDTCORStorage sharedInstance].storedEvents.count > 0; + }, + 5.0); + + NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; + [notifCenter postNotificationName:kGDTCORApplicationWillTerminateNotification object:nil]; + GDTCORWaitForBlock( + ^BOOL { + NSFileManager *fm = [NSFileManager defaultManager]; + return [fm fileExistsAtPath:[GDTCORStorage archivePath] isDirectory:NULL]; + }, + 5.0); +} + +@end diff --git a/GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.h b/GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestPrioritizer.h similarity index 84% rename from GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.h rename to GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestPrioritizer.h index 3b8a3055bb5..134d7b42f40 100644 --- a/GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.h +++ b/GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestPrioritizer.h @@ -16,9 +16,9 @@ #import -#import +#import /** An integration test prioritization class. */ -@interface GDTLifecycleTestPrioritizer : NSObject +@interface GDTCORLifecycleTestPrioritizer : NSObject @end diff --git a/GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.m b/GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestPrioritizer.m similarity index 56% rename from GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.m rename to GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestPrioritizer.m index 728fbc9d600..4afefef49e7 100644 --- a/GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.m +++ b/GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestPrioritizer.m @@ -14,50 +14,51 @@ * limitations under the License. */ -#import "GDTTests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.h" +#import "GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestPrioritizer.h" -#import +#import -@interface GDTLifecycleTestPrioritizer () +@interface GDTCORLifecycleTestPrioritizer () /** Events that are only supposed to be uploaded whilst on wifi. */ -@property(nonatomic) NSMutableSet *events; +@property(nonatomic) NSMutableSet *events; /** The queue on which this prioritizer operates. */ @property(nonatomic) dispatch_queue_t queue; @end -@implementation GDTLifecycleTestPrioritizer +@implementation GDTCORLifecycleTestPrioritizer - (instancetype)init { self = [super init]; if (self) { - _queue = dispatch_queue_create("com.google.GDTLifecycleTestPrioritizer", DISPATCH_QUEUE_SERIAL); + _queue = + dispatch_queue_create("com.google.GDTCORLifecycleTestPrioritizer", DISPATCH_QUEUE_SERIAL); _events = [[NSMutableSet alloc] init]; - [[GDTRegistrar sharedInstance] registerPrioritizer:self target:kGDTTargetTest]; + [[GDTCORRegistrar sharedInstance] registerPrioritizer:self target:kGDTCORTargetTest]; } return self; } -- (void)prioritizeEvent:(GDTStoredEvent *)event { +- (void)prioritizeEvent:(GDTCORStoredEvent *)event { dispatch_async(_queue, ^{ [self.events addObject:event]; }); } -- (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)conditions { - __block GDTUploadPackage *uploadPackage = - [[GDTUploadPackage alloc] initWithTarget:kGDTTargetTest]; +- (GDTCORUploadPackage *)uploadPackageWithConditions:(GDTCORUploadConditions)conditions { + __block GDTCORUploadPackage *uploadPackage = + [[GDTCORUploadPackage alloc] initWithTarget:kGDTCORTargetTest]; dispatch_sync(_queue, ^{ uploadPackage.events = self.events; }); return uploadPackage; } -- (void)packageDelivered:(GDTUploadPackage *)package successful:(BOOL)successful { +- (void)packageDelivered:(GDTCORUploadPackage *)package successful:(BOOL)successful { dispatch_async(_queue, ^{ - for (GDTStoredEvent *event in package.events) { + for (GDTCORStoredEvent *event in package.events) { [self.events removeObject:event]; } }); diff --git a/GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestUploader.h b/GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestUploader.h similarity index 78% rename from GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestUploader.h rename to GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestUploader.h index 97cee36a7c1..2e140655861 100644 --- a/GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestUploader.h +++ b/GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestUploader.h @@ -16,11 +16,11 @@ #import -#import +#import -#import "GDTTests/Lifecycle/Helpers/GDTLifecycleTestUploader.h" +#import "GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestUploader.h" /** An integration test uploader. */ -@interface GDTLifecycleTestUploader : NSObject +@interface GDTCORLifecycleTestUploader : NSObject @end diff --git a/GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestUploader.m b/GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestUploader.m similarity index 71% rename from GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestUploader.m rename to GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestUploader.m index c713adec662..c869e22aab7 100644 --- a/GoogleDataTransport/GDTTests/Lifecycle/Helpers/GDTLifecycleTestUploader.m +++ b/GoogleDataTransport/GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestUploader.m @@ -14,14 +14,14 @@ * limitations under the License. */ -#import "GDTTests/Lifecycle/Helpers/GDTLifecycleTestUploader.h" +#import "GDTCORTests/Lifecycle/Helpers/GDTCORLifecycleTestUploader.h" -@implementation GDTLifecycleTestUploader +@implementation GDTCORLifecycleTestUploader -- (void)uploadPackage:(GDTUploadPackage *)package { +- (void)uploadPackage:(GDTCORUploadPackage *)package { } -- (BOOL)readyToUploadWithConditions:(GDTUploadConditions)conditions { +- (BOOL)readyToUploadWithConditions:(GDTCORUploadConditions)conditions { return YES; } diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORAssertTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORAssertTest.m new file mode 100644 index 00000000000..0c3112d3989 --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORAssertTest.m @@ -0,0 +1,41 @@ +/* + * Copyright 2019 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. + */ + +#undef NS_BLOCK_ASSERTIONS + +#import + +#import + +@interface GDTCORAssertNotBlockedTest : XCTestCase + +@end + +@implementation GDTCORAssertNotBlockedTest + +/** Tests that asserting is innocuous and doesn't throw. */ +- (void)testNonFatallyAssertingDoesntThrow { + GDTCORAssert(NO, @"test assertion"); +} + +/** Tests that fatally asserting throws. */ +- (void)testFatallyAssertingThrows { + void (^assertionBlock)(void) = ^{ + GDTCORFatalAssert(NO, @"test assertion") + }; + XCTAssertThrows(assertionBlock()); +} +@end diff --git a/GoogleDataTransport/GDTTests/Unit/GDTClockTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORClockTest.m similarity index 74% rename from GoogleDataTransport/GDTTests/Unit/GDTClockTest.m rename to GoogleDataTransport/GDTCORTests/Unit/GDTCORClockTest.m index a6127267732..2bc0b757aeb 100644 --- a/GoogleDataTransport/GDTTests/Unit/GDTClockTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORClockTest.m @@ -14,49 +14,49 @@ * limitations under the License. */ -#import "GDTTests/Unit/GDTTestCase.h" +#import "GDTCORTests/Unit/GDTCORTestCase.h" -#import "GDTLibrary/Public/GDTClock.h" +#import "GDTCORLibrary/Public/GDTCORClock.h" -@interface GDTClockTest : GDTTestCase +@interface GDTCORClockTest : GDTCORTestCase @end -@implementation GDTClockTest +@implementation GDTCORClockTest /** Tests the default initializer. */ - (void)testInit { - XCTAssertNotNil([[GDTClockTest alloc] init]); + XCTAssertNotNil([[GDTCORClockTest alloc] init]); } /** Tests taking a snapshot. */ - (void)testSnapshot { - GDTClock *snapshot; - XCTAssertNoThrow(snapshot = [GDTClock snapshot]); + GDTCORClock *snapshot; + XCTAssertNoThrow(snapshot = [GDTCORClock snapshot]); XCTAssertGreaterThan(snapshot.timeMillis, 0); } /** Tests that the hash of two snapshots right after each other isn't equal. */ - (void)testHash { - GDTClock *snapshot1 = [GDTClock snapshot]; - GDTClock *snapshot2 = [GDTClock snapshot]; + GDTCORClock *snapshot1 = [GDTCORClock snapshot]; + GDTCORClock *snapshot2 = [GDTCORClock snapshot]; XCTAssertNotEqual([snapshot1 hash], [snapshot2 hash]); } /** Tests that the class supports NSSecureEncoding. */ - (void)testSupportsSecureEncoding { - XCTAssertTrue([GDTClock supportsSecureCoding]); + XCTAssertTrue([GDTCORClock supportsSecureCoding]); } /** Tests encoding and decoding a clock using a keyed archiver. */ - (void)testEncoding { - GDTClock *clock = [GDTClock snapshot]; - GDTClock *unarchivedClock; + GDTCORClock *clock = [GDTCORClock snapshot]; + GDTCORClock *unarchivedClock; if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { NSData *clockData = [NSKeyedArchiver archivedDataWithRootObject:clock requiringSecureCoding:YES error:nil]; - unarchivedClock = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTClock class] + unarchivedClock = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTCORClock class] fromData:clockData error:nil]; } else { @@ -71,16 +71,16 @@ - (void)testEncoding { /** Tests creating a clock that represents a future time. */ - (void)testClockSnapshotInTheFuture { - GDTClock *clock1 = [GDTClock snapshot]; - GDTClock *clock2 = [GDTClock clockSnapshotInTheFuture:1]; + GDTCORClock *clock1 = [GDTCORClock snapshot]; + GDTCORClock *clock2 = [GDTCORClock clockSnapshotInTheFuture:1]; XCTAssertTrue([clock2 isAfter:clock1]); } /** Tests creating a snapshot in the future and comparing using isAfter: */ - (void)testIsAfter { - GDTClock *clock1 = [GDTClock clockSnapshotInTheFuture:123456]; + GDTCORClock *clock1 = [GDTCORClock clockSnapshotInTheFuture:123456]; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5.0]]; - GDTClock *clock2 = [GDTClock snapshot]; + GDTCORClock *clock2 = [GDTCORClock snapshot]; XCTAssertFalse([clock2 isAfter:clock1]); } diff --git a/GoogleDataTransport/GDTTests/Unit/GDTDataFutureTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORDataFutureTest.m similarity index 70% rename from GoogleDataTransport/GDTTests/Unit/GDTDataFutureTest.m rename to GoogleDataTransport/GDTCORTests/Unit/GDTCORDataFutureTest.m index 5ba75f687a6..51e3cbb7ffe 100644 --- a/GoogleDataTransport/GDTTests/Unit/GDTDataFutureTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORDataFutureTest.m @@ -14,23 +14,23 @@ * limitations under the License. */ -#import "GDTTests/Unit/GDTTestCase.h" +#import "GDTCORTests/Unit/GDTCORTestCase.h" -#import "GDTLibrary/Public/GDTDataFuture.h" +#import "GDTCORLibrary/Public/GDTCORDataFuture.h" -@interface GDTDataFutureTest : GDTTestCase +@interface GDTCORDataFutureTest : GDTCORTestCase @end -@implementation GDTDataFutureTest +@implementation GDTCORDataFutureTest /** Tests the default initializer. */ - (void)testInit { - XCTAssertNotNil([[GDTDataFuture alloc] init]); + XCTAssertNotNil([[GDTCORDataFuture alloc] init]); } - (void)testSecureCodingSupport { - XCTAssertTrue([GDTDataFuture supportsSecureCoding]); + XCTAssertTrue([GDTCORDataFuture supportsSecureCoding]); } @end diff --git a/GoogleDataTransport/GDTTests/Unit/GDTEventTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCOREventTest.m similarity index 76% rename from GoogleDataTransport/GDTTests/Unit/GDTEventTest.m rename to GoogleDataTransport/GDTCORTests/Unit/GDTCOREventTest.m index 3609e3aa50d..2c72f7ae5a7 100644 --- a/GoogleDataTransport/GDTTests/Unit/GDTEventTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCOREventTest.m @@ -14,33 +14,33 @@ * limitations under the License. */ -#import "GDTTests/Unit/GDTTestCase.h" +#import "GDTCORTests/Unit/GDTCORTestCase.h" -#import +#import -#import "GDTLibrary/Private/GDTEvent_Private.h" +#import "GDTCORLibrary/Private/GDTCOREvent_Private.h" -@interface GDTEventTest : GDTTestCase +@interface GDTCOREventTest : GDTCORTestCase @end -@implementation GDTEventTest +@implementation GDTCOREventTest /** Tests the designated initializer. */ - (void)testInit { - XCTAssertNotNil([[GDTEvent alloc] initWithMappingID:@"1" target:1]); - XCTAssertThrows([[GDTEvent alloc] initWithMappingID:@"" target:1]); + XCTAssertNotNil([[GDTCOREvent alloc] initWithMappingID:@"1" target:1]); + XCTAssertNil([[GDTCOREvent alloc] initWithMappingID:@"" target:1]); } /** Tests NSKeyedArchiver encoding and decoding. */ - (void)testArchiving { - XCTAssertTrue([GDTEvent supportsSecureCoding]); - GDTClock *clockSnapshot = [GDTClock snapshot]; + XCTAssertTrue([GDTCOREvent supportsSecureCoding]); + GDTCORClock *clockSnapshot = [GDTCORClock snapshot]; int64_t timeMillis = clockSnapshot.timeMillis; int64_t timezoneOffsetSeconds = clockSnapshot.timezoneOffsetSeconds; - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"testID" target:42]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"testID" target:42]; event.dataObjectTransportBytes = [@"someData" dataUsingEncoding:NSUTF8StringEncoding]; - event.qosTier = GDTEventQoSTelemetry; + event.qosTier = GDTCOREventQoSTelemetry; event.clockSnapshot = clockSnapshot; NSData *archiveData; @@ -55,9 +55,9 @@ - (void)testArchiving { } // To ensure that all the objects being retained by the original event are dealloc'd. event = nil; - GDTEvent *decodedEvent; + GDTCOREvent *decodedEvent; if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { - decodedEvent = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTEvent class] + decodedEvent = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTCOREvent class] fromData:archiveData error:nil]; } else { @@ -69,7 +69,7 @@ - (void)testArchiving { XCTAssertEqual(decodedEvent.target, 42); XCTAssertEqualObjects(decodedEvent.dataObjectTransportBytes, [@"someData" dataUsingEncoding:NSUTF8StringEncoding]); - XCTAssertEqual(decodedEvent.qosTier, GDTEventQoSTelemetry); + XCTAssertEqual(decodedEvent.qosTier, GDTCOREventQoSTelemetry); XCTAssertEqual(decodedEvent.clockSnapshot.timeMillis, timeMillis); XCTAssertEqual(decodedEvent.clockSnapshot.timezoneOffsetSeconds, timezoneOffsetSeconds); } diff --git a/GoogleDataTransport/GDTTests/Unit/GDTRegistrarTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORRegistrarTest.m similarity index 61% rename from GoogleDataTransport/GDTTests/Unit/GDTRegistrarTest.m rename to GoogleDataTransport/GDTCORTests/Unit/GDTCORRegistrarTest.m index c9d0f2b6cfa..cd70b1d2de5 100644 --- a/GoogleDataTransport/GDTTests/Unit/GDTRegistrarTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORRegistrarTest.m @@ -14,21 +14,21 @@ * limitations under the License. */ -#import "GDTTests/Unit/GDTTestCase.h" +#import "GDTCORTests/Unit/GDTCORTestCase.h" -#import +#import -#import "GDTLibrary/Private/GDTRegistrar_Private.h" -#import "GDTTests/Unit/Helpers/GDTTestPrioritizer.h" -#import "GDTTests/Unit/Helpers/GDTTestUploader.h" +#import "GDTCORLibrary/Private/GDTCORRegistrar_Private.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestUploader.h" -@interface GDTRegistrarTest : GDTTestCase +@interface GDTCORRegistrarTest : GDTCORTestCase -@property(nonatomic) GDTTarget target; +@property(nonatomic) GDTCORTarget target; @end -@implementation GDTRegistrarTest +@implementation GDTCORRegistrarTest - (void)setUp { [super setUp]; @@ -37,21 +37,21 @@ - (void)setUp { /** Tests the default initializer. */ - (void)testInit { - XCTAssertNotNil([[GDTRegistrarTest alloc] init]); + XCTAssertNotNil([[GDTCORRegistrarTest alloc] init]); } /** Test registering an uploader. */ - (void)testRegisterUpload { - GDTRegistrar *registrar = [GDTRegistrar sharedInstance]; - GDTTestUploader *uploader = [[GDTTestUploader alloc] init]; + GDTCORRegistrar *registrar = [GDTCORRegistrar sharedInstance]; + GDTCORTestUploader *uploader = [[GDTCORTestUploader alloc] init]; XCTAssertNoThrow([registrar registerUploader:uploader target:self.target]); XCTAssertEqual(uploader, registrar.targetToUploader[@(_target)]); } /** Test registering a prioritizer. */ - (void)testRegisterPrioritizer { - GDTRegistrar *registrar = [GDTRegistrar sharedInstance]; - GDTTestPrioritizer *prioritizer = [[GDTTestPrioritizer alloc] init]; + GDTCORRegistrar *registrar = [GDTCORRegistrar sharedInstance]; + GDTCORTestPrioritizer *prioritizer = [[GDTCORTestPrioritizer alloc] init]; XCTAssertNoThrow([registrar registerPrioritizer:prioritizer target:self.target]); XCTAssertEqual(prioritizer, registrar.targetToPrioritizer[@(_target)]); } diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORStorageTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORStorageTest.m new file mode 100644 index 00000000000..0093f8d0f2c --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORStorageTest.m @@ -0,0 +1,363 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GDTCORTests/Unit/GDTCORTestCase.h" + +#import +#import + +#import "GDTCORLibrary/Private/GDTCOREvent_Private.h" +#import "GDTCORLibrary/Private/GDTCORRegistrar_Private.h" +#import "GDTCORLibrary/Private/GDTCORStorage.h" +#import "GDTCORLibrary/Private/GDTCORStorage_Private.h" +#import "GDTCORLibrary/Public/GDTCORRegistrar.h" + +#import "GDTCORTests/Unit/Helpers/GDTCORAssertHelper.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestUploader.h" + +#import "GDTCORTests/Common/Fakes/GDTCORUploadCoordinatorFake.h" + +#import "GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.h" +#import "GDTCORTests/Common/Categories/GDTCORStorage+Testing.h" + +static NSInteger target = kGDTCORTargetCCT; + +@interface GDTCORStorageTest : GDTCORTestCase + +/** The test backend implementation. */ +@property(nullable, nonatomic) GDTCORTestUploader *testBackend; + +/** The test prioritizer implementation. */ +@property(nullable, nonatomic) GDTCORTestPrioritizer *testPrioritizer; + +/** The uploader fake. */ +@property(nonatomic) GDTCORUploadCoordinatorFake *uploaderFake; + +@end + +@implementation GDTCORStorageTest + +- (void)setUp { + [super setUp]; + self.testBackend = [[GDTCORTestUploader alloc] init]; + self.testPrioritizer = [[GDTCORTestPrioritizer alloc] init]; + [[GDTCORRegistrar sharedInstance] registerUploader:_testBackend target:target]; + [[GDTCORRegistrar sharedInstance] registerPrioritizer:_testPrioritizer target:target]; + self.uploaderFake = [[GDTCORUploadCoordinatorFake alloc] init]; + [GDTCORStorage sharedInstance].uploadCoordinator = self.uploaderFake; +} + +- (void)tearDown { + [super tearDown]; + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + }); + // Destroy these objects before the next test begins. + self.testBackend = nil; + self.testPrioritizer = nil; + [[GDTCORRegistrar sharedInstance] reset]; + [[GDTCORStorage sharedInstance] reset]; + [GDTCORStorage sharedInstance].uploadCoordinator = [GDTCORUploadCoordinator sharedInstance]; + self.uploaderFake = nil; +} + +/** Tests the singleton pattern. */ +- (void)testInit { + XCTAssertEqual([GDTCORStorage sharedInstance], [GDTCORStorage sharedInstance]); +} + +/** Tests storing an event. */ +- (void)testStoreEvent { + // event is autoreleased, and the pool needs to drain. + @autoreleasepool { + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; + event.clockSnapshot = [GDTCORClock snapshot]; + XCTAssertNoThrow([[GDTCORStorage sharedInstance] storeEvent:event]); + } + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 1); + XCTAssertEqual([GDTCORStorage sharedInstance].targetToEventSet[@(target)].count, 1); + NSURL *eventFile = [[GDTCORStorage sharedInstance].storedEvents lastObject].dataFuture.fileURL; + XCTAssertNotNil(eventFile); + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); + NSError *error; + XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:eventFile error:&error]); + XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); + }); +} + +/** Tests removing an event. */ +- (void)testRemoveEvent { + // event is autoreleased, and the pool needs to drain. + @autoreleasepool { + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; + event.clockSnapshot = [GDTCORClock snapshot]; + XCTAssertNoThrow([[GDTCORStorage sharedInstance] storeEvent:event]); + } + __block NSURL *eventFile; + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + eventFile = [[GDTCORStorage sharedInstance].storedEvents lastObject].dataFuture.fileURL; + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); + }); + [[GDTCORStorage sharedInstance] removeEvents:[GDTCORStorage sharedInstance].storedEvents.set]; + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 0); + XCTAssertEqual([GDTCORStorage sharedInstance].targetToEventSet[@(target)].count, 0); + }); +} + +/** Tests removing a set of events. */ +- (void)testRemoveEvents { + GDTCORStorage *storage = [GDTCORStorage sharedInstance]; + __block GDTCORStoredEvent *storedEvent1, *storedEvent2, *storedEvent3; + + // events are autoreleased, and the pool needs to drain. + @autoreleasepool { + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + event.dataObjectTransportBytes = [@"testString1" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNoThrow([storage storeEvent:event]); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + storedEvent1 = [storage.storedEvents lastObject]; + }); + + event = [[GDTCOREvent alloc] initWithMappingID:@"100" target:target]; + event.dataObjectTransportBytes = [@"testString2" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNoThrow([storage storeEvent:event]); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + storedEvent2 = [storage.storedEvents lastObject]; + }); + + event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + event.dataObjectTransportBytes = [@"testString3" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNoThrow([storage storeEvent:event]); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + storedEvent3 = [storage.storedEvents lastObject]; + }); + } + NSSet *eventSet = + [NSSet setWithObjects:storedEvent1, storedEvent2, storedEvent3, nil]; + [storage removeEvents:eventSet]; + dispatch_sync(storage.storageQueue, ^{ + XCTAssertFalse([storage.storedEvents containsObject:storedEvent1]); + XCTAssertFalse([storage.storedEvents containsObject:storedEvent2]); + XCTAssertFalse([storage.storedEvents containsObject:storedEvent3]); + XCTAssertEqual(storage.targetToEventSet[@(target)].count, 0); + for (GDTCORStoredEvent *event in eventSet) { + XCTAssertFalse( + [[NSFileManager defaultManager] fileExistsAtPath:event.dataFuture.fileURL.path]); + } + }); +} + +/** Tests storing a few different events. */ +- (void)testStoreMultipleEvents { + __block GDTCORStoredEvent *storedEvent1, *storedEvent2, *storedEvent3; + + // events are autoreleased, and the pool needs to drain. + @autoreleasepool { + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + event.dataObjectTransportBytes = [@"testString1" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNoThrow([[GDTCORStorage sharedInstance] storeEvent:event]); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + storedEvent1 = [[GDTCORStorage sharedInstance].storedEvents lastObject]; + }); + + event = [[GDTCOREvent alloc] initWithMappingID:@"100" target:target]; + event.dataObjectTransportBytes = [@"testString2" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNoThrow([[GDTCORStorage sharedInstance] storeEvent:event]); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + storedEvent2 = [[GDTCORStorage sharedInstance].storedEvents lastObject]; + }); + + event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + event.dataObjectTransportBytes = [@"testString3" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNoThrow([[GDTCORStorage sharedInstance] storeEvent:event]); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + storedEvent3 = [[GDTCORStorage sharedInstance].storedEvents lastObject]; + }); + } + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 3); + XCTAssertEqual([GDTCORStorage sharedInstance].targetToEventSet[@(target)].count, 3); + + NSURL *event1File = storedEvent1.dataFuture.fileURL; + XCTAssertNotNil(event1File); + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:event1File.path]); + NSError *error; + XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:event1File error:&error]); + XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); + + NSURL *event2File = storedEvent2.dataFuture.fileURL; + XCTAssertNotNil(event2File); + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:event2File.path]); + error = nil; + XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:event2File error:&error]); + XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); + + NSURL *event3File = storedEvent3.dataFuture.fileURL; + XCTAssertNotNil(event3File); + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:event3File.path]); + error = nil; + XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:event3File error:&error]); + XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); + }); +} + +/** Tests enforcing that a prioritizer does not retain an event in memory. */ +- (void)testEventDeallocationIsEnforced { + __weak GDTCOREvent *weakEvent; + GDTCORStoredEvent *storedEvent; + @autoreleasepool { + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + weakEvent = event; + event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; + event.clockSnapshot = [GDTCORClock snapshot]; + // Store the event and wait for the expectation. + [[GDTCORStorage sharedInstance] storeEvent:event]; + GDTCORDataFuture *dataFuture = + [[GDTCORDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:@"/test"]]; + storedEvent = [event storedEventWithDataFuture:dataFuture]; + } + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertNil(weakEvent); + XCTAssertNotNil(storedEvent); + }); + + NSURL *eventFile; + eventFile = [[GDTCORStorage sharedInstance].storedEvents lastObject].dataFuture.fileURL; + + // This isn't strictly necessary because of the -waitForExpectations above. + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); + }); + + // Ensure event was removed. + [[GDTCORStorage sharedInstance] removeEvents:[GDTCORStorage sharedInstance].storedEvents.set]; + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 0); + XCTAssertEqual([GDTCORStorage sharedInstance].targetToEventSet[@(target)].count, 0); + }); +} + +/** Tests encoding and decoding the storage singleton correctly. */ +- (void)testNSSecureCoding { + XCTAssertTrue([GDTCORStorage supportsSecureCoding]); + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + event.clockSnapshot = [GDTCORClock snapshot]; + event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNoThrow([[GDTCORStorage sharedInstance] storeEvent:event]); + event = nil; + __block NSData *storageData; + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { + storageData = [NSKeyedArchiver archivedDataWithRootObject:[GDTCORStorage sharedInstance] + requiringSecureCoding:YES + error:nil]; + } else { +#if !defined(TARGET_OS_MACCATALYST) + storageData = [NSKeyedArchiver archivedDataWithRootObject:[GDTCORStorage sharedInstance]]; +#endif + } + }); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertNotNil([[GDTCORStorage sharedInstance].storedEvents lastObject]); + }); + [[GDTCORStorage sharedInstance] removeEvents:[GDTCORStorage sharedInstance].storedEvents.set]; + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertNil([[GDTCORStorage sharedInstance].storedEvents lastObject]); + }); + GDTCORStorage *unarchivedStorage; + NSError *error; + if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { + unarchivedStorage = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTCORStorage class] + fromData:storageData + error:&error]; + } else { +#if !defined(TARGET_OS_MACCATALYST) + unarchivedStorage = [NSKeyedUnarchiver unarchiveObjectWithData:storageData]; +#endif + } + XCTAssertNotNil([unarchivedStorage.storedEvents lastObject]); +} + +/** Tests encoding and decoding the storage singleton when calling -sharedInstance. */ +- (void)testNSSecureCodingWithSharedInstance { + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; + event.clockSnapshot = [GDTCORClock snapshot]; + XCTAssertNoThrow([[GDTCORStorage sharedInstance] storeEvent:event]); + event = nil; + __block NSData *storageData; + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { + storageData = [NSKeyedArchiver archivedDataWithRootObject:[GDTCORStorage sharedInstance] + requiringSecureCoding:YES + error:nil]; + } else { +#if !defined(TARGET_OS_MACCATALYST) + storageData = [NSKeyedArchiver archivedDataWithRootObject:[GDTCORStorage sharedInstance]]; +#endif + } + }); + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertNotNil([[GDTCORStorage sharedInstance].storedEvents lastObject]); + }); + [[GDTCORStorage sharedInstance] removeEvents:[GDTCORStorage sharedInstance].storedEvents.set]; + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertNil([[GDTCORStorage sharedInstance].storedEvents lastObject]); + }); + GDTCORStorage *unarchivedStorage; + if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { + unarchivedStorage = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTCORStorage class] + fromData:storageData + error:nil]; + } else { +#if !defined(TARGET_OS_MACCATALYST) + unarchivedStorage = [NSKeyedUnarchiver unarchiveObjectWithData:storageData]; +#endif + } + XCTAssertNotNil([unarchivedStorage.storedEvents lastObject]); +} + +/** Tests sending a fast priority event causes an upload attempt. */ +- (void)testQoSTierFast { + // event is autoreleased, and the pool needs to drain. + @autoreleasepool { + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"404" target:target]; + event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; + event.qosTier = GDTCOREventQoSFast; + event.clockSnapshot = [GDTCORClock snapshot]; + XCTAssertFalse(self.uploaderFake.forceUploadCalled); + XCTAssertNoThrow([[GDTCORStorage sharedInstance] storeEvent:event]); + } + dispatch_sync([GDTCORStorage sharedInstance].storageQueue, ^{ + XCTAssertTrue(self.uploaderFake.forceUploadCalled); + XCTAssertEqual([GDTCORStorage sharedInstance].storedEvents.count, 1); + XCTAssertEqual([GDTCORStorage sharedInstance].targetToEventSet[@(target)].count, 1); + NSURL *eventFile = [[GDTCORStorage sharedInstance].storedEvents lastObject].dataFuture.fileURL; + XCTAssertNotNil(eventFile); + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); + NSError *error; + XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:eventFile error:&error]); + XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); + }); +} + +@end diff --git a/GoogleDataTransport/GDTTests/Unit/GDTStoredEventTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORStoredEventTest.m similarity index 50% rename from GoogleDataTransport/GDTTests/Unit/GDTStoredEventTest.m rename to GoogleDataTransport/GDTCORTests/Unit/GDTCORStoredEventTest.m index d18e46200ce..c75cb11355d 100644 --- a/GoogleDataTransport/GDTTests/Unit/GDTStoredEventTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORStoredEventTest.m @@ -14,70 +14,74 @@ * limitations under the License. */ -#import "GDTTests/Unit/GDTTestCase.h" +#import "GDTCORTests/Unit/GDTCORTestCase.h" -#import -#import +#import +#import -@interface GDTStoredEventTest : GDTTestCase +@interface GDTCORStoredEventTest : GDTCORTestCase @end -@implementation GDTStoredEventTest +@implementation GDTCORStoredEventTest /** Tests the default initializer. */ - (void)testInit { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"testing" target:1]; - event.clockSnapshot = [GDTClock snapshot]; - GDTDataFuture *dataFuture = [[GDTDataFuture alloc] initWithFileURL:[NSURL URLWithString:@"1"]]; - GDTStoredEvent *storedEvent = [[GDTStoredEvent alloc] initWithEvent:event dataFuture:dataFuture]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"testing" target:1]; + event.clockSnapshot = [GDTCORClock snapshot]; + GDTCORDataFuture *dataFuture = + [[GDTCORDataFuture alloc] initWithFileURL:[NSURL URLWithString:@"1"]]; + GDTCORStoredEvent *storedEvent = [[GDTCORStoredEvent alloc] initWithEvent:event + dataFuture:dataFuture]; XCTAssertNotNil(storedEvent); } /** Tests encoding and decoding. */ - (void)testNSSecureCoding { - XCTAssertTrue([GDTStoredEvent supportsSecureCoding]); - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"testing" target:1]; - event.clockSnapshot = [GDTClock snapshot]; - event.qosTier = GDTEventQoSTelemetry; - GDTDataFuture *dataFuture = [[GDTDataFuture alloc] initWithFileURL:[NSURL URLWithString:@"1"]]; - GDTStoredEvent *storedEvent = [[GDTStoredEvent alloc] initWithEvent:event dataFuture:dataFuture]; + XCTAssertTrue([GDTCORStoredEvent supportsSecureCoding]); + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"testing" target:1]; + event.clockSnapshot = [GDTCORClock snapshot]; + event.qosTier = GDTCOREventQoSTelemetry; + GDTCORDataFuture *dataFuture = + [[GDTCORDataFuture alloc] initWithFileURL:[NSURL URLWithString:@"1"]]; + GDTCORStoredEvent *storedEvent = [[GDTCORStoredEvent alloc] initWithEvent:event + dataFuture:dataFuture]; XCTAssertNotNil(storedEvent); XCTAssertNotNil(storedEvent.mappingID); XCTAssertNotNil(storedEvent.target); - XCTAssertEqual(storedEvent.qosTier, GDTEventQoSTelemetry); + XCTAssertEqual(storedEvent.qosTier, GDTCOREventQoSTelemetry); XCTAssertNotNil(storedEvent.clockSnapshot); XCTAssertNil(storedEvent.customPrioritizationParams); XCTAssertNotNil(storedEvent.dataFuture.fileURL); } -/** Tests equality between GDTStoredEvents. */ +/** Tests equality between GDTCORStoredEvents. */ - (void)testIsEqualAndHash { - GDTEvent *event1 = [[GDTEvent alloc] initWithMappingID:@"1018" target:1]; - event1.clockSnapshot = [GDTClock snapshot]; + GDTCOREvent *event1 = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:1]; + event1.clockSnapshot = [GDTCORClock snapshot]; [event1.clockSnapshot setValue:@(1553534573010) forKeyPath:@"timeMillis"]; [event1.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; [event1.clockSnapshot setValue:@(1552576634359451) forKeyPath:@"kernelBootTime"]; [event1.clockSnapshot setValue:@(961141365197) forKeyPath:@"uptime"]; - event1.qosTier = GDTEventQosDefault; + event1.qosTier = GDTCOREventQosDefault; event1.customPrioritizationParams = @{@"customParam1" : @"aValue1"}; - GDTDataFuture *dataFuture1 = - [[GDTDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:@"/tmp/fake.txt"]]; - GDTStoredEvent *storedEvent1 = [[GDTStoredEvent alloc] initWithEvent:event1 - dataFuture:dataFuture1]; + GDTCORDataFuture *dataFuture1 = + [[GDTCORDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:@"/tmp/fake.txt"]]; + GDTCORStoredEvent *storedEvent1 = [[GDTCORStoredEvent alloc] initWithEvent:event1 + dataFuture:dataFuture1]; - GDTEvent *event2 = [[GDTEvent alloc] initWithMappingID:@"1018" target:1]; - event2.clockSnapshot = [GDTClock snapshot]; + GDTCOREvent *event2 = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:1]; + event2.clockSnapshot = [GDTCORClock snapshot]; [event2.clockSnapshot setValue:@(1553534573010) forKeyPath:@"timeMillis"]; [event2.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; [event2.clockSnapshot setValue:@(1552576634359451) forKeyPath:@"kernelBootTime"]; [event2.clockSnapshot setValue:@(961141365197) forKeyPath:@"uptime"]; - event2.qosTier = GDTEventQosDefault; + event2.qosTier = GDTCOREventQosDefault; event2.customPrioritizationParams = @{@"customParam1" : @"aValue1"}; - GDTDataFuture *dataFuture2 = - [[GDTDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:@"/tmp/fake.txt"]]; - GDTStoredEvent *storedEvent2 = [[GDTStoredEvent alloc] initWithEvent:event2 - dataFuture:dataFuture2]; + GDTCORDataFuture *dataFuture2 = + [[GDTCORDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:@"/tmp/fake.txt"]]; + GDTCORStoredEvent *storedEvent2 = [[GDTCORStoredEvent alloc] initWithEvent:event2 + dataFuture:dataFuture2]; XCTAssertEqual([storedEvent1 hash], [storedEvent2 hash]); XCTAssertEqualObjects(storedEvent1, storedEvent2); diff --git a/GoogleDataTransport/GDTTests/Unit/GDTTestCase.h b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.h similarity index 89% rename from GoogleDataTransport/GDTTests/Unit/GDTTestCase.h rename to GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.h index 2a28ff1d494..0e131c8acd8 100644 --- a/GoogleDataTransport/GDTTests/Unit/GDTTestCase.h +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.h @@ -16,13 +16,13 @@ #import -#import "GDTTests/Unit/Helpers/GDTAssertHelper.h" +#import "GDTCORTests/Unit/Helpers/GDTCORAssertHelper.h" NS_ASSUME_NONNULL_BEGIN /** This class defines shared testing infrastructure across all unit tests in GoogleDataTransport. */ -@interface GDTTestCase : XCTestCase +@interface GDTCORTestCase : XCTestCase @end diff --git a/GoogleDataTransport/GDTTests/Unit/GDTTestCase.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.m similarity index 58% rename from GoogleDataTransport/GDTTests/Unit/GDTTestCase.m rename to GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.m index 10f3edc857e..5e2f7bf19c0 100644 --- a/GoogleDataTransport/GDTTests/Unit/GDTTestCase.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTestCase.m @@ -14,22 +14,22 @@ * limitations under the License. */ -#import "GDTTests/Unit/GDTTestCase.h" +#import "GDTCORTests/Unit/GDTCORTestCase.h" -#import "GDTTests/Common/Categories/GDTUploadCoordinator+Testing.h" +#import "GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.h" -#import "GDTLibrary/Private/GDTReachability_Private.h" +#import "GDTCORLibrary/Private/GDTCORReachability_Private.h" -@implementation GDTTestCase +@implementation GDTCORTestCase - (void)setUp { - [GDTReachability sharedInstance].flags = kSCNetworkReachabilityFlagsReachable; - [[GDTUploadCoordinator sharedInstance] stopTimer]; - [[GDTUploadCoordinator sharedInstance] reset]; + [GDTCORReachability sharedInstance].flags = kSCNetworkReachabilityFlagsReachable; + [[GDTCORUploadCoordinator sharedInstance] stopTimer]; + [[GDTCORUploadCoordinator sharedInstance] reset]; } - (void)tearDown { - [GDTAssertHelper setAssertionBlock:nil]; + [GDTCORAssertHelper setAssertionBlock:nil]; } @end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransformerTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransformerTest.m new file mode 100644 index 00000000000..6dc4d93668a --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransformerTest.m @@ -0,0 +1,113 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GDTCORTests/Unit/GDTCORTestCase.h" + +#import +#import + +#import "GDTCORLibrary/Private/GDTCORStorage.h" +#import "GDTCORLibrary/Private/GDTCORTransformer.h" +#import "GDTCORLibrary/Private/GDTCORTransformer_Private.h" + +#import "GDTCORTests/Unit/Helpers/GDTCORAssertHelper.h" +#import "GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.h" + +#import "GDTCORTests/Common/Fakes/GDTCORStorageFake.h" + +@interface GDTCORTransformerTestNilingTransformer : NSObject + +@end + +@implementation GDTCORTransformerTestNilingTransformer + +- (GDTCOREvent *)transform:(GDTCOREvent *)eventEvent { + return nil; +} + +@end + +@interface GDTCORTransformerTestNewEventTransformer : NSObject + +@end + +@implementation GDTCORTransformerTestNewEventTransformer + +- (GDTCOREvent *)transform:(GDTCOREvent *)eventEvent { + return [[GDTCOREvent alloc] initWithMappingID:@"new" target:1]; +} + +@end + +@interface GDTCORTransformerTest : GDTCORTestCase + +@end + +@implementation GDTCORTransformerTest + +- (void)setUp { + [super setUp]; + dispatch_sync([GDTCORTransformer sharedInstance].eventWritingQueue, ^{ + [GDTCORTransformer sharedInstance].storageInstance = [[GDTCORStorageFake alloc] init]; + }); +} + +- (void)tearDown { + [super tearDown]; + dispatch_sync([GDTCORTransformer sharedInstance].eventWritingQueue, ^{ + [GDTCORTransformer sharedInstance].storageInstance = [GDTCORStorage sharedInstance]; + }); +} + +/** Tests the default initializer. */ +- (void)testInit { + XCTAssertNotNil([[GDTCORTransformer alloc] init]); +} + +/** Tests the pointer equality of result of the -sharedInstance method. */ +- (void)testSharedInstance { + XCTAssertEqual([GDTCORTransformer sharedInstance], [GDTCORTransformer sharedInstance]); +} + +/** Tests writing a event without a transformer. */ +- (void)testWriteEventWithoutTransformers { + GDTCORTransformer *transformer = [GDTCORTransformer sharedInstance]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1" target:1]; + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] init]; + XCTAssertNoThrow([transformer transformEvent:event withTransformers:nil]); +} + +/** Tests writing a event with a transformer that nils out the event. */ +- (void)testWriteEventWithTransformersThatNilTheEvent { + GDTCORTransformer *transformer = [GDTCORTransformer sharedInstance]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"2" target:1]; + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] init]; + NSArray> *transformers = + @[ [[GDTCORTransformerTestNilingTransformer alloc] init] ]; + XCTAssertNoThrow([transformer transformEvent:event withTransformers:transformers]); +} + +/** Tests writing a event with a transformer that creates a new event. */ +- (void)testWriteEventWithTransformersThatCreateANewEvent { + GDTCORTransformer *transformer = [GDTCORTransformer sharedInstance]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"2" target:1]; + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] init]; + NSArray> *transformers = + @[ [[GDTCORTransformerTestNewEventTransformer alloc] init] ]; + XCTAssertNoThrow([transformer transformEvent:event withTransformers:transformers]); +} + +@end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransportTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransportTest.m new file mode 100644 index 00000000000..8d823823b24 --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORTransportTest.m @@ -0,0 +1,61 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GDTCORTests/Unit/GDTCORTestCase.h" + +#import +#import + +#import "GDTCORLibrary/Private/GDTCORTransport_Private.h" + +#import "GDTCORTests/Common/Fakes/GDTCORTransformerFake.h" +#import "GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.h" + +@interface GDTCORTransportTest : GDTCORTestCase + +@end + +@implementation GDTCORTransportTest + +/** Tests the default initializer. */ +- (void)testInit { + XCTAssertNotNil([[GDTCORTransport alloc] initWithMappingID:@"1" transformers:nil target:1]); + XCTAssertNil([[GDTCORTransport alloc] initWithMappingID:@"" transformers:nil target:1]); +} + +/** Tests sending a telemetry event. */ +- (void)testSendTelemetryEvent { + GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"1" + transformers:nil + target:1]; + transport.transformerInstance = [[GDTCORTransformerFake alloc] init]; + GDTCOREvent *event = [transport eventForTransport]; + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] init]; + XCTAssertNoThrow([transport sendTelemetryEvent:event]); +} + +/** Tests sending a data event. */ +- (void)testSendDataEvent { + GDTCORTransport *transport = [[GDTCORTransport alloc] initWithMappingID:@"1" + transformers:nil + target:1]; + transport.transformerInstance = [[GDTCORTransformerFake alloc] init]; + GDTCOREvent *event = [transport eventForTransport]; + event.dataObject = [[GDTCORDataObjectTesterSimple alloc] init]; + XCTAssertNoThrow([transport sendDataEvent:event]); +} + +@end diff --git a/GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadCoordinatorTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadCoordinatorTest.m new file mode 100644 index 00000000000..77748a648e9 --- /dev/null +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadCoordinatorTest.m @@ -0,0 +1,163 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "GDTCORTests/Unit/GDTCORTestCase.h" + +#import "GDTCORLibrary/Private/GDTCORUploadCoordinator.h" + +#import "GDTCORTests/Common/Categories/GDTCORRegistrar+Testing.h" +#import "GDTCORTests/Common/Categories/GDTCORUploadCoordinator+Testing.h" + +#import "GDTCORTests/Common/Fakes/GDTCORStorageFake.h" + +#import "GDTCORTests/Unit/Helpers/GDTCOREventGenerator.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestUploader.h" + +@interface GDTCORUploadCoordinatorTest : GDTCORTestCase + +/** A storage fake to inject into GDTCORUploadCoordinator. */ +@property(nonatomic) GDTCORStorageFake *storageFake; + +/** A test prioritizer. */ +@property(nonatomic) GDTCORTestPrioritizer *prioritizer; + +/** A test uploader. */ +@property(nonatomic) GDTCORTestUploader *uploader; + +@end + +@implementation GDTCORUploadCoordinatorTest + +- (void)setUp { + [super setUp]; + self.storageFake = [[GDTCORStorageFake alloc] init]; + self.prioritizer = [[GDTCORTestPrioritizer alloc] init]; + self.uploader = [[GDTCORTestUploader alloc] init]; + + [[GDTCORRegistrar sharedInstance] registerPrioritizer:_prioritizer target:kGDTCORTargetTest]; + [[GDTCORRegistrar sharedInstance] registerUploader:_uploader target:kGDTCORTargetTest]; + + GDTCORUploadCoordinator *uploadCoordinator = [GDTCORUploadCoordinator sharedInstance]; + uploadCoordinator.storage = self.storageFake; + uploadCoordinator.timerInterval = NSEC_PER_SEC; + uploadCoordinator.timerLeeway = 0; +} + +- (void)tearDown { + [super tearDown]; + [[GDTCORUploadCoordinator sharedInstance] reset]; + [[GDTCORRegistrar sharedInstance] reset]; + self.storageFake = nil; + self.prioritizer = nil; + self.uploader = nil; +} + +/** Tests the default initializer. */ +- (void)testSharedInstance { + XCTAssertEqual([GDTCORUploadCoordinator sharedInstance], + [GDTCORUploadCoordinator sharedInstance]); +} + +/** Tests that forcing a event upload works. */ +- (void)testForceUploadEvents { + self.prioritizer.events = [GDTCOREventGenerator generate3StoredEvents]; + XCTestExpectation *expectation = [self expectationWithDescription:@"uploader will upload"]; + self.uploader.uploadPackageBlock = ^(GDTCORUploadPackage *_Nonnull package) { + [expectation fulfill]; + }; + XCTAssertNoThrow( + [[GDTCORUploadCoordinator sharedInstance] forceUploadForTarget:kGDTCORTargetTest]); + [self waitForExpectations:@[ expectation ] timeout:1.0]; +} + +/** Tests the timer is running at the desired frequency. */ +- (void)testTimerIsRunningAtDesiredFrequency { + __block int numberOfTimesCalled = 0; + self.prioritizer.uploadPackageWithConditionsBlock = ^{ + numberOfTimesCalled++; + }; + dispatch_sync([GDTCORUploadCoordinator sharedInstance].coordinationQueue, ^{ + // Timer should fire 1 times a second. + [GDTCORUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC; + [GDTCORUploadCoordinator sharedInstance].timerLeeway = 0; + }); + [[GDTCORUploadCoordinator sharedInstance] startTimer]; + + // Run for 5 seconds. + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]]; + + // It's expected that the timer called the prioritizer 5 times +/- 1 during that 1 second + the + // coordinator running before that. + dispatch_sync([GDTCORUploadCoordinator sharedInstance].coordinationQueue, ^{ + XCTAssertGreaterThan(numberOfTimesCalled, 4); // Some latency is expected on a busy system. + }); +} + +/** Tests uploading events via the coordinator timer. */ +- (void)testUploadingEventsViaTimer { + __block int uploadAttempts = 0; + self.prioritizer.events = [GDTCOREventGenerator generate3StoredEvents]; + self.uploader.uploadPackageBlock = ^(GDTCORUploadPackage *_Nonnull package) { + [package completeDelivery]; + uploadAttempts++; + }; + [GDTCORUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC / 10; + [GDTCORUploadCoordinator sharedInstance].timerLeeway = 0; + + [[GDTCORUploadCoordinator sharedInstance] startTimer]; + + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + dispatch_sync([GDTCORUploadCoordinator sharedInstance].coordinationQueue, ^{ + // More than two attempts should have been made. + XCTAssertGreaterThan(uploadAttempts, 2); + }); +} + +/** Tests the situation in which the uploader failed to upload the events for some reason. */ +- (void)testThatAFailedUploadResultsInAnEventualRetry { + __block int uploadAttempts = 0; + self.prioritizer.events = [GDTCOREventGenerator generate3StoredEvents]; + self.uploader.uploadPackageBlock = ^(GDTCORUploadPackage *_Nonnull package) { + [package retryDeliveryInTheFuture]; + uploadAttempts++; + }; + [GDTCORUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC / 10; + [GDTCORUploadCoordinator sharedInstance].timerLeeway = 0; + + [[GDTCORUploadCoordinator sharedInstance] startTimer]; + + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; + dispatch_sync([GDTCORUploadCoordinator sharedInstance].coordinationQueue, ^{ + // More than two attempts should have been made. + XCTAssertGreaterThan(uploadAttempts, 2); + }); +} + +/** Tests that encoding and decoding works without crashing. */ +- (void)testNSSecureCoding { + GDTCORUploadPackage *package = [[GDTCORUploadPackage alloc] initWithTarget:kGDTCORTargetTest]; + GDTCORUploadCoordinator *coordinator = [[GDTCORUploadCoordinator alloc] init]; + coordinator.targetToInFlightPackages[@(kGDTCORTargetTest)] = package; + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:coordinator]; + + // Unarchiving the coordinator always ends up altering the singleton instance. + GDTCORUploadCoordinator *unarchivedCoordinator = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + XCTAssertEqualObjects([GDTCORUploadCoordinator sharedInstance], unarchivedCoordinator); +} + +@end diff --git a/GoogleDataTransport/GDTTests/Unit/GDTUploadPackageTest.m b/GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadPackageTest.m similarity index 71% rename from GoogleDataTransport/GDTTests/Unit/GDTUploadPackageTest.m rename to GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadPackageTest.m index cdaeb0dc118..e12b6d48c3d 100644 --- a/GoogleDataTransport/GDTTests/Unit/GDTUploadPackageTest.m +++ b/GoogleDataTransport/GDTCORTests/Unit/GDTCORUploadPackageTest.m @@ -14,20 +14,20 @@ * limitations under the License. */ -#import "GDTTests/Unit/GDTTestCase.h" +#import "GDTCORTests/Unit/GDTCORTestCase.h" -#import -#import -#import +#import +#import +#import -#import "GDTLibrary/Private/GDTUploadPackage_Private.h" +#import "GDTCORLibrary/Private/GDTCORUploadPackage_Private.h" -#import "GDTTests/Unit/Helpers/GDTEventGenerator.h" -#import "GDTTests/Unit/Helpers/GDTTestPrioritizer.h" -#import "GDTTests/Unit/Helpers/GDTTestUploadPackage.h" -#import "GDTTests/Unit/Helpers/GDTTestUploader.h" +#import "GDTCORTests/Unit/Helpers/GDTCOREventGenerator.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestUploader.h" -@interface GDTUploadPackageTest : GDTTestCase +@interface GDTCORUploadPackageTest : GDTCORTestCase /** If YES, -packageDelivered:successful was called. */ @property(nonatomic) BOOL packageDeliveredCalledSuccessful; @@ -40,7 +40,7 @@ @interface GDTUploadPackageTest : GDTTestCase *set = [GDTEventGenerator generate3StoredEvents]; + NSMutableSet *set = [GDTCOREventGenerator generate3StoredEvents]; uploadPackage.events = set; uploadPackageCopy = [uploadPackage copy]; XCTAssertNotEqual(uploadPackage, uploadPackageCopy); - GDTStoredEvent *newEvent = [[GDTEventGenerator generate3StoredEvents] anyObject]; + GDTCORStoredEvent *newEvent = [[GDTCOREventGenerator generate3StoredEvents] anyObject]; [set addObject:newEvent]; XCTAssertFalse([uploadPackageCopy.events containsObject:newEvent]); XCTAssertNotEqualObjects(uploadPackage.events, uploadPackageCopy.events); @@ -106,18 +107,19 @@ - (void)testRegisterUpload { } - (void)testEncoding { - GDTUploadPackage *uploadPackage = [[GDTUploadPackage alloc] initWithTarget:kGDTTargetTest]; - NSMutableSet *set = [GDTEventGenerator generate3StoredEvents]; + GDTCORUploadPackage *uploadPackage = + [[GDTCORUploadPackage alloc] initWithTarget:kGDTCORTargetTest]; + NSMutableSet *set = [GDTCOREventGenerator generate3StoredEvents]; uploadPackage.events = set; uploadPackage.handler = self; - GDTUploadPackage *recreatedPackage; + GDTCORUploadPackage *recreatedPackage; NSError *error; if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { NSData *packageData = [NSKeyedArchiver archivedDataWithRootObject:uploadPackage requiringSecureCoding:YES error:&error]; - recreatedPackage = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTUploadPackage class] + recreatedPackage = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTCORUploadPackage class] fromData:packageData error:&error]; XCTAssertNil(error); @@ -132,8 +134,9 @@ - (void)testEncoding { - (void)testExpiration { XCTAssertFalse(self.packageExpiredCalled); - GDTUploadPackage *uploadPackage = [[GDTUploadPackage alloc] initWithTarget:kGDTTargetTest]; - uploadPackage.deliverByTime = [GDTClock clockSnapshotInTheFuture:1000]; + GDTCORUploadPackage *uploadPackage = + [[GDTCORUploadPackage alloc] initWithTarget:kGDTCORTargetTest]; + uploadPackage.deliverByTime = [GDTCORClock clockSnapshotInTheFuture:1000]; uploadPackage.handler = self; NSPredicate *pred = [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTAssertHelper.h b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORAssertHelper.h similarity index 67% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTAssertHelper.h rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORAssertHelper.h index 372d4c146ad..1ef6fb554cb 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTAssertHelper.h +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORAssertHelper.h @@ -16,15 +16,15 @@ #import -#import "GDTLibrary/Private/GDTAssert.h" +#import "GDTCORLibrary/Public/GDTCORAssert.h" NS_ASSUME_NONNULL_BEGIN -/** Allows the setting a block to be used in the GDTAssert macro instead of a call to NSAssert. */ -@interface GDTAssertHelper : NSObject +/** Allows the setting a block to be used in the GDTCORAssert macro instead of assertion log. */ +@interface GDTCORAssertHelper : NSObject -/** A class property that can be run instead of NSAssert. */ -@property(class, nullable, nonatomic) GDTAssertionBlock assertionBlock; +/** A class property that can be run instead of normal assertion logging. */ +@property(class, nullable, nonatomic) GDTCORAssertionBlock assertionBlock; @end diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTAssertHelper.m b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORAssertHelper.m similarity index 74% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTAssertHelper.m rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORAssertHelper.m index 8ee49a48c6f..9ceb4df83c6 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTAssertHelper.m +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORAssertHelper.m @@ -14,18 +14,18 @@ * limitations under the License. */ -#import "GDTTests/Unit/Helpers/GDTAssertHelper.h" +#import "GDTCORTests/Unit/Helpers/GDTCORAssertHelper.h" -@implementation GDTAssertHelper +@implementation GDTCORAssertHelper // The backing store for the class variable assertionBlock. -static GDTAssertionBlock gSharedAssertionBlock; +static GDTCORAssertionBlock gSharedAssertionBlock; -+ (GDTAssertionBlock)assertionBlock { ++ (GDTCORAssertionBlock)assertionBlock { return gSharedAssertionBlock; } -+ (void)setAssertionBlock:(GDTAssertionBlock)assertionBlock { ++ (void)setAssertionBlock:(GDTCORAssertionBlock)assertionBlock { gSharedAssertionBlock = assertionBlock; } diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTDataObjectTesterClasses.h b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.h similarity index 86% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTDataObjectTesterClasses.h rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.h index c5c46d097cf..207408df799 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTDataObjectTesterClasses.h +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.h @@ -16,12 +16,12 @@ #import -#import +#import NS_ASSUME_NONNULL_BEGIN /** A class to represent a simple data object proto. */ -@interface GDTDataObjectTesterSimple : NSObject +@interface GDTCORDataObjectTesterSimple : NSObject /** A string that will be turned into bytes. */ @property(nonatomic) NSString *aString; diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTDataObjectTesterClasses.m b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.m similarity index 87% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTDataObjectTesterClasses.m rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.m index f43fc8c2dec..fb41e29e302 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTDataObjectTesterClasses.m +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.m @@ -14,9 +14,9 @@ * limitations under the License. */ -#import "GDTTests/Unit/Helpers/GDTDataObjectTesterClasses.h" +#import "GDTCORTests/Unit/Helpers/GDTCORDataObjectTesterClasses.h" -@implementation GDTDataObjectTesterSimple +@implementation GDTCORDataObjectTesterSimple - (instancetype)init { self = [super init]; diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTEventGenerator.h b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCOREventGenerator.h similarity index 85% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTEventGenerator.h rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCOREventGenerator.h index 07bbb80a5bb..79b269b0791 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTEventGenerator.h +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCOREventGenerator.h @@ -15,17 +15,17 @@ */ #import -@class GDTStoredEvent; +@class GDTCORStoredEvent; NS_ASSUME_NONNULL_BEGIN -@interface GDTEventGenerator : NSObject +@interface GDTCOREventGenerator : NSObject /** Generates 3 stored events with consecutive clock snapshots. * * @return A set of 3 generated stored events. */ -+ (NSMutableSet *)generate3StoredEvents; ++ (NSMutableSet *)generate3StoredEvents; @end diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTEventGenerator.m b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCOREventGenerator.m similarity index 64% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTEventGenerator.m rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCOREventGenerator.m index 7cd1bb815df..fec3f484868 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTEventGenerator.m +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCOREventGenerator.m @@ -14,34 +14,35 @@ * limitations under the License. */ -#import "GDTTests/Unit/Helpers/GDTEventGenerator.h" +#import "GDTCORTests/Unit/Helpers/GDTCOREventGenerator.h" -#import -#import -#import +#import +#import +#import -#import "GDTLibrary/Private/GDTEvent_Private.h" +#import "GDTCORLibrary/Private/GDTCOREvent_Private.h" -@implementation GDTEventGenerator +@implementation GDTCOREventGenerator -+ (NSMutableSet *)generate3StoredEvents { ++ (NSMutableSet *)generate3StoredEvents { static NSUInteger counter = 0; NSString *cachePath = NSTemporaryDirectory(); NSString *filePath = [cachePath stringByAppendingPathComponent:[NSString stringWithFormat:@"test-%ld.txt", (unsigned long)counter]]; int howManyToGenerate = 3; - NSMutableSet *set = [[NSMutableSet alloc] initWithCapacity:howManyToGenerate]; + NSMutableSet *set = + [[NSMutableSet alloc] initWithCapacity:howManyToGenerate]; for (int i = 0; i < howManyToGenerate; i++) { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"1337" target:50]; - event.clockSnapshot = [GDTClock snapshot]; - event.qosTier = GDTEventQosDefault; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1337" target:50]; + event.clockSnapshot = [GDTCORClock snapshot]; + event.qosTier = GDTCOREventQosDefault; event.dataObjectTransportBytes = [@"testing!" dataUsingEncoding:NSUTF8StringEncoding]; [[NSFileManager defaultManager] createFileAtPath:filePath contents:[NSData data] attributes:nil]; - GDTDataFuture *dataFuture = - [[GDTDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:filePath]]; + GDTCORDataFuture *dataFuture = + [[GDTCORDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:filePath]]; [set addObject:[event storedEventWithDataFuture:dataFuture]]; counter++; } diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestPrioritizer.h b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h similarity index 81% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestPrioritizer.h rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h index b57fbb3b68d..88b6fd0d618 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestPrioritizer.h +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h @@ -16,20 +16,20 @@ #import -#import +#import NS_ASSUME_NONNULL_BEGIN /** This class implements the event prioritizer protocol for testing purposes, providing APIs to * allow tests to alter the prioritizer behavior without creating a bunch of specialized classes. */ -@interface GDTTestPrioritizer : NSObject +@interface GDTCORTestPrioritizer : NSObject /** The events in the package given by -uploadPackageWithConditions. */ -@property(nullable, nonatomic) NSSet *events; +@property(nullable, nonatomic) NSSet *events; /** Allows the running of a block of code during -prioritizeEvent. */ -@property(nullable, nonatomic) void (^prioritizeEventBlock)(GDTStoredEvent *event); +@property(nullable, nonatomic) void (^prioritizeEventBlock)(GDTCORStoredEvent *event); /** A block that can run before -uploadPackageWithConditions completes. */ @property(nullable, nonatomic) void (^uploadPackageWithConditionsBlock)(void); diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestPrioritizer.m b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.m similarity index 66% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestPrioritizer.m rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.m index 47f1d90f580..f250e74b1b0 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestPrioritizer.m +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.m @@ -14,22 +14,23 @@ * limitations under the License. */ -#import "GDTTests/Unit/Helpers/GDTTestPrioritizer.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h" -#import "GDTTests/Unit/Helpers/GDTTestUploadPackage.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.h" -@implementation GDTTestPrioritizer +@implementation GDTCORTestPrioritizer -- (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)conditions { +- (GDTCORUploadPackage *)uploadPackageWithConditions:(GDTCORUploadConditions)conditions { if (_uploadPackageWithConditionsBlock) { _uploadPackageWithConditionsBlock(); } - GDTUploadPackage *uploadPackage = [[GDTUploadPackage alloc] initWithTarget:kGDTTargetTest]; + GDTCORUploadPackage *uploadPackage = + [[GDTCORUploadPackage alloc] initWithTarget:kGDTCORTargetTest]; uploadPackage.events = _events; return uploadPackage; } -- (void)prioritizeEvent:(GDTStoredEvent *)event { +- (void)prioritizeEvent:(GDTCORStoredEvent *)event { if (_prioritizeEventBlock) { _prioritizeEventBlock(event); } diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploadPackage.h b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.h similarity index 84% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploadPackage.h rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.h index 453cfd60279..82fcb9a2676 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploadPackage.h +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.h @@ -14,9 +14,9 @@ * limitations under the License. */ -#import "GDTTests/Unit/Helpers/GDTTestPrioritizer.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestPrioritizer.h" /** An upload package used in testing. */ -@interface GDTTestUploadPackage : GDTUploadPackage +@interface GDTCORTestUploadPackage : GDTCORUploadPackage @end diff --git a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploadPackage.m b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.m similarity index 83% rename from GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploadPackage.m rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.m index 20d8a40a431..c3e9be5ea62 100644 --- a/GoogleDataTransport/GDTTests/Integration/Helpers/GDTIntegrationTestUploadPackage.m +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.m @@ -14,8 +14,8 @@ * limitations under the License. */ -#import "GDTTests/Integration/Helpers/GDTIntegrationTestUploadPackage.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestUploadPackage.h" -@implementation GDTIntegrationTestUploadPackage +@implementation GDTCORTestUploadPackage @end diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploader.h b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploader.h similarity index 80% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploader.h rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploader.h index e052772e232..6b07e474b46 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploader.h +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploader.h @@ -16,19 +16,19 @@ #import -#import +#import -@class GDTUploadPackage; +@class GDTCORUploadPackage; NS_ASSUME_NONNULL_BEGIN /** This class implements a backend uploader protocol for testing purposes, providing APIs to allow * tests to alter the uploader behavior without creating a bunch of specialized classes. */ -@interface GDTTestUploader : NSObject +@interface GDTCORTestUploader : NSObject /** A block that can be ran in -uploadPackage:. */ -@property(nullable, nonatomic) void (^uploadPackageBlock)(GDTUploadPackage *package); +@property(nullable, nonatomic) void (^uploadPackageBlock)(GDTCORUploadPackage *package); @end diff --git a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploader.m b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploader.m similarity index 76% rename from GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploader.m rename to GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploader.m index b9177234e5e..23a27ca3a50 100644 --- a/GoogleDataTransport/GDTTests/Unit/Helpers/GDTTestUploader.m +++ b/GoogleDataTransport/GDTCORTests/Unit/Helpers/GDTCORTestUploader.m @@ -14,11 +14,11 @@ * limitations under the License. */ -#import "GDTTests/Unit/Helpers/GDTTestUploader.h" +#import "GDTCORTests/Unit/Helpers/GDTCORTestUploader.h" -@implementation GDTTestUploader +@implementation GDTCORTestUploader -- (void)uploadPackage:(GDTUploadPackage *)package { +- (void)uploadPackage:(GDTCORUploadPackage *)package { if (_uploadPackageBlock) { _uploadPackageBlock(package); } else { @@ -26,7 +26,7 @@ - (void)uploadPackage:(GDTUploadPackage *)package { } } -- (BOOL)readyToUploadWithConditions:(GDTUploadConditions)conditions { +- (BOOL)readyToUploadWithConditions:(GDTCORUploadConditions)conditions { return YES; } diff --git a/GoogleDataTransport/GDTLibrary/GDTLifecycle.m b/GoogleDataTransport/GDTLibrary/GDTLifecycle.m deleted file mode 100644 index a49dcff0051..00000000000 --- a/GoogleDataTransport/GDTLibrary/GDTLifecycle.m +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2019 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 "GDTLibrary/Public/GDTLifecycle.h" - -#import - -#import "GDTLibrary/Private/GDTRegistrar_Private.h" -#import "GDTLibrary/Private/GDTStorage_Private.h" -#import "GDTLibrary/Private/GDTTransformer_Private.h" -#import "GDTLibrary/Private/GDTUploadCoordinator.h" - -@implementation GDTLifecycle - -+ (void)load { - [self sharedInstance]; -} - -/** Creates/returns the singleton instance of this class. - * - * @return The singleton instance of this class. - */ -+ (instancetype)sharedInstance { - static GDTLifecycle *sharedInstance; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[GDTLifecycle alloc] init]; - }); - return sharedInstance; -} - -- (instancetype)init { - self = [super init]; - if (self) { - NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; - [notificationCenter addObserver:self - selector:@selector(applicationDidEnterBackground:) - name:kGDTApplicationDidEnterBackgroundNotification - object:nil]; - [notificationCenter addObserver:self - selector:@selector(applicationWillEnterForeground:) - name:kGDTApplicationWillEnterForegroundNotification - object:nil]; - - NSString *name = kGDTApplicationWillTerminateNotification; - [notificationCenter addObserver:self - selector:@selector(applicationWillTerminate:) - name:name - object:nil]; - } - return self; -} - -- (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -- (void)applicationDidEnterBackground:(NSNotification *)notification { - GDTApplication *application = [GDTApplication sharedApplication]; - if ([[GDTTransformer sharedInstance] respondsToSelector:@selector(appWillBackground:)]) { - [[GDTTransformer sharedInstance] appWillBackground:application]; - } - if ([[GDTStorage sharedInstance] respondsToSelector:@selector(appWillBackground:)]) { - [[GDTStorage sharedInstance] appWillBackground:application]; - } - if ([[GDTUploadCoordinator sharedInstance] respondsToSelector:@selector(appWillBackground:)]) { - [[GDTUploadCoordinator sharedInstance] appWillBackground:application]; - } - if ([[GDTRegistrar sharedInstance] respondsToSelector:@selector(appWillBackground:)]) { - [[GDTRegistrar sharedInstance] appWillBackground:application]; - } -} - -- (void)applicationWillEnterForeground:(NSNotification *)notification { - GDTApplication *application = [GDTApplication sharedApplication]; - if ([[GDTTransformer sharedInstance] respondsToSelector:@selector(appWillForeground:)]) { - [[GDTTransformer sharedInstance] appWillForeground:application]; - } - if ([[GDTStorage sharedInstance] respondsToSelector:@selector(appWillForeground:)]) { - [[GDTStorage sharedInstance] appWillForeground:application]; - } - if ([[GDTUploadCoordinator sharedInstance] respondsToSelector:@selector(appWillForeground:)]) { - [[GDTUploadCoordinator sharedInstance] appWillForeground:application]; - } - if ([[GDTRegistrar sharedInstance] respondsToSelector:@selector(appWillForeground:)]) { - [[GDTRegistrar sharedInstance] appWillForeground:application]; - } -} - -- (void)applicationWillTerminate:(NSNotification *)notification { - GDTApplication *application = [GDTApplication sharedApplication]; - if ([[GDTTransformer sharedInstance] respondsToSelector:@selector(appWillTerminate:)]) { - [[GDTTransformer sharedInstance] appWillTerminate:application]; - } - if ([[GDTStorage sharedInstance] respondsToSelector:@selector(appWillTerminate:)]) { - [[GDTStorage sharedInstance] appWillTerminate:application]; - } - if ([[GDTUploadCoordinator sharedInstance] respondsToSelector:@selector(appWillTerminate:)]) { - [[GDTUploadCoordinator sharedInstance] appWillTerminate:application]; - } - if ([[GDTRegistrar sharedInstance] respondsToSelector:@selector(appWillTerminate:)]) { - [[GDTRegistrar sharedInstance] appWillTerminate:application]; - } -} - -@end diff --git a/GoogleDataTransport/GDTLibrary/GDTTransformer.m b/GoogleDataTransport/GDTLibrary/GDTTransformer.m deleted file mode 100644 index 89e36a89db9..00000000000 --- a/GoogleDataTransport/GDTLibrary/GDTTransformer.m +++ /dev/null @@ -1,102 +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. - */ - -#import "GDTLibrary/Private/GDTTransformer.h" -#import "GDTLibrary/Private/GDTTransformer_Private.h" - -#import -#import -#import - -#import "GDTLibrary/Private/GDTAssert.h" -#import "GDTLibrary/Private/GDTStorage.h" - -@implementation GDTTransformer - -+ (instancetype)sharedInstance { - static GDTTransformer *eventTransformer; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - eventTransformer = [[self alloc] init]; - }); - return eventTransformer; -} - -- (instancetype)init { - self = [super init]; - if (self) { - _eventWritingQueue = dispatch_queue_create("com.google.GDTTransformer", DISPATCH_QUEUE_SERIAL); - _storageInstance = [GDTStorage sharedInstance]; - } - return self; -} - -- (void)transformEvent:(GDTEvent *)event - withTransformers:(NSArray> *)transformers { - GDTAssert(event, @"You can't write a nil event"); - - __block GDTBackgroundIdentifier bgID = GDTBackgroundIdentifierInvalid; - if (_runningInBackground) { - bgID = [[GDTApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ - [[GDTApplication sharedApplication] endBackgroundTask:bgID]; - }]; - } - dispatch_async(_eventWritingQueue, ^{ - GDTEvent *transformedEvent = event; - for (id transformer in transformers) { - if ([transformer respondsToSelector:@selector(transform:)]) { - transformedEvent = [transformer transform:transformedEvent]; - if (!transformedEvent) { - return; - } - } else { - GDTLogError(GDTMCETransformerDoesntImplementTransform, - @"Transformer doesn't implement transform: %@", transformer); - return; - } - } - [self.storageInstance storeEvent:transformedEvent]; - if (self->_runningInBackground) { - [[GDTApplication sharedApplication] endBackgroundTask:bgID]; - } - }); -} - -#pragma mark - GDTLifecycleProtocol - -- (void)appWillForeground:(GDTApplication *)app { - dispatch_async(_eventWritingQueue, ^{ - self->_runningInBackground = NO; - }); -} - -- (void)appWillBackground:(GDTApplication *)app { - // Create an immediate background task to run until the end of the current queue of work. - __block GDTBackgroundIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ - [app endBackgroundTask:bgID]; - }]; - dispatch_async(_eventWritingQueue, ^{ - [app endBackgroundTask:bgID]; - }); -} - -- (void)appWillTerminate:(GDTApplication *)application { - // Flush the queue immediately. - dispatch_sync(_eventWritingQueue, ^{ - }); -} - -@end diff --git a/GoogleDataTransport/GDTLibrary/GDTTransport.m b/GoogleDataTransport/GDTLibrary/GDTTransport.m deleted file mode 100644 index a1696c7562d..00000000000 --- a/GoogleDataTransport/GDTLibrary/GDTTransport.m +++ /dev/null @@ -1,65 +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. - */ - -#import -#import "GDTLibrary/Private/GDTTransport_Private.h" - -#import -#import - -#import "GDTLibrary/Private/GDTAssert.h" -#import "GDTLibrary/Private/GDTTransformer.h" - -@implementation GDTTransport - -- (instancetype)initWithMappingID:(NSString *)mappingID - transformers:(nullable NSArray> *)transformers - target:(NSInteger)target { - self = [super init]; - if (self) { - GDTAssert(mappingID.length > 0, @"A mapping ID cannot be nil or empty"); - GDTAssert(target > 0, @"A target cannot be negative or 0"); - _mappingID = mappingID; - _transformers = transformers; - _target = target; - _transformerInstance = [GDTTransformer sharedInstance]; - } - return self; -} - -- (void)sendTelemetryEvent:(GDTEvent *)event { - // TODO: Determine if sending an event before registration is allowed. - GDTAssert(event, @"You can't send a nil event"); - GDTEvent *copiedEvent = [event copy]; - copiedEvent.qosTier = GDTEventQoSTelemetry; - copiedEvent.clockSnapshot = [GDTClock snapshot]; - [self.transformerInstance transformEvent:copiedEvent withTransformers:_transformers]; -} - -- (void)sendDataEvent:(GDTEvent *)event { - // TODO: Determine if sending an event before registration is allowed. - GDTAssert(event, @"You can't send a nil event"); - GDTAssert(event.qosTier != GDTEventQoSTelemetry, @"Use -sendTelemetryEvent, please."); - GDTEvent *copiedEvent = [event copy]; - copiedEvent.clockSnapshot = [GDTClock snapshot]; - [self.transformerInstance transformEvent:copiedEvent withTransformers:_transformers]; -} - -- (GDTEvent *)eventForTransport { - return [[GDTEvent alloc] initWithMappingID:_mappingID target:_target]; -} - -@end diff --git a/GoogleDataTransport/GDTLibrary/Private/GDTAssert.h b/GoogleDataTransport/GDTLibrary/Private/GDTAssert.h deleted file mode 100644 index 4f157ed8625..00000000000 --- a/GoogleDataTransport/GDTLibrary/Private/GDTAssert.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2019 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 - -/** A block type that could be run instead of NSAssert. No return type, no params. */ -typedef void (^GDTAssertionBlock)(void); - -/** Returns the result of executing a soft-linked method present in unit tests that allows a block - * to be run in lieu of a call to NSAssert. This helps ameliorate issues with catching exceptions - * that occur on a dispatch_queue. - * - * @return A block that can be run instead of calling NSAssert, or nil. - */ -FOUNDATION_EXPORT GDTAssertionBlock _Nullable GDTAssertionBlockToRunInsteadOfNSAssert(void); - -#if !defined(NS_BLOCK_ASSERTIONS) - -/** Asserts using NSAssert, unless a block was specified to be run instead. - * - * @param condition The condition you'd expect to be YES. - */ -#define GDTAssert(condition, ...) \ - do { \ - if (__builtin_expect(!(condition), 0)) { \ - GDTAssertionBlock assertionBlock = GDTAssertionBlockToRunInsteadOfNSAssert(); \ - if (assertionBlock) { \ - assertionBlock(); \ - } else { \ - NSAssert(condition, __VA_ARGS__); \ - } \ - } \ - } while (0); - -#else - -#define GDTAssert(condition, ...) \ - do { \ - } while (0); - -#endif // !defined(NS_BLOCK_ASSERTIONS) diff --git a/GoogleDataTransport/GDTTestApp/app.swift b/GoogleDataTransport/GDTTestApp/app.swift new file mode 100644 index 00000000000..02be9fd2a36 --- /dev/null +++ b/GoogleDataTransport/GDTTestApp/app.swift @@ -0,0 +1,49 @@ +/* + * Copyright 2019 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 GoogleDataTransport + +// iOS and tvOS specifics. +#if os(iOS) || os(tvOS) + import UIKit + + @UIApplicationMain + class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + return true + } + } + + public class ViewController: UIViewController { + let transport: GDTCORTransport = GDTCORTransport(mappingID: "1234", transformers: nil, target: GDTCORTarget.test.rawValue) + } + +// macOS specifics. +#elseif os(macOS) + import Cocoa + + @NSApplicationMain class Main: NSObject, NSApplicationDelegate { + var windowController: NSWindowController! + + func applicationDidFinishLaunching(aNotification: NSNotification) {} + } + + public class ViewController: NSViewController { + let transport: GDTCORTransport = GDTCORTransport(mappingID: "1234", transformers: nil, target: GDTCORTarget.test.rawValue) + } +#endif diff --git a/GoogleDataTransport/GDTTestApp/gdthelpers.swift b/GoogleDataTransport/GDTTestApp/gdthelpers.swift new file mode 100644 index 00000000000..c2617620189 --- /dev/null +++ b/GoogleDataTransport/GDTTestApp/gdthelpers.swift @@ -0,0 +1,40 @@ +/* + * Copyright 2019 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 GoogleDataTransport + +// For use by GDT. +class TestDataObject: NSObject, GDTCOREventDataObject { + func transportBytes() -> Data { + return "Normally, some SDK's data object would populate this. \(Date())".data(using: String.Encoding.utf8)! + } +} + +class TestPrioritizer: NSObject, GDTCORPrioritizer { + func prioritizeEvent(_ event: GDTCORStoredEvent) {} + + func uploadPackage(with conditions: GDTCORUploadConditions) -> GDTCORUploadPackage { + return GDTCORUploadPackage(target: GDTCORTarget.test) + } +} + +class TestUploader: NSObject, GDTCORUploader { + func readyToUpload(with conditions: GDTCORUploadConditions) -> Bool { + return false + } + + func uploadPackage(_ package: GDTCORUploadPackage) {} +} diff --git a/GoogleDataTransport/GDTTestApp/globals.swift b/GoogleDataTransport/GDTTestApp/globals.swift new file mode 100644 index 00000000000..caccc8eaae2 --- /dev/null +++ b/GoogleDataTransport/GDTTestApp/globals.swift @@ -0,0 +1,25 @@ +/* + * Copyright 2019 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 + +public struct Globals { + public static var SharedViewController: ViewController? + + public static let MonkeyTestLength: TimeInterval = 20.0 + + public static let MonkeyTestLengthPlusBuffer: TimeInterval = MonkeyTestLength + 10.0 +} diff --git a/GoogleDataTransport/GDTTestApp/ios/Main.storyboard b/GoogleDataTransport/GDTTestApp/ios/Main.storyboard new file mode 100644 index 00000000000..32cb2fca5b2 --- /dev/null +++ b/GoogleDataTransport/GDTTestApp/ios/Main.storyboard @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GoogleDataTransport/GDTTestApp/macos/Main.storyboard b/GoogleDataTransport/GDTTestApp/macos/Main.storyboard new file mode 100644 index 00000000000..6ac7971e597 --- /dev/null +++ b/GoogleDataTransport/GDTTestApp/macos/Main.storyboard @@ -0,0 +1,800 @@ + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GoogleDataTransport/GDTTestApp/tvos/Main.storyboard b/GoogleDataTransport/GDTTestApp/tvos/Main.storyboard new file mode 100644 index 00000000000..f7adb1dee97 --- /dev/null +++ b/GoogleDataTransport/GDTTestApp/tvos/Main.storyboard @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GoogleDataTransport/GDTTestApp/viewcontroller.swift b/GoogleDataTransport/GDTTestApp/viewcontroller.swift new file mode 100644 index 00000000000..573c29f108b --- /dev/null +++ b/GoogleDataTransport/GDTTestApp/viewcontroller.swift @@ -0,0 +1,98 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import Dispatch +import GoogleDataTransport + +public extension ViewController { + override func viewDidLoad() { + super.viewDidLoad() + GDTCORRegistrar.sharedInstance().register(TestUploader(), target: GDTCORTarget.test) + GDTCORRegistrar.sharedInstance().register(TestPrioritizer(), target: GDTCORTarget.test) + Globals.SharedViewController = self + } + + @IBAction func generateDataEvent(sender: AnyObject?) { + print("Generating data event") + let event: GDTCOREvent = transport.eventForTransport() + event.dataObject = TestDataObject() + transport.sendDataEvent(event) + } + + @IBAction func generateTelemetryEvent(sender: AnyObject?) { + print("Generating telemetry event") + let event: GDTCOREvent = transport.eventForTransport() + event.dataObject = TestDataObject() + transport.sendTelemetryEvent(event) + } + + @IBAction func generateHighPriorityEvent(sender: AnyObject?) { + print("Generating high priority event") + let event: GDTCOREvent = transport.eventForTransport() + event.dataObject = TestDataObject() + event.qosTier = GDTCOREventQoS.qoSFast + transport.sendDataEvent(event) + } + + @IBAction func generateWifiOnlyEvent(sender: AnyObject?) { + print("Generating wifi only event") + let event: GDTCOREvent = transport.eventForTransport() + event.dataObject = TestDataObject() + event.qosTier = GDTCOREventQoS.qoSWifiOnly + transport.sendDataEvent(event) + } + + @IBAction func generateDailyEvent(sender: AnyObject?) { + print("Generating daily event") + let event: GDTCOREvent = transport.eventForTransport() + event.dataObject = TestDataObject() + event.qosTier = GDTCOREventQoS.qoSDaily + transport.sendDataEvent(event) + } + + func beginMonkeyTest(completion: () -> Void) { + print("Beginning monkey test") + + let sema: DispatchSemaphore = DispatchSemaphore(value: 0) + var generateEvents = true + DispatchQueue.global().asyncAfter(deadline: .now() + Globals.MonkeyTestLength) { + generateEvents = false + sema.signal() + } + + func generateEvent() { + DispatchQueue.global().asyncAfter(deadline: .now() + Double.random(in: 0 ..< 3.0)) { + let generationFunctions = [ + self.generateDataEvent, + self.generateTelemetryEvent, + self.generateHighPriorityEvent, + self.generateWifiOnlyEvent, + self.generateDailyEvent, + ] + let randomIndex: Int = Int.random(in: 0 ..< generationFunctions.count) + generationFunctions[randomIndex](self) + } + RunLoop.current.run(until: Date(timeIntervalSinceNow: Double.random(in: 0 ..< 1.5))) + if generateEvents { + generateEvent() + } + } + generateEvent() + sema.wait() + completion() + } +} diff --git a/GoogleDataTransport/GDTTests/Lifecycle/GDTLifecycleTest.m b/GoogleDataTransport/GDTTests/Lifecycle/GDTLifecycleTest.m deleted file mode 100644 index 72ae77328e0..00000000000 --- a/GoogleDataTransport/GDTTests/Lifecycle/GDTLifecycleTest.m +++ /dev/null @@ -1,181 +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. - */ - -#import - -#import -#import -#import - -#import "GDTLibrary/Private/GDTStorage_Private.h" -#import "GDTLibrary/Private/GDTTransformer_Private.h" -#import "GDTLibrary/Private/GDTUploadCoordinator.h" - -#import "GDTTests/Lifecycle/Helpers/GDTLifecycleTestPrioritizer.h" -#import "GDTTests/Lifecycle/Helpers/GDTLifecycleTestUploader.h" - -#import "GDTTests/Common/Categories/GDTStorage+Testing.h" -#import "GDTTests/Common/Categories/GDTUploadCoordinator+Testing.h" - -/** Waits for the result of waitBlock to be YES, or times out and fails. - * - * @param waitBlock The block to periodically execute. - * @param timeInterval The timeout. - */ -#define GDTWaitForBlock(waitBlock, timeInterval) \ - { \ - NSPredicate *pred = \ - [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, \ - NSDictionary *_Nullable bindings) { \ - return waitBlock(); \ - }]; \ - XCTestExpectation *expectation = [self expectationForPredicate:pred \ - evaluatedWithObject:[[NSObject alloc] init] \ - handler:^BOOL { \ - return YES; \ - }]; \ - [self waitForExpectations:@[ expectation ] timeout:timeInterval]; \ - } - -/** A test-only event data object used in this integration test. */ -@interface GDTLifecycleTestEvent : NSObject - -@end - -@implementation GDTLifecycleTestEvent - -- (NSData *)transportBytes { - // In real usage, protobuf's -data method or a custom implementation using nanopb are used. - return [[NSString stringWithFormat:@"%@", [NSDate date]] dataUsingEncoding:NSUTF8StringEncoding]; -} - -@end - -@interface GDTLifecycleTest : XCTestCase - -/** The test prioritizer. */ -@property(nonatomic) GDTLifecycleTestPrioritizer *prioritizer; - -/** The test uploader. */ -@property(nonatomic) GDTLifecycleTestUploader *uploader; - -@end - -@implementation GDTLifecycleTest - -- (void)setUp { - [super setUp]; - // Don't check the error, because it'll be populated in cases where the file doesn't exist. - NSError *error; - [[NSFileManager defaultManager] removeItemAtPath:[GDTStorage archivePath] error:&error]; - self.uploader = [[GDTLifecycleTestUploader alloc] init]; - [[GDTRegistrar sharedInstance] registerUploader:self.uploader target:kGDTTargetTest]; - - self.prioritizer = [[GDTLifecycleTestPrioritizer alloc] init]; - [[GDTRegistrar sharedInstance] registerPrioritizer:self.prioritizer target:kGDTTargetTest]; - [[GDTStorage sharedInstance] reset]; - [[GDTUploadCoordinator sharedInstance] reset]; -} - -/** Tests that the library serializes itself to disk when the app backgrounds. */ -- (void)testBackgrounding { - GDTTransport *transport = [[GDTTransport alloc] initWithMappingID:@"test" - transformers:nil - target:kGDTTargetTest]; - GDTEvent *event = [transport eventForTransport]; - event.dataObject = [[GDTLifecycleTestEvent alloc] init]; - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); - [transport sendDataEvent:event]; - GDTWaitForBlock( - ^BOOL { - return [GDTStorage sharedInstance].storedEvents.count > 0; - }, - 5.0); - - NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; - [notifCenter postNotificationName:kGDTApplicationDidEnterBackgroundNotification object:nil]; - XCTAssertTrue([GDTStorage sharedInstance].runningInBackground); - XCTAssertTrue([GDTUploadCoordinator sharedInstance].runningInBackground); - GDTWaitForBlock( - ^BOOL { - NSFileManager *fm = [NSFileManager defaultManager]; - return [fm fileExistsAtPath:[GDTStorage archivePath] isDirectory:NULL]; - }, - 5.0); -} - -/** Tests that the library deserializes itself from disk when the app foregrounds. */ -- (void)testForegrounding { - GDTTransport *transport = [[GDTTransport alloc] initWithMappingID:@"test" - transformers:nil - target:kGDTTargetTest]; - GDTEvent *event = [transport eventForTransport]; - event.dataObject = [[GDTLifecycleTestEvent alloc] init]; - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); - [transport sendDataEvent:event]; - GDTWaitForBlock( - ^BOOL { - return [GDTStorage sharedInstance].storedEvents.count > 0; - }, - 5.0); - - NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; - [notifCenter postNotificationName:kGDTApplicationDidEnterBackgroundNotification object:nil]; - - GDTWaitForBlock( - ^BOOL { - NSFileManager *fm = [NSFileManager defaultManager]; - return [fm fileExistsAtPath:[GDTStorage archivePath] isDirectory:NULL]; - }, - 5.0); - - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; - [notifCenter postNotificationName:kGDTApplicationWillEnterForegroundNotification object:nil]; - XCTAssertFalse([GDTStorage sharedInstance].runningInBackground); - XCTAssertFalse([GDTUploadCoordinator sharedInstance].runningInBackground); - GDTWaitForBlock( - ^BOOL { - return [GDTStorage sharedInstance].storedEvents.count > 0; - }, - 5.0); -} - -/** Tests that the library gracefully stops doing stuff when terminating. */ -- (void)testTermination { - GDTTransport *transport = [[GDTTransport alloc] initWithMappingID:@"test" - transformers:nil - target:kGDTTargetTest]; - GDTEvent *event = [transport eventForTransport]; - event.dataObject = [[GDTLifecycleTestEvent alloc] init]; - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); - [transport sendDataEvent:event]; - GDTWaitForBlock( - ^BOOL { - return [GDTStorage sharedInstance].storedEvents.count > 0; - }, - 5.0); - - NSNotificationCenter *notifCenter = [NSNotificationCenter defaultCenter]; - [notifCenter postNotificationName:kGDTApplicationWillTerminateNotification object:nil]; - GDTWaitForBlock( - ^BOOL { - NSFileManager *fm = [NSFileManager defaultManager]; - return [fm fileExistsAtPath:[GDTStorage archivePath] isDirectory:NULL]; - }, - 5.0); -} - -@end diff --git a/GoogleDataTransport/GDTTests/Monkey/GDTMonkeyTest.swift b/GoogleDataTransport/GDTTests/Monkey/GDTMonkeyTest.swift new file mode 100644 index 00000000000..c5d4c4d07c4 --- /dev/null +++ b/GoogleDataTransport/GDTTests/Monkey/GDTMonkeyTest.swift @@ -0,0 +1,38 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import XCTest + +#if os(iOS) + import GoogleDataTransport_iOS_TestApp +#elseif os(macOS) + import GoogleDataTransport_macOS_TestApp +#elseif os(tvOS) + import GoogleDataTransport_tvOS_TestApp +#endif + +class GDTMonkeyTest: XCTestCase { + func testGDT() { + let viewController: ViewController? = Globals.SharedViewController + XCTAssertNotNil(viewController) + + let expectation: XCTestExpectation = self.expectation(description: "Runs without crashing") + viewController?.beginMonkeyTest { + expectation.fulfill() + } + waitForExpectations(timeout: Globals.MonkeyTestLengthPlusBuffer, handler: nil) + } +} diff --git a/GoogleDataTransport/GDTTests/Unit/GDTStorageTest.m b/GoogleDataTransport/GDTTests/Unit/GDTStorageTest.m deleted file mode 100644 index 32828e89fca..00000000000 --- a/GoogleDataTransport/GDTTests/Unit/GDTStorageTest.m +++ /dev/null @@ -1,357 +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. - */ - -#import "GDTTests/Unit/GDTTestCase.h" - -#import -#import - -#import "GDTLibrary/Private/GDTEvent_Private.h" -#import "GDTLibrary/Private/GDTRegistrar_Private.h" -#import "GDTLibrary/Private/GDTStorage.h" -#import "GDTLibrary/Private/GDTStorage_Private.h" -#import "GDTLibrary/Public/GDTRegistrar.h" - -#import "GDTTests/Unit/Helpers/GDTAssertHelper.h" -#import "GDTTests/Unit/Helpers/GDTTestPrioritizer.h" -#import "GDTTests/Unit/Helpers/GDTTestUploader.h" - -#import "GDTTests/Common/Fakes/GDTUploadCoordinatorFake.h" - -#import "GDTTests/Common/Categories/GDTRegistrar+Testing.h" -#import "GDTTests/Common/Categories/GDTStorage+Testing.h" - -static NSInteger target = 1337; - -@interface GDTStorageTest : GDTTestCase - -/** The test backend implementation. */ -@property(nullable, nonatomic) GDTTestUploader *testBackend; - -/** The test prioritizer implementation. */ -@property(nullable, nonatomic) GDTTestPrioritizer *testPrioritizer; - -/** The uploader fake. */ -@property(nonatomic) GDTUploadCoordinatorFake *uploaderFake; - -@end - -@implementation GDTStorageTest - -- (void)setUp { - [super setUp]; - self.testBackend = [[GDTTestUploader alloc] init]; - self.testPrioritizer = [[GDTTestPrioritizer alloc] init]; - [[GDTRegistrar sharedInstance] registerUploader:_testBackend target:target]; - [[GDTRegistrar sharedInstance] registerPrioritizer:_testPrioritizer target:target]; - self.uploaderFake = [[GDTUploadCoordinatorFake alloc] init]; - [GDTStorage sharedInstance].uploadCoordinator = self.uploaderFake; -} - -- (void)tearDown { - [super tearDown]; - // Destroy these objects before the next test begins. - self.testBackend = nil; - self.testPrioritizer = nil; - [[GDTRegistrar sharedInstance] reset]; - [[GDTStorage sharedInstance] reset]; - [GDTStorage sharedInstance].uploadCoordinator = [GDTUploadCoordinator sharedInstance]; - self.uploaderFake = nil; -} - -/** Tests the singleton pattern. */ -- (void)testInit { - XCTAssertEqual([GDTStorage sharedInstance], [GDTStorage sharedInstance]); -} - -/** Tests storing an event. */ -- (void)testStoreEvent { - // event is autoreleased, and the pool needs to drain. - @autoreleasepool { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; - event.clockSnapshot = [GDTClock snapshot]; - XCTAssertNoThrow([[GDTStorage sharedInstance] storeEvent:event]); - } - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 1); - XCTAssertEqual([GDTStorage sharedInstance].targetToEventSet[@(target)].count, 1); - NSURL *eventFile = [[GDTStorage sharedInstance].storedEvents lastObject].dataFuture.fileURL; - XCTAssertNotNil(eventFile); - XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); - NSError *error; - XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:eventFile error:&error]); - XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); - }); -} - -/** Tests removing an event. */ -- (void)testRemoveEvent { - // event is autoreleased, and the pool needs to drain. - @autoreleasepool { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; - event.clockSnapshot = [GDTClock snapshot]; - XCTAssertNoThrow([[GDTStorage sharedInstance] storeEvent:event]); - } - __block NSURL *eventFile; - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - eventFile = [[GDTStorage sharedInstance].storedEvents lastObject].dataFuture.fileURL; - XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); - }); - [[GDTStorage sharedInstance] removeEvents:[GDTStorage sharedInstance].storedEvents.set]; - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); - XCTAssertEqual([GDTStorage sharedInstance].targetToEventSet[@(target)].count, 0); - }); -} - -/** Tests removing a set of events. */ -- (void)testRemoveEvents { - GDTStorage *storage = [GDTStorage sharedInstance]; - __block GDTStoredEvent *storedEvent1, *storedEvent2, *storedEvent3; - - // events are autoreleased, and the pool needs to drain. - @autoreleasepool { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - event.dataObjectTransportBytes = [@"testString1" dataUsingEncoding:NSUTF8StringEncoding]; - XCTAssertNoThrow([storage storeEvent:event]); - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - storedEvent1 = [storage.storedEvents lastObject]; - }); - - event = [[GDTEvent alloc] initWithMappingID:@"100" target:target]; - event.dataObjectTransportBytes = [@"testString2" dataUsingEncoding:NSUTF8StringEncoding]; - XCTAssertNoThrow([storage storeEvent:event]); - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - storedEvent2 = [storage.storedEvents lastObject]; - }); - - event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - event.dataObjectTransportBytes = [@"testString3" dataUsingEncoding:NSUTF8StringEncoding]; - XCTAssertNoThrow([storage storeEvent:event]); - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - storedEvent3 = [storage.storedEvents lastObject]; - }); - } - NSSet *eventSet = - [NSSet setWithObjects:storedEvent1, storedEvent2, storedEvent3, nil]; - [storage removeEvents:eventSet]; - dispatch_sync(storage.storageQueue, ^{ - XCTAssertFalse([storage.storedEvents containsObject:storedEvent1]); - XCTAssertFalse([storage.storedEvents containsObject:storedEvent2]); - XCTAssertFalse([storage.storedEvents containsObject:storedEvent3]); - XCTAssertEqual(storage.targetToEventSet[@(target)].count, 0); - for (GDTStoredEvent *event in eventSet) { - XCTAssertFalse( - [[NSFileManager defaultManager] fileExistsAtPath:event.dataFuture.fileURL.path]); - } - }); -} - -/** Tests storing a few different events. */ -- (void)testStoreMultipleEvents { - __block GDTStoredEvent *storedEvent1, *storedEvent2, *storedEvent3; - - // events are autoreleased, and the pool needs to drain. - @autoreleasepool { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - event.dataObjectTransportBytes = [@"testString1" dataUsingEncoding:NSUTF8StringEncoding]; - XCTAssertNoThrow([[GDTStorage sharedInstance] storeEvent:event]); - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - storedEvent1 = [[GDTStorage sharedInstance].storedEvents lastObject]; - }); - - event = [[GDTEvent alloc] initWithMappingID:@"100" target:target]; - event.dataObjectTransportBytes = [@"testString2" dataUsingEncoding:NSUTF8StringEncoding]; - XCTAssertNoThrow([[GDTStorage sharedInstance] storeEvent:event]); - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - storedEvent2 = [[GDTStorage sharedInstance].storedEvents lastObject]; - }); - - event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - event.dataObjectTransportBytes = [@"testString3" dataUsingEncoding:NSUTF8StringEncoding]; - XCTAssertNoThrow([[GDTStorage sharedInstance] storeEvent:event]); - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - storedEvent3 = [[GDTStorage sharedInstance].storedEvents lastObject]; - }); - } - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 3); - XCTAssertEqual([GDTStorage sharedInstance].targetToEventSet[@(target)].count, 3); - - NSURL *event1File = storedEvent1.dataFuture.fileURL; - XCTAssertNotNil(event1File); - XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:event1File.path]); - NSError *error; - XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:event1File error:&error]); - XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); - - NSURL *event2File = storedEvent2.dataFuture.fileURL; - XCTAssertNotNil(event2File); - XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:event2File.path]); - error = nil; - XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:event2File error:&error]); - XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); - - NSURL *event3File = storedEvent3.dataFuture.fileURL; - XCTAssertNotNil(event3File); - XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:event3File.path]); - error = nil; - XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:event3File error:&error]); - XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); - }); -} - -/** Tests enforcing that a prioritizer does not retain an event in memory. */ -- (void)testEventDeallocationIsEnforced { - __weak GDTEvent *weakEvent; - GDTStoredEvent *storedEvent; - @autoreleasepool { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - weakEvent = event; - event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; - event.clockSnapshot = [GDTClock snapshot]; - // Store the event and wait for the expectation. - [[GDTStorage sharedInstance] storeEvent:event]; - GDTDataFuture *dataFuture = - [[GDTDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:@"/test"]]; - storedEvent = [event storedEventWithDataFuture:dataFuture]; - } - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertNil(weakEvent); - XCTAssertNotNil(storedEvent); - }); - - NSURL *eventFile; - eventFile = [[GDTStorage sharedInstance].storedEvents lastObject].dataFuture.fileURL; - - // This isn't strictly necessary because of the -waitForExpectations above. - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); - }); - - // Ensure event was removed. - [[GDTStorage sharedInstance] removeEvents:[GDTStorage sharedInstance].storedEvents.set]; - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertFalse([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 0); - XCTAssertEqual([GDTStorage sharedInstance].targetToEventSet[@(target)].count, 0); - }); -} - -/** Tests encoding and decoding the storage singleton correctly. */ -- (void)testNSSecureCoding { - XCTAssertTrue([GDTStorage supportsSecureCoding]); - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - event.clockSnapshot = [GDTClock snapshot]; - event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; - XCTAssertNoThrow([[GDTStorage sharedInstance] storeEvent:event]); - event = nil; - NSData *storageData; - if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { - storageData = [NSKeyedArchiver archivedDataWithRootObject:[GDTStorage sharedInstance] - requiringSecureCoding:YES - error:nil]; - } else { -#if !defined(TARGET_OS_MACCATALYST) - storageData = [NSKeyedArchiver archivedDataWithRootObject:[GDTStorage sharedInstance]]; -#endif - } - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertNotNil([[GDTStorage sharedInstance].storedEvents lastObject]); - }); - [[GDTStorage sharedInstance] removeEvents:[GDTStorage sharedInstance].storedEvents.set]; - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertNil([[GDTStorage sharedInstance].storedEvents lastObject]); - }); - GDTStorage *unarchivedStorage; - NSError *error; - if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { - unarchivedStorage = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTStorage class] - fromData:storageData - error:&error]; - } else { -#if !defined(TARGET_OS_MACCATALYST) - unarchivedStorage = [NSKeyedUnarchiver unarchiveObjectWithData:storageData]; -#endif - } - XCTAssertNotNil([unarchivedStorage.storedEvents lastObject]); -} - -/** Tests encoding and decoding the storage singleton when calling -sharedInstance. */ -- (void)testNSSecureCodingWithSharedInstance { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; - event.clockSnapshot = [GDTClock snapshot]; - XCTAssertNoThrow([[GDTStorage sharedInstance] storeEvent:event]); - event = nil; - NSData *storageData; - if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { - storageData = [NSKeyedArchiver archivedDataWithRootObject:[GDTStorage sharedInstance] - requiringSecureCoding:YES - error:nil]; - } else { -#if !defined(TARGET_OS_MACCATALYST) - storageData = [NSKeyedArchiver archivedDataWithRootObject:[GDTStorage sharedInstance]]; -#endif - } - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertNotNil([[GDTStorage sharedInstance].storedEvents lastObject]); - }); - [[GDTStorage sharedInstance] removeEvents:[GDTStorage sharedInstance].storedEvents.set]; - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertNil([[GDTStorage sharedInstance].storedEvents lastObject]); - }); - GDTStorage *unarchivedStorage; - if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { - unarchivedStorage = [NSKeyedUnarchiver unarchivedObjectOfClass:[GDTStorage class] - fromData:storageData - error:nil]; - } else { -#if !defined(TARGET_OS_MACCATALYST) - unarchivedStorage = [NSKeyedUnarchiver unarchiveObjectWithData:storageData]; -#endif - } - XCTAssertNotNil([unarchivedStorage.storedEvents lastObject]); -} - -/** Tests sending a fast priority event causes an upload attempt. */ -- (void)testQoSTierFast { - // event is autoreleased, and the pool needs to drain. - @autoreleasepool { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"404" target:target]; - event.dataObjectTransportBytes = [@"testString" dataUsingEncoding:NSUTF8StringEncoding]; - event.qosTier = GDTEventQoSFast; - event.clockSnapshot = [GDTClock snapshot]; - XCTAssertFalse(self.uploaderFake.forceUploadCalled); - XCTAssertNoThrow([[GDTStorage sharedInstance] storeEvent:event]); - } - dispatch_sync([GDTStorage sharedInstance].storageQueue, ^{ - XCTAssertTrue(self.uploaderFake.forceUploadCalled); - XCTAssertEqual([GDTStorage sharedInstance].storedEvents.count, 1); - XCTAssertEqual([GDTStorage sharedInstance].targetToEventSet[@(target)].count, 1); - NSURL *eventFile = [[GDTStorage sharedInstance].storedEvents lastObject].dataFuture.fileURL; - XCTAssertNotNil(eventFile); - XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:eventFile.path]); - NSError *error; - XCTAssertTrue([[NSFileManager defaultManager] removeItemAtURL:eventFile error:&error]); - XCTAssertNil(error, @"There was an error deleting the eventFile: %@", error); - }); -} - -@end diff --git a/GoogleDataTransport/GDTTests/Unit/GDTTransformerTest.m b/GoogleDataTransport/GDTTests/Unit/GDTTransformerTest.m deleted file mode 100644 index 93843b730ed..00000000000 --- a/GoogleDataTransport/GDTTests/Unit/GDTTransformerTest.m +++ /dev/null @@ -1,113 +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. - */ - -#import "GDTTests/Unit/GDTTestCase.h" - -#import -#import - -#import "GDTLibrary/Private/GDTStorage.h" -#import "GDTLibrary/Private/GDTTransformer.h" -#import "GDTLibrary/Private/GDTTransformer_Private.h" - -#import "GDTTests/Unit/Helpers/GDTAssertHelper.h" -#import "GDTTests/Unit/Helpers/GDTDataObjectTesterClasses.h" - -#import "GDTTests/Common/Fakes/GDTStorageFake.h" - -@interface GDTTransformerTestNilingTransformer : NSObject - -@end - -@implementation GDTTransformerTestNilingTransformer - -- (GDTEvent *)transform:(GDTEvent *)eventEvent { - return nil; -} - -@end - -@interface GDTTransformerTestNewEventTransformer : NSObject - -@end - -@implementation GDTTransformerTestNewEventTransformer - -- (GDTEvent *)transform:(GDTEvent *)eventEvent { - return [[GDTEvent alloc] initWithMappingID:@"new" target:1]; -} - -@end - -@interface GDTTransformerTest : GDTTestCase - -@end - -@implementation GDTTransformerTest - -- (void)setUp { - [super setUp]; - dispatch_sync([GDTTransformer sharedInstance].eventWritingQueue, ^{ - [GDTTransformer sharedInstance].storageInstance = [[GDTStorageFake alloc] init]; - }); -} - -- (void)tearDown { - [super tearDown]; - dispatch_sync([GDTTransformer sharedInstance].eventWritingQueue, ^{ - [GDTTransformer sharedInstance].storageInstance = [GDTStorage sharedInstance]; - }); -} - -/** Tests the default initializer. */ -- (void)testInit { - XCTAssertNotNil([[GDTTransformer alloc] init]); -} - -/** Tests the pointer equality of result of the -sharedInstance method. */ -- (void)testSharedInstance { - XCTAssertEqual([GDTTransformer sharedInstance], [GDTTransformer sharedInstance]); -} - -/** Tests writing a event without a transformer. */ -- (void)testWriteEventWithoutTransformers { - GDTTransformer *transformer = [GDTTransformer sharedInstance]; - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"1" target:1]; - event.dataObject = [[GDTDataObjectTesterSimple alloc] init]; - XCTAssertNoThrow([transformer transformEvent:event withTransformers:nil]); -} - -/** Tests writing a event with a transformer that nils out the event. */ -- (void)testWriteEventWithTransformersThatNilTheEvent { - GDTTransformer *transformer = [GDTTransformer sharedInstance]; - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"2" target:1]; - event.dataObject = [[GDTDataObjectTesterSimple alloc] init]; - NSArray> *transformers = - @[ [[GDTTransformerTestNilingTransformer alloc] init] ]; - XCTAssertNoThrow([transformer transformEvent:event withTransformers:transformers]); -} - -/** Tests writing a event with a transformer that creates a new event. */ -- (void)testWriteEventWithTransformersThatCreateANewEvent { - GDTTransformer *transformer = [GDTTransformer sharedInstance]; - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"2" target:1]; - event.dataObject = [[GDTDataObjectTesterSimple alloc] init]; - NSArray> *transformers = - @[ [[GDTTransformerTestNewEventTransformer alloc] init] ]; - XCTAssertNoThrow([transformer transformEvent:event withTransformers:transformers]); -} - -@end diff --git a/GoogleDataTransport/GDTTests/Unit/GDTTransportTest.m b/GoogleDataTransport/GDTTests/Unit/GDTTransportTest.m deleted file mode 100644 index 96aa99586f7..00000000000 --- a/GoogleDataTransport/GDTTests/Unit/GDTTransportTest.m +++ /dev/null @@ -1,57 +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. - */ - -#import "GDTTests/Unit/GDTTestCase.h" - -#import -#import - -#import "GDTLibrary/Private/GDTTransport_Private.h" - -#import "GDTTests/Common/Fakes/GDTTransformerFake.h" -#import "GDTTests/Unit/Helpers/GDTDataObjectTesterClasses.h" - -@interface GDTTransportTest : GDTTestCase - -@end - -@implementation GDTTransportTest - -/** Tests the default initializer. */ -- (void)testInit { - XCTAssertNotNil([[GDTTransport alloc] initWithMappingID:@"1" transformers:nil target:1]); - XCTAssertThrows([[GDTTransport alloc] initWithMappingID:@"" transformers:nil target:1]); -} - -/** Tests sending a telemetry event. */ -- (void)testSendTelemetryEvent { - GDTTransport *transport = [[GDTTransport alloc] initWithMappingID:@"1" transformers:nil target:1]; - transport.transformerInstance = [[GDTTransformerFake alloc] init]; - GDTEvent *event = [transport eventForTransport]; - event.dataObject = [[GDTDataObjectTesterSimple alloc] init]; - XCTAssertNoThrow([transport sendTelemetryEvent:event]); -} - -/** Tests sending a data event. */ -- (void)testSendDataEvent { - GDTTransport *transport = [[GDTTransport alloc] initWithMappingID:@"1" transformers:nil target:1]; - transport.transformerInstance = [[GDTTransformerFake alloc] init]; - GDTEvent *event = [transport eventForTransport]; - event.dataObject = [[GDTDataObjectTesterSimple alloc] init]; - XCTAssertNoThrow([transport sendDataEvent:event]); -} - -@end diff --git a/GoogleDataTransport/GDTTests/Unit/GDTUploadCoordinatorTest.m b/GoogleDataTransport/GDTTests/Unit/GDTUploadCoordinatorTest.m deleted file mode 100644 index e60839faacc..00000000000 --- a/GoogleDataTransport/GDTTests/Unit/GDTUploadCoordinatorTest.m +++ /dev/null @@ -1,149 +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. - */ - -#import "GDTTests/Unit/GDTTestCase.h" - -#import "GDTLibrary/Private/GDTUploadCoordinator.h" - -#import "GDTTests/Common/Categories/GDTRegistrar+Testing.h" -#import "GDTTests/Common/Categories/GDTUploadCoordinator+Testing.h" - -#import "GDTTests/Common/Fakes/GDTStorageFake.h" - -#import "GDTTests/Unit/Helpers/GDTEventGenerator.h" -#import "GDTTests/Unit/Helpers/GDTTestPrioritizer.h" -#import "GDTTests/Unit/Helpers/GDTTestUploadPackage.h" -#import "GDTTests/Unit/Helpers/GDTTestUploader.h" - -@interface GDTUploadCoordinatorTest : GDTTestCase - -/** A storage fake to inject into GDTUploadCoordinator. */ -@property(nonatomic) GDTStorageFake *storageFake; - -/** A test prioritizer. */ -@property(nonatomic) GDTTestPrioritizer *prioritizer; - -/** A test uploader. */ -@property(nonatomic) GDTTestUploader *uploader; - -@end - -@implementation GDTUploadCoordinatorTest - -- (void)setUp { - [super setUp]; - self.storageFake = [[GDTStorageFake alloc] init]; - self.prioritizer = [[GDTTestPrioritizer alloc] init]; - self.uploader = [[GDTTestUploader alloc] init]; - - [[GDTRegistrar sharedInstance] registerPrioritizer:_prioritizer target:kGDTTargetTest]; - [[GDTRegistrar sharedInstance] registerUploader:_uploader target:kGDTTargetTest]; - - GDTUploadCoordinator *uploadCoordinator = [GDTUploadCoordinator sharedInstance]; - uploadCoordinator.storage = self.storageFake; - uploadCoordinator.timerInterval = NSEC_PER_SEC; - uploadCoordinator.timerLeeway = 0; -} - -- (void)tearDown { - [super tearDown]; - [[GDTUploadCoordinator sharedInstance] reset]; - [[GDTRegistrar sharedInstance] reset]; - self.storageFake = nil; - self.prioritizer = nil; - self.uploader = nil; -} - -/** Tests the default initializer. */ -- (void)testSharedInstance { - XCTAssertEqual([GDTUploadCoordinator sharedInstance], [GDTUploadCoordinator sharedInstance]); -} - -/** Tests that forcing a event upload works. */ -- (void)testForceUploadEvents { - self.prioritizer.events = [GDTEventGenerator generate3StoredEvents]; - XCTestExpectation *expectation = [self expectationWithDescription:@"uploader will upload"]; - self.uploader.uploadPackageBlock = ^(GDTUploadPackage *_Nonnull package) { - [expectation fulfill]; - }; - XCTAssertNoThrow([[GDTUploadCoordinator sharedInstance] forceUploadForTarget:kGDTTargetTest]); - [self waitForExpectations:@[ expectation ] timeout:1.0]; -} - -/** Tests the timer is running at the desired frequency. */ -- (void)testTimerIsRunningAtDesiredFrequency { - __block int numberOfTimesCalled = 0; - self.prioritizer.uploadPackageWithConditionsBlock = ^{ - numberOfTimesCalled++; - }; - dispatch_sync([GDTUploadCoordinator sharedInstance].coordinationQueue, ^{ - // Timer should fire 1 times a second. - [GDTUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC; - [GDTUploadCoordinator sharedInstance].timerLeeway = 0; - }); - [[GDTUploadCoordinator sharedInstance] startTimer]; - - // Run for 5 seconds. - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]]; - - // It's expected that the timer called the prioritizer 5 times +/- 1 during that 1 second + the - // coordinator running before that. - dispatch_sync([GDTUploadCoordinator sharedInstance].coordinationQueue, ^{ - XCTAssertGreaterThan(numberOfTimesCalled, 4); // Some latency is expected on a busy system. - }); -} - -/** Tests uploading events via the coordinator timer. */ -- (void)testUploadingEventsViaTimer { - __block int uploadAttempts = 0; - self.prioritizer.events = [GDTEventGenerator generate3StoredEvents]; - self.uploader.uploadPackageBlock = ^(GDTUploadPackage *_Nonnull package) { - [package completeDelivery]; - uploadAttempts++; - }; - [GDTUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC / 10; - [GDTUploadCoordinator sharedInstance].timerLeeway = 0; - - [[GDTUploadCoordinator sharedInstance] startTimer]; - - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; - dispatch_sync([GDTUploadCoordinator sharedInstance].coordinationQueue, ^{ - // More than two attempts should have been made. - XCTAssertGreaterThan(uploadAttempts, 2); - }); -} - -/** Tests the situation in which the uploader failed to upload the events for some reason. */ -- (void)testThatAFailedUploadResultsInAnEventualRetry { - __block int uploadAttempts = 0; - self.prioritizer.events = [GDTEventGenerator generate3StoredEvents]; - self.uploader.uploadPackageBlock = ^(GDTUploadPackage *_Nonnull package) { - [package retryDeliveryInTheFuture]; - uploadAttempts++; - }; - [GDTUploadCoordinator sharedInstance].timerInterval = NSEC_PER_SEC / 10; - [GDTUploadCoordinator sharedInstance].timerLeeway = 0; - - [[GDTUploadCoordinator sharedInstance] startTimer]; - - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; - dispatch_sync([GDTUploadCoordinator sharedInstance].coordinationQueue, ^{ - // More than two attempts should have been made. - XCTAssertGreaterThan(uploadAttempts, 2); - }); -} - -@end diff --git a/GoogleDataTransportCCTSupport.podspec b/GoogleDataTransportCCTSupport.podspec index 84fa71128a6..9ddc3cde44b 100644 --- a/GoogleDataTransportCCTSupport.podspec +++ b/GoogleDataTransportCCTSupport.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'GoogleDataTransportCCTSupport' - s.version = '1.0.1' + s.version = '1.1.0' s.summary = 'Support library for the GoogleDataTransport CCT backend target.' @@ -21,7 +21,7 @@ Support library to provide event prioritization and uploading for the GoogleData s.osx.deployment_target = '10.11' s.tvos.deployment_target = '10.0' - # To develop or run the tests, >= 1.6.0 must be installed. + # To develop or run the tests, >= 1.8.0.beta.1 must be installed. s.cocoapods_version = '>= 1.4.0' s.static_framework = true @@ -30,8 +30,10 @@ Support library to provide event prioritization and uploading for the GoogleData s.source_files = 'GoogleDataTransportCCTSupport/GDTCCTLibrary/**/*' s.private_header_files = 'GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/*.h' - s.dependency 'GoogleDataTransport', '~> 1.1' - s.dependency 'nanopb' + s.libraries = ['z'] + + s.dependency 'GoogleDataTransport', '~> 2.0' + s.dependency 'nanopb', '~> 0.3.901' header_search_paths = { 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}/GoogleDataTransportCCTSupport/"' diff --git a/GoogleDataTransportCCTSupport/CHANGELOG.md b/GoogleDataTransportCCTSupport/CHANGELOG.md index eb68ade0709..fc74d0f67b2 100644 --- a/GoogleDataTransportCCTSupport/CHANGELOG.md +++ b/GoogleDataTransportCCTSupport/CHANGELOG.md @@ -1,3 +1,15 @@ +# v1.1.0 +- Updates GDT dependency to GDTCOR prefixed version. + +# v1.0.4 +- Balances background task creation with background task ending. (#3759) + +# v1.0.3 +- Remove all NSAsserts in favor of GDTCORAssert. + +# v1.0.2 +- More safely handle backgrounding. + # v1.0.1 - Removed unused fields from firebasecore.proto. diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTNanopbHelpers.m b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTNanopbHelpers.m index e723304d24a..1142b235d75 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTNanopbHelpers.m +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTNanopbHelpers.m @@ -22,7 +22,7 @@ #import #endif // TARGET_OS_IOS || TARGET_OS_TV -#import +#import #import #import @@ -50,8 +50,8 @@ pb_ostream_t sizestream = PB_OSTREAM_SIZING; // Encode 1 time to determine the size. if (!pb_encode(&sizestream, gdt_cct_BatchedLogRequest_fields, batchedLogRequest)) { - GDTLogError(GDTMCEGeneralError, @"Error in nanopb encoding for size: %s", - PB_GET_ERROR(&sizestream)); + GDTCORLogError(GDTCORMCEGeneralError, @"Error in nanopb encoding for size: %s", + PB_GET_ERROR(&sizestream)); } // Encode a 2nd time to actually get the bytes from it. @@ -59,8 +59,8 @@ CFMutableDataRef dataRef = CFDataCreateMutable(CFAllocatorGetDefault(), bufferSize); pb_ostream_t ostream = pb_ostream_from_buffer((void *)CFDataGetBytePtr(dataRef), bufferSize); if (!pb_encode(&ostream, gdt_cct_BatchedLogRequest_fields, batchedLogRequest)) { - GDTLogError(GDTMCEGeneralError, @"Error in nanopb encoding for bytes: %s", - PB_GET_ERROR(&ostream)); + GDTCORLogError(GDTCORMCEGeneralError, @"Error in nanopb encoding for bytes: %s", + PB_GET_ERROR(&ostream)); } CFDataSetLength(dataRef, ostream.bytes_written); @@ -68,7 +68,7 @@ } gdt_cct_BatchedLogRequest GDTCCTConstructBatchedLogRequest( - NSDictionary *> *logMappingIDToLogSet) { + NSDictionary *> *logMappingIDToLogSet) { gdt_cct_BatchedLogRequest batchedLogRequest = gdt_cct_BatchedLogRequest_init_default; NSUInteger numberOfLogRequests = logMappingIDToLogSet.count; gdt_cct_LogRequest *logRequests = malloc(sizeof(gdt_cct_LogRequest) * numberOfLogRequests); @@ -76,7 +76,7 @@ gdt_cct_BatchedLogRequest GDTCCTConstructBatchedLogRequest( __block int i = 0; [logMappingIDToLogSet enumerateKeysAndObjectsUsingBlock:^( NSString *_Nonnull logMappingID, - NSSet *_Nonnull logSet, BOOL *_Nonnull stop) { + NSSet *_Nonnull logSet, BOOL *_Nonnull stop) { int32_t logSource = [logMappingID intValue]; gdt_cct_LogRequest logRequest = GDTCCTConstructLogRequest(logSource, logSet); logRequests[i] = logRequest; @@ -89,9 +89,10 @@ gdt_cct_BatchedLogRequest GDTCCTConstructBatchedLogRequest( } gdt_cct_LogRequest GDTCCTConstructLogRequest(int32_t logSource, - NSSet *_Nonnull logSet) { + NSSet *_Nonnull logSet) { if (logSet.count == 0) { - GDTLogError(GDTMCEGeneralError, @"%@", @"An empty event set can't be serialized to proto."); + GDTCORLogError(GDTCORMCEGeneralError, @"%@", + @"An empty event set can't be serialized to proto."); gdt_cct_LogRequest logRequest = gdt_cct_LogRequest_init_default; return logRequest; } @@ -102,7 +103,7 @@ gdt_cct_LogRequest GDTCCTConstructLogRequest(int32_t logSource, logRequest.has_client_info = 1; logRequest.log_event = malloc(sizeof(gdt_cct_LogEvent) * logSet.count); int i = 0; - for (GDTStoredEvent *log in logSet) { + for (GDTCORStoredEvent *log in logSet) { gdt_cct_LogEvent logEvent = GDTCCTConstructLogEvent(log); logRequest.log_event[i] = logEvent; i++; @@ -112,7 +113,7 @@ gdt_cct_LogRequest GDTCCTConstructLogRequest(int32_t logSource, return logRequest; } -gdt_cct_LogEvent GDTCCTConstructLogEvent(GDTStoredEvent *event) { +gdt_cct_LogEvent GDTCCTConstructLogEvent(GDTCORStoredEvent *event) { gdt_cct_LogEvent logEvent = gdt_cct_LogEvent_init_default; logEvent.event_time_ms = event.clockSnapshot.timeMillis; logEvent.has_event_time_ms = 1; @@ -127,8 +128,8 @@ gdt_cct_LogEvent GDTCCTConstructLogEvent(GDTStoredEvent *event) { options:0 error:&error]; if (error) { - GDTLogError(GDTMCEGeneralError, @"There was an error reading extension bytes from disk: %@", - error); + GDTCORLogError(GDTCORMCEGeneralError, + @"There was an error reading extension bytes from disk: %@", error); return logEvent; } logEvent.source_extension = GDTCCTEncodeData(extensionBytes); // read bytes from the file. diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTPrioritizer.m b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTPrioritizer.m index 414cc731537..94859a7490c 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTPrioritizer.m +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTPrioritizer.m @@ -16,10 +16,10 @@ #import "GDTCCTLibrary/Private/GDTCCTPrioritizer.h" -#import -#import -#import -#import +#import +#import +#import +#import const static int64_t kMillisPerDay = 8.64e+7; @@ -27,7 +27,7 @@ @implementation GDTCCTPrioritizer + (void)load { GDTCCTPrioritizer *prioritizer = [GDTCCTPrioritizer sharedInstance]; - [[GDTRegistrar sharedInstance] registerPrioritizer:prioritizer target:kGDTTargetCCT]; + [[GDTCORRegistrar sharedInstance] registerPrioritizer:prioritizer target:kGDTCORTargetCCT]; } + (instancetype)sharedInstance { @@ -48,26 +48,26 @@ - (instancetype)init { return self; } -#pragma mark - GDTPrioritizer Protocol +#pragma mark - GDTCORPrioritizer Protocol -- (void)prioritizeEvent:(GDTStoredEvent *)event { +- (void)prioritizeEvent:(GDTCORStoredEvent *)event { dispatch_async(_queue, ^{ [self.events addObject:event]; }); } -- (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)conditions { - GDTUploadPackage *package = [[GDTUploadPackage alloc] initWithTarget:kGDTTargetCCT]; +- (GDTCORUploadPackage *)uploadPackageWithConditions:(GDTCORUploadConditions)conditions { + GDTCORUploadPackage *package = [[GDTCORUploadPackage alloc] initWithTarget:kGDTCORTargetCCT]; dispatch_sync(_queue, ^{ - NSSet *logEventsThatWillBeSent; + NSSet *logEventsThatWillBeSent; // A high priority event effectively flushes all events to be sent. - if ((conditions & GDTUploadConditionHighPriority) == GDTUploadConditionHighPriority) { + if ((conditions & GDTCORUploadConditionHighPriority) == GDTCORUploadConditionHighPriority) { package.events = self.events; return; } // If on wifi, upload logs that are ok to send on wifi. - if ((conditions & GDTUploadConditionWifiData) == GDTUploadConditionWifiData) { + if ((conditions & GDTCORUploadConditionWifiData) == GDTCORUploadConditionWifiData) { logEventsThatWillBeSent = [self logEventsOkToSendOnWifi]; } else { logEventsThatWillBeSent = [self logEventsOkToSendOnMobileData]; @@ -76,13 +76,13 @@ - (GDTUploadPackage *)uploadPackageWithConditions:(GDTUploadConditions)condition // If it's been > 24h since the last daily upload, upload logs with the daily QoS. if (self.timeOfLastDailyUpload) { int64_t millisSinceLastUpload = - [GDTClock snapshot].timeMillis - self.timeOfLastDailyUpload.timeMillis; + [GDTCORClock snapshot].timeMillis - self.timeOfLastDailyUpload.timeMillis; if (millisSinceLastUpload > kMillisPerDay) { logEventsThatWillBeSent = [logEventsThatWillBeSent setByAddingObjectsFromSet:[self logEventsOkToSendDaily]]; } } else { - self.timeOfLastDailyUpload = [GDTClock snapshot]; + self.timeOfLastDailyUpload = [GDTCORClock snapshot]; logEventsThatWillBeSent = [logEventsThatWillBeSent setByAddingObjectsFromSet:[self logEventsOkToSendDaily]]; } @@ -108,21 +108,21 @@ typedef NS_ENUM(NSInteger, GDTCCTQoSTier) { GDTCCTQoSWifiOnly = 5, }; -/** Converts a GDTEventQoS to a GDTCCTQoS tier. +/** Converts a GDTCOREventQoS to a GDTCCTQoS tier. * - * @param qosTier The GDTEventQoS value. + * @param qosTier The GDTCOREventQoS value. * @return A static NSNumber that represents the CCT QoS tier. */ FOUNDATION_STATIC_INLINE -NSNumber *GDTCCTQosTierFromGDTEventQosTier(GDTEventQoS qosTier) { +NSNumber *GDTCCTQosTierFromGDTCOREventQosTier(GDTCOREventQoS qosTier) { switch (qosTier) { - case GDTEventQoSWifiOnly: + case GDTCOREventQoSWifiOnly: return @(GDTCCTQoSWifiOnly); break; - case GDTEventQoSTelemetry: + case GDTCOREventQoSTelemetry: // falls through. - case GDTEventQoSDaily: + case GDTCOREventQoSDaily: return @(GDTCCTQoSDaily); break; @@ -137,10 +137,10 @@ typedef NS_ENUM(NSInteger, GDTCCTQoSTier) { * @note This should be called from a thread safe method. * @return A set of logs that are ok to upload whilst on mobile data. */ -- (NSSet *)logEventsOkToSendOnMobileData { - return - [self.events objectsPassingTest:^BOOL(GDTStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { - return [GDTCCTQosTierFromGDTEventQosTier(event.qosTier) isEqual:@(GDTCCTQoSDefault)]; +- (NSSet *)logEventsOkToSendOnMobileData { + return [self.events + objectsPassingTest:^BOOL(GDTCORStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { + return [GDTCCTQosTierFromGDTCOREventQosTier(event.qosTier) isEqual:@(GDTCCTQoSDefault)]; }]; } @@ -149,10 +149,10 @@ typedef NS_ENUM(NSInteger, GDTCCTQoSTier) { * @note This should be called from a thread safe method. * @return A set of logs that are ok to upload whilst on wifi. */ -- (NSSet *)logEventsOkToSendOnWifi { - return - [self.events objectsPassingTest:^BOOL(GDTStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { - NSNumber *qosTier = GDTCCTQosTierFromGDTEventQosTier(event.qosTier); +- (NSSet *)logEventsOkToSendOnWifi { + return [self.events + objectsPassingTest:^BOOL(GDTCORStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { + NSNumber *qosTier = GDTCCTQosTierFromGDTCOREventQosTier(event.qosTier); return [qosTier isEqual:@(GDTCCTQoSDefault)] || [qosTier isEqual:@(GDTCCTQoSWifiOnly)] || [qosTier isEqual:@(GDTCCTQoSDaily)]; }]; @@ -163,25 +163,25 @@ typedef NS_ENUM(NSInteger, GDTCCTQoSTier) { * @note This should be called from a thread safe method. * @return A set of logs that are ok to upload only once per day. */ -- (NSSet *)logEventsOkToSendDaily { - return - [self.events objectsPassingTest:^BOOL(GDTStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { - return [GDTCCTQosTierFromGDTEventQosTier(event.qosTier) isEqual:@(GDTCCTQoSDaily)]; +- (NSSet *)logEventsOkToSendDaily { + return [self.events + objectsPassingTest:^BOOL(GDTCORStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { + return [GDTCCTQosTierFromGDTCOREventQosTier(event.qosTier) isEqual:@(GDTCCTQoSDaily)]; }]; } -#pragma mark - GDTUploadPackageProtocol +#pragma mark - GDTCORUploadPackageProtocol -- (void)packageDelivered:(GDTUploadPackage *)package successful:(BOOL)successful { +- (void)packageDelivered:(GDTCORUploadPackage *)package successful:(BOOL)successful { dispatch_async(_queue, ^{ - NSSet *events = [package.events copy]; - for (GDTStoredEvent *event in events) { + NSSet *events = [package.events copy]; + for (GDTCORStoredEvent *event in events) { [self.events removeObject:event]; } }); } -- (void)packageExpired:(GDTUploadPackage *)package { +- (void)packageExpired:(GDTCORUploadPackage *)package { [self packageDelivered:package successful:YES]; } diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTUploader.m b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTUploader.m index feeaa6a3751..7baf3477a44 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTUploader.m +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTCCTUploader.m @@ -16,9 +16,9 @@ #import "GDTCCTLibrary/Private/GDTCCTUploader.h" -#import -#import -#import +#import +#import +#import #import #import @@ -34,8 +34,8 @@ @interface GDTCCTUploader () // Redeclared as readwrite. @property(nullable, nonatomic, readwrite) NSURLSessionUploadTask *currentTask; -/** If running in the background, the current background ID. */ -@property(nonatomic) GDTBackgroundIdentifier backgroundID; +/** Set to YES if running in the background. */ +@property(nonatomic) BOOL runningInBackground; @end @@ -43,7 +43,7 @@ @implementation GDTCCTUploader + (void)load { GDTCCTUploader *uploader = [GDTCCTUploader sharedInstance]; - [[GDTRegistrar sharedInstance] registerUploader:uploader target:kGDTTargetCCT]; + [[GDTCORRegistrar sharedInstance] registerUploader:uploader target:kGDTCORTargetCCT]; } + (instancetype)sharedInstance { @@ -61,7 +61,6 @@ - (instancetype)init { _uploaderQueue = dispatch_queue_create("com.google.GDTCCTUploader", DISPATCH_QUEUE_SERIAL); NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; _uploaderSession = [NSURLSession sessionWithConfiguration:config]; - _backgroundID = GDTBackgroundIdentifierInvalid; } return self; } @@ -85,42 +84,53 @@ - (NSURL *)defaultServerURL { return defaultServerURL; } -- (void)uploadPackage:(GDTUploadPackage *)package { +- (void)uploadPackage:(GDTCORUploadPackage *)package { + GDTCORBackgroundIdentifier bgID = GDTCORBackgroundIdentifierInvalid; + if (_runningInBackground) { + bgID = [[GDTCORApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + } + }]; + } + dispatch_async(_uploaderQueue, ^{ if (self->_currentTask || self->_currentUploadPackage) { - GDTLogWarning(GDTMCWUploadFailed, @"%@", - @"An upload shouldn't be initiated with another in progress."); + GDTCORLogWarning(GDTCORMCWUploadFailed, @"%@", + @"An upload shouldn't be initiated with another in progress."); return; } NSURL *serverURL = self.serverURL ? self.serverURL : [self defaultServerURL]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:serverURL]; request.HTTPMethod = @"POST"; - id completionHandler = - ^(NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error) { - if (error) { - GDTLogWarning(GDTMCWUploadFailed, @"There was an error uploading events: %@", error); - } - NSError *decodingError; - gdt_cct_LogResponse logResponse = GDTCCTDecodeLogResponse(data, &decodingError); - if (!decodingError && logResponse.has_next_request_wait_millis) { - self->_nextUploadTime = - [GDTClock clockSnapshotInTheFuture:logResponse.next_request_wait_millis]; - } else { - // 15 minutes from now. - self->_nextUploadTime = [GDTClock clockSnapshotInTheFuture:15 * 60 * 1000]; - } - pb_release(gdt_cct_LogResponse_fields, &logResponse); - [package completeDelivery]; - if (self->_backgroundID != GDTBackgroundIdentifierInvalid) { - [[GDTApplication sharedApplication] endBackgroundTask:self->_backgroundID]; - self->_backgroundID = GDTBackgroundIdentifierInvalid; - } - self.currentTask = nil; - self.currentUploadPackage = nil; - }; + id completionHandler = ^(NSData *_Nullable data, NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + GDTCORLogWarning(GDTCORMCWUploadFailed, @"There was an error uploading events: %@", error); + } + NSError *decodingError; + gdt_cct_LogResponse logResponse = GDTCCTDecodeLogResponse(data, &decodingError); + if (!decodingError && logResponse.has_next_request_wait_millis) { + self->_nextUploadTime = + [GDTCORClock clockSnapshotInTheFuture:logResponse.next_request_wait_millis]; + } else { + // 15 minutes from now. + self->_nextUploadTime = [GDTCORClock clockSnapshotInTheFuture:15 * 60 * 1000]; + } + pb_release(gdt_cct_LogResponse_fields, &logResponse); + [package completeDelivery]; + + // End the background task if there was one. + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + } + self.currentTask = nil; + self.currentUploadPackage = nil; + }; self->_currentUploadPackage = package; - NSData *requestProtoData = [self constructRequestProtoFromPackage:(GDTUploadPackage *)package]; + NSData *requestProtoData = + [self constructRequestProtoFromPackage:(GDTCORUploadPackage *)package]; self.currentTask = [self.uploaderSession uploadTaskWithRequest:request fromData:requestProtoData completionHandler:completionHandler]; @@ -128,7 +138,7 @@ - (void)uploadPackage:(GDTUploadPackage *)package { }); } -- (BOOL)readyToUploadWithConditions:(GDTUploadConditions)conditions { +- (BOOL)readyToUploadWithConditions:(GDTCORUploadConditions)conditions { __block BOOL result = NO; dispatch_sync(_uploaderQueue, ^{ if (self->_currentUploadPackage) { @@ -139,11 +149,11 @@ - (BOOL)readyToUploadWithConditions:(GDTUploadConditions)conditions { result = NO; return; } - if ((conditions & GDTUploadConditionHighPriority) == GDTUploadConditionHighPriority) { + if ((conditions & GDTCORUploadConditionHighPriority) == GDTCORUploadConditionHighPriority) { result = YES; return; } else if (self->_nextUploadTime) { - result = [[GDTClock snapshot] isAfter:self->_nextUploadTime]; + result = [[GDTCORClock snapshot] isAfter:self->_nextUploadTime]; return; } result = YES; @@ -158,12 +168,12 @@ - (BOOL)readyToUploadWithConditions:(GDTUploadConditions)conditions { * @param package The upload package used to construct the request proto bytes. * @return Proto bytes representing a gdt_cct_LogRequest object. */ -- (nonnull NSData *)constructRequestProtoFromPackage:(GDTUploadPackage *)package { +- (nonnull NSData *)constructRequestProtoFromPackage:(GDTCORUploadPackage *)package { // Segment the log events by log type. - NSMutableDictionary *> *logMappingIDToLogSet = + NSMutableDictionary *> *logMappingIDToLogSet = [[NSMutableDictionary alloc] init]; [package.events - enumerateObjectsUsingBlock:^(GDTStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { + enumerateObjectsUsingBlock:^(GDTCORStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { NSMutableSet *logSet = logMappingIDToLogSet[event.mappingID]; logSet = logSet ? logSet : [[NSMutableSet alloc] init]; [logSet addObject:event]; @@ -178,9 +188,9 @@ - (nonnull NSData *)constructRequestProtoFromPackage:(GDTUploadPackage *)package return data ? data : [[NSData alloc] init]; } -#pragma mark - GDTUploadPackageProtocol +#pragma mark - GDTCORUploadPackageProtocol -- (void)packageExpired:(GDTUploadPackage *)package { +- (void)packageExpired:(GDTCORUploadPackage *)package { dispatch_async(_uploaderQueue, ^{ [self.currentTask cancel]; self.currentTask = nil; @@ -188,15 +198,27 @@ - (void)packageExpired:(GDTUploadPackage *)package { }); } -#pragma mark - GDTLifecycleProtocol +#pragma mark - GDTCORLifecycleProtocol -- (void)appWillBackground:(GDTApplication *)app { - _backgroundID = [app beginBackgroundTaskWithExpirationHandler:^{ - [app endBackgroundTask:self->_backgroundID]; +- (void)appWillBackground:(GDTCORApplication *)app { + _runningInBackground = YES; + __block GDTCORBackgroundIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [app endBackgroundTask:bgID]; + } }]; + if (bgID != GDTCORBackgroundIdentifierInvalid) { + dispatch_async(_uploaderQueue, ^{ + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + }); + } +} + +- (void)appWillForeground:(GDTCORApplication *)app { + _runningInBackground = NO; } -- (void)appWillTerminate:(GDTApplication *)application { +- (void)appWillTerminate:(GDTCORApplication *)application { dispatch_sync(_uploaderQueue, ^{ [self.currentTask cancel]; [self.currentUploadPackage completeDelivery]; diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTFLLPrioritizer.m b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTFLLPrioritizer.m new file mode 100644 index 00000000000..819c27187a8 --- /dev/null +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTFLLPrioritizer.m @@ -0,0 +1,188 @@ +/* + * Copyright 2019 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 "GDTCCTLibrary/Private/GDTFLLPrioritizer.h" + +#import +#import +#import +#import + +const static int64_t kMillisPerDay = 8.64e+7; + +@implementation GDTFLLPrioritizer + ++ (void)load { + GDTFLLPrioritizer *prioritizer = [GDTFLLPrioritizer sharedInstance]; + [[GDTCORRegistrar sharedInstance] registerPrioritizer:prioritizer target:kGDTCORTargetFLL]; +} + ++ (instancetype)sharedInstance { + static GDTFLLPrioritizer *sharedInstance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[GDTFLLPrioritizer alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _queue = dispatch_queue_create("com.google.GDTFLLPrioritizer", DISPATCH_QUEUE_SERIAL); + _events = [[NSMutableSet alloc] init]; + } + return self; +} + +#pragma mark - GDTCORPrioritizer Protocol + +- (void)prioritizeEvent:(GDTCORStoredEvent *)event { + dispatch_async(_queue, ^{ + [self.events addObject:event]; + }); +} + +- (GDTCORUploadPackage *)uploadPackageWithConditions:(GDTCORUploadConditions)conditions { + GDTCORUploadPackage *package = [[GDTCORUploadPackage alloc] initWithTarget:kGDTCORTargetFLL]; + dispatch_sync(_queue, ^{ + NSSet *logEventsThatWillBeSent; + // A high priority event effectively flushes all events to be sent. + if ((conditions & GDTCORUploadConditionHighPriority) == GDTCORUploadConditionHighPriority) { + package.events = self.events; + return; + } + + // If on wifi, upload logs that are ok to send on wifi. + if ((conditions & GDTCORUploadConditionWifiData) == GDTCORUploadConditionWifiData) { + logEventsThatWillBeSent = [self logEventsOkToSendOnWifi]; + } else { + logEventsThatWillBeSent = [self logEventsOkToSendOnMobileData]; + } + + // If it's been > 24h since the last daily upload, upload logs with the daily QoS. + if (self.timeOfLastDailyUpload) { + int64_t millisSinceLastUpload = + [GDTCORClock snapshot].timeMillis - self.timeOfLastDailyUpload.timeMillis; + if (millisSinceLastUpload > kMillisPerDay) { + logEventsThatWillBeSent = + [logEventsThatWillBeSent setByAddingObjectsFromSet:[self logEventsOkToSendDaily]]; + } + } else { + self.timeOfLastDailyUpload = [GDTCORClock snapshot]; + logEventsThatWillBeSent = + [logEventsThatWillBeSent setByAddingObjectsFromSet:[self logEventsOkToSendDaily]]; + } + package.events = logEventsThatWillBeSent; + }); + return package; +} + +#pragma mark - Private helper methods + +/** The different possible quality of service specifiers. High values indicate high priority. */ +typedef NS_ENUM(NSInteger, GDTFLLQoSTier) { + /** The QoS tier wasn't set, and won't ever be sent. */ + GDTFLLQoSDefault = 0, + + /** This event is internal telemetry data that should not be sent on its own if possible. */ + GDTFLLQoSTelemetry = 1, + + /** This event should be sent, but in a batch only roughly once per day. */ + GDTFLLQoSDaily = 2, + + /** This event should only be uploaded on wifi. */ + GDTFLLQoSWifiOnly = 5, +}; + +/** Converts a GDTCOREventQoS to a GDTFLLQoS tier. + * + * @param qosTier The GDTCOREventQoS value. + * @return A static NSNumber that represents the CCT QoS tier. + */ +FOUNDATION_STATIC_INLINE +NSNumber *GDTCCTQosTierFromGDTCOREventQosTier(GDTCOREventQoS qosTier) { + switch (qosTier) { + case GDTCOREventQoSWifiOnly: + return @(GDTFLLQoSWifiOnly); + break; + + case GDTCOREventQoSTelemetry: + // falls through. + case GDTCOREventQoSDaily: + return @(GDTFLLQoSDaily); + break; + + default: + return @(GDTFLLQoSDefault); + break; + } +} + +/** Returns a set of logs that are ok to upload whilst on mobile data. + * + * @note This should be called from a thread safe method. + * @return A set of logs that are ok to upload whilst on mobile data. + */ +- (NSSet *)logEventsOkToSendOnMobileData { + return [self.events + objectsPassingTest:^BOOL(GDTCORStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { + return [GDTCCTQosTierFromGDTCOREventQosTier(event.qosTier) isEqual:@(GDTFLLQoSDefault)]; + }]; +} + +/** Returns a set of logs that are ok to upload whilst on wifi. + * + * @note This should be called from a thread safe method. + * @return A set of logs that are ok to upload whilst on wifi. + */ +- (NSSet *)logEventsOkToSendOnWifi { + return [self.events + objectsPassingTest:^BOOL(GDTCORStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { + NSNumber *qosTier = GDTCCTQosTierFromGDTCOREventQosTier(event.qosTier); + return [qosTier isEqual:@(GDTFLLQoSDefault)] || [qosTier isEqual:@(GDTFLLQoSWifiOnly)] || + [qosTier isEqual:@(GDTFLLQoSDaily)]; + }]; +} + +/** Returns a set of logs that only should have a single upload attempt per day. + * + * @note This should be called from a thread safe method. + * @return A set of logs that are ok to upload only once per day. + */ +- (NSSet *)logEventsOkToSendDaily { + return [self.events + objectsPassingTest:^BOOL(GDTCORStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { + return [GDTCCTQosTierFromGDTCOREventQosTier(event.qosTier) isEqual:@(GDTFLLQoSDaily)]; + }]; +} + +#pragma mark - GDTCORUploadPackageProtocol + +- (void)packageDelivered:(GDTCORUploadPackage *)package successful:(BOOL)successful { + dispatch_async(_queue, ^{ + NSSet *events = [package.events copy]; + for (GDTCORStoredEvent *event in events) { + [self.events removeObject:event]; + } + }); +} + +- (void)packageExpired:(GDTCORUploadPackage *)package { + [self packageDelivered:package successful:YES]; +} + +@end diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTFLLUploader.m b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTFLLUploader.m new file mode 100644 index 00000000000..6f1eb0e77ed --- /dev/null +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/GDTFLLUploader.m @@ -0,0 +1,334 @@ +/* + * Copyright 2019 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 "GDTCCTLibrary/Private/GDTFLLUploader.h" + +#import + +#import +#import +#import + +#import +#import +#import + +#import "GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h" +#import "GDTCCTLibrary/Private/GDTFLLPrioritizer.h" + +#import "GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h" + +@interface GDTFLLUploader () + +// Redeclared as readwrite. +@property(nullable, nonatomic, readwrite) NSURLSessionUploadTask *currentTask; + +/** Set to YES if running in the background. */ +@property(nonatomic) BOOL runningInBackground; + +@end + +@implementation GDTFLLUploader + ++ (void)load { + GDTFLLUploader *uploader = [GDTFLLUploader sharedInstance]; + [[GDTCORRegistrar sharedInstance] registerUploader:uploader target:kGDTCORTargetFLL]; +} + ++ (instancetype)sharedInstance { + static GDTFLLUploader *sharedInstance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[GDTFLLUploader alloc] init]; + }); + return sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _uploaderQueue = dispatch_queue_create("com.google.GDTFLLUploader", DISPATCH_QUEUE_SERIAL); + NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; + _uploaderSession = [NSURLSession sessionWithConfiguration:config]; + } + return self; +} + +- (NSURL *)defaultServerURL { + static NSURL *defaultServerURL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // These strings should be interleaved to construct the real URL. This is just to (hopefully) + // fool github URL scanning bots. + const char *p1 = "hts/frbslgigp.ogepscmv/ieo/eaybtho"; + const char *p2 = "tp:/ieaeogn-agolai.o/1frlglgc/aclg"; + const char defaultURL[69] = { + p1[0], p2[0], p1[1], p2[1], p1[2], p2[2], p1[3], p2[3], p1[4], p2[4], + p1[5], p2[5], p1[6], p2[6], p1[7], p2[7], p1[8], p2[8], p1[9], p2[9], + p1[10], p2[10], p1[11], p2[11], p1[12], p2[12], p1[13], p2[13], p1[14], p2[14], + p1[15], p2[15], p1[16], p2[16], p1[17], p2[17], p1[18], p2[18], p1[19], p2[19], + p1[20], p2[20], p1[21], p2[21], p1[22], p2[22], p1[23], p2[23], p1[24], p2[24], + p1[25], p2[25], p1[26], p2[26], p1[27], p2[27], p1[28], p2[28], p1[29], p2[29], + p1[30], p2[30], p1[31], p2[31], p1[32], p2[32], p1[33], p2[33], '\0'}; + defaultServerURL = [NSURL URLWithString:[NSString stringWithUTF8String:defaultURL]]; + }); + return defaultServerURL; +} + +- (NSString *)defaultAPIKey { + static NSString *defaultServerKey; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // These strings should be interleaved to construct the real key. + const char *p1 = "AzSBG0honD6A-PxV5nBc"; + const char *p2 = "Iay44Iwtu2vV0AOrz1C"; + const char defaultKey[40] = {p1[0], p2[0], p1[1], p2[1], p1[2], p2[2], p1[3], p2[3], + p1[4], p2[4], p1[5], p2[5], p1[6], p2[6], p1[7], p2[7], + p1[8], p2[8], p1[9], p2[9], p1[10], p2[10], p1[11], p2[11], + p1[12], p2[12], p1[13], p2[13], p1[14], p2[14], p1[15], p2[15], + p1[16], p2[16], p1[17], p2[17], p1[18], p2[18], p1[19], '\0'}; + defaultServerKey = [NSString stringWithUTF8String:defaultKey]; + }); + return defaultServerKey; +} + +- (void)uploadPackage:(GDTCORUploadPackage *)package { + GDTCORBackgroundIdentifier bgID = GDTCORBackgroundIdentifierInvalid; + if (_runningInBackground) { + bgID = [[GDTCORApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + } + }]; + } + + dispatch_async(_uploaderQueue, ^{ + if (self->_currentTask || self->_currentUploadPackage) { + GDTCORLogWarning(GDTCORMCWUploadFailed, @"%@", + @"An upload shouldn't be initiated with another in progress."); + return; + } + NSURL *serverURL = self.serverURL ? self.serverURL : [self defaultServerURL]; + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:serverURL]; + [request setValue:[self defaultAPIKey] forHTTPHeaderField:@"X-Goog-Api-Key"]; + [request setValue:@"application/x-protobuf" forHTTPHeaderField:@"Content-Type"]; + [request setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"]; + [request setValue:@"gzip" forHTTPHeaderField:@"Accept-Encoding"]; + request.HTTPMethod = @"POST"; + + id completionHandler = ^(NSData *_Nullable data, NSURLResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + GDTCORLogWarning(GDTCORMCWUploadFailed, @"There was an error uploading events: %@", error); + } + NSError *decodingError; + gdt_cct_LogResponse logResponse = GDTCCTDecodeLogResponse(data, &decodingError); + if (!decodingError && logResponse.has_next_request_wait_millis) { + self->_nextUploadTime = + [GDTCORClock clockSnapshotInTheFuture:logResponse.next_request_wait_millis]; + } else { + // 15 minutes from now. + self->_nextUploadTime = [GDTCORClock clockSnapshotInTheFuture:15 * 60 * 1000]; + } + pb_release(gdt_cct_LogResponse_fields, &logResponse); + + // Only retry if one of these codes is returned. + if (((NSHTTPURLResponse *)response).statusCode == 429 || + ((NSHTTPURLResponse *)response).statusCode == 503) { + [package retryDeliveryInTheFuture]; + } else { + [package completeDelivery]; + } + + // End the background task if there was one. + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + } + self.currentTask = nil; + self.currentUploadPackage = nil; + }; + self->_currentUploadPackage = package; + NSData *requestProtoData = + [self constructRequestProtoFromPackage:(GDTCORUploadPackage *)package]; + NSData *gzippedData = [GDTFLLUploader gzippedData:requestProtoData]; + self.currentTask = [self.uploaderSession uploadTaskWithRequest:request + fromData:gzippedData + completionHandler:completionHandler]; + [self.currentTask resume]; + }); +} + +- (BOOL)readyToUploadWithConditions:(GDTCORUploadConditions)conditions { + __block BOOL result = NO; + dispatch_sync(_uploaderQueue, ^{ + if (self->_currentUploadPackage) { + result = NO; + return; + } + if (self->_currentTask) { + result = NO; + return; + } + if ((conditions & GDTCORUploadConditionHighPriority) == GDTCORUploadConditionHighPriority) { + result = YES; + return; + } else if (self->_nextUploadTime) { + result = [[GDTCORClock snapshot] isAfter:self->_nextUploadTime]; + return; + } + result = YES; + }); + return result; +} + +#pragma mark - Private helper methods + +/** Compresses the given data and returns a new data object. + * + * @note Reduced version from GULNSData+zlib.m of GoogleUtilities. + * @return Compressed data, or nil if there was an error. + */ ++ (nullable NSData *)gzippedData:(NSData *)data { +#if defined(__LP64__) && __LP64__ + // Don't support > 32bit length for 64 bit, see note in header. + if (data.length > UINT_MAX) { + return nil; + } +#endif + + const uint kChunkSize = 1024; + + const void *bytes = [data bytes]; + NSUInteger length = [data length]; + + int level = Z_DEFAULT_COMPRESSION; + if (!bytes || !length) { + return nil; + } + + z_stream strm; + bzero(&strm, sizeof(z_stream)); + + int memLevel = 8; // Default. + int windowBits = 15 + 16; // Enable gzip header instead of zlib header. + + int retCode; + if ((retCode = deflateInit2(&strm, level, Z_DEFLATED, windowBits, memLevel, + Z_DEFAULT_STRATEGY)) != Z_OK) { + return nil; + } + + // Hint the size at 1/4 the input size. + NSMutableData *result = [NSMutableData dataWithCapacity:(length / 4)]; + unsigned char output[kChunkSize]; + + // Setup the input. + strm.avail_in = (unsigned int)length; + strm.next_in = (unsigned char *)bytes; + + // Collect the data. + do { + // update what we're passing in + strm.avail_out = kChunkSize; + strm.next_out = output; + retCode = deflate(&strm, Z_FINISH); + if ((retCode != Z_OK) && (retCode != Z_STREAM_END)) { + deflateEnd(&strm); + return nil; + } + // Collect what we got. + unsigned gotBack = kChunkSize - strm.avail_out; + if (gotBack > 0) { + [result appendBytes:output length:gotBack]; + } + + } while (retCode == Z_OK); + + // If the loop exits, it used all input and the stream ended. + NSAssert(strm.avail_in == 0, + @"Should have finished deflating without using all input, %u bytes left", strm.avail_in); + NSAssert(retCode == Z_STREAM_END, + @"thought we finished deflate w/o getting a result of stream end, code %d", retCode); + + // Clean up. + deflateEnd(&strm); + + return result; +} + +/** Constructs data given an upload package. + * + * @param package The upload package used to construct the request proto bytes. + * @return Proto bytes representing a gdt_cct_LogRequest object. + */ +- (nonnull NSData *)constructRequestProtoFromPackage:(GDTCORUploadPackage *)package { + // Segment the log events by log type. + NSMutableDictionary *> *logMappingIDToLogSet = + [[NSMutableDictionary alloc] init]; + [package.events + enumerateObjectsUsingBlock:^(GDTCORStoredEvent *_Nonnull event, BOOL *_Nonnull stop) { + NSMutableSet *logSet = logMappingIDToLogSet[event.mappingID]; + logSet = logSet ? logSet : [[NSMutableSet alloc] init]; + [logSet addObject:event]; + logMappingIDToLogSet[event.mappingID] = logSet; + }]; + + gdt_cct_BatchedLogRequest batchedLogRequest = + GDTCCTConstructBatchedLogRequest(logMappingIDToLogSet); + + NSData *data = GDTCCTEncodeBatchedLogRequest(&batchedLogRequest); + pb_release(gdt_cct_BatchedLogRequest_fields, &batchedLogRequest); + return data ? data : [[NSData alloc] init]; +} + +#pragma mark - GDTCORUploadPackageProtocol + +- (void)packageExpired:(GDTCORUploadPackage *)package { + dispatch_async(_uploaderQueue, ^{ + [self.currentTask cancel]; + self.currentTask = nil; + self.currentUploadPackage = nil; + }); +} + +#pragma mark - GDTCORLifecycleProtocol + +- (void)appWillBackground:(GDTCORApplication *)app { + _runningInBackground = YES; + __block GDTCORBackgroundIdentifier bgID = [app beginBackgroundTaskWithExpirationHandler:^{ + if (bgID != GDTCORBackgroundIdentifierInvalid) { + [app endBackgroundTask:bgID]; + } + }]; + if (bgID != GDTCORBackgroundIdentifierInvalid) { + dispatch_async(_uploaderQueue, ^{ + [[GDTCORApplication sharedApplication] endBackgroundTask:bgID]; + }); + } +} + +- (void)appWillForeground:(GDTCORApplication *)app { + _runningInBackground = NO; +} + +- (void)appWillTerminate:(GDTCORApplication *)application { + dispatch_sync(_uploaderQueue, ^{ + [self.currentTask cancel]; + [self.currentUploadPackage completeDelivery]; + }); +} + +@end diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h index 792b5fb39e5..08081cc727a 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h @@ -16,7 +16,7 @@ #import -#import +#import #import "GDTCCTLibrary/Protogen/nanopb/cct.nanopb.h" @@ -63,7 +63,7 @@ NSData *GDTCCTEncodeBatchedLogRequest(gdt_cct_BatchedLogRequest *batchedLogReque */ FOUNDATION_EXPORT gdt_cct_BatchedLogRequest GDTCCTConstructBatchedLogRequest( - NSDictionary *> *logMappingIDToLogSet); + NSDictionary *> *logMappingIDToLogSet); /** Constructs a log request given a log source and a set of events. * @@ -72,15 +72,15 @@ gdt_cct_BatchedLogRequest GDTCCTConstructBatchedLogRequest( * @param logSet The set of events to send in this log request. */ FOUNDATION_EXPORT -gdt_cct_LogRequest GDTCCTConstructLogRequest(int32_t logSource, NSSet *logSet); +gdt_cct_LogRequest GDTCCTConstructLogRequest(int32_t logSource, NSSet *logSet); -/** Constructs a gdt_cct_LogEvent given a GDTStoredEvent*. +/** Constructs a gdt_cct_LogEvent given a GDTCORStoredEvent*. * - * @param event The GDTStoredEvent to convert. + * @param event The GDTCORStoredEvent to convert. * @return The new gdt_cct_LogEvent object. */ FOUNDATION_EXPORT -gdt_cct_LogEvent GDTCCTConstructLogEvent(GDTStoredEvent *event); +gdt_cct_LogEvent GDTCCTConstructLogEvent(GDTCORStoredEvent *event); /** Constructs a gdt_cct_ClientInfo representing the client device. * diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTPrioritizer.h b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTPrioritizer.h index 5986435cb1b..1908a31b817 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTPrioritizer.h +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTPrioritizer.h @@ -16,22 +16,22 @@ #import -#import -#import +#import +#import NS_ASSUME_NONNULL_BEGIN /** Manages the prioritization of events from GoogleDataTransport. */ -@interface GDTCCTPrioritizer : NSObject +@interface GDTCCTPrioritizer : NSObject /** The queue on which this prioritizer operates. */ @property(nonatomic) dispatch_queue_t queue; /** All log events that have been processed by this prioritizer. */ -@property(nonatomic) NSMutableSet *events; +@property(nonatomic) NSMutableSet *events; /** The most recent attempted upload of daily uploaded logs. */ -@property(nonatomic) GDTClock *timeOfLastDailyUpload; +@property(nonatomic) GDTCORClock *timeOfLastDailyUpload; /** Creates and/or returns the singleton instance of this class. * diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTUploader.h b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTUploader.h index dc4c8ace139..cb618631676 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTUploader.h +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTCCTUploader.h @@ -16,12 +16,12 @@ #import -#import +#import NS_ASSUME_NONNULL_BEGIN /** Class capable of uploading events to the CCT backend. */ -@interface GDTCCTUploader : NSObject +@interface GDTCCTUploader : NSObject /** The queue on which all CCT uploading will occur. */ @property(nonatomic, readonly) dispatch_queue_t uploaderQueue; @@ -36,10 +36,10 @@ NS_ASSUME_NONNULL_BEGIN @property(nullable, nonatomic, readonly) NSURLSessionUploadTask *currentTask; /** Current upload package. */ -@property(nullable, nonatomic) GDTUploadPackage *currentUploadPackage; +@property(nullable, nonatomic) GDTCORUploadPackage *currentUploadPackage; /** The next upload time. */ -@property(nullable, nonatomic) GDTClock *nextUploadTime; +@property(nullable, nonatomic) GDTCORClock *nextUploadTime; /** Creates and/or returns the singleton instance of this class. * diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTFLLPrioritizer.h b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTFLLPrioritizer.h new file mode 100644 index 00000000000..b7622f2ead9 --- /dev/null +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTFLLPrioritizer.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 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 + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** Manages the prioritization of events from GoogleDataTransport. */ +@interface GDTFLLPrioritizer : NSObject + +/** The queue on which this prioritizer operates. */ +@property(nonatomic) dispatch_queue_t queue; + +/** All log events that have been processed by this prioritizer. */ +@property(nonatomic) NSMutableSet *events; + +/** The most recent attempted upload of daily uploaded logs. */ +@property(nonatomic) GDTCORClock *timeOfLastDailyUpload; + +/** Creates and/or returns the singleton instance of this class. + * + * @return The singleton instance of this class. + */ ++ (instancetype)sharedInstance; + +NS_ASSUME_NONNULL_END + +@end diff --git a/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTFLLUploader.h b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTFLLUploader.h new file mode 100644 index 00000000000..c737f63545f --- /dev/null +++ b/GoogleDataTransportCCTSupport/GDTCCTLibrary/Private/GDTFLLUploader.h @@ -0,0 +1,52 @@ +/* + * Copyright 2019 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 + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** Class capable of uploading events to the CCT backend. */ +@interface GDTFLLUploader : NSObject + +/** The queue on which all CCT uploading will occur. */ +@property(nonatomic, readonly) dispatch_queue_t uploaderQueue; + +/** The server URL to upload to. Look at .m for the default value. */ +@property(nonatomic) NSURL *serverURL; + +/** The URL session that will attempt upload. */ +@property(nonatomic, readonly) NSURLSession *uploaderSession; + +/** The current upload task. */ +@property(nullable, nonatomic, readonly) NSURLSessionUploadTask *currentTask; + +/** Current upload package. */ +@property(nullable, nonatomic) GDTCORUploadPackage *currentUploadPackage; + +/** The next upload time. */ +@property(nullable, nonatomic) GDTCORClock *nextUploadTime; + +/** Creates and/or returns the singleton instance of this class. + * + * @return The singleton instance of this class. + */ ++ (instancetype)sharedInstance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Integration/GDTCCTIntegrationTest.m b/GoogleDataTransportCCTSupport/GDTCCTTests/Integration/GDTCCTIntegrationTest.m index 5a8df126620..6039e8dfb3c 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTTests/Integration/GDTCCTIntegrationTest.m +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Integration/GDTCCTIntegrationTest.m @@ -16,9 +16,9 @@ #import -#import -#import -#import +#import +#import +#import #import @@ -27,7 +27,7 @@ typedef void (^GDTCCTIntegrationTestBlock)(NSURLSessionUploadTask *_Nullable); -@interface GDTCCTTestDataObject : NSObject +@interface GDTCCTTestDataObject : NSObject @end @@ -55,7 +55,7 @@ @interface GDTCCTIntegrationTest : XCTestCase @property(nonatomic) BOOL generateEvents; /** The transporter used by the test. */ -@property(nonatomic) GDTTransport *transport; +@property(nonatomic) GDTCORTransport *transport; @end @@ -70,15 +70,15 @@ - (void)setUp { if (success) { self.okToRunTest = (flags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable; - self.transport = [[GDTTransport alloc] initWithMappingID:@"1018" - transformers:nil - target:kGDTTargetCCT]; + self.transport = [[GDTCORTransport alloc] initWithMappingID:@"1018" + transformers:nil + target:kGDTCORTargetCCT]; } } /** Generates an event and sends it through the transport infrastructure. */ - (void)generateEvent { - GDTEvent *event = [self.transport eventForTransport]; + GDTCOREvent *event = [self.transport eventForTransport]; event.dataObject = [[GDTCCTTestDataObject alloc] init]; [self.transport sendDataEvent:event]; } @@ -86,7 +86,7 @@ - (void)generateEvent { /** Generates events recursively at random intervals between 0 and 5 seconds. */ - (void)recursivelyGenerateEvent { if (self.generateEvents) { - GDTEvent *event = [self.transport eventForTransport]; + GDTCOREvent *event = [self.transport eventForTransport]; event.dataObject = [[GDTCCTTestDataObject alloc] init]; [self.transport sendDataEvent:event]; dispatch_after( @@ -142,9 +142,9 @@ - (void)testSendingDataToCCT { })]; // Send a high priority event to flush events. - GDTEvent *event = [self.transport eventForTransport]; + GDTCOREvent *event = [self.transport eventForTransport]; event.dataObject = [[GDTCCTTestDataObject alloc] init]; - event.qosTier = GDTEventQoSFast; + event.qosTier = GDTCOREventQoSFast; [self.transport sendDataEvent:event]; [self waitForExpectations:@[ taskCreatedExpectation, taskDoneExpectation ] timeout:25.0]; diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Integration/GDTFLLIntegrationTest.m b/GoogleDataTransportCCTSupport/GDTCCTTests/Integration/GDTFLLIntegrationTest.m new file mode 100644 index 00000000000..7bc58115557 --- /dev/null +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Integration/GDTFLLIntegrationTest.m @@ -0,0 +1,177 @@ +/* + * Copyright 2019 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 + +#import +#import +#import + +#import + +#import "GDTCCTLibrary/Private/GDTFLLPrioritizer.h" +#import "GDTCCTLibrary/Private/GDTFLLUploader.h" + +typedef void (^GDTFLLIntegrationTestBlock)(NSURLSessionUploadTask *_Nullable); + +@interface GDTFLLTestDataObject : NSObject + +@end + +@implementation GDTFLLTestDataObject + +- (NSData *)transportBytes { + // Return some random event data corresponding to mapping ID 1018. + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + NSArray *dataFiles = @[ + @"message-32347456.dat", @"message-35458880.dat", @"message-39882816.dat", + @"message-40043840.dat", @"message-40657984.dat" + ]; + NSURL *fileURL = [testBundle URLForResource:dataFiles[arc4random_uniform(5)] withExtension:nil]; + return [NSData dataWithContentsOfURL:fileURL]; +} + +@end + +@interface GDTFLLIntegrationTest : XCTestCase + +/** If YES, the network conditions were good enough to allow running integration tests. */ +@property(nonatomic) BOOL okToRunTest; + +/** If YES, allow the recursive generating of events. */ +@property(nonatomic) BOOL generateEvents; + +/** The transporter used by the test. */ +@property(nonatomic) GDTCORTransport *transport; + +@end + +@implementation GDTFLLIntegrationTest + +- (void)setUp { + self.generateEvents = YES; + SCNetworkReachabilityRef reachabilityRef = + SCNetworkReachabilityCreateWithName(CFAllocatorGetDefault(), "https://google.com"); + SCNetworkReachabilityFlags flags; + Boolean success = SCNetworkReachabilityGetFlags(reachabilityRef, &flags); + if (success) { + self.okToRunTest = + (flags & kSCNetworkReachabilityFlagsReachable) == kSCNetworkReachabilityFlagsReachable; + self.transport = [[GDTCORTransport alloc] initWithMappingID:@"1018" + transformers:nil + target:kGDTCORTargetFLL]; + } +} + +/** Generates an event and sends it through the transport infrastructure. */ +- (void)generateEvent { + GDTCOREvent *event = [self.transport eventForTransport]; + event.dataObject = [[GDTFLLTestDataObject alloc] init]; + [self.transport sendDataEvent:event]; +} + +/** Generates events recursively at random intervals between 0 and 5 seconds. */ +- (void)recursivelyGenerateEvent { + if (self.generateEvents) { + GDTCOREvent *event = [self.transport eventForTransport]; + event.dataObject = [[GDTFLLTestDataObject alloc] init]; + [self.transport sendDataEvent:event]; + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(arc4random_uniform(6) * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [self recursivelyGenerateEvent]; + }); + } +} + +/** Tests sending data to FLL with a high priority event if network conditions are good. */ +- (void)testSendingDataToFLL { + if (!self.okToRunTest) { + NSLog(@"Skipping the integration test, as the network conditions weren't good enough."); + return; + } + + NSUInteger lengthOfTestToRunInSeconds = 10; + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); + dispatch_source_set_event_handler(timer, ^{ + static int numberOfTimesCalled = 0; + numberOfTimesCalled++; + if (numberOfTimesCalled < lengthOfTestToRunInSeconds) { + [self generateEvent]; + } else { + dispatch_source_cancel(timer); + } + }); + dispatch_resume(timer); + + // Run for a bit, several seconds longer than the previous bit. + [[NSRunLoop currentRunLoop] + runUntilDate:[NSDate dateWithTimeIntervalSinceNow:lengthOfTestToRunInSeconds + 5]]; + + XCTestExpectation *taskCreatedExpectation = [self expectationWithDescription:@"task created"]; + XCTestExpectation *taskDoneExpectation = [self expectationWithDescription:@"task done"]; + + taskCreatedExpectation.assertForOverFulfill = NO; + taskDoneExpectation.assertForOverFulfill = NO; + + [[GDTFLLUploader sharedInstance] + addObserver:self + forKeyPath:@"currentTask" + options:NSKeyValueObservingOptionNew + context:(__bridge void *_Nullable)(^(NSURLSessionUploadTask *_Nullable task) { + if (task) { + [taskCreatedExpectation fulfill]; + } else { + [taskDoneExpectation fulfill]; + } + })]; + + // Send a high priority event to flush events. + GDTCOREvent *event = [self.transport eventForTransport]; + event.dataObject = [[GDTFLLTestDataObject alloc] init]; + event.qosTier = GDTCOREventQoSFast; + [self.transport sendDataEvent:event]; + + [self waitForExpectations:@[ taskCreatedExpectation, taskDoneExpectation ] timeout:60.0]; + + // Just run for a minute whilst generating events. + NSInteger secondsToRun = 65; + [self generateEvents]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(secondsToRun * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + self.generateEvents = NO; + }); + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:secondsToRun]]; +} + +// KVO is utilized here to know whether or not the task has completed. +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:@"currentTask"]) { + NSURLSessionUploadTask *task = change[NSKeyValueChangeNewKey]; + typedef void (^GDTFLLIntegrationTestBlock)(NSURLSessionUploadTask *_Nullable); + if (context) { + GDTFLLIntegrationTestBlock block = (__bridge GDTFLLIntegrationTestBlock)context; + block([task isKindOfClass:[NSNull class]] ? nil : task); + } + } +} + +@end diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTNanopbHelpersTest.m b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTNanopbHelpersTest.m index e0267048b7e..dccdbb5a446 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTNanopbHelpersTest.m +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTNanopbHelpersTest.m @@ -32,7 +32,7 @@ @interface GDTCCTNanopbHelpersTest : XCTestCase @implementation GDTCCTNanopbHelpersTest - (void)setUp { - self.generator = [[GDTCCTEventGenerator alloc] init]; + self.generator = [[GDTCCTEventGenerator alloc] initWithTarget:kGDTCORTargetCCT]; } - (void)tearDown { @@ -42,14 +42,16 @@ - (void)tearDown { /** Tests that the event generator is generating consistent events. */ - (void)testGeneratingFiveConsistentEvents { - NSArray *events1 = [self.generator generateTheFiveConsistentStoredEvents]; - NSArray *events2 = [self.generator generateTheFiveConsistentStoredEvents]; + NSArray *events1 = [self.generator generateTheFiveConsistentStoredEvents]; + NSArray *events2 = [self.generator generateTheFiveConsistentStoredEvents]; XCTAssertEqual(events1.count, events2.count); XCTAssertEqual(events1.count, 5); for (int i = 0; i < events1.count; i++) { - GDTStoredEvent *storedEvent1 = events1[i]; - GDTStoredEvent *storedEvent2 = events2[i]; - XCTAssertEqualObjects(storedEvent1, storedEvent2); + GDTCORStoredEvent *storedEvent1 = events1[i]; + GDTCORStoredEvent *storedEvent2 = events2[i]; + NSData *storedEvent1Data = [NSData dataWithContentsOfURL:storedEvent1.dataFuture.fileURL]; + NSData *storedEvent2Data = [NSData dataWithContentsOfURL:storedEvent2.dataFuture.fileURL]; + XCTAssertEqualObjects(storedEvent1Data, storedEvent2Data); } } @@ -62,9 +64,18 @@ - (void)testConstructBatchedLogRequest { ]; NSMutableSet *storedEvents = [[NSMutableSet alloc] init]; for (NSString *dataFile in testData) { - NSURL *fileURL = [testBundle URLForResource:dataFile withExtension:nil]; + NSData *messageData = [NSData dataWithContentsOfURL:[testBundle URLForResource:dataFile + withExtension:nil]]; + XCTAssertNotNil(messageData); + NSString *cachePath = NSTemporaryDirectory(); + NSString *filePath = [cachePath + stringByAppendingPathComponent:[NSString stringWithFormat:@"test-%lf.txt", + CFAbsoluteTimeGetCurrent()]]; + [messageData writeToFile:filePath atomically:YES]; + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; XCTAssertNotNil(fileURL); - [storedEvents addObject:[_generator generateStoredEvent:GDTEventQosDefault fileURL:fileURL]]; + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:filePath]); + [storedEvents addObject:[_generator generateStoredEvent:GDTCOREventQosDefault fileURL:fileURL]]; } gdt_cct_BatchedLogRequest batch = gdt_cct_BatchedLogRequest_init_default; XCTAssertNoThrow((batch = GDTCCTConstructBatchedLogRequest(@{@"1018" : storedEvents}))); @@ -80,9 +91,18 @@ - (void)testEncodeBatchedLogRequest { ]; NSMutableSet *storedEvents = [[NSMutableSet alloc] init]; for (NSString *dataFile in testData) { - NSURL *fileURL = [testBundle URLForResource:dataFile withExtension:nil]; + NSData *messageData = [NSData dataWithContentsOfURL:[testBundle URLForResource:dataFile + withExtension:nil]]; + XCTAssertNotNil(messageData); + NSString *cachePath = NSTemporaryDirectory(); + NSString *filePath = [cachePath + stringByAppendingPathComponent:[NSString stringWithFormat:@"test-%lf.txt", + CFAbsoluteTimeGetCurrent()]]; + [messageData writeToFile:filePath atomically:YES]; + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; XCTAssertNotNil(fileURL); - [storedEvents addObject:[_generator generateStoredEvent:GDTEventQosDefault fileURL:fileURL]]; + XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:filePath]); + [storedEvents addObject:[_generator generateStoredEvent:GDTCOREventQosDefault fileURL:fileURL]]; } gdt_cct_BatchedLogRequest batch = GDTCCTConstructBatchedLogRequest(@{@"1018" : storedEvents}); NSData *encodedBatchLogRequest; @@ -93,8 +113,9 @@ - (void)testEncodeBatchedLogRequest { /** Tests that the bytes generated are decodable. */ - (void)testBytesAreDecodable { - NSArray *storedEventsA = [self.generator generateTheFiveConsistentStoredEvents]; - NSSet *storedEvents = [NSSet setWithArray:storedEventsA]; + NSArray *storedEventsA = + [self.generator generateTheFiveConsistentStoredEvents]; + NSSet *storedEvents = [NSSet setWithArray:storedEventsA]; gdt_cct_BatchedLogRequest batch = GDTCCTConstructBatchedLogRequest(@{@"1018" : storedEvents}); NSData *encodedBatchLogRequest = GDTCCTEncodeBatchedLogRequest(&batch); gdt_cct_BatchedLogRequest decodedBatch = gdt_cct_BatchedLogRequest_init_default; diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTPrioritizerTest.m b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTPrioritizerTest.m index f388f06b4ca..fd3e4175e1b 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTPrioritizerTest.m +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTPrioritizerTest.m @@ -30,7 +30,7 @@ @interface GDTCCTPrioritizerTest : XCTestCase @implementation GDTCCTPrioritizerTest - (void)setUp { - self.generator = [[GDTCCTEventGenerator alloc] init]; + self.generator = [[GDTCCTEventGenerator alloc] initWithTarget:kGDTCORTargetFLL]; } - (void)tearDown { @@ -39,100 +39,104 @@ - (void)tearDown { } /** Tests prioritizing events. */ -- (void)testPrioritizeEvent { +- (void)testCCTPrioritizeEvent { GDTCCTPrioritizer *prioritizer = [[GDTCCTPrioritizer alloc] init]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; dispatch_sync(prioritizer.queue, ^{ XCTAssertEqual(prioritizer.events.count, 1); }); } /** Tests prioritizing multiple events. */ -- (void)testPrioritizeMultipleEvents { +- (void)testCCTPrioritizeMultipleEvents { GDTCCTPrioritizer *prioritizer = [[GDTCCTPrioritizer alloc] init]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; dispatch_sync(prioritizer.queue, ^{ XCTAssertEqual(prioritizer.events.count, 9); }); } /** Tests unprioritizing events. */ -- (void)testPackageDelivered { +- (void)testCCTPackageDelivered { GDTCCTPrioritizer *prioritizer = [[GDTCCTPrioritizer alloc] init]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; dispatch_sync(prioritizer.queue, ^{ XCTAssertEqual(prioritizer.events.count, 9); }); - GDTUploadPackage *package = [prioritizer uploadPackageWithConditions:GDTUploadConditionWifiData]; + GDTCORUploadPackage *package = + [prioritizer uploadPackageWithConditions:GDTCORUploadConditionWifiData]; [prioritizer packageDelivered:package successful:YES]; - package = [prioritizer uploadPackageWithConditions:GDTUploadConditionWifiData]; + package = [prioritizer uploadPackageWithConditions:GDTCORUploadConditionWifiData]; XCTAssertEqual(package.events.count, 0); } /** Tests providing events for upload. */ -- (void)testEventsForUpload { +- (void)testCCTEventsForUpload { GDTCCTPrioritizer *prioritizer = [[GDTCCTPrioritizer alloc] init]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQoSWifiOnly]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQoSTelemetry]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQoSWifiOnly]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQosDefault]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQoSDaily]]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQoSTelemetry]]; - GDTUploadPackage *package = [prioritizer uploadPackageWithConditions:GDTUploadConditionWifiData]; - for (GDTStoredEvent *storedEvent in package.events) { - XCTAssertTrue( - storedEvent.qosTier == GDTEventQoSTelemetry || storedEvent.qosTier == GDTEventQoSWifiOnly || - storedEvent.qosTier == GDTEventQosDefault || storedEvent.qosTier == GDTEventQoSDaily); + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSWifiOnly]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSTelemetry]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSWifiOnly]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSDaily]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSTelemetry]]; + GDTCORUploadPackage *package = + [prioritizer uploadPackageWithConditions:GDTCORUploadConditionWifiData]; + for (GDTCORStoredEvent *storedEvent in package.events) { + XCTAssertTrue(storedEvent.qosTier == GDTCOREventQoSTelemetry || + storedEvent.qosTier == GDTCOREventQoSWifiOnly || + storedEvent.qosTier == GDTCOREventQosDefault || + storedEvent.qosTier == GDTCOREventQoSDaily); } XCTAssertEqual(package.events.count, 9); - package = [prioritizer uploadPackageWithConditions:GDTUploadConditionMobileData]; - for (GDTStoredEvent *storedEvent in package.events) { - XCTAssertTrue(storedEvent.qosTier == GDTEventQoSTelemetry || - storedEvent.qosTier == GDTEventQosDefault); + package = [prioritizer uploadPackageWithConditions:GDTCORUploadConditionMobileData]; + for (GDTCORStoredEvent *storedEvent in package.events) { + XCTAssertTrue(storedEvent.qosTier == GDTCOREventQoSTelemetry || + storedEvent.qosTier == GDTCOREventQosDefault); } XCTAssertEqual(package.events.count, 4); } /** Tests providing daily uploaded events. */ -- (void)testDailyUpload { +- (void)testCCTDailyUpload { GDTCCTPrioritizer *prioritizer = [[GDTCCTPrioritizer alloc] init]; - GDTStoredEvent *dailyEvent = [_generator generateStoredEvent:GDTEventQoSDaily]; - [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTEventQoSWifiOnly]]; - GDTStoredEvent *telemetryEvent = [_generator generateStoredEvent:GDTEventQoSTelemetry]; + GDTCORStoredEvent *dailyEvent = [_generator generateStoredEvent:GDTCOREventQoSDaily]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSWifiOnly]]; + GDTCORStoredEvent *telemetryEvent = [_generator generateStoredEvent:GDTCOREventQoSTelemetry]; [prioritizer prioritizeEvent:dailyEvent]; [prioritizer prioritizeEvent:telemetryEvent]; - GDTUploadPackage *package = [prioritizer uploadPackageWithConditions:GDTUploadConditionWifiData]; + GDTCORUploadPackage *package = + [prioritizer uploadPackageWithConditions:GDTCORUploadConditionWifiData]; // If no previous daily upload time existed, the daily event will be included. XCTAssertTrue([package.events containsObject:dailyEvent]); // If a previous daily upload time exists, but now is not > 24h from then, it is excluded. - package = [prioritizer uploadPackageWithConditions:GDTUploadConditionMobileData]; + package = [prioritizer uploadPackageWithConditions:GDTCORUploadConditionMobileData]; XCTAssertFalse([package.events containsObject:dailyEvent]); // If a previous daily upload time exists and it's > 24h ago, daily logs are included. - prioritizer.timeOfLastDailyUpload = [GDTClock snapshot]; + prioritizer.timeOfLastDailyUpload = [GDTCORClock snapshot]; int64_t previousTime = prioritizer.timeOfLastDailyUpload.timeMillis - (24 * 60 * 60 * 1000 + 1); [prioritizer.timeOfLastDailyUpload setValue:@(previousTime) forKeyPath:@"timeMillis"]; - package = [prioritizer uploadPackageWithConditions:GDTUploadConditionMobileData]; + package = [prioritizer uploadPackageWithConditions:GDTCORUploadConditionMobileData]; XCTAssertTrue([package.events containsObject:dailyEvent]); XCTAssertTrue([package.events containsObject:telemetryEvent]); } diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTUploaderTest.m b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTUploaderTest.m index 3d84087891e..09cdd6e60d9 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTUploaderTest.m +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTCCTUploaderTest.m @@ -16,7 +16,7 @@ #import -#import +#import #import "GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h" #import "GDTCCTLibrary/Private/GDTCCTUploader.h" @@ -37,7 +37,7 @@ @interface GDTCCTUploaderTest : XCTestCase @implementation GDTCCTUploaderTest - (void)setUp { - self.generator = [[GDTCCTEventGenerator alloc] init]; + self.generator = [[GDTCCTEventGenerator alloc] initWithTarget:kGDTCORTargetCCT]; self.testServer = [[GDTCCTTestServer alloc] init]; [self.testServer registerLogBatchPath]; [self.testServer start]; @@ -50,11 +50,12 @@ - (void)tearDown { [self.testServer stop]; } -- (void)testUploadGivenConditions { - NSArray *storedEventsA = [self.generator generateTheFiveConsistentStoredEvents]; - NSSet *storedEvents = [NSSet setWithArray:storedEventsA]; +- (void)testCCTUploadGivenConditions { + NSArray *storedEventsA = + [self.generator generateTheFiveConsistentStoredEvents]; + NSSet *storedEvents = [NSSet setWithArray:storedEventsA]; - GDTUploadPackage *package = [[GDTUploadPackage alloc] initWithTarget:kGDTTargetCCT]; + GDTCORUploadPackage *package = [[GDTCORUploadPackage alloc] initWithTarget:kGDTCORTargetCCT]; package.events = storedEvents; GDTCCTUploader *uploader = [[GDTCCTUploader alloc] init]; uploader.serverURL = [self.testServer.serverURL URLByAppendingPathComponent:@"logBatch"]; diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTFLLPrioritizerTest.m b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTFLLPrioritizerTest.m new file mode 100644 index 00000000000..2162c500d93 --- /dev/null +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTFLLPrioritizerTest.m @@ -0,0 +1,144 @@ +/* + * Copyright 2019 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 + +#import "GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.h" + +#import "GDTCCTLibrary/Private/GDTFLLPrioritizer.h" + +@interface GDTFLLPrioritizerTest : XCTestCase + +/** An event generator for testing. */ +@property(nonatomic) GDTCCTEventGenerator *generator; + +@end + +@implementation GDTFLLPrioritizerTest + +- (void)setUp { + self.generator = [[GDTCCTEventGenerator alloc] initWithTarget:kGDTCORTargetCCT]; +} + +- (void)tearDown { + [super tearDown]; + [self.generator deleteGeneratedFilesFromDisk]; +} + +/** Tests prioritizing events. */ +- (void)testFLLPrioritizeEvent { + GDTFLLPrioritizer *prioritizer = [[GDTFLLPrioritizer alloc] init]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + dispatch_sync(prioritizer.queue, ^{ + XCTAssertEqual(prioritizer.events.count, 1); + }); +} + +/** Tests prioritizing multiple events. */ +- (void)testFLLPrioritizeMultipleEvents { + GDTFLLPrioritizer *prioritizer = [[GDTFLLPrioritizer alloc] init]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + dispatch_sync(prioritizer.queue, ^{ + XCTAssertEqual(prioritizer.events.count, 9); + }); +} + +/** Tests unprioritizing events. */ +- (void)testFLLPackageDelivered { + GDTFLLPrioritizer *prioritizer = [[GDTFLLPrioritizer alloc] init]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + dispatch_sync(prioritizer.queue, ^{ + XCTAssertEqual(prioritizer.events.count, 9); + }); + GDTCORUploadPackage *package = + [prioritizer uploadPackageWithConditions:GDTCORUploadConditionWifiData]; + [prioritizer packageDelivered:package successful:YES]; + package = [prioritizer uploadPackageWithConditions:GDTCORUploadConditionWifiData]; + XCTAssertEqual(package.events.count, 0); +} + +/** Tests providing events for upload. */ +- (void)testFLLEventsForUpload { + GDTFLLPrioritizer *prioritizer = [[GDTFLLPrioritizer alloc] init]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSWifiOnly]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSTelemetry]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSWifiOnly]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQosDefault]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSDaily]]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSTelemetry]]; + GDTCORUploadPackage *package = + [prioritizer uploadPackageWithConditions:GDTCORUploadConditionWifiData]; + for (GDTCORStoredEvent *storedEvent in package.events) { + XCTAssertTrue(storedEvent.qosTier == GDTCOREventQoSTelemetry || + storedEvent.qosTier == GDTCOREventQoSWifiOnly || + storedEvent.qosTier == GDTCOREventQosDefault || + storedEvent.qosTier == GDTCOREventQoSDaily); + } + XCTAssertEqual(package.events.count, 9); + package = [prioritizer uploadPackageWithConditions:GDTCORUploadConditionMobileData]; + for (GDTCORStoredEvent *storedEvent in package.events) { + XCTAssertTrue(storedEvent.qosTier == GDTCOREventQoSTelemetry || + storedEvent.qosTier == GDTCOREventQosDefault); + } + XCTAssertEqual(package.events.count, 4); +} + +/** Tests providing daily uploaded events. */ +- (void)testFLLDailyUpload { + GDTFLLPrioritizer *prioritizer = [[GDTFLLPrioritizer alloc] init]; + GDTCORStoredEvent *dailyEvent = [_generator generateStoredEvent:GDTCOREventQoSDaily]; + [prioritizer prioritizeEvent:[_generator generateStoredEvent:GDTCOREventQoSWifiOnly]]; + GDTCORStoredEvent *telemetryEvent = [_generator generateStoredEvent:GDTCOREventQoSTelemetry]; + [prioritizer prioritizeEvent:dailyEvent]; + [prioritizer prioritizeEvent:telemetryEvent]; + GDTCORUploadPackage *package = + [prioritizer uploadPackageWithConditions:GDTCORUploadConditionWifiData]; + // If no previous daily upload time existed, the daily event will be included. + XCTAssertTrue([package.events containsObject:dailyEvent]); + + // If a previous daily upload time exists, but now is not > 24h from then, it is excluded. + package = [prioritizer uploadPackageWithConditions:GDTCORUploadConditionMobileData]; + XCTAssertFalse([package.events containsObject:dailyEvent]); + + // If a previous daily upload time exists and it's > 24h ago, daily logs are included. + prioritizer.timeOfLastDailyUpload = [GDTCORClock snapshot]; + int64_t previousTime = prioritizer.timeOfLastDailyUpload.timeMillis - (24 * 60 * 60 * 1000 + 1); + [prioritizer.timeOfLastDailyUpload setValue:@(previousTime) forKeyPath:@"timeMillis"]; + package = [prioritizer uploadPackageWithConditions:GDTCORUploadConditionMobileData]; + XCTAssertTrue([package.events containsObject:dailyEvent]); + XCTAssertTrue([package.events containsObject:telemetryEvent]); +} + +@end diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTFLLUploaderTest.m b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTFLLUploaderTest.m new file mode 100644 index 00000000000..e84b2b85109 --- /dev/null +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/GDTFLLUploaderTest.m @@ -0,0 +1,83 @@ +/* + * Copyright 2019 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 + +#import + +#import "GDTCCTLibrary/Private/GDTCCTNanopbHelpers.h" +#import "GDTCCTLibrary/Private/GDTFLLUploader.h" + +#import "GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.h" +#import "GDTCCTTests/Unit/TestServer/GDTCCTTestServer.h" + +@interface GDTFLLUploaderTest : XCTestCase + +/** An event generator for testing. */ +@property(nonatomic) GDTCCTEventGenerator *generator; + +/** The local HTTP server to use for testing. */ +@property(nonatomic) GDTCCTTestServer *testServer; + +@end + +@implementation GDTFLLUploaderTest + +- (void)setUp { + self.generator = [[GDTCCTEventGenerator alloc] initWithTarget:kGDTCORTargetFLL]; + self.testServer = [[GDTCCTTestServer alloc] init]; + [self.testServer registerLogBatchPath]; + [self.testServer start]; + XCTAssertTrue(self.testServer.isRunning); +} + +- (void)tearDown { + [super tearDown]; + [self.generator deleteGeneratedFilesFromDisk]; + [self.testServer stop]; +} + +- (void)testFLLUploadGivenConditions { + NSArray *storedEventsA = + [self.generator generateTheFiveConsistentStoredEvents]; + NSSet *storedEvents = [NSSet setWithArray:storedEventsA]; + + GDTCORUploadPackage *package = [[GDTCORUploadPackage alloc] initWithTarget:kGDTCORTargetFLL]; + package.events = storedEvents; + GDTFLLUploader *uploader = [[GDTFLLUploader alloc] init]; + uploader.serverURL = [self.testServer.serverURL URLByAppendingPathComponent:@"logBatch"]; + __weak id weakSelf = self; + XCTestExpectation *responseSentExpectation = [self expectationWithDescription:@"response sent"]; + self.testServer.responseCompletedBlock = + ^(GCDWebServerRequest *_Nonnull request, GCDWebServerResponse *_Nonnull response) { + // Redefining the self var addresses strong self capturing in the XCTAssert macros. + id self = weakSelf; + XCTAssertNotNil(self); + [responseSentExpectation fulfill]; + XCTAssertEqual(response.statusCode, 200); + XCTAssertTrue(response.hasBody); + }; + [uploader uploadPackage:package]; + dispatch_sync(uploader.uploaderQueue, ^{ + XCTAssertNotNil(uploader.currentTask); + }); + [self waitForExpectations:@[ responseSentExpectation ] timeout:30.0]; + dispatch_sync(uploader.uploaderQueue, ^{ + XCTAssertNil(uploader.currentTask); + }); +} + +@end diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.h b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.h index edd9281aa4a..283c0a06cba 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.h +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.h @@ -16,8 +16,9 @@ #import -#import -#import +#import +#import +#import /** Generates fake stored events. Beware, this is not threadsafe and methods shouldn't be called * concurrently. @@ -25,30 +26,42 @@ @interface GDTCCTEventGenerator : NSObject /** All events generated by this instance. */ -@property(nonatomic, readonly) NSMutableSet *allGeneratedEvents; +@property(nonatomic, readonly) NSMutableSet *allGeneratedEvents; + +/** The target events will be sent to. */ +@property(nonatomic, readonly) GDTCORTarget target; + +- (instancetype)init NS_UNAVAILABLE; /** Deletes the empty files created in a temporary directory. */ - (void)deleteGeneratedFilesFromDisk; -/** Generates a GDTStoredEvent, complete with a file specified in the eventFileURL property. +/** Instantiates an instance with the given target. + * + * @param target The GDT target to send events to. + * @return An instance that generates events for the given target. + */ +- (instancetype)initWithTarget:(GDTCORTarget)target NS_DESIGNATED_INITIALIZER; + +/** Generates a GDTCORStoredEvent, complete with a file specified in the eventFileURL property. * * @param qosTier The QoS tier the event should have. * @return A newly allocated fake stored event. */ -- (GDTStoredEvent *)generateStoredEvent:(GDTEventQoS)qosTier; +- (GDTCORStoredEvent *)generateStoredEvent:(GDTCOREventQoS)qosTier; -/** Generates a GDTStoredEvent, complete with a file specified in the eventFileURL property. +/** Generates a GDTCORStoredEvent, complete with a file specified in the eventFileURL property. * * @param qosTier The QoS tier the event should have. * @param fileURL The file URL containing bytes of some event. * @return A newly allocated fake stored event. */ -- (GDTStoredEvent *)generateStoredEvent:(GDTEventQoS)qosTier fileURL:(NSURL *)fileURL; +- (GDTCORStoredEvent *)generateStoredEvent:(GDTCOREventQoS)qosTier fileURL:(NSURL *)fileURL; /** Generates five consistent stored events. * - * @return An array of five newly allocated but consistent GDTStoredEvents. + * @return An array of five newly allocated but consistent GDTCORStoredEvents. */ -- (NSArray *)generateTheFiveConsistentStoredEvents; +- (NSArray *)generateTheFiveConsistentStoredEvents; @end diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.m b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.m index 7433eb42d2a..5c47b567640 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.m +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.m @@ -16,122 +16,146 @@ #import "GDTCCTTests/Unit/Helpers/GDTCCTEventGenerator.h" -#import +#import +#import @implementation GDTCCTEventGenerator -// Atomic, but not threadsafe. -static volatile NSUInteger gCounter = 0; +- (instancetype)initWithTarget:(GDTCORTarget)target { + self = [super init]; + if (self) { + _target = target; + _allGeneratedEvents = [[NSMutableSet alloc] init]; + } + return self; +} - (void)deleteGeneratedFilesFromDisk { - for (GDTStoredEvent *storedEvent in self.allGeneratedEvents) { + for (GDTCORStoredEvent *storedEvent in self.allGeneratedEvents) { NSError *error; [[NSFileManager defaultManager] removeItemAtURL:storedEvent.dataFuture.fileURL error:&error]; - NSAssert(error == nil, @"There was an error deleting a temporary event file."); + GDTCORAssert(error == nil, @"There was an error deleting a temporary event file."); } } -- (GDTStoredEvent *)generateStoredEvent:(GDTEventQoS)qosTier { +- (GDTCORStoredEvent *)generateStoredEvent:(GDTCOREventQoS)qosTier { NSString *cachePath = NSTemporaryDirectory(); NSString *filePath = [cachePath - stringByAppendingPathComponent:[NSString stringWithFormat:@"test-%ld.txt", - (unsigned long)gCounter]]; - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"1018" target:kGDTTargetCCT]; - event.clockSnapshot = [GDTClock snapshot]; + stringByAppendingPathComponent:[NSString stringWithFormat:@"test-%lf.txt", + CFAbsoluteTimeGetCurrent()]]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:_target]; + event.clockSnapshot = [GDTCORClock snapshot]; event.qosTier = qosTier; [[NSFileManager defaultManager] createFileAtPath:filePath contents:[NSData data] attributes:nil]; - gCounter++; - GDTDataFuture *future = [[GDTDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:filePath]]; - GDTStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; + GDTCORDataFuture *future = + [[GDTCORDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:filePath]]; + GDTCORStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; [self.allGeneratedEvents addObject:storedEvent]; return storedEvent; } -- (GDTStoredEvent *)generateStoredEvent:(GDTEventQoS)qosTier fileURL:(NSURL *)fileURL { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"1018" target:kGDTTargetCCT]; - event.clockSnapshot = [GDTClock snapshot]; +- (GDTCORStoredEvent *)generateStoredEvent:(GDTCOREventQoS)qosTier fileURL:(NSURL *)fileURL { + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:_target]; + event.clockSnapshot = [GDTCORClock snapshot]; event.qosTier = qosTier; - gCounter++; - GDTDataFuture *future = - [[GDTDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:fileURL.path]]; - GDTStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; + GDTCORDataFuture *future = + [[GDTCORDataFuture alloc] initWithFileURL:[NSURL fileURLWithPath:fileURL.path]]; + GDTCORStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; [self.allGeneratedEvents addObject:storedEvent]; return storedEvent; } -- (NSArray *)generateTheFiveConsistentStoredEvents { - NSMutableArray *storedEvents = [[NSMutableArray alloc] init]; +/** Generates a file URL that has the message resource data copied into it. + * + * @param messageResource The message resource name to copy. + * @return A new file containing the data of the message resource. + */ +- (NSURL *)writeConsistentMessageToDisk:(NSString *)messageResource { NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + NSString *cachePath = NSTemporaryDirectory(); + NSString *filePath = [cachePath + stringByAppendingPathComponent:[NSString stringWithFormat:@"test-%lf.txt", + CFAbsoluteTimeGetCurrent()]]; + NSAssert([[NSFileManager defaultManager] fileExistsAtPath:filePath] == NO, + @"There should be no duplicate files generated."); + NSData *messageData = [NSData dataWithContentsOfURL:[testBundle URLForResource:messageResource + withExtension:nil]]; + [messageData writeToFile:filePath atomically:YES]; + return [NSURL fileURLWithPath:filePath]; +} + +- (NSArray *)generateTheFiveConsistentStoredEvents { + NSMutableArray *storedEvents = [[NSMutableArray alloc] init]; { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"1018" target:kGDTTargetCCT]; - event.clockSnapshot = [GDTClock snapshot]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:_target]; + event.clockSnapshot = [GDTCORClock snapshot]; [event.clockSnapshot setValue:@(1111111111111) forKeyPath:@"timeMillis"]; [event.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; [event.clockSnapshot setValue:@(1111111111111222) forKeyPath:@"kernelBootTime"]; [event.clockSnapshot setValue:@(1235567890) forKeyPath:@"uptime"]; - event.qosTier = GDTEventQosDefault; + event.qosTier = GDTCOREventQosDefault; event.customPrioritizationParams = @{@"customParam" : @1337}; - GDTDataFuture *future = [[GDTDataFuture alloc] - initWithFileURL:[testBundle URLForResource:@"message-32347456.dat" withExtension:nil]]; - GDTStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; + NSURL *messageDataURL = [self writeConsistentMessageToDisk:@"message-32347456.dat"]; + GDTCORDataFuture *future = [[GDTCORDataFuture alloc] initWithFileURL:messageDataURL]; + GDTCORStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; [storedEvents addObject:storedEvent]; } { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"1018" target:kGDTTargetCCT]; - event.clockSnapshot = [GDTClock snapshot]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:_target]; + event.clockSnapshot = [GDTCORClock snapshot]; [event.clockSnapshot setValue:@(1111111111111) forKeyPath:@"timeMillis"]; [event.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; [event.clockSnapshot setValue:@(1111111111111333) forKeyPath:@"kernelBootTime"]; [event.clockSnapshot setValue:@(1236567890) forKeyPath:@"uptime"]; - event.qosTier = GDTEventQoSWifiOnly; - GDTDataFuture *future = [[GDTDataFuture alloc] - initWithFileURL:[testBundle URLForResource:@"message-35458880.dat" withExtension:nil]]; - GDTStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; + event.qosTier = GDTCOREventQoSWifiOnly; + NSURL *messageDataURL = [self writeConsistentMessageToDisk:@"message-35458880.dat"]; + GDTCORDataFuture *future = [[GDTCORDataFuture alloc] initWithFileURL:messageDataURL]; + GDTCORStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; [storedEvents addObject:storedEvent]; } { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"1018" target:kGDTTargetCCT]; - event.clockSnapshot = [GDTClock snapshot]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:_target]; + event.clockSnapshot = [GDTCORClock snapshot]; [event.clockSnapshot setValue:@(1111111111111) forKeyPath:@"timeMillis"]; [event.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; [event.clockSnapshot setValue:@(1111111111111444) forKeyPath:@"kernelBootTime"]; [event.clockSnapshot setValue:@(1237567890) forKeyPath:@"uptime"]; - event.qosTier = GDTEventQosDefault; - GDTDataFuture *future = [[GDTDataFuture alloc] - initWithFileURL:[testBundle URLForResource:@"message-39882816.dat" withExtension:nil]]; - GDTStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; + event.qosTier = GDTCOREventQosDefault; + NSURL *messageDataURL = [self writeConsistentMessageToDisk:@"message-39882816.dat"]; + GDTCORDataFuture *future = [[GDTCORDataFuture alloc] initWithFileURL:messageDataURL]; + GDTCORStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; [storedEvents addObject:storedEvent]; } { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"1018" target:kGDTTargetCCT]; - event.clockSnapshot = [GDTClock snapshot]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:_target]; + event.clockSnapshot = [GDTCORClock snapshot]; [event.clockSnapshot setValue:@(1111111111111) forKeyPath:@"timeMillis"]; [event.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; [event.clockSnapshot setValue:@(1111111111111555) forKeyPath:@"kernelBootTime"]; [event.clockSnapshot setValue:@(1238567890) forKeyPath:@"uptime"]; - event.qosTier = GDTEventQosDefault; + event.qosTier = GDTCOREventQosDefault; event.customPrioritizationParams = @{@"customParam1" : @"aValue1"}; - GDTDataFuture *future = [[GDTDataFuture alloc] - initWithFileURL:[testBundle URLForResource:@"message-40043840.dat" withExtension:nil]]; - GDTStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; + NSURL *messageDataURL = [self writeConsistentMessageToDisk:@"message-40043840.dat"]; + GDTCORDataFuture *future = [[GDTCORDataFuture alloc] initWithFileURL:messageDataURL]; + GDTCORStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; [storedEvents addObject:storedEvent]; } { - GDTEvent *event = [[GDTEvent alloc] initWithMappingID:@"1018" target:kGDTTargetCCT]; - event.clockSnapshot = [GDTClock snapshot]; + GDTCOREvent *event = [[GDTCOREvent alloc] initWithMappingID:@"1018" target:_target]; + event.clockSnapshot = [GDTCORClock snapshot]; [event.clockSnapshot setValue:@(1111111111111) forKeyPath:@"timeMillis"]; [event.clockSnapshot setValue:@(-25200) forKeyPath:@"timezoneOffsetSeconds"]; [event.clockSnapshot setValue:@(1111111111111666) forKeyPath:@"kernelBootTime"]; [event.clockSnapshot setValue:@(1239567890) forKeyPath:@"uptime"]; - event.qosTier = GDTEventQoSTelemetry; + event.qosTier = GDTCOREventQoSTelemetry; event.customPrioritizationParams = @{@"customParam2" : @(34)}; - GDTDataFuture *future = [[GDTDataFuture alloc] - initWithFileURL:[testBundle URLForResource:@"message-40657984.dat" withExtension:nil]]; - GDTStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; + NSURL *messageDataURL = [self writeConsistentMessageToDisk:@"message-40657984.dat"]; + GDTCORDataFuture *future = [[GDTCORDataFuture alloc] initWithFileURL:messageDataURL]; + GDTCORStoredEvent *storedEvent = [event storedEventWithDataFuture:future]; [storedEvents addObject:storedEvent]; } return storedEvents; diff --git a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/TestServer/GDTCCTTestServer.m b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/TestServer/GDTCCTTestServer.m index abb81818793..918956a9362 100644 --- a/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/TestServer/GDTCCTTestServer.m +++ b/GoogleDataTransportCCTSupport/GDTCCTTests/Unit/TestServer/GDTCCTTestServer.m @@ -16,6 +16,8 @@ #import "GDTCCTTests/Unit/TestServer/GDTCCTTestServer.h" +#import + #import #import @@ -50,16 +52,16 @@ - (void)dealloc { } - (void)start { - NSAssert(self.server.isRunning == NO, @"The server should not be already running."); + GDTCORAssert(self.server.isRunning == NO, @"The server should not be already running."); NSError *error; [self.server startWithOptions:@{GCDWebServerOption_Port : @0, GCDWebServerOption_BindToLocalhost : @YES} error:&error]; - NSAssert(error == nil, @"Error when starting server: %@", error); + GDTCORAssert(error == nil, @"Error when starting server: %@", error); } - (void)stop { - NSAssert(self.server.isRunning, @"The server should be running before stopping."); + GDTCORAssert(self.server.isRunning, @"The server should be running before stopping."); [self.server stop]; } @@ -85,7 +87,7 @@ - (NSData *)responseData { pb_ostream_t sizestream = PB_OSTREAM_SIZING; // Encode 1 time to determine the size. if (!pb_encode(&sizestream, gdt_cct_LogResponse_fields, &logResponse)) { - NSCAssert(NO, @"Error in nanopb encoding for size: %s", PB_GET_ERROR(&sizestream)); + GDTCORAssert(NO, @"Error in nanopb encoding for size: %s", PB_GET_ERROR(&sizestream)); } // Encode a 2nd time to actually get the bytes from it. @@ -93,7 +95,7 @@ - (NSData *)responseData { CFMutableDataRef dataRef = CFDataCreateMutable(CFAllocatorGetDefault(), bufferSize); pb_ostream_t ostream = pb_ostream_from_buffer((void *)CFDataGetBytePtr(dataRef), bufferSize); if (!pb_encode(&sizestream, gdt_cct_LogResponse_fields, &logResponse)) { - NSCAssert(NO, @"Error in nanopb encoding for bytes: %s", PB_GET_ERROR(&ostream)); + GDTCORAssert(NO, @"Error in nanopb encoding for bytes: %s", PB_GET_ERROR(&ostream)); } CFDataSetLength(dataRef, ostream.bytes_written); pb_release(gdt_cct_LogResponse_fields, &logResponse); diff --git a/GoogleUtilities.podspec b/GoogleUtilities.podspec index 3a756f94f61..3e213acd6e7 100644 --- a/GoogleUtilities.podspec +++ b/GoogleUtilities.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleUtilities' - s.version = '6.2.5' + s.version = '6.3.0' s.summary = 'Google Utilities for iOS (plus community support for macOS and tvOS)' s.description = <<-DESC @@ -104,6 +104,11 @@ other Google CocoaPods. They're not intended for direct public usage. ud.dependency 'GoogleUtilities/Logger' end + s.subspec 'SecureCoding' do |sc| + sc.source_files = 'GoogleUtilities/SecureCoding/**/*.[hm]' + sc.public_header_files = 'GoogleUtilities/SecureCoding/Public/*.h' + end + s.test_spec 'unit' do |unit_tests| # All tests require arc except Tests/Network/third_party/GTMHTTPServer.m unit_tests.source_files = 'GoogleUtilities/Example/Tests/**/*.[mh]' diff --git a/GoogleUtilities/CHANGELOG.md b/GoogleUtilities/CHANGELOG.md index aff738c4318..62c8270d269 100644 --- a/GoogleUtilities/CHANGELOG.md +++ b/GoogleUtilities/CHANGELOG.md @@ -1,3 +1,7 @@ +# 6.3.0 +- GULSecureCoding introduced. (#3707) +- Mark unused variables. (#3854) + # 6.2.5 - Remove test-only method and update tests to include Catalyst. (#3544) diff --git a/GoogleUtilities/Example/Tests/Network/GULNetworkTest.m b/GoogleUtilities/Example/Tests/Network/GULNetworkTest.m index 0fb973bf4df..ad758ffe004 100644 --- a/GoogleUtilities/Example/Tests/Network/GULNetworkTest.m +++ b/GoogleUtilities/Example/Tests/Network/GULNetworkTest.m @@ -130,6 +130,10 @@ - (void)testSessionNetwork_POST_foreground { queue:_backgroundQueue usingBackgroundSession:NO completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { + XCTAssertNotNil(data); + NSString *responseBody = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(responseBody, @"Hello, World!"); [self verifyResponse:response error:error]; [self verifyRequest]; XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request"); @@ -317,6 +321,10 @@ - (void)testSessionNetwork_POST_background { queue:_backgroundQueue usingBackgroundSession:YES completionHandler:^(NSHTTPURLResponse *response, NSData *data, NSError *error) { + XCTAssertNotNil(data); + NSString *responseBody = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + XCTAssertEqualObjects(responseBody, @"Hello, World!"); [self verifyResponse:response error:error]; [self verifyRequest]; XCTAssertFalse(self->_network.hasUploadInProgress, "There must be no pending request"); diff --git a/GoogleUtilities/Example/Tests/SecureCoding/GULSecureCodingTests.m b/GoogleUtilities/Example/Tests/SecureCoding/GULSecureCodingTests.m new file mode 100644 index 00000000000..cbf1ef22511 --- /dev/null +++ b/GoogleUtilities/Example/Tests/SecureCoding/GULSecureCodingTests.m @@ -0,0 +1,100 @@ +// Copyright 2019 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 + +#import + +@interface SecureCodingIncompatibleObject : NSObject +@end + +@implementation SecureCodingIncompatibleObject + +- (void)encodeWithCoder:(nonnull NSCoder *)coder { +} + +- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder { + return [self init]; +} + +@end + +@interface GULSecureCodingTests : XCTestCase + +@end + +@implementation GULSecureCodingTests + +- (void)testArchiveUnarchive { + NSDictionary *objectToArchive = @{@"key1" : @"value1", @"key2" : @(2)}; + + NSError *error; + NSData *archiveData = [GULSecureCoding archivedDataWithRootObject:objectToArchive error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(archiveData); + + NSDictionary *unarchivedObject = [GULSecureCoding unarchivedObjectOfClass:[NSDictionary class] + fromData:archiveData + error:&error]; + XCTAssertNil(error); + XCTAssert([objectToArchive isEqualToDictionary:unarchivedObject]); +} + +- (void)testArchivingIncompatibleObjectError { + SecureCodingIncompatibleObject *objectToArchive = [[SecureCodingIncompatibleObject alloc] init]; + + NSError *error; + NSData *archiveData = [GULSecureCoding archivedDataWithRootObject:objectToArchive error:&error]; + XCTAssertNotNil(error); + XCTAssertNil(archiveData); +} + +- (void)testUnarchivingClassMismatchError { + NSDictionary *objectToArchive = @{@"key1" : @"value1", @"key2" : @(2)}; + NSError *error; + NSData *archiveData = [GULSecureCoding archivedDataWithRootObject:objectToArchive error:&error]; + XCTAssertNil(error); + XCTAssertNotNil(archiveData); + + NSArray *unarchivedObject = [GULSecureCoding unarchivedObjectOfClass:[NSArray class] + fromData:archiveData + error:&error]; + XCTAssertNotNil(error); + XCTAssertNil(unarchivedObject); +} + +- (void)testUnarchivingCorruptedDataError { + NSData *corruptedData = [@"abc" dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSString *unarchivedObject = [GULSecureCoding unarchivedObjectOfClass:[NSString class] + fromData:corruptedData + error:&error]; + XCTAssertNotNil(error); + XCTAssertNil(unarchivedObject); +} + +- (void)testArchiveUnarchiveWithNULLError { + SecureCodingIncompatibleObject *objectToArchive = [[SecureCodingIncompatibleObject alloc] init]; + + NSData *archiveData = [GULSecureCoding archivedDataWithRootObject:objectToArchive error:NULL]; + XCTAssertNil(archiveData); + + NSData *corruptedData = [@"abc" dataUsingEncoding:NSUTF8StringEncoding]; + NSDictionary *unarchivedObject = [GULSecureCoding unarchivedObjectOfClass:[NSDictionary class] + fromData:corruptedData + error:NULL]; + XCTAssertNil(unarchivedObject); +} + +@end diff --git a/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m b/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m index 4df487af2ba..e56a5c8da78 100644 --- a/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m +++ b/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m @@ -99,12 +99,9 @@ - (void)copySelector:(SEL)selector fromClass:(Class)aClass isClassSelector:(BOOL Class targetClass = isClassSelector ? object_getClass(_generatedClass) : _generatedClass; IMP implementation = method_getImplementation(method); const char *typeEncoding = method_getTypeEncoding(method); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-variable" - BOOL success = class_addMethod(targetClass, selector, implementation, typeEncoding); + BOOL success __unused = class_addMethod(targetClass, selector, implementation, typeEncoding); NSAssert(success, @"Unable to add selector %@ to class %@", NSStringFromSelector(selector), NSStringFromClass(targetClass)); -#pragma clang diagnostic pop } - (void)setAssociatedObjectWithKey:(NSString *)key @@ -139,12 +136,9 @@ - (void)swizzle { NSAssert(class_getInstanceSize(_originalClass) == class_getInstanceSize(_generatedClass), @"The instance size of the generated class must be equal to the original class."); objc_registerClassPair(_generatedClass); -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-variable" - Class doubleCheckOriginalClass = object_setClass(_swizzledObject, _generatedClass); + Class doubleCheckOriginalClass __unused = object_setClass(_swizzledObject, _generatedClass); NSAssert(_originalClass == doubleCheckOriginalClass, @"The original class must be the same as the class returned by object_setClass"); -#pragma clang diagnostic pop } else { NSAssert(NO, @"You can't swizzle a nil object"); } diff --git a/GoogleUtilities/Network/GULMutableDictionary.m b/GoogleUtilities/Network/GULMutableDictionary.m index d281eb4450b..946471f69bc 100644 --- a/GoogleUtilities/Network/GULMutableDictionary.m +++ b/GoogleUtilities/Network/GULMutableDictionary.m @@ -45,14 +45,14 @@ - (NSString *)description { - (id)objectForKey:(id)key { __block id object; dispatch_sync(_queue, ^{ - object = self->_objects[key]; + object = [self->_objects objectForKey:key]; }); return object; } - (void)setObject:(id)object forKey:(id)key { dispatch_async(_queue, ^{ - self->_objects[key] = object; + [self->_objects setObject:object forKey:key]; }); } @@ -77,13 +77,17 @@ - (NSUInteger)count { } - (id)objectForKeyedSubscript:(id)key { - // The method this calls is already synchronized. - return [self objectForKey:key]; + __block id object; + dispatch_sync(_queue, ^{ + object = self->_objects[key]; + }); + return object; } - (void)setObject:(id)obj forKeyedSubscript:(id)key { - // The method this calls is already synchronized. - [self setObject:obj forKey:key]; + dispatch_async(_queue, ^{ + self->_objects[key] = obj; + }); } - (NSDictionary *)dictionary { diff --git a/GoogleUtilities/Network/GULNetworkURLSession.m b/GoogleUtilities/Network/GULNetworkURLSession.m index 416a7360195..a74872c2bd2 100644 --- a/GoogleUtilities/Network/GULNetworkURLSession.m +++ b/GoogleUtilities/Network/GULNetworkURLSession.m @@ -22,8 +22,9 @@ #import "Private/GULNetworkMessageCode.h" @interface GULNetworkURLSession () + NSURLSessionDataDelegate, + NSURLSessionDownloadDelegate, + NSURLSessionTaskDelegate> @end @implementation GULNetworkURLSession { @@ -221,6 +222,24 @@ - (nullable NSString *)sessionIDFromAsyncGETRequest:(NSURLRequest *)request return _sessionID; } +#pragma mark - NSURLSessionDataDelegate + +/// Called by the NSURLSession when the data task has received some of the expected data. +/// Once the session is completed, URLSession:task:didCompleteWithError will be called and the +/// completion handler will be called with the downloaded data. +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data { + @synchronized(self) { + NSMutableData *mutableData = [[NSMutableData alloc] init]; + if (_downloadedData) { + mutableData = _downloadedData.mutableCopy; + } + [mutableData appendData:data]; + _downloadedData = mutableData; + } +} + #pragma mark - NSURLSessionTaskDelegate /// Called by the NSURLSession once the download task is completed. The file is saved in the diff --git a/GoogleUtilities/SecureCoding/GULSecureCoding.m b/GoogleUtilities/SecureCoding/GULSecureCoding.m new file mode 100644 index 00000000000..30f1fe0f2d9 --- /dev/null +++ b/GoogleUtilities/SecureCoding/GULSecureCoding.m @@ -0,0 +1,91 @@ +// Copyright 2019 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 "Public/GULSecureCoding.h" + +NSString *const kGULSecureCodingError = @"GULSecureCodingError"; + +@implementation GULSecureCoding + ++ (nullable id)unarchivedObjectOfClass:(Class)class + fromData:(NSData *)data + error:(NSError **)outError { + id object; +#if __has_builtin(__builtin_available) + if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { + object = [NSKeyedUnarchiver unarchivedObjectOfClass:class fromData:data error:outError]; + } else +#endif // __has_builtin(__builtin_available) + { + @try { + NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data]; + unarchiver.requiresSecureCoding = YES; + + object = [unarchiver decodeObjectOfClass:class forKey:NSKeyedArchiveRootObjectKey]; + } @catch (NSException *exception) { + if (outError) { + *outError = [self archivingErrorWithException:exception]; + } + } + + if (object == nil && outError && *outError == nil) { + NSString *failureReason = @"NSKeyedUnarchiver failed to unarchive data."; + *outError = [NSError errorWithDomain:kGULSecureCodingError + code:-1 + userInfo:@{NSLocalizedFailureReasonErrorKey : failureReason}]; + } + } + + return object; +} + ++ (nullable NSData *)archivedDataWithRootObject:(id)object error:(NSError **)outError { + NSData *archiveData; +#if __has_builtin(__builtin_available) + if (@available(macOS 10.13, iOS 11.0, tvOS 11.0, *)) { + archiveData = [NSKeyedArchiver archivedDataWithRootObject:object + requiringSecureCoding:YES + error:outError]; + } else +#endif // __has_builtin(__builtin_available) + { + @try { + NSMutableData *data = [NSMutableData data]; + NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data]; + archiver.requiresSecureCoding = YES; + + [archiver encodeObject:object forKey:NSKeyedArchiveRootObjectKey]; + [archiver finishEncoding]; + + archiveData = [data copy]; + } @catch (NSException *exception) { + if (outError) { + *outError = [self archivingErrorWithException:exception]; + } + } + } + + return archiveData; +} + ++ (NSError *)archivingErrorWithException:(NSException *)exception { + NSString *failureReason = [NSString + stringWithFormat:@"NSKeyedArchiver exception with name: %@, reason: %@, userInfo: %@", + exception.name, exception.reason, exception.userInfo]; + NSDictionary *errorUserInfo = @{NSLocalizedFailureReasonErrorKey : failureReason}; + + return [NSError errorWithDomain:kGULSecureCodingError code:-1 userInfo:errorUserInfo]; +} + +@end diff --git a/GoogleUtilities/SecureCoding/Public/GULSecureCoding.h b/GoogleUtilities/SecureCoding/Public/GULSecureCoding.h new file mode 100644 index 00000000000..7aabe4c95a4 --- /dev/null +++ b/GoogleUtilities/SecureCoding/Public/GULSecureCoding.h @@ -0,0 +1,32 @@ +// Copyright 2019 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 + +NS_ASSUME_NONNULL_BEGIN + +/** The class wraps `NSKeyedArchiver` and `NSKeyedUnarchiver` API to provide a unified secure coding + * methods for iOS versions before and after 11. + */ +@interface GULSecureCoding : NSObject + ++ (nullable id)unarchivedObjectOfClass:(Class)class + fromData:(NSData *)data + error:(NSError **)outError; + ++ (nullable NSData *)archivedDataWithRootObject:(id)object error:(NSError **)outError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m b/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m index fe860565dbd..1c9d87a6bb6 100644 --- a/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m +++ b/InAppMessaging/Example/Tests/FIRIAMFetchResponseParserTests.m @@ -24,7 +24,7 @@ #import "UIColor+FIRIAMHexString.h" @interface FIRIAMFetchResponseParserTests : XCTestCase -@property(nonatomic, copy) NSString *jsonResposne; +@property(nonatomic, copy) NSString *jsonResponse; @property(nonatomic) FIRIAMFetchResponseParser *parser; @property(nonatomic) id mockTimeFetcher; @end @@ -48,11 +48,11 @@ - (void)testRegularConversion { NSTimeInterval currentMoment = 100000000; OCMStub([self.mockTimeFetcher currentTimestampInSeconds]).andReturn(currentMoment); - self.jsonResposne = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath + self.jsonResponse = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath encoding:NSUTF8StringEncoding error:nil]; - NSData *data = [self.jsonResposne dataUsingEncoding:NSUTF8StringEncoding]; + NSData *data = [self.jsonResponse dataUsingEncoding:NSUTF8StringEncoding]; NSError *errorJson = nil; NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions @@ -138,11 +138,11 @@ - (void)testParsingTestMessage { pathForResource:@"TestJsonDataWithTestMessageFromFetch" ofType:@"txt"]; - self.jsonResposne = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath + self.jsonResponse = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath encoding:NSUTF8StringEncoding error:nil]; - NSData *data = [self.jsonResposne dataUsingEncoding:NSUTF8StringEncoding]; + NSData *data = [self.jsonResponse dataUsingEncoding:NSUTF8StringEncoding]; NSError *errorJson = nil; NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions @@ -174,11 +174,11 @@ - (void)testParsingInvalidTestMessageNodes { pathForResource:@"JsonDataWithInvalidMessagesFromFetch" ofType:@"txt"]; - self.jsonResposne = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath + self.jsonResponse = [[NSString alloc] initWithContentsOfFile:testJsonDataFilePath encoding:NSUTF8StringEncoding error:nil]; - NSData *data = [self.jsonResposne dataUsingEncoding:NSUTF8StringEncoding]; + NSData *data = [self.jsonResponse dataUsingEncoding:NSUTF8StringEncoding]; NSError *errorJson = nil; NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions diff --git a/Metrics/Tests/MetricsTests/example_report.json b/Metrics/Tests/MetricsTests/example_report.json index 08f775a87d6..b9e0e74200d 100644 --- a/Metrics/Tests/MetricsTests/example_report.json +++ b/Metrics/Tests/MetricsTests/example_report.json @@ -3404,7 +3404,7 @@ "coverage": 0.8181818181818182 }, { - "name": "-[FIROptions isAnalyticsCollectionExpicitlySet]", + "name": "-[FIROptions isAnalyticsCollectionExplicitlySet]", "coverage": 1 }, { @@ -4837,7 +4837,7 @@ "coverage": 0.8181818181818182 }, { - "name": "-[FIROptions isAnalyticsCollectionExpicitlySet]", + "name": "-[FIROptions isAnalyticsCollectionExplicitlySet]", "coverage": 1 }, { @@ -11611,7 +11611,7 @@ "coverage": 0.8181818181818182 }, { - "name": "-[FIROptions isAnalyticsCollectionExpicitlySet]", + "name": "-[FIROptions isAnalyticsCollectionExplicitlySet]", "coverage": 1 }, { @@ -15694,7 +15694,7 @@ "coverage": 0.8181818181818182 }, { - "name": "-[FIROptions isAnalyticsCollectionExpicitlySet]", + "name": "-[FIROptions isAnalyticsCollectionExplicitlySet]", "coverage": 1 }, { @@ -21414,7 +21414,7 @@ "coverage": 0.8181818181818182 }, { - "name": "-[FIROptions isAnalyticsCollectionExpicitlySet]", + "name": "-[FIROptions isAnalyticsCollectionExplicitlySet]", "coverage": 1 }, { @@ -25941,7 +25941,7 @@ "coverage": 0.8181818181818182 }, { - "name": "-[FIROptions isAnalyticsCollectionExpicitlySet]", + "name": "-[FIROptions isAnalyticsCollectionExplicitlySet]", "coverage": 1 }, { @@ -37299,7 +37299,7 @@ "coverage": 0.8181818181818182 }, { - "name": "-[FIROptions isAnalyticsCollectionExpicitlySet]", + "name": "-[FIROptions isAnalyticsCollectionExplicitlySet]", "coverage": 1 }, { diff --git a/README.md b/README.md index d75ae8cb12c..f55d642180e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ This repository contains a subset of the Firebase iOS SDK source. It currently includes FirebaseCore, FirebaseABTesting, FirebaseAuth, FirebaseDatabase, FirebaseFirestore, FirebaseFunctions, FirebaseInstanceID, FirebaseInAppMessaging, -FirebaseInAppMessagingDisplay, FirebaseMessaging and FirebaseStorage. +FirebaseInAppMessagingDisplay, FirebaseMessaging, FirebaseRemoteConfig, and +FirebaseStorage. The repository also includes GoogleUtilities source. The [GoogleUtilities](GoogleUtilities/README.md) pod is @@ -98,13 +99,17 @@ Travis will verify that any code changes are done in a style compliant way. Inst These commands will get the right versions: ``` -brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/773cb75d360b58f32048f5964038d09825a507c8/Formula/clang-format.rb -brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/3dfea1004e0736754bbf49673cca8aaed8a94089/Formula/swiftformat.rb +brew upgrade https://raw.githubusercontent.com/Homebrew/homebrew-core/e3496d9/Formula/clang-format.rb +brew upgrade https://raw.githubusercontent.com/Homebrew/homebrew-core/7963c3d/Formula/swiftformat.rb ``` Note: if you already have a newer version of these installed you may need to `brew switch` to this version. +To update this section, find the versions of clang-format and swiftformat.rb to +match the versions in the CI failure logs +[here](https://github.com/Homebrew/homebrew-core/tree/master/Formula). + ### Running Unit Tests Select a scheme and press Command-u to build a component and run its unit tests. @@ -179,8 +184,8 @@ participate in the Firebase community. ### macOS and tvOS Thanks to contributions from the community, FirebaseABTesting, FirebaseAuth, FirebaseCore, -FirebaseDatabase, FirebaseMessaging, -FirebaseFirestore, FirebaseFunctions and FirebaseStorage now compile, run unit tests, and work on +FirebaseDatabase, FirebaseMessaging, FirebaseFirestore, +FirebaseFunctions, FirebaseRemoteConfig, and FirebaseStorage now compile, run unit tests, and work on macOS and tvOS. For tvOS, checkout the [Sample](Example/tvOSSample). @@ -202,6 +207,7 @@ pod 'FirebaseDatabase' pod 'FirebaseFirestore' pod 'FirebaseFunctions' pod 'FirebaseMessaging' +pod 'FirebaseRemoteConfig' pod 'FirebaseStorage' ``` diff --git a/Releases/Manifests/6.7.0.json b/Releases/Manifests/6.7.0.json new file mode 100644 index 00000000000..a440ac5bfc0 --- /dev/null +++ b/Releases/Manifests/6.7.0.json @@ -0,0 +1,13 @@ +{ + "FirebaseABTesting":"3.1.0", + "FirebaseAnalyticsInterop":"1.4.0", + "FirebaseAuth":"6.2.3", + "FirebaseCore":"6.2.1", + "FirebaseDatabase":"6.1.0", + "FirebaseDynamicLinks":"4.0.3", + "FirebaseInAppMessagingDisplay":"0.15.4", + "FirebaseFirestore":"1.4.4", + "FirebaseMessaging":"4.1.3", + "GoogleDataTransportCCTSupport":"1.0.2", + "GoogleDataTransport":"1.1.2" +} diff --git a/Releases/Manifests/6.8.0.json b/Releases/Manifests/6.8.0.json new file mode 100644 index 00000000000..9f6589de0e4 --- /dev/null +++ b/Releases/Manifests/6.8.0.json @@ -0,0 +1,10 @@ +{ + "FirebaseABTesting":"3.1.1", + "FirebaseCore":"6.2.2", + "FirebaseDynamicLinks":"4.0.4", + "FirebaseFirestore":"1.5.0", + "FirebaseInAppMessaging":"0.15.4", + "FirebaseInstanceID":"4.2.4", + "FirebaseMessaging":"4.1.4", + "FirebaseRemoteConfig":"4.4.0" +} diff --git a/Releases/Manifests/6.8.1.json b/Releases/Manifests/6.8.1.json new file mode 100644 index 00000000000..26ba6c82c3b --- /dev/null +++ b/Releases/Manifests/6.8.1.json @@ -0,0 +1,6 @@ +{ + "FirebaseCore":"6.2.3", + "FirebaseDynamicLinks":"4.0.5", + "FirebaseInstanceID":"4.2.5", + "FirebaseStorage":"3.4.1" +} diff --git a/Releases/Manifests/6.9.0.json b/Releases/Manifests/6.9.0.json new file mode 100644 index 00000000000..d79ec30f8e0 --- /dev/null +++ b/Releases/Manifests/6.9.0.json @@ -0,0 +1,9 @@ +{ + "FirebaseCore":"6.3.0", + "FirebaseCoreDiagnostics":"1.1.0", + "FirebaseFirestore":"1.5.1", + "FirebaseMessaging":"4.1.5", + "FirebaseRemoteConfig":"4.4.1", + "GoogleDataTransport":"2.0.0", + "GoogleDataTransportCCTSupport":"1.1.0" +} diff --git a/SymbolCollisionTest/Podfile b/SymbolCollisionTest/Podfile index 196ff8f76af..b45e4f3e917 100644 --- a/SymbolCollisionTest/Podfile +++ b/SymbolCollisionTest/Podfile @@ -8,7 +8,7 @@ target 'SymbolCollisionTest' do # use_frameworks! # Firebase Pods - pod 'Firebase', '6.6.0' + pod 'Firebase', '6.9.0' pod 'FirebaseAnalytics' pod 'FirebaseAuth' pod 'FirebaseCore' @@ -48,7 +48,7 @@ target 'SymbolCollisionTest' do pod 'GoogleIDFASupport' pod 'GoogleInterchangeUtilities' pod 'GoogleMaps' - pod 'GoogleMobileVision' +# pod 'GoogleMobileVision' # After Firebase 6.8.0, conflicts with FirebaseML pod 'GoogleNetworkingUtilities' pod 'GoogleParsingUtilities' pod 'GooglePlacePicker' diff --git a/ZipBuilder/Package.swift b/ZipBuilder/Package.swift index 1c4d2d60672..59bc1df1287 100644 --- a/ZipBuilder/Package.swift +++ b/ZipBuilder/Package.swift @@ -21,12 +21,19 @@ import PackageDescription let package = Package( name: "ZipBuilder", + products: [ + .executable(name: "ReleasePackager", targets: ["ZipBuilder"]), + ], dependencies: [ .package(url: "https://github.com/apple/swift-protobuf.git", .exact("1.2.0")), ], targets: [ .target( name: "ZipBuilder", + dependencies: ["ManifestReader"] + ), + .target( + name: "ManifestReader", dependencies: ["SwiftProtobuf"] ), ] diff --git a/ZipBuilder/README.md b/ZipBuilder/README.md index fd651847c01..e484e48e7b9 100644 --- a/ZipBuilder/README.md +++ b/ZipBuilder/README.md @@ -41,7 +41,7 @@ These arguments assume you're running the command from the `ZipBuilder` director - `-templateDir $(pwd)/Template` - This should always be the same. -Optional comon arguments: +Optional common arguments: - `-updatePodRepo false` - This is for speedups when `pod repo update` has already been run recently. @@ -119,3 +119,7 @@ files and folders. ### Prefer File `URL`s over Strings Instead of relying on `String`s to represent file paths, use `URL`s as soon as possible to avoid any missed or double slashes along with other issues. + +## Updating protobuf generated Swift files +- Install [Swift Protobuf](https://github.com/apple/swift-protobuf#building-and-installing-the-code-generator-plugin) +- Run `protoc Sources/ManifestReader/*.proto --swift_opt=Visibility=Public --swift_out=./` diff --git a/ZipBuilder/Sources/ZipBuilder/FirebaseSDKs.pb.swift b/ZipBuilder/Sources/ManifestReader/FirebaseSDKs.pb.swift similarity index 83% rename from ZipBuilder/Sources/ZipBuilder/FirebaseSDKs.pb.swift rename to ZipBuilder/Sources/ManifestReader/FirebaseSDKs.pb.swift index 66b9054b458..84e894f648b 100644 --- a/ZipBuilder/Sources/ZipBuilder/FirebaseSDKs.pb.swift +++ b/ZipBuilder/Sources/ManifestReader/FirebaseSDKs.pb.swift @@ -36,108 +36,108 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP } /// A list of all Firebase SDKs. -struct ZipBuilder_FirebaseSDKs { +public struct ZipBuilder_FirebaseSDKs { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. - var sdk: [ZipBuilder_SDK] = [] + public var sdk: [ZipBuilder_SDK] = [] - var unknownFields = SwiftProtobuf.UnknownStorage() + public var unknownFields = SwiftProtobuf.UnknownStorage() - init() {} + public init() {} } /// Represents a single SDK that should be released. -struct ZipBuilder_SDK { +public struct ZipBuilder_SDK { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// SDK name - var name: String { + public var name: String { get {return _storage._name} set {_uniqueStorage()._name = newValue} } /// MPM name for the blueprint. For internal use only. - var mpmName: String { + public var mpmName: String { get {return _storage._mpmName} set {_uniqueStorage()._mpmName = newValue} } /// Public version - var publicVersion: String { + public var publicVersion: String { get {return _storage._publicVersion} set {_uniqueStorage()._publicVersion = newValue} } /// List of MPM patterns to build - var mpmPattern: [String] { + public var mpmPattern: [String] { get {return _storage._mpmPattern} set {_uniqueStorage()._mpmPattern = newValue} } /// An optional list of additional build flags. For internal use only. - var buildFlags: ZipBuilder_BuildFlag { + public var buildFlags: ZipBuilder_BuildFlag { get {return _storage._buildFlags ?? ZipBuilder_BuildFlag()} set {_uniqueStorage()._buildFlags = newValue} } /// Returns true if `buildFlags` has been explicitly set. - var hasBuildFlags: Bool {return _storage._buildFlags != nil} + public var hasBuildFlags: Bool {return _storage._buildFlags != nil} /// Clears the value of `buildFlags`. Subsequent reads from it will return its default value. - mutating func clearBuildFlags() {_uniqueStorage()._buildFlags = nil} + public mutating func clearBuildFlags() {_uniqueStorage()._buildFlags = nil} /// List of MPM patterns to build (optional nightly override). For internal use only. - var nightlyMpmPattern: [String] { + public var nightlyMpmPattern: [String] { get {return _storage._nightlyMpmPattern} set {_uniqueStorage()._nightlyMpmPattern = newValue} } /// Whether or not the SDK is built from open-source. For internal use only. - var openSource: Bool { + public var openSource: Bool { get {return _storage._openSource} set {_uniqueStorage()._openSource = newValue} } /// Whether or not to strip the i386 architecture from the build. - var stripI386: Bool { + public var stripI386: Bool { get {return _storage._stripI386} set {_uniqueStorage()._stripI386 = newValue} } /// List of build targets. For internal use only. - var buildTarget: [String] { + public var buildTarget: [String] { get {return _storage._buildTarget} set {_uniqueStorage()._buildTarget = newValue} } /// Whether or not to strip both the i386 and armv7 architectures from the - /// build. For internal use only. - var strip32Bits: Bool { + /// build. All SDKs that use this flag are built internally so this should be ignored. + public var strip32Bits: Bool { get {return _storage._strip32Bits} set {_uniqueStorage()._strip32Bits = newValue} } - var unknownFields = SwiftProtobuf.UnknownStorage() + public var unknownFields = SwiftProtobuf.UnknownStorage() - init() {} + public init() {} fileprivate var _storage = _StorageClass.defaultInstance } /// Any extra build flags needed to build the SDK. For internal use only. -struct ZipBuilder_BuildFlag { +public struct ZipBuilder_BuildFlag { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// An additional build flag needed to build the SDK - var flag: [String] = [] + public var flag: [String] = [] - var unknownFields = SwiftProtobuf.UnknownStorage() + public var unknownFields = SwiftProtobuf.UnknownStorage() - init() {} + public init() {} } // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -145,12 +145,12 @@ struct ZipBuilder_BuildFlag { fileprivate let _protobuf_package = "ZipBuilder" extension ZipBuilder_FirebaseSDKs: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".FirebaseSDKs" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + public static let protoMessageName: String = _protobuf_package + ".FirebaseSDKs" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "sdk"), ] - mutating func decodeMessage(decoder: inout D) throws { + public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { switch fieldNumber { case 1: try decoder.decodeRepeatedMessageField(value: &self.sdk) @@ -159,14 +159,14 @@ extension ZipBuilder_FirebaseSDKs: SwiftProtobuf.Message, SwiftProtobuf._Message } } - func traverse(visitor: inout V) throws { + public func traverse(visitor: inout V) throws { if !self.sdk.isEmpty { try visitor.visitRepeatedMessageField(value: self.sdk, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: ZipBuilder_FirebaseSDKs, rhs: ZipBuilder_FirebaseSDKs) -> Bool { + public static func ==(lhs: ZipBuilder_FirebaseSDKs, rhs: ZipBuilder_FirebaseSDKs) -> Bool { if lhs.sdk != rhs.sdk {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true @@ -174,8 +174,8 @@ extension ZipBuilder_FirebaseSDKs: SwiftProtobuf.Message, SwiftProtobuf._Message } extension ZipBuilder_SDK: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".SDK" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + public static let protoMessageName: String = _protobuf_package + ".SDK" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "name"), 2: .standard(proto: "mpm_name"), 3: .standard(proto: "public_version"), @@ -225,7 +225,7 @@ extension ZipBuilder_SDK: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement return _storage } - mutating func decodeMessage(decoder: inout D) throws { + public mutating func decodeMessage(decoder: inout D) throws { _ = _uniqueStorage() try withExtendedLifetime(_storage) { (_storage: _StorageClass) in while let fieldNumber = try decoder.nextFieldNumber() { @@ -246,7 +246,7 @@ extension ZipBuilder_SDK: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement } } - func traverse(visitor: inout V) throws { + public func traverse(visitor: inout V) throws { try withExtendedLifetime(_storage) { (_storage: _StorageClass) in if !_storage._name.isEmpty { try visitor.visitSingularStringField(value: _storage._name, fieldNumber: 1) @@ -282,7 +282,7 @@ extension ZipBuilder_SDK: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: ZipBuilder_SDK, rhs: ZipBuilder_SDK) -> Bool { + public static func ==(lhs: ZipBuilder_SDK, rhs: ZipBuilder_SDK) -> Bool { if lhs._storage !== rhs._storage { let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in let _storage = _args.0 @@ -307,12 +307,12 @@ extension ZipBuilder_SDK: SwiftProtobuf.Message, SwiftProtobuf._MessageImplement } extension ZipBuilder_BuildFlag: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".BuildFlag" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + public static let protoMessageName: String = _protobuf_package + ".BuildFlag" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "flag"), ] - mutating func decodeMessage(decoder: inout D) throws { + public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { switch fieldNumber { case 1: try decoder.decodeRepeatedStringField(value: &self.flag) @@ -321,14 +321,14 @@ extension ZipBuilder_BuildFlag: SwiftProtobuf.Message, SwiftProtobuf._MessageImp } } - func traverse(visitor: inout V) throws { + public func traverse(visitor: inout V) throws { if !self.flag.isEmpty { try visitor.visitRepeatedStringField(value: self.flag, fieldNumber: 1) } try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: ZipBuilder_BuildFlag, rhs: ZipBuilder_BuildFlag) -> Bool { + public static func ==(lhs: ZipBuilder_BuildFlag, rhs: ZipBuilder_BuildFlag) -> Bool { if lhs.flag != rhs.flag {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true diff --git a/ZipBuilder/FirebaseSDKs.proto b/ZipBuilder/Sources/ManifestReader/FirebaseSDKs.proto similarity index 100% rename from ZipBuilder/FirebaseSDKs.proto rename to ZipBuilder/Sources/ManifestReader/FirebaseSDKs.proto diff --git a/ZipBuilder/Sources/ZipBuilder/ManifestReader.swift b/ZipBuilder/Sources/ManifestReader/ManifestReader.swift similarity index 91% rename from ZipBuilder/Sources/ZipBuilder/ManifestReader.swift rename to ZipBuilder/Sources/ManifestReader/ManifestReader.swift index 56d14071d35..f5083a993e9 100644 --- a/ZipBuilder/Sources/ZipBuilder/ManifestReader.swift +++ b/ZipBuilder/Sources/ManifestReader/ManifestReader.swift @@ -18,12 +18,12 @@ import Foundation /// Common functions for Firebase iOS SDK Release manifests. Intentionally empty, this enum is used /// as a namespace. -enum ManifestReader {} +public enum ManifestReader {} extension ManifestReader { /// Load all the publicly released SDKs and their versions. Will cause a fatal error if the file /// cannot be read or proto cannot be generated. - static func loadAllReleasedSDKs(fromTextproto textproto: URL) -> ZipBuilder_FirebaseSDKs { + public static func loadAllReleasedSDKs(fromTextproto textproto: URL) -> ZipBuilder_FirebaseSDKs { // Read the textproto and create it from the proto's generated API. Fail if anything fails in // the process. do { @@ -44,7 +44,7 @@ extension ManifestReader { /// /// - Parameter textproto: The path to the textproto file describing the current release. /// - Returns: An instance of ZipBuilder_Release describing specific versions to build. - static func loadCurrentRelease(fromTextproto textproto: URL) -> ZipBuilder_Release { + public static func loadCurrentRelease(fromTextproto textproto: URL) -> ZipBuilder_Release { // Read the textproto and create it from the proto's generated API. Fail if anything fails in // the process. do { diff --git a/ZipBuilder/Sources/ZipBuilder/Release.pb.swift b/ZipBuilder/Sources/ManifestReader/Release.pb.swift similarity index 80% rename from ZipBuilder/Sources/ZipBuilder/Release.pb.swift rename to ZipBuilder/Sources/ManifestReader/Release.pb.swift index 9cac799471f..37d20517744 100644 --- a/ZipBuilder/Sources/ZipBuilder/Release.pb.swift +++ b/ZipBuilder/Sources/ManifestReader/Release.pb.swift @@ -35,57 +35,57 @@ fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAP typealias Version = _2 } -struct ZipBuilder_Release { +public struct ZipBuilder_Release { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// Targeted release date - var targetDate: String = String() + public var targetDate: String = String() /// Release name (e.g. M15 - Beryllium) - var name: String = String() + public var name: String = String() /// Release code (e.g. M15) - var code: String = String() + public var code: String = String() /// Whether or not the release went out - var released: Bool = false + public var released: Bool = false /// List of SDKs that are part of the release - var sdk: [ZipBuilder_ReleasingSDK] = [] + public var sdk: [ZipBuilder_ReleasingSDK] = [] - var unknownFields = SwiftProtobuf.UnknownStorage() + public var unknownFields = SwiftProtobuf.UnknownStorage() - init() {} + public init() {} } -struct ZipBuilder_ReleasingSDK { +public struct ZipBuilder_ReleasingSDK { // SwiftProtobuf.Message conformance is added in an extension below. See the // `Message` and `Message+*Additions` files in the SwiftProtobuf library for // methods supported on all messages. /// SDK name as from all_firebase_ios_sdks.textproto - var sdkName: String = String() + public var sdkName: String = String() /// The version of SDK to release - var sdkVersion: String = String() + public var sdkVersion: String = String() /// Whether or not a launchal is required for this release - var launchcalRequired: Bool = false + public var launchcalRequired: Bool = false /// Ariane link - var launchcalLink: String = String() + public var launchcalLink: String = String() /// An link to the change log - var changelogLink: String = String() + public var changelogLink: String = String() /// A link to the release hotlist - var hotlistLink: String = String() + public var hotlistLink: String = String() - var unknownFields = SwiftProtobuf.UnknownStorage() + public var unknownFields = SwiftProtobuf.UnknownStorage() - init() {} + public init() {} } // MARK: - Code below here is support for the SwiftProtobuf runtime. @@ -93,8 +93,8 @@ struct ZipBuilder_ReleasingSDK { fileprivate let _protobuf_package = "ZipBuilder" extension ZipBuilder_Release: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".Release" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + public static let protoMessageName: String = _protobuf_package + ".Release" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "target_date"), 2: .same(proto: "name"), 3: .same(proto: "code"), @@ -102,7 +102,7 @@ extension ZipBuilder_Release: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 5: .same(proto: "sdk"), ] - mutating func decodeMessage(decoder: inout D) throws { + public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { switch fieldNumber { case 1: try decoder.decodeSingularStringField(value: &self.targetDate) @@ -115,7 +115,7 @@ extension ZipBuilder_Release: SwiftProtobuf.Message, SwiftProtobuf._MessageImple } } - func traverse(visitor: inout V) throws { + public func traverse(visitor: inout V) throws { if !self.targetDate.isEmpty { try visitor.visitSingularStringField(value: self.targetDate, fieldNumber: 1) } @@ -134,7 +134,7 @@ extension ZipBuilder_Release: SwiftProtobuf.Message, SwiftProtobuf._MessageImple try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: ZipBuilder_Release, rhs: ZipBuilder_Release) -> Bool { + public static func ==(lhs: ZipBuilder_Release, rhs: ZipBuilder_Release) -> Bool { if lhs.targetDate != rhs.targetDate {return false} if lhs.name != rhs.name {return false} if lhs.code != rhs.code {return false} @@ -146,8 +146,8 @@ extension ZipBuilder_Release: SwiftProtobuf.Message, SwiftProtobuf._MessageImple } extension ZipBuilder_ReleasingSDK: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding { - static let protoMessageName: String = _protobuf_package + ".ReleasingSDK" - static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ + public static let protoMessageName: String = _protobuf_package + ".ReleasingSDK" + public static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .standard(proto: "sdk_name"), 2: .standard(proto: "sdk_version"), 3: .standard(proto: "launchcal_required"), @@ -156,7 +156,7 @@ extension ZipBuilder_ReleasingSDK: SwiftProtobuf.Message, SwiftProtobuf._Message 6: .standard(proto: "hotlist_link"), ] - mutating func decodeMessage(decoder: inout D) throws { + public mutating func decodeMessage(decoder: inout D) throws { while let fieldNumber = try decoder.nextFieldNumber() { switch fieldNumber { case 1: try decoder.decodeSingularStringField(value: &self.sdkName) @@ -170,7 +170,7 @@ extension ZipBuilder_ReleasingSDK: SwiftProtobuf.Message, SwiftProtobuf._Message } } - func traverse(visitor: inout V) throws { + public func traverse(visitor: inout V) throws { if !self.sdkName.isEmpty { try visitor.visitSingularStringField(value: self.sdkName, fieldNumber: 1) } @@ -192,7 +192,7 @@ extension ZipBuilder_ReleasingSDK: SwiftProtobuf.Message, SwiftProtobuf._Message try unknownFields.traverse(visitor: &visitor) } - static func ==(lhs: ZipBuilder_ReleasingSDK, rhs: ZipBuilder_ReleasingSDK) -> Bool { + public static func ==(lhs: ZipBuilder_ReleasingSDK, rhs: ZipBuilder_ReleasingSDK) -> Bool { if lhs.sdkName != rhs.sdkName {return false} if lhs.sdkVersion != rhs.sdkVersion {return false} if lhs.launchcalRequired != rhs.launchcalRequired {return false} diff --git a/ZipBuilder/Sources/ZipBuilder/Release.proto b/ZipBuilder/Sources/ManifestReader/Release.proto similarity index 100% rename from ZipBuilder/Sources/ZipBuilder/Release.proto rename to ZipBuilder/Sources/ManifestReader/Release.proto diff --git a/ZipBuilder/Sources/ZipBuilder/CarthageUtils.swift b/ZipBuilder/Sources/ZipBuilder/CarthageUtils.swift index 08e10f0a9a8..9b7d428968d 100644 --- a/ZipBuilder/Sources/ZipBuilder/CarthageUtils.swift +++ b/ZipBuilder/Sources/ZipBuilder/CarthageUtils.swift @@ -175,8 +175,7 @@ public extension CarthageUtils { /// /// - Parameters: /// - destination: The destination directory for the Firebase framework. - /// - rootDir: The root directory that contains other required files (like the modulemap and - /// Firebase header). + /// - rootDir: The root directory that contains other required files (like the Firebase header). /// - templateDir: The template directory containing the dummy Firebase library. private static func createFirebaseFramework(inDir destination: URL, rootDir: URL, @@ -198,13 +197,20 @@ public extension CarthageUtils { // Copy the Firebase header and modulemap that was created in the Zip file. let header = rootDir.appendingPathComponent(Constants.ProjectPath.firebaseHeader) - let modulemap = rootDir.appendingPathComponent(Constants.ProjectPath.modulemap) do { try fm.copyItem(at: header, to: headersDir.appendingPathComponent(header.lastPathComponent)) - try fm.copyItem(at: modulemap, - to: modulesDir.appendingPathComponent(modulemap.lastPathComponent)) + + // Generate the new modulemap since it differs from the Zip modulemap. + let carthageModulemap = """ + framework module Firebase { + header "Firebase.h" + export * + } + """ + let modulemapPath = modulesDir.appendingPathComponent("module.modulemap") + try carthageModulemap.write(to: modulemapPath, atomically: true, encoding: .utf8) } catch { - fatalError("Couldn't copy required files for Firebase framework in Carthage. \(error)") + fatalError("Couldn't write required files for Firebase framework in Carthage. \(error)") } // Copy the dummy Firebase library. diff --git a/ZipBuilder/Sources/ZipBuilder/CocoaPod.swift b/ZipBuilder/Sources/ZipBuilder/CocoaPod.swift index d7ca9854cd5..6761986923d 100644 --- a/ZipBuilder/Sources/ZipBuilder/CocoaPod.swift +++ b/ZipBuilder/Sources/ZipBuilder/CocoaPod.swift @@ -93,49 +93,8 @@ public enum CocoaPod: String, CaseIterable { /// of frameworks get pulled in. public func duplicateFrameworksToRemove() -> [String] { switch self { - case .mlVision: - return ["BarcodeDetector.framework", - "FaceDetector.framework", - "LabelDetector.framework", - "TextDetector.framework"] - case .mlVisionBarcodeModel: - return ["FaceDetector.framework", - "GTMSessionFetcher.framework", - "GoogleMobileVision.framework", - "LabelDetector.framework", - "Protobuf.framework", - "TextDetector.framework"] - case .mlVisionFaceModel: - return ["BarcodeDetector.framework", - "GTMSessionFetcher.framework", - "GoogleMobileVision.framework", - "LabelDetector.framework", - "Protobuf.framework", - "TextDetector.framework"] - case .mlVisionLabelModel: - return ["BarcodeDetector.framework", - "FaceDetector.framework", - "GTMSessionFetcher.framework", - "GoogleMobileVision.framework", - "Protobuf.framework", - "TextDetector.framework"] - case .mlVisionTextModel: - return ["BarcodeDetector.framework", - "FaceDetector.framework", - "GTMSessionFetcher.framework", - "GoogleMobileVision.framework", - "LabelDetector.framework", - "Protobuf.framework"] - case .mlVisionAutoML: - return ["BarcodeDetector.framework", - "FaceDetector.framework", - "LabelDetector.framework", - "TextDetector.framework"] - case .mlVisionObjectDetection: - return ["BarcodeDetector.framework", - "FaceDetector.framework", - "LabelDetector.framework", - "TextDetector.framework"] + case .mlVisionBarcodeModel, .mlVisionFaceModel, .mlVisionLabelModel, .mlVisionTextModel: + return ["GTMSessionFetcher.framework", "Protobuf.framework"] case .abTesting, .adMob, .analytics, @@ -154,6 +113,9 @@ public enum CocoaPod: String, CaseIterable { .mlNLLanguageID, .mlNLSmartReply, .mlNLTranslate, + .mlVision, + .mlVisionAutoML, + .mlVisionObjectDetection, .performance, .remoteConfig, .storage: @@ -162,19 +124,6 @@ public enum CocoaPod: String, CaseIterable { return [] } } - - /// Returns a group of duplicate Resources that should be removed, if any. - public func duplicateResourcesToRemove() -> [String] { - switch self { - case .mlVisionFaceModel: - return ["GoogleMVTextDetectorResources.bundle"] - case .mlVisionTextModel: - return ["GoogleMVFaceDetectorResources.bundle"] - default: - // By default, no resources should be removed. - return [] - } - } } /// Add comparitor for OperatingSystemVersion. We only need the `>` since we don't care about equals diff --git a/ZipBuilder/Sources/ZipBuilder/CocoaPodUtils.swift b/ZipBuilder/Sources/ZipBuilder/CocoaPodUtils.swift index 563d8452e33..f6a4457ca95 100644 --- a/ZipBuilder/Sources/ZipBuilder/CocoaPodUtils.swift +++ b/ZipBuilder/Sources/ZipBuilder/CocoaPodUtils.swift @@ -31,17 +31,6 @@ public enum CocoaPodUtils { /// The location of the pod on disk. var installedLocation: URL - - /// A key that can be generated and used for identifying pods due to binary differences. - var cacheKey: String? - - /// Default initializer. Explicitly declared to take advantage of default arguments. - init(name: String, version: String, installedLocation: URL, cacheKey: String? = nil) { - self.name = name - self.version = version - self.installedLocation = installedLocation - self.cacheKey = cacheKey - } } /// Executes the `pod cache clean --all` command to remove any cached CocoaPods. @@ -97,14 +86,9 @@ public enum CocoaPodUtils { "information for installed Pods.") } - // Generate the cache key for this framework. We will use the list of subspecs used in the Pod - // to generate this, since a Pod like GoogleUtilities could build different sources based on - // what subspecs are included. - let cacheKey = self.cacheKey(forPod: podName, fromPodfileLock: podfileLock) let podInfo = PodInfo(name: podName, version: version, - installedLocation: podDir, - cacheKey: cacheKey) + installedLocation: podDir) installedPods.append(podInfo) } @@ -228,73 +212,6 @@ public enum CocoaPodUtils { return (framework, version) } - /// Generates a key representing the unique combination of all subspecs used for that Pod. This is - /// necessary for Pods like GoogleUtilities, where we will need to include all subspecs as part of - /// a build. Otherwise we could accidentally use a cached framework that doesn't include all the - /// code necessary to function. - /// - /// - Parameters: - /// - framework: The framework being built. - /// - podfileLock: The contents of the Podfile.lock for the project. - /// - Returns: A key to describe the full set of subspecs used to build the framework, or an empty - /// String if there were no specific subspecs used. - private static func cacheKey(forPod podName: String, - fromPodfileLock podfileLock: String) -> String? { - // Ignore the umbrella Firebase pod, cacheing doesn't make sense. - guard podName != "Firebase" else { return nil } - - // Get the first section of the Podfile containing only Pods installed, the only thing we care - // about. - guard let podsInstalled = podfileLock.components(separatedBy: "DEPENDENCIES:").first else { - fatalError(""" - Could not generate cache key for \(podName) from Podfile.lock contents - is this a valid - Podfile.lock? - ---------- Podfile.lock contents ---------- - \(podfileLock) - ------------------------------------------- - """) - } - - // Only get the lines that start with " - ", and have the framework we're looking for since - // they are the top level pods that are installed. - // Example result of a single line: `- GoogleUtilities/Environment (~> 5.2)`. - let lines = podsInstalled.components(separatedBy: .newlines).filter { - $0.hasPrefix(" - ") && $0.contains(podName) - } - - // Get a list of all the subspecs used to build this framework, and use that to generate the - // cache key. - var uniqueSubspecs = Set() - for line in lines.sorted() { - // Separate the line into readable chunks, using a space and quote as a separator. - // Example result: `["-", "GoogleUtilities/Environment", "(~>", "5.2)"]`. - let components = line.components(separatedBy: CharacterSet(charactersIn: " \"")) - - // The Pod and subspec will be the only variables we care about, filter out the rest. - // Example result: 'GoogleUtilities/Environment' or `FirebaseCore`. Only Pods with a subspec - // should be included here, which are always in the format of `PodName/SubspecName`. - guard let fullPodName = components.filter({ $0.contains("\(podName)/") }).first else { - continue - } - - // The fullPodName will be something like `GoogleUtilities/UserDefaults`, get the subspec - // name. - let subspec = fullPodName.replacingOccurrences(of: "\(podName)/", with: "") - if !subspec.isEmpty { - uniqueSubspecs.insert(subspec) - } - } - - // Return nil if there are no subpsecs used, since no cache key is necessary. - guard !uniqueSubspecs.isEmpty else { - return nil - } - - // Assemble the cache key based on the framework name, and all subspecs (sorted alphabetically - // for repeatability) separated by a `+` (as was previously used). - return podName + "+" + uniqueSubspecs.sorted().joined(separator: "+") - } - /// Create the contents of a Podfile for an array of subspecs. This assumes the array of subspecs /// is not empty. private static func generatePodfile(for pods: [CocoaPod], diff --git a/ZipBuilder/Sources/ZipBuilder/FileManager+Utils.swift b/ZipBuilder/Sources/ZipBuilder/FileManager+Utils.swift index 21d895d19d5..a6536aeb9e9 100644 --- a/ZipBuilder/Sources/ZipBuilder/FileManager+Utils.swift +++ b/ZipBuilder/Sources/ZipBuilder/FileManager+Utils.swift @@ -110,9 +110,12 @@ public extension FileManager { /// Returns a deterministic path of a temporary directory for the given name. Note: This does /// *not* create the directory if it doesn't exist, merely generates the name for creation. func temporaryDirectory(withName name: String) -> URL { - // Get access to the temporary directory. + // Get access to the temporary directory. This could be passed in via `LaunchArgs`, or use the + // default temporary directory. let tempDir: URL - if #available(OSX 10.12, *) { + if let root = LaunchArgs.shared.buildRoot { + tempDir = root + } else if #available(OSX 10.12, *) { tempDir = temporaryDirectory } else { tempDir = URL(fileURLWithPath: NSTemporaryDirectory()) diff --git a/ZipBuilder/Sources/ZipBuilder/FrameworkBuilder.swift b/ZipBuilder/Sources/ZipBuilder/FrameworkBuilder.swift index b7ef0fa9a8a..6203738f334 100755 --- a/ZipBuilder/Sources/ZipBuilder/FrameworkBuilder.swift +++ b/ZipBuilder/Sources/ZipBuilder/FrameworkBuilder.swift @@ -76,78 +76,43 @@ struct FrameworkBuilder { /// - Parameters: /// - framework: The name of the Framework being built. /// - version: String representation of the version. - /// - cacheKey: The key used for caching this framework build. If nil, the framework name will - /// be used. - /// - cacheEnabled: Flag for enabling the cache. Defaults to false. /// - Parameter logsOutputDir: The path to the directory to place build logs. /// - Returns: A URL to the framework that was built (or pulled from the cache). public func buildFramework(withName podName: String, version: String, - cacheKey: String?, - cacheEnabled: Bool = false, logsOutputDir: URL? = nil) -> URL { print("Building \(podName)") -// Cache is temporarily disabled due to pod cache list issues. - // Get the CocoaPods cache to see if we can pull from any frameworks already built. -// let podsCache = CocoaPodUtils.listPodCache(inDir: projectDir) -// -// guard let cachedVersions = podsCache[podName] else { -// fatalError("Cannot find a pod cache for framework \(podName).") -// } -// -// guard let podInfo = cachedVersions[version] else { -// fatalError(""" -// Cannot find a pod cache for framework \(podName) at version \(version). -// Something could be wrong with your CocoaPods cache - try running the following: -// -// pod cache clean '\(podName)' --all -// """) -// } -// -// // TODO: Figure out if we need the MD5 at all. - let md5 = podName -// let md5 = Shell.calculateMD5(for: podInfo.installedLocation) - // Get (or create) the cache directory for storing built frameworks. let fileManager = FileManager.default var cachedFrameworkRoot: URL do { let cacheDir = try fileManager.firebaseCacheDirectory() - cachedFrameworkRoot = cacheDir.appendingPathComponents([podName, version, md5]) - if let cacheKey = cacheKey { - cachedFrameworkRoot.appendPathComponent(cacheKey) - } + cachedFrameworkRoot = cacheDir.appendingPathComponents([podName, version]) } catch { fatalError("Could not create caches directory for building frameworks: \(error)") } // Build the full cached framework path. let cachedFrameworkDir = cachedFrameworkRoot.appendingPathComponent("\(podName).framework") - let cachedFrameworkExists = fileManager.directoryExists(at: cachedFrameworkDir) - if cachedFrameworkExists, cacheEnabled { - print("Framework \(podName) version \(version) has already been built and cached at " + - "\(cachedFrameworkDir)") - return cachedFrameworkDir - } else { - let frameworkDir = compileFrameworkAndResources(withName: podName) - do { - // Remove the previously cached framework, if it exists, otherwise the `moveItem` call will - // fail. - if cachedFrameworkExists { - try fileManager.removeItem(at: cachedFrameworkDir) - } else if !fileManager.directoryExists(at: cachedFrameworkRoot) { - // If the root directory doesn't exist, create it so the `moveItem` will succeed. - try fileManager.createDirectory(at: cachedFrameworkRoot, - withIntermediateDirectories: true) - } - - // Move the newly built framework to the cache directory. - try fileManager.moveItem(at: frameworkDir, to: cachedFrameworkDir) - return cachedFrameworkDir - } catch { - fatalError("Could not move built frameworks into the cached frameworks directory: \(error)") + let frameworkDir = compileFrameworkAndResources(withName: podName) + do { + // Remove the previously cached framework if it exists, otherwise the `moveItem` call will + // fail. + fileManager.removeDirectoryIfExists(at: cachedFrameworkDir) + + // Create the root cache directory if it doesn't exist. + if !fileManager.directoryExists(at: cachedFrameworkRoot) { + // If the root directory doesn't exist, create it so the `moveItem` will succeed. + try fileManager.createDirectory(at: cachedFrameworkRoot, + withIntermediateDirectories: true) } + + // Move the newly built framework to the cache directory. + try fileManager.moveItem(at: frameworkDir, to: cachedFrameworkDir) + return cachedFrameworkDir + } catch { + fatalError("Could not move built frameworks into the cached frameworks directory: \(error)") } } @@ -476,12 +441,22 @@ struct FrameworkBuilder { // imports for nested folders. let aliasedHeaders = try fileManager.recursivelySearch(for: .headers, in: headersDir) let mappedHeaders: [(relativePath: String, resolvedLocation: URL)] = aliasedHeaders.map { + // The `headersDir` and `aliasedHeader` prefixes may be different, but they both should have + // `Pods/Headers/` in the path. Ignore everything up until that, then strip the remainder of + // the `headersDir` from the `aliasedHeader` in order to get path relative to the headers + // directory. + let trimmedHeader = removeHeaderPathPrefix(from: $0) + let trimmedDir = removeHeaderPathPrefix(from: headersDir) + var relativePath = trimmedHeader.replacingOccurrences(of: trimmedDir, with: "") + + // Remove any leading `/` for the relative path. + if relativePath.starts(with: "/") { + _ = relativePath.removeFirst() + } + // Standardize the URL because the aliasedHeaders could be at `/private/var` or `/var` which - // are symlinked to each other on macOS. This will let us remove the `headersDir` prefix and - // be left with just the relative path we need. - let standardized = $0.standardizedFileURL - let relativePath = standardized.path.replacingOccurrences(of: "\(headersDir.path)/", with: "") - let resolvedLocation = standardized.resolvingSymlinksInPath() + // are symlinked to each other on macOS. + let resolvedLocation = $0.standardizedFileURL.resolvingSymlinksInPath() return (relativePath, resolvedLocation) } @@ -499,4 +474,16 @@ struct FrameworkBuilder { try fileManager.copyItem(at: location, to: finalPath) } } + + private func removeHeaderPathPrefix(from url: URL) -> String { + let fullPath = url.standardizedFileURL.path + guard let foundRange = fullPath.range(of: "Pods/Headers/") else { + fatalError("Could not copy headers for framework: full path do not contain `Pods/Headers`:" + + fullPath) + } + + // Replace everything from the start of the string until the end of the `Pods/Headers/`. + let toRemove = fullPath.startIndex ..< foundRange.upperBound + return fullPath.replacingCharacters(in: toRemove, with: "") + } } diff --git a/ZipBuilder/Sources/ZipBuilder/LaunchArgs.swift b/ZipBuilder/Sources/ZipBuilder/LaunchArgs.swift index 38e5477f8ac..7108bed16cb 100644 --- a/ZipBuilder/Sources/ZipBuilder/LaunchArgs.swift +++ b/ZipBuilder/Sources/ZipBuilder/LaunchArgs.swift @@ -39,10 +39,9 @@ extension FileManager: FileChecker {} struct LaunchArgs { /// Keys associated with the launch args. See `Usage` for descriptions of each flag. private enum Key: String, CaseIterable { - case cacheEnabled + case buildRoot case carthageDir case customSpecRepos - case deleteCache case existingVersions case outputDir case releasingSDKs @@ -53,16 +52,14 @@ struct LaunchArgs { /// Usage description for the key. var usage: String { switch self { - case .cacheEnabled: - return "A flag to control using the cache for frameworks." + case .buildRoot: + return "The root directory for build artifacts. If `nil`, a temporary directory will be " + + "used." case .carthageDir: return "The directory pointing to all Carthage JSON manifests. Passing this flag enables" + "the Carthage build." case .customSpecRepos: return "A comma separated list of custom CocoaPod Spec repos." - case .deleteCache: - return "A flag to empty the cache. Note: if this flag and the `cacheEnabled` flag is " + - "set, it will fail since that's probably unintended." case .existingVersions: return "The file path to a textproto file containing the existing released SDK versions, " + "of type `ZipBuilder_FirebaseSDKs`." @@ -86,6 +83,9 @@ struct LaunchArgs { /// verify expected version numbers. let allSDKsPath: URL? + /// The root directory for build artifacts. If `nil`, a temporary directory will be used. + let buildRoot: URL? + /// The directory pointing to all Carthage JSON manifests. Passing this flag enables the Carthage /// build. let carthageDir: URL? @@ -106,18 +106,15 @@ struct LaunchArgs { /// based frameworks. let templateDir: URL - /// A flag to control using the cache for frameworks. - let cacheEnabled: Bool - - /// A flag to delete the cache from the cache directory. - let deleteCache: Bool - /// The release candidate number, zero indexed. let rcNumber: Int? /// A flag to update the Pod Repo or not. let updatePodRepo: Bool + /// The shared instance for processing launch args using default arguments. + static let shared: LaunchArgs = LaunchArgs() + /// Initializes with values pulled from the instance of UserDefaults passed in. /// /// - Parameters: @@ -180,23 +177,15 @@ struct LaunchArgs { outputDir = nil } - // Parse the release candidate number. This should only be used in conjunction with the other - // release related flags. - if let rcFlag = defaults.string(forKey: Key.rc.rawValue) { - guard let parsedFlag = Int(rcFlag) else { - LaunchArgs.exitWithUsageAndLog("Could not parse \(Key.rc) key: value passed in is not " + - "an integer. Value: \(rcFlag)") - } - + // Parse the release candidate number. Note: if the String passed in isn't an integer, ignore + // it and don't fail since we can append something else to the filenames. + if let rcFlag = defaults.string(forKey: Key.rc.rawValue), + !rcFlag.isEmpty, + let parsedFlag = Int(rcFlag) { + print("Parsed release candidate version number \(parsedFlag).") rcNumber = parsedFlag } else { - // TEMPORARY REMOVAL: We don't currently pass in the RC to Kokoro, so ignore the missing flag. - // Check if we have other release related flags. If so, fail since we need an RC number. -// guard currentReleasePath == nil else { -// LaunchArgs.exitWithUsageAndLog("Invalid combination of keys: \(Key.rc) must be passed " + -// "in when specifiying \(Key.releasingSDKs).") -// } - + print("Did not parse a release candidate version number.") rcNumber = nil } @@ -232,17 +221,21 @@ struct LaunchArgs { carthageDir = nil } - updatePodRepo = defaults.bool(forKey: Key.updatePodRepo.rawValue) - - // Parse the cache keys. If no value is provided for each, it defaults to `false`. - cacheEnabled = defaults.bool(forKey: Key.cacheEnabled.rawValue) - deleteCache = defaults.bool(forKey: Key.deleteCache.rawValue) + // Parse the Carthage directory key. + if let buildRoot = defaults.string(forKey: Key.buildRoot.rawValue) { + let url = URL(fileURLWithPath: buildRoot) + guard fileChecker.directoryExists(at: url) else { + LaunchArgs.exitWithUsageAndLog("Could not parse \(Key.buildRoot) key: value " + + "passed in is not a file URL or the directory does not exist. Value: \(buildRoot)") + } - if deleteCache, cacheEnabled { - LaunchArgs.exitWithUsageAndLog("Invalid pair - attempted to delete the cache and enable " + - "it at the same time. Please remove on of the keys and try " + - "again.") + self.buildRoot = url.standardizedFileURL + } else { + // No argument was passed in. + buildRoot = nil } + + updatePodRepo = defaults.bool(forKey: Key.updatePodRepo.rawValue) } /// Prints an error that occurred, the proper usage String, and quits the application. diff --git a/ZipBuilder/Sources/ZipBuilder/ZipBuilder.swift b/ZipBuilder/Sources/ZipBuilder/ZipBuilder.swift index b4903b9135b..458a369be3e 100644 --- a/ZipBuilder/Sources/ZipBuilder/ZipBuilder.swift +++ b/ZipBuilder/Sources/ZipBuilder/ZipBuilder.swift @@ -16,6 +16,8 @@ import Foundation +import ManifestReader + /// Misc. constants used in the build tool. public struct Constants { /// Constants related to the Xcode project template. @@ -115,22 +117,15 @@ struct ZipBuilder { /// Paths needed throughout the process of packaging the Zip file. private let paths: FilesystemPaths - /// Determines if the cache should be used or not. - private let useCache: Bool - /// Default initializer. If allSDKsPath and currentReleasePath are provided, it will also verify /// that the /// /// - Parameters: /// - paths: Paths that are needed throughout the process of packaging the Zip file. /// - customSpecRepo: A custom spec repo to be used for fetching CocoaPods from. - /// - useCache: Enables or disables the cache. - init(paths: FilesystemPaths, - customSpecRepos: [URL]? = nil, - useCache: Bool = false) { + init(paths: FilesystemPaths, customSpecRepos: [URL]? = nil) { self.paths = paths self.customSpecRepos = customSpecRepos - self.useCache = useCache } // TODO: This function contains a lot of "copy these paths to this directory, fail if there are @@ -183,8 +178,8 @@ struct ZipBuilder { fatalError("Could not get contents of the README template: \(error)") } - // Break the `podsToInstall` into a variable since it's helpful when debugging non-cache builds - // to just install a subset of pods, like the following line: + // Break the `podsToInstall` into a variable since it's helpful when debugging builds to just + // install a subset of pods, like the following line: // let podsToInstall: [CocoaPod] = [.core, .analytics, .storage] let podsToInstall = CocoaPod.allCases @@ -219,9 +214,7 @@ struct ZipBuilder { // Generate the frameworks. Each key is the pod name and the URLs are all frameworks to be // copied in each product's directory. - let frameworks = generateFrameworks(fromPods: installedPods, - inProjectDir: projectDir, - useCache: useCache) + let frameworks = generateFrameworks(fromPods: installedPods, inProjectDir: projectDir) for (framework, paths) in frameworks { print("Frameworks for pod: \(framework) were compiled at \(paths)") @@ -683,7 +676,6 @@ struct ZipBuilder { /// .framework file already). private func generateFrameworks(fromPods pods: [CocoaPodUtils.PodInfo], inProjectDir projectDir: URL, - useCache: Bool = false, carthageBuild: Bool = false) -> [String: [URL]] { // Verify the Pods folder exists and we can get the contents of it. let fileManager = FileManager.default @@ -727,11 +719,9 @@ struct ZipBuilder { // If there are no frameworks, it's an open source pod and we need to compile the source to // get a framework. if foundFrameworks.isEmpty { - let builder = FrameworkBuilder(projectDir: projectDir) + let builder = FrameworkBuilder(projectDir: projectDir, carthageBuild: carthageBuild) let framework = builder.buildFramework(withName: pod.name, version: pod.version, - cacheKey: pod.cacheKey, - cacheEnabled: useCache, logsOutputDir: paths.logsOutputDir) frameworks = [framework] diff --git a/ZipBuilder/Sources/ZipBuilder/main.swift b/ZipBuilder/Sources/ZipBuilder/main.swift index d2713c0f4eb..fc21bb92702 100644 --- a/ZipBuilder/Sources/ZipBuilder/main.swift +++ b/ZipBuilder/Sources/ZipBuilder/main.swift @@ -16,19 +16,17 @@ import Foundation -// Get the launch arguments, parsed by user defaults. -let args = LaunchArgs() - -// Clear the cache if requested. -if args.deleteCache { - do { - let cacheDir = try FileManager.default.firebaseCacheDirectory() - try FileManager.default.removeItem(at: cacheDir) - } catch { - fatalError("Could not empty the cache before building the zip file: \(error)") - } +// Delete the cache directory, if it exists. +do { + let cacheDir = try FileManager.default.firebaseCacheDirectory() + FileManager.default.removeDirectoryIfExists(at: cacheDir) +} catch { + fatalError("Could not remove the cache before packaging the release: \(error)") } +// Get the launch arguments, parsed by user defaults. +let args = LaunchArgs.shared + // Keep timing for how long it takes to build the zip file for information purposes. let buildStart = Date() var cocoaPodsUpdateMessage: String = "" @@ -43,9 +41,7 @@ var paths = ZipBuilder.FilesystemPaths(templateDir: args.templateDir) paths.allSDKsPath = args.allSDKsPath paths.currentReleasePath = args.currentReleasePath paths.logsOutputDir = args.outputDir?.appendingPathComponent("build_logs") -let builder = ZipBuilder(paths: paths, - customSpecRepos: args.customSpecRepos, - useCache: args.cacheEnabled) +let builder = ZipBuilder(paths: paths, customSpecRepos: args.customSpecRepos) do { // Build the zip file and get the path. @@ -73,6 +69,8 @@ do { var output = carthageDir.appendingPathComponent(firebaseVersion) if let rcNumber = args.rcNumber { output.appendPathComponent("rc\(rcNumber)") + } else { + output.appendPathComponent("latest-non-rc") } try fileManager.createDirectory(at: output, withIntermediateDirectories: true) CarthageUtils.generateCarthageRelease(fromPackagedDir: carthagePath, @@ -89,7 +87,12 @@ do { // Save the directory for later copying. carthageRoot = carthageDir } catch { - fatalError("Could not copy output directory for Carthage build: \(error)") + // TODO: This can fail on CI due to size requirements, let's fail gracefully in the meantime + // and not block the rest of the build. + // fatalError("Could not copy output directory for Carthage build: \(error)") + print("--------- CARTHAGE ERROR ---------") + print("Could not copy output directory for Carthage build: \(error)") + print("------- END CARTHAGE ERROR -------") } } @@ -107,15 +110,7 @@ do { // Move all the bundles in the frameworks out to a common "Resources" directory to match the // existing Zip structure. let resourcesDir = fullPath.appendingPathComponent("Resources") - let bundles = try ResourcesManager.moveAllBundles(inDirectory: fullPath, to: resourcesDir) - - // Remove any extra bundles that were packaged, if possible, by using the folder name and - // getting the CocoaPod selected. - if let pod = CocoaPod(rawValue: fileOrFolder) { - let duplicateResources = pod.duplicateResourcesToRemove() - let toRemove = bundles.filter { duplicateResources.contains($0.lastPathComponent) } - try toRemove.forEach(fileManager.removeItem(at:)) - } + _ = try ResourcesManager.moveAllBundles(inDirectory: fullPath, to: resourcesDir) } } @@ -123,6 +118,8 @@ do { var candidateName = "Firebase-\(firebaseVersion)" if let rcNumber = args.rcNumber { candidateName += "-rc\(rcNumber)" + } else { + candidateName += "-latest-manual" } candidateName += ".zip" let zipped = Zip.zipContents(ofDir: location, name: candidateName) diff --git a/ZipBuilder/Template/NOTICES b/ZipBuilder/Template/NOTICES index ad93dba4eae..06d4bd233d9 100644 --- a/ZipBuilder/Template/NOTICES +++ b/ZipBuilder/Template/NOTICES @@ -1,37 +1,11 @@ -Google LevelDB -Copyright (c) 2011 The LevelDB Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation andor other materials provided with the distribution. - -Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - --- - -Square Socket Rocket -Copyright 2012 Square Inc. - -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. - --- - APLevelDB -Created by Adam Preble on 12312. +Created by Adam Preble on 1/23/12. Copyright (c) 2012 Adam Preble. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, andor sell +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: @@ -46,19 +20,24 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + Portions of APLevelDB are based on LevelDB-ObjC: -https://github.com/hoisie/LevelDB-ObjC + https:github.com/hoisie/LevelDB-ObjC Specifically the SliceFromString/StringFromSlice macros, and the structure of the enumeration methods. License for those potions follows: + Copyright (c) 2011 Pave Labs + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -67,309 +46,9988 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- +Abseil -sqlite3 -2001 September 15 + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ -The author disclaims copyright to this source code. In place of -a legal notice, here is a blessing: + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - May you do good and not evil. - May you find forgiveness for yourself and forgive others. - May you share freely, never taking more than you give. + 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -This header file defines the interface that the SQLite library -presents to client programs. If a C-function, structure, datatype, -or constant definition does not appear in this file, then it is -not a published API of SQLite, is subject to change without -notice, and should not be referenced by programs that use SQLite. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. -Some of the definitions that are in this file are marked as -"experimental". Experimental interfaces are normally new -features recently added to SQLite. We do not anticipate changes -to experimental interfaces but reserve the right to make minor changes -if experience from use "in the wild" suggest such changes are prudent. + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. -The official C-language API documentation for SQLite is derived -from comments in this file. This file is the authoritative source -on how SQLite interfaces are suppose to operate. + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. -The name of this file under configuration management is "sqlite.h.in". -The makefile makes some minor changes to this file (such as inserting -the version number) and changes its name to "sqlite3.h" as -part of the build process. + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. --- + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. -sysutsname.h -Copyright (c) 2000 Apple Computer, Inc. All rights reserved. + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). -This file contains Original Code andor Modifications of Original Code -as defined in and that are subject to the Apple Public Source License -Version 2.0 (the 'License'). You may not use this file except in -compliance with the License. The rights granted to you under the License -may not be used to create, or enable the creation or redistribution of, -unlawful or unlicensed copies of an Apple operating system, or to -circumvent, violate, or enable the circumvention or violation of, any -terms of an Apple operating system software license agreement. + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. -Please obtain a copy of the License at -http://www.opensource.apple.com/apsl and read it before using this file. + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." -The Original Code and all software distributed under the License are -distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER -EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, -INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. -Please see the License for the specific language governing rights and -limitations under the License. + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. -Copyright 1993,1995 NeXT Computer Inc. All Rights Reserved -Copyright (c) 1994 The Regents of the University of California. All rights reserved. -This code is derived from software contributed to Berkeley by -Chuck Karish of Mindcraft, Inc. -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation andor other materials provided with the distribution. -3. All advertising materials mentioning features or use of this software - must display the following acknowledgement: -* This product includes software developed by the University of -* California, Berkeley and its contributors. -4. Neither the name of the University nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS -OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF -SUCH DAMAGE. + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. --- + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. -GTMNSData+zlib.h -Copyright 2007-2008 Google Inc. + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: -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 + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and -http://www.apache.org/licenses/LICENSE-2.0 + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and -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. + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and --- + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. -GTMDefines.h -Copyright 2008 Google Inc. + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. -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 + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. -http://www.apache.org/licenses/LICENSE-2.0 + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. -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. + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. --- + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. -ProtocolBuffer -Copyright 2008 Cyrus Najmabadi -Copyright 2011 Google Inc. + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. -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 + END OF TERMS AND CONDITIONS - http://www.apache.org/licenses/LICENSE-2.0 + APPENDIX: How to apply the Apache License to your work. -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. + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. --- + Copyright [yyyy] [name of copyright owner] -GTMDefines.h -Copyright 2008 Google Inc. + 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 -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 + https://www.apache.org/licenses/LICENSE-2.0 -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. -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. --- +BoringSSL +BoringSSL is a fork of OpenSSL. As such, large parts of it fall under OpenSSL +licensing. Files that are completely new have a Google copyright and an ISC +license. This license is reproduced at the bottom of this file. -fbase64.c +Contributors to BoringSSL are required to follow the CLA rules for Chromium: +https://cla.developers.google.com/clas -Copyright (c) 1996 by Internet Software Consortium. -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS -ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES -OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE -CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL -DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR -PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS -ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +Files in third_party/ have their own licenses, as described therein. The MIT +license, for third_party/fiat, which, unlike other third_party directories, is +compiled into non-test libraries, is included below. + +The OpenSSL toolkit stays under a dual license, i.e. both the conditions of the +OpenSSL License and the original SSLeay license apply to the toolkit. See below +for the actual license texts. Actually both licenses are BSD-style Open Source +licenses. In case of any license issues related to OpenSSL please contact +openssl-core@openssl.org. + +The following are Google-internal bug numbers where explicit permission from +some authors is recorded for use of their work. (This is purely for our own +record keeping.) + 27287199 + 27287880 + 27287883 + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2011 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + + +ISC license used for completely new code in BoringSSL: + +/* Copyright (c) 2015, Google Inc. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ + + +The code in third_party/fiat carries the MIT license: + +Copyright (c) 2015-2016 the fiat-crypto authors (see +https://github.com/mit-plv/fiat-crypto/blob/master/AUTHORS). + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Portions Copyright (c) 1995 by International Business Machines, Inc. -International Business Machines, Inc. (hereinafter called IBM) grants -permission under its copyrights to use, copy, modify, and distribute this -Software with or without fee, provided that the above copyright notice and -all paragraphs of this notice appear in all copies, and that the name of IBM -not be used in connection with the marketing of any product incorporating -the Software or modifications thereof, without specific, written prior -permission. -To the extent it has a right to do so, IBM grants an immunity from suit -under its patents, if any, for the use, sale or manufacture of products to -the extent that such products are used for performing Domain Name System -dynamic updates in TCP/IP networks by means of the Software. No immunity is -granted for any product per se or for any other function of any product. -THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES, -INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL, -DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING -OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN -IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES. -OPENBSD ORIGINAL: lib/libc/net/base64.c */ +The code in third_party/sike also carries the MIT license: --- +Copyright (c) Microsoft Corporation. All rights reserved. -FIRAppEnvironmentUtil.m +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The following copyright from Landon J. Fuller applies to the isAppEncrypted function. -Copyright (c) 2017 Landon J. Fuller -All rights reserved. -Permission is hereby granted, free of charge, to any person obtaining a copy of this software -and associated documentation files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or -substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING -BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -Comment from iPhone Dev Wiki -Crack Prevention: -App Store binaries are signed by both their developer and Apple. This encrypts the binary so -that decryption keys are needed in order to make the binary readable. When iOS executes the -binary, the decryption keys are used to decrypt the binary into a readable state where it is -then loaded into memory and executed. iOS can tell the encryption status of a binary via the -cryptid structure member of LC_ENCRYPTION_INFO MachO load command. If cryptid is a non-zero -value then the binary is encrypted. -'Cracking' works by letting the kernel decrypt the binary then siphoning the decrypted data into -a new binary file, resigning, and repackaging. This will only work on jailbroken devices as -codesignature validation has been removed. Resigning takes place because while the codesignature -doesn't have to be valid thanks to the jailbreak, it does have to be in place unless you have -AppSync or similar to disable codesignature checks. -More information at Landon Fuller's blog +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. --- +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE -tflite -Copyright 2017 The TensorFlow Authors. All Rights Reserved. +Licenses for support code +------------------------- -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 +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: - http://www.apache.org/licenses/LICENSE-2.0 +Copyright (c) 2009 The Go Authors. All rights reserved. -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. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: --- + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. -FirebaseCore +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -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 +BoringSSL uses the Chromium test infrastructure to run a continuous build, +trybots etc. The scripts which manage this, and the script for generating build +metadata, are under the Chromium license. Distributing code linked against +BoringSSL does not trigger this license. - http://www.apache.org/licenses/LICENSE-2.0 +Copyright 2015 The Chromium Authors. All rights reserved. -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. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: --- + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. -google_api_objectivec_client_for_rest +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -Copyright 2011 Google Inc. +Brotli +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. -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 +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: - http://www.apache.org/licenses/LICENSE-2.0 +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -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. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. --- +Chrome Certificate Verifier +// Copyright 2015 The Chromium Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -ocmock +Closure Compiler + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Copyright (c) 2006-2016 Erik Doernenburg and contributors + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -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 + 1. Definitions. - http://www.apache.org/licenses/LICENSE-2.0 + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -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. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +Closure Library + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + +Crunchy Crypt + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +Darts-clone +# The BSD 2-clause license + +Copyright (c) 2008-2014, Susumu Yata +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Dimsum + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +Drishti +Copyright 2019 The Drishti Authors. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017, The Drishti Authors. + + 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. + +DrishtiOSS + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +Edge TPU + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Google LLC + + 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. + +Eigen 3 +Eigen 3.3.90 +The corresponding source for this library is available at +https://eigen.googlesource.com/mirror/ + +Eigen is primarily MPL2 licensed. See COPYING.MPL2 and these links: + http://www.mozilla.org/MPL/2.0/ + http://www.mozilla.org/MPL/2.0/FAQ.html + +Some files contain third-party code under BSD, whence +the other COPYING.* files here. + +If you want to guarantee that the Eigen code that you are #including +is licensed under the MPL2 and possibly more permissive licenses (like +BSD), #define this preprocessor symbol: EIGEN_MPL2_ONLY +For example, with most compilers, you could add this to your project + CXXFLAGS: -DEIGEN_MPL2_ONLY +This will cause a compilation error to be generated if you #include +any code that is covered by more restrictive licences than MPL2. + +---------------------------------------------------------------------- +Following applies to: +./test/sparseqr.cpp +./test/half_float.cpp +./test/zerosized.cpp +./test/nesting_ops.cpp +./test/sizeoverflow.cpp +./test/swap.cpp +./test/product_mmtr.cpp +./test/stdvector_overload.cpp +./test/product_symm.cpp +./test/sparse_block.cpp +./test/eigen2support.cpp +./test/upperbidiagonalization.cpp +./test/numext.cpp +./test/adjoint.cpp +./test/AnnoyingScalar.h +./test/mpl2only.cpp +./test/stddeque.cpp +./test/householder.cpp +./test/product_small.cpp +./test/product_syrk.cpp +./test/inplace_decomposition.cpp +./test/vectorwiseop.cpp +./test/meta.cpp +./test/stdvector.cpp +./test/sparseLM.cpp +./test/diagonalmatrices.cpp +./test/stdlist_overload.cpp +./test/block.cpp +./test/cholmod_support.cpp +./test/basicstuff.cpp +./test/triangular.cpp +./test/product.h +./test/vectorization_logic.cpp +./test/dontalign.cpp +./test/first_aligned.cpp +./test/mapped_matrix.cpp +./test/umfpack_support.cpp +./test/product_selfadjoint.cpp +./test/smallvectors.cpp +./test/corners.cpp +./test/product_trsolve.cpp +./test/determinant.cpp +./test/stdlist.cpp +./test/unalignedcount.cpp +./test/qr.cpp +./test/svd_common.h +./test/ref.cpp +./test/symbolic_index.cpp +./test/geo_transformations.cpp +./test/geo_eulerangles.cpp +./test/eigensolver_selfadjoint.cpp +./test/stddeque_overload.cpp +./test/jacobisvd.cpp +./test/nullary.cpp +./test/inverse.cpp +./test/integer_types.cpp +./test/metis_support.cpp +./test/exceptions.cpp +./test/packetmath.cpp +./test/schur_complex.cpp +./test/type_alias.cpp +./test/unalignedassert.cpp +./test/geo_quaternion.cpp +./test/lu.cpp +./test/qr_fullpivoting.cpp +./test/denseLM.cpp +./test/linearstructure.cpp +./test/rand.cpp +./test/conservative_resize.cpp +./test/eigensolver_generalized_real.cpp +./test/pastix_support.cpp +./test/sparse_solver.h +./test/num_dimensions.cpp +./test/simplicial_cholesky.cpp +./test/hessenberg.cpp +./test/array_reverse.cpp +./test/special_numbers.cpp +./test/array_for_matrix.cpp +./test/product_large.cpp +./test/resize.cpp +./test/sparse_solvers.cpp +./test/selfadjoint.cpp +./test/schur_real.cpp +./test/sparse_basic.cpp +./test/conjugate_gradient.cpp +./test/real_qz.cpp +./test/bandmatrix.cpp +./test/dense_storage.cpp +./test/permutationmatrices.cpp +./test/array_cwise.cpp +./test/qr_colpivoting.cpp +./test/array_replicate.cpp +./test/rvalue_types.cpp +./test/stable_norm.cpp +./test/geo_homogeneous.cpp +./test/main.h +./test/eigensolver_complex.cpp +./test/product_trmm.cpp +./test/bicgstab.cpp +./test/redux.cpp +./test/klu_support.cpp +./test/geo_alignedbox.cpp +./test/is_same_dense.cpp +./test/sparse_permutations.cpp +./test/sparse_vector.cpp +./test/diagonal.cpp +./test/sparse.h +./test/mapstride.cpp +./test/visitor.cpp +./test/geo_hyperplane.cpp +./test/bdcsvd.cpp +./test/product_trmv.cpp +./test/nestbyvalue.cpp +./test/array_of_string.cpp +./test/superlu_support.cpp +./test/sizeof.cpp +./test/boostmultiprec.cpp +./test/commainitializer.cpp +./test/constructor.cpp +./test/mixingtypes.cpp +./test/miscmatrices.cpp +./test/mapstaticmethods.cpp +./test/product_notemporary.cpp +./test/initializer_list_construction.cpp +./test/incomplete_cholesky.cpp +./test/geo_parametrizedline.cpp +./test/indexed_view.cpp +./test/qtvector.cpp +./test/sparselu.cpp +./test/sparse_product.cpp +./test/dynalloc.cpp +./test/fastmath.cpp +./test/prec_inverse_4x4.cpp +./test/umeyama.cpp +./test/reshape.cpp +./test/product_extra.cpp +./test/jacobi.cpp +./test/sparse_ref.cpp +./test/nomalloc.cpp +./test/spqr_support.cpp +./test/lscg.cpp +./test/cholesky.cpp +./test/eigensolver_generic.cpp +./test/geo_orthomethods.cpp +./test/svd_fill.h +./test/stl_iterators.cpp +./Eigen/src/MetisSupport/MetisSupport.h +./Eigen/src/CholmodSupport/CholmodSupport.h +./Eigen/src/QR/CompleteOrthogonalDecomposition.h +./Eigen/src/QR/FullPivHouseholderQR.h +./Eigen/src/QR/HouseholderQR.h +./Eigen/src/QR/ColPivHouseholderQR.h +./Eigen/src/plugins/CommonCwiseUnaryOps.h +./Eigen/src/plugins/BlockMethods.h +./Eigen/src/plugins/CommonCwiseBinaryOps.h +./Eigen/src/plugins/MatrixCwiseUnaryOps.h +./Eigen/src/plugins/IndexedViewMethods.h +./Eigen/src/plugins/MatrixCwiseBinaryOps.h +./Eigen/src/SVD/UpperBidiagonalization.h +./Eigen/src/SVD/SVDBase.h +./Eigen/src/SVD/BDCSVD.h +./Eigen/src/SVD/JacobiSVD.h +./Eigen/src/SparseLU/SparseLU_relax_snode.h +./Eigen/src/SparseLU/SparseLU_column_dfs.h +./Eigen/src/SparseLU/SparseLU_SupernodalMatrix.h +./Eigen/src/SparseLU/SparseLU_pivotL.h +./Eigen/src/SparseLU/SparseLU.h +./Eigen/src/SparseLU/SparseLU_pruneL.h +./Eigen/src/SparseLU/SparseLU_copy_to_ucol.h +./Eigen/src/SparseLU/SparseLU_heap_relax_snode.h +./Eigen/src/SparseLU/SparseLU_kernel_bmod.h +./Eigen/src/SparseLU/SparseLU_panel_dfs.h +./Eigen/src/SparseLU/SparseLU_panel_bmod.h +./Eigen/src/SparseLU/SparseLU_Structs.h +./Eigen/src/SparseLU/SparseLUImpl.h +./Eigen/src/SparseLU/SparseLU_Memory.h +./Eigen/src/SparseLU/SparseLU_column_bmod.h +./Eigen/src/SparseLU/SparseLU_gemm_kernel.h +./Eigen/src/SparseLU/SparseLU_Utils.h +./Eigen/src/OrderingMethods/Eigen_Colamd.h +./Eigen/src/OrderingMethods/Ordering.h +./Eigen/src/OrderingMethods/Amd.h +./Eigen/src/UmfPackSupport/UmfPackSupport.h +./Eigen/src/Geometry/Umeyama.h +./Eigen/src/Geometry/Transform.h +./Eigen/src/Geometry/OrthoMethods.h +./Eigen/src/Geometry/Hyperplane.h +./Eigen/src/Geometry/Homogeneous.h +./Eigen/src/Geometry/RotationBase.h +./Eigen/src/Geometry/EulerAngles.h +./Eigen/src/Geometry/Translation.h +./Eigen/src/Geometry/Rotation2D.h +./Eigen/src/Geometry/Scaling.h +./Eigen/src/Geometry/AlignedBox.h +./Eigen/src/Geometry/ParametrizedLine.h +./Eigen/src/Geometry/Quaternion.h +./Eigen/src/Geometry/AngleAxis.h +./Eigen/src/Geometry/arch/Geometry_SSE.h +./Eigen/src/KLUSupport/KLUSupport.h +./Eigen/src/misc/Kernel.h +./Eigen/src/misc/RealSvd2x2.h +./Eigen/src/misc/Image.h +./Eigen/src/StlSupport/details.h +./Eigen/src/StlSupport/StdList.h +./Eigen/src/StlSupport/StdDeque.h +./Eigen/src/StlSupport/StdVector.h +./Eigen/src/SparseQR/SparseQR.h +./Eigen/src/SuperLUSupport/SuperLUSupport.h +./Eigen/src/Householder/Householder.h +./Eigen/src/Householder/HouseholderSequence.h +./Eigen/src/Householder/BlockHouseholder.h +./Eigen/src/Eigenvalues/SelfAdjointEigenSolver.h +./Eigen/src/Eigenvalues/EigenSolver.h +./Eigen/src/Eigenvalues/GeneralizedEigenSolver.h +./Eigen/src/Eigenvalues/Tridiagonalization.h +./Eigen/src/Eigenvalues/HessenbergDecomposition.h +./Eigen/src/Eigenvalues/RealQZ.h +./Eigen/src/Eigenvalues/RealSchur.h +./Eigen/src/Eigenvalues/ComplexSchur.h +./Eigen/src/Eigenvalues/ComplexEigenSolver.h +./Eigen/src/Eigenvalues/MatrixBaseEigenvalues.h +./Eigen/src/Eigenvalues/GeneralizedSelfAdjointEigenSolver.h +./Eigen/src/SparseCholesky/SimplicialCholesky.h +./Eigen/src/SparseCholesky/SimplicialCholesky_impl.h +./Eigen/src/Cholesky/LLT.h +./Eigen/src/Cholesky/LDLT.h +./Eigen/src/Jacobi/Jacobi.h +./Eigen/src/PaStiXSupport/PaStiXSupport.h +./Eigen/src/SPQRSupport/SuiteSparseQRSupport.h +./Eigen/src/LU/Determinant.h +./Eigen/src/LU/InverseImpl.h +./Eigen/src/LU/PartialPivLU.h +./Eigen/src/LU/arch/Inverse_SSE.h +./Eigen/src/LU/FullPivLU.h +./Eigen/src/Core/Map.h +./Eigen/src/Core/VectorwiseOp.h +./Eigen/src/Core/VectorBlock.h +./Eigen/src/Core/Array.h +./Eigen/src/Core/Assign.h +./Eigen/src/Core/Dot.h +./Eigen/src/Core/NestByValue.h +./Eigen/src/Core/CoreEvaluators.h +./Eigen/src/Core/ReturnByValue.h +./Eigen/src/Core/SelfCwiseBinaryOp.h +./Eigen/src/Core/GlobalFunctions.h +./Eigen/src/Core/Transpositions.h +./Eigen/src/Core/Fuzzy.h +./Eigen/src/Core/NoAlias.h +./Eigen/src/Core/CwiseNullaryOp.h +./Eigen/src/Core/NumTraits.h +./Eigen/src/Core/IndexedView.h +./Eigen/src/Core/ArrayWrapper.h +./Eigen/src/Core/util/SymbolicIndex.h +./Eigen/src/Core/util/BlasUtil.h +./Eigen/src/Core/util/Constants.h +./Eigen/src/Core/util/IntegralConstant.h +./Eigen/src/Core/util/ReshapedHelper.h +./Eigen/src/Core/util/StaticAssert.h +./Eigen/src/Core/util/IndexedViewHelper.h +./Eigen/src/Core/util/ConfigureVectorization.h +./Eigen/src/Core/util/ForwardDeclarations.h +./Eigen/src/Core/util/Meta.h +./Eigen/src/Core/util/XprHelper.h +./Eigen/src/Core/util/Macros.h +./Eigen/src/Core/util/Memory.h +./Eigen/src/Core/Product.h +./Eigen/src/Core/Replicate.h +./Eigen/src/Core/ArrayBase.h +./Eigen/src/Core/functors/NullaryFunctors.h +./Eigen/src/Core/functors/StlFunctors.h +./Eigen/src/Core/functors/AssignmentFunctors.h +./Eigen/src/Core/functors/UnaryFunctors.h +./Eigen/src/Core/functors/TernaryFunctors.h +./Eigen/src/Core/functors/BinaryFunctors.h +./Eigen/src/Core/Redux.h +./Eigen/src/Core/EigenBase.h +./Eigen/src/Core/SolverBase.h +./Eigen/src/Core/ProductEvaluators.h +./Eigen/src/Core/Block.h +./Eigen/src/Core/SolveTriangular.h +./Eigen/src/Core/ArithmeticSequence.h +./Eigen/src/Core/MatrixBase.h +./Eigen/src/Core/PlainObjectBase.h +./Eigen/src/Core/Transpose.h +./Eigen/src/Core/IO.h +./Eigen/src/Core/MathFunctions.h +./Eigen/src/Core/Stride.h +./Eigen/src/Core/MathFunctionsImpl.h +./Eigen/src/Core/StableNorm.h +./Eigen/src/Core/DiagonalProduct.h +./Eigen/src/Core/products/GeneralMatrixMatrix.h +./Eigen/src/Core/products/GeneralMatrixVector.h +./Eigen/src/Core/products/SelfadjointMatrixVector.h +./Eigen/src/Core/products/GeneralBlockPanelKernel.h +./Eigen/src/Core/products/TriangularSolverMatrix.h +./Eigen/src/Core/products/SelfadjointMatrixMatrix.h +./Eigen/src/Core/products/Parallelizer.h +./Eigen/src/Core/products/SelfadjointRank2Update.h +./Eigen/src/Core/products/TriangularMatrixMatrix.h +./Eigen/src/Core/products/TriangularMatrixVector.h +./Eigen/src/Core/products/SelfadjointProduct.h +./Eigen/src/Core/products/GeneralMatrixMatrixTriangular.h +./Eigen/src/Core/products/TriangularSolverVector.h +./Eigen/src/Core/CwiseUnaryView.h +./Eigen/src/Core/CommaInitializer.h +./Eigen/src/Core/DenseStorage.h +./Eigen/src/Core/DenseBase.h +./Eigen/src/Core/PartialReduxEvaluator.h +./Eigen/src/Core/CoreIterators.h +./Eigen/src/Core/PermutationMatrix.h +./Eigen/src/Core/CwiseTernaryOp.h +./Eigen/src/Core/Reverse.h +./Eigen/src/Core/Reshaped.h +./Eigen/src/Core/Inverse.h +./Eigen/src/Core/TriangularMatrix.h +./Eigen/src/Core/BooleanRedux.h +./Eigen/src/Core/ForceAlignedAccess.h +./Eigen/src/Core/Ref.h +./Eigen/src/Core/StlIterators.h +./Eigen/src/Core/BandMatrix.h +./Eigen/src/Core/ConditionEstimator.h +./Eigen/src/Core/Diagonal.h +./Eigen/src/Core/DiagonalMatrix.h +./Eigen/src/Core/AssignEvaluator.h +./Eigen/src/Core/CwiseBinaryOp.h +./Eigen/src/Core/Visitor.h +./Eigen/src/Core/GenericPacketMath.h +./Eigen/src/Core/SelfAdjointView.h +./Eigen/src/Core/Random.h +./Eigen/src/Core/Solve.h +./Eigen/src/Core/arch/AltiVec/MathFunctions.h +./Eigen/src/Core/arch/AltiVec/PacketMath.h +./Eigen/src/Core/arch/AltiVec/Complex.h +./Eigen/src/Core/arch/MSA/MathFunctions.h +./Eigen/src/Core/arch/MSA/Complex.h +./Eigen/src/Core/arch/MSA/PacketMath.h +./Eigen/src/Core/arch/GPU/Half.h +./Eigen/src/Core/arch/GPU/PacketMathHalf.h +./Eigen/src/Core/arch/GPU/MathFunctions.h +./Eigen/src/Core/arch/GPU/PacketMath.h +./Eigen/src/Core/arch/GPU/TypeCasting.h +./Eigen/src/Core/arch/NEON/MathFunctions.h +./Eigen/src/Core/arch/NEON/Complex.h +./Eigen/src/Core/arch/NEON/PacketMath.h +./Eigen/src/Core/arch/NEON/TypeCasting.h +./Eigen/src/Core/arch/AVX/MathFunctions.h +./Eigen/src/Core/arch/AVX/TypeCasting.h +./Eigen/src/Core/arch/AVX/Complex.h +./Eigen/src/Core/arch/AVX/PacketMath.h +./Eigen/src/Core/arch/SYCL/InteropHeaders.h +./Eigen/src/Core/arch/SYCL/PacketMath.h +./Eigen/src/Core/arch/SYCL/TypeCasting.h +./Eigen/src/Core/arch/SYCL/MathFunctions.h +./Eigen/src/Core/arch/Default/GenericPacketMathFunctions.h +./Eigen/src/Core/arch/Default/ConjHelper.h +./Eigen/src/Core/arch/Default/Settings.h +./Eigen/src/Core/arch/AVX512/MathFunctions.h +./Eigen/src/Core/arch/AVX512/PacketMath.h +./Eigen/src/Core/arch/AVX512/Complex.h +./Eigen/src/Core/arch/SSE/PacketMath.h +./Eigen/src/Core/arch/SSE/Complex.h +./Eigen/src/Core/arch/SSE/TypeCasting.h +./Eigen/src/Core/arch/SSE/MathFunctions.h +./Eigen/src/Core/arch/ZVector/MathFunctions.h +./Eigen/src/Core/arch/ZVector/PacketMath.h +./Eigen/src/Core/arch/ZVector/Complex.h +./Eigen/src/Core/arch/CUDA/Complex.h +./Eigen/src/Core/Swap.h +./Eigen/src/Core/MapBase.h +./Eigen/src/Core/GeneralProduct.h +./Eigen/src/Core/Matrix.h +./Eigen/src/Core/Select.h +./Eigen/src/Core/CwiseUnaryOp.h +./Eigen/src/Core/DenseCoeffsBase.h +./Eigen/src/SparseCore/SparseCwiseUnaryOp.h +./Eigen/src/SparseCore/TriangularSolver.h +./Eigen/src/SparseCore/SparseView.h +./Eigen/src/SparseCore/SparseSolverBase.h +./Eigen/src/SparseCore/SparseTranspose.h +./Eigen/src/SparseCore/SparseDenseProduct.h +./Eigen/src/SparseCore/SparseMap.h +./Eigen/src/SparseCore/SparseProduct.h +./Eigen/src/SparseCore/SparseUtil.h +./Eigen/src/SparseCore/SparsePermutation.h +./Eigen/src/SparseCore/SparseTriangularView.h +./Eigen/src/SparseCore/SparseSelfAdjointView.h +./Eigen/src/SparseCore/SparseMatrixBase.h +./Eigen/src/SparseCore/AmbiVector.h +./Eigen/src/SparseCore/SparseAssign.h +./Eigen/src/SparseCore/SparseRedux.h +./Eigen/src/SparseCore/SparseDot.h +./Eigen/src/SparseCore/SparseCwiseBinaryOp.h +./Eigen/src/SparseCore/SparseCompressedBase.h +./Eigen/src/SparseCore/SparseSparseProductWithPruning.h +./Eigen/src/SparseCore/SparseColEtree.h +./Eigen/src/SparseCore/SparseRef.h +./Eigen/src/SparseCore/CompressedStorage.h +./Eigen/src/SparseCore/MappedSparseMatrix.h +./Eigen/src/SparseCore/SparseDiagonalProduct.h +./Eigen/src/SparseCore/SparseFuzzy.h +./Eigen/src/SparseCore/ConservativeSparseSparseProduct.h +./Eigen/src/SparseCore/SparseMatrix.h +./Eigen/src/SparseCore/SparseVector.h +./Eigen/src/SparseCore/SparseBlock.h +./Eigen/src/IterativeLinearSolvers/SolveWithGuess.h +./Eigen/src/IterativeLinearSolvers/IterativeSolverBase.h +./Eigen/src/IterativeLinearSolvers/BiCGSTAB.h +./Eigen/src/IterativeLinearSolvers/ConjugateGradient.h +./Eigen/src/IterativeLinearSolvers/BasicPreconditioners.h +./Eigen/src/IterativeLinearSolvers/IncompleteCholesky.h +./Eigen/src/IterativeLinearSolvers/IncompleteLUT.h +./Eigen/src/IterativeLinearSolvers/LeastSquareConjugateGradient.h +./unsupported/Eigen/src/Eigenvalues/ArpackSelfAdjointEigenSolver.h +./unsupported/Eigen/src/SpecialFunctions/arch/GPU/GpuSpecialFunctions.h +./unsupported/Eigen/src/SpecialFunctions/SpecialFunctionsHalf.h +./unsupported/Eigen/src/SpecialFunctions/SpecialFunctionsImpl.h +./unsupported/Eigen/src/SpecialFunctions/SpecialFunctionsFunctors.h +./unsupported/Eigen/src/SpecialFunctions/SpecialFunctionsArrayAPI.h +./unsupported/Eigen/src/SpecialFunctions/SpecialFunctionsPacketMath.h +./unsupported/Eigen/src/Polynomials/Companion.h +./unsupported/Eigen/src/Polynomials/PolynomialUtils.h +./unsupported/Eigen/src/Polynomials/PolynomialSolver.h +./unsupported/Eigen/src/Splines/Spline.h +./unsupported/Eigen/src/Splines/SplineFwd.h +./unsupported/Eigen/src/Splines/SplineFitting.h +./unsupported/Eigen/src/BVH/KdBVH.h +./unsupported/Eigen/src/BVH/BVAlgorithms.h +./unsupported/Eigen/src/AutoDiff/AutoDiffJacobian.h +./unsupported/Eigen/src/AutoDiff/AutoDiffVector.h +./unsupported/Eigen/src/AutoDiff/AutoDiffScalar.h +./unsupported/Eigen/src/MatrixFunctions/MatrixSquareRoot.h +./unsupported/Eigen/src/MatrixFunctions/MatrixPower.h +./unsupported/Eigen/src/MatrixFunctions/MatrixExponential.h +./unsupported/Eigen/src/MatrixFunctions/MatrixLogarithm.h +./unsupported/Eigen/src/MatrixFunctions/StemFunction.h +./unsupported/Eigen/src/MatrixFunctions/MatrixFunction.h +./unsupported/Eigen/src/Skyline/SkylineStorage.h +./unsupported/Eigen/src/Skyline/SkylineMatrixBase.h +./unsupported/Eigen/src/Skyline/SkylineMatrix.h +./unsupported/Eigen/src/Skyline/SkylineInplaceLU.h +./unsupported/Eigen/src/Skyline/SkylineProduct.h +./unsupported/Eigen/src/Skyline/SkylineUtil.h +./unsupported/Eigen/src/FFT/ei_kissfft_impl.h +./unsupported/Eigen/src/FFT/ei_fftw_impl.h +./unsupported/Eigen/src/LevenbergMarquardt/LevenbergMarquardt.h +./unsupported/Eigen/src/NonLinearOptimization/HybridNonLinearSolver.h +./unsupported/Eigen/src/NonLinearOptimization/LevenbergMarquardt.h +./unsupported/Eigen/src/KroneckerProduct/KroneckerTensorProduct.h +./unsupported/Eigen/src/NumericalDiff/NumericalDiff.h +./unsupported/Eigen/src/IterativeSolvers/IncompleteLU.h +./unsupported/Eigen/src/IterativeSolvers/MINRES.h +./unsupported/Eigen/src/IterativeSolvers/DGMRES.h +./unsupported/Eigen/src/IterativeSolvers/Scaling.h +./unsupported/Eigen/src/IterativeSolvers/GMRES.h +./unsupported/Eigen/src/MoreVectorization/MathFunctions.h +./unsupported/Eigen/src/EulerAngles/EulerAngles.h +./unsupported/Eigen/src/EulerAngles/EulerSystem.h +./unsupported/Eigen/src/SparseExtra/BlockOfDynamicSparseMatrix.h +./unsupported/Eigen/src/SparseExtra/DynamicSparseMatrix.h +./unsupported/Eigen/src/SparseExtra/BlockSparseMatrix.h +./unsupported/Eigen/src/SparseExtra/RandomSetter.h +./unsupported/Eigen/src/SparseExtra/MatrixMarketIterator.h +./unsupported/Eigen/src/SparseExtra/MarketIO.h +./unsupported/Eigen/CXX11/src/TensorSymmetry/StaticSymmetry.h +./unsupported/Eigen/CXX11/src/TensorSymmetry/Symmetry.h +./unsupported/Eigen/CXX11/src/TensorSymmetry/DynamicSymmetry.h +./unsupported/Eigen/CXX11/src/TensorSymmetry/util/TemplateGroupTheory.h +./unsupported/Eigen/CXX11/src/util/EmulateCXX11Meta.h +./unsupported/Eigen/CXX11/src/util/CXX11Meta.h +./unsupported/Eigen/CXX11/src/util/MaxSizeVector.h +./unsupported/Eigen/CXX11/src/util/EmulateArray.h +./unsupported/Eigen/CXX11/src/util/CXX11Workarounds.h +./unsupported/Eigen/CXX11/src/ThreadPool/ThreadYield.h +./unsupported/Eigen/CXX11/src/ThreadPool/NonBlockingThreadPool.h +./unsupported/Eigen/CXX11/src/ThreadPool/RunQueue.h +./unsupported/Eigen/CXX11/src/ThreadPool/ThreadCancel.h +./unsupported/Eigen/CXX11/src/ThreadPool/ThreadPoolInterface.h +./unsupported/Eigen/CXX11/src/ThreadPool/ThreadLocal.h +./unsupported/Eigen/CXX11/src/ThreadPool/Barrier.h +./unsupported/Eigen/CXX11/src/ThreadPool/EventCount.h +./unsupported/Eigen/CXX11/src/ThreadPool/ThreadEnvironment.h +./unsupported/Eigen/CXX11/src/Tensor/TensorRef.h +./unsupported/Eigen/CXX11/src/Tensor/TensorFixedSize.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSyclRun.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSyclTuple.h +./unsupported/Eigen/CXX11/src/Tensor/TensorTraits.h +./unsupported/Eigen/CXX11/src/Tensor/TensorStorage.h +./unsupported/Eigen/CXX11/src/Tensor/TensorTrace.h +./unsupported/Eigen/CXX11/src/Tensor/TensorDeviceThreadPool.h +./unsupported/Eigen/CXX11/src/Tensor/TensorReductionGpu.h +./unsupported/Eigen/CXX11/src/Tensor/TensorContractionThreadPool.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSyclPlaceHolderExpr.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSyclExprConstructor.h +./unsupported/Eigen/CXX11/src/Tensor/TensorIntDiv.h +./unsupported/Eigen/CXX11/src/Tensor/TensorExecutor.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSyclConvertToDeviceExpression.h +./unsupported/Eigen/CXX11/src/Tensor/Tensor.h +./unsupported/Eigen/CXX11/src/Tensor/TensorDeviceGpu.h +./unsupported/Eigen/CXX11/src/Tensor/TensorPatch.h +./unsupported/Eigen/CXX11/src/Tensor/TensorMorphing.h +./unsupported/Eigen/CXX11/src/Tensor/TensorInflation.h +./unsupported/Eigen/CXX11/src/Tensor/TensorStriding.h +./unsupported/Eigen/CXX11/src/Tensor/TensorScan.h +./unsupported/Eigen/CXX11/src/Tensor/TensorChipping.h +./unsupported/Eigen/CXX11/src/Tensor/TensorCustomOp.h +./unsupported/Eigen/CXX11/src/Tensor/TensorDeviceSycl.h +./unsupported/Eigen/CXX11/src/Tensor/TensorGenerator.h +./unsupported/Eigen/CXX11/src/Tensor/TensorReductionSycl.h +./unsupported/Eigen/CXX11/src/Tensor/TensorArgMaxSycl.h +./unsupported/Eigen/CXX11/src/Tensor/TensorConvolution.h +./unsupported/Eigen/CXX11/src/Tensor/TensorBase.h +./unsupported/Eigen/CXX11/src/Tensor/TensorDimensions.h +./unsupported/Eigen/CXX11/src/Tensor/TensorReduction.h +./unsupported/Eigen/CXX11/src/Tensor/TensorPadding.h +./unsupported/Eigen/CXX11/src/Tensor/TensorUInt128.h +./unsupported/Eigen/CXX11/src/Tensor/TensorArgMax.h +./unsupported/Eigen/CXX11/src/Tensor/TensorMeta.h +./unsupported/Eigen/CXX11/src/Tensor/TensorExpr.h +./unsupported/Eigen/CXX11/src/Tensor/TensorIO.h +./unsupported/Eigen/CXX11/src/Tensor/TensorContraction.h +./unsupported/Eigen/CXX11/src/Tensor/TensorDeviceDefault.h +./unsupported/Eigen/CXX11/src/Tensor/TensorReverse.h +./unsupported/Eigen/CXX11/src/Tensor/TensorShuffling.h +./unsupported/Eigen/CXX11/src/Tensor/TensorConvolutionSycl.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSyclFunctors.h +./unsupported/Eigen/CXX11/src/Tensor/TensorMap.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSycl.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSyclExtractFunctors.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSyclExtractAccessor.h +./unsupported/Eigen/CXX11/src/Tensor/TensorEvaluator.h +./unsupported/Eigen/CXX11/src/Tensor/TensorConcatenation.h +./unsupported/Eigen/CXX11/src/Tensor/TensorGpuHipCudaDefines.h +./unsupported/Eigen/CXX11/src/Tensor/TensorInitializer.h +./unsupported/Eigen/CXX11/src/Tensor/TensorBlock.h +./unsupported/Eigen/CXX11/src/Tensor/TensorIndexList.h +./unsupported/Eigen/CXX11/src/Tensor/TensorGpuHipCudaUndefines.h +./unsupported/Eigen/CXX11/src/Tensor/TensorContractionMapper.h +./unsupported/Eigen/CXX11/src/Tensor/TensorCostModel.h +./unsupported/Eigen/CXX11/src/Tensor/TensorForcedEval.h +./unsupported/Eigen/CXX11/src/Tensor/TensorGlobalFunctions.h +./unsupported/Eigen/CXX11/src/Tensor/TensorContractionSycl.h +./unsupported/Eigen/CXX11/src/Tensor/TensorImagePatch.h +./unsupported/Eigen/CXX11/src/Tensor/TensorContractionBlocking.h +./unsupported/Eigen/CXX11/src/Tensor/TensorMacros.h +./unsupported/Eigen/CXX11/src/Tensor/TensorDevice.h +./unsupported/Eigen/CXX11/src/Tensor/TensorBroadcasting.h +./unsupported/Eigen/CXX11/src/Tensor/TensorSyclLeafCount.h +./unsupported/Eigen/CXX11/src/Tensor/TensorRandom.h +./unsupported/Eigen/CXX11/src/Tensor/TensorFFT.h +./unsupported/Eigen/CXX11/src/Tensor/TensorFunctors.h +./unsupported/Eigen/CXX11/src/Tensor/TensorContractionGpu.h +./unsupported/Eigen/CXX11/src/Tensor/TensorForwardDeclarations.h +./unsupported/Eigen/CXX11/src/Tensor/TensorDimensionList.h +./unsupported/Eigen/CXX11/src/Tensor/TensorConversion.h +./unsupported/Eigen/CXX11/src/Tensor/TensorEvalTo.h +./unsupported/Eigen/CXX11/src/Tensor/TensorAssign.h +./unsupported/Eigen/CXX11/src/Tensor/TensorLayoutSwap.h +./unsupported/Eigen/CXX11/src/FixedPoint/MatMatProduct.h +./unsupported/Eigen/CXX11/src/FixedPoint/MatMatProductNEON.h +./unsupported/Eigen/CXX11/src/FixedPoint/MatVecProduct.h +./unsupported/Eigen/CXX11/src/FixedPoint/FixedPointTypes.h +./unsupported/Eigen/CXX11/src/FixedPoint/MatMatProductAVX2.h +./unsupported/bench/bench_svd.cpp +./unsupported/test/cxx11_tensor_image_patch_sycl.cpp +./unsupported/test/cxx11_tensor_expr.cpp +./unsupported/test/FFTW.cpp +./unsupported/test/cxx11_tensor_reverse_sycl.cpp +./unsupported/test/cxx11_tensor_comparisons.cpp +./unsupported/test/cxx11_tensor_intdiv.cpp +./unsupported/test/autodiff.cpp +./unsupported/test/cxx11_tensor_executor.cpp +./unsupported/test/cxx11_tensor_reduction.cpp +./unsupported/test/cxx11_tensor_device_sycl.cpp +./unsupported/test/minres.cpp +./unsupported/test/cxx11_tensor_striding.cpp +./unsupported/test/cxx11_tensor_chipping.cpp +./unsupported/test/cxx11_tensor_convolution_sycl.cpp +./unsupported/test/openglsupport.cpp +./unsupported/test/cxx11_tensor_ifft.cpp +./unsupported/test/polynomialutils.cpp +./unsupported/test/cxx11_tensor_block_access.cpp +./unsupported/test/cxx11_tensor_morphing.cpp +./unsupported/test/cxx11_tensor_casts.cpp +./unsupported/test/cxx11_tensor_shuffling_sycl.cpp +./unsupported/test/cxx11_tensor_morphing_sycl.cpp +./unsupported/test/forward_adolc.cpp +./unsupported/test/cxx11_tensor_layout_swap.cpp +./unsupported/test/cxx11_tensor_move.cpp +./unsupported/test/EulerAngles.cpp +./unsupported/test/cxx11_tensor_trace.cpp +./unsupported/test/alignedvector3.cpp +./unsupported/test/cxx11_tensor_lvalue.cpp +./unsupported/test/cxx11_tensor_argmax.cpp +./unsupported/test/cxx11_tensor_broadcast_sycl.cpp +./unsupported/test/autodiff_scalar.cpp +./unsupported/test/sparse_extra.cpp +./unsupported/test/cxx11_tensor_of_strings.cpp +./unsupported/test/cxx11_tensor_empty.cpp +./unsupported/test/cxx11_tensor_patch.cpp +./unsupported/test/cxx11_tensor_sycl.cpp +./unsupported/test/cxx11_tensor_forced_eval_sycl.cpp +./unsupported/test/cxx11_tensor_inflation_sycl.cpp +./unsupported/test/BVH.cpp +./unsupported/test/cxx11_tensor_generator.cpp +./unsupported/test/cxx11_meta.cpp +./unsupported/test/matrix_functions.h +./unsupported/test/kronecker_product.cpp +./unsupported/test/matrix_function.cpp +./unsupported/test/cxx11_tensor_thread_pool.cpp +./unsupported/test/cxx11_non_blocking_thread_pool.cpp +./unsupported/test/cxx11_tensor_fft.cpp +./unsupported/test/cxx11_tensor_assign.cpp +./unsupported/test/cxx11_tensor_simple.cpp +./unsupported/test/cxx11_tensor_of_complex.cpp +./unsupported/test/cxx11_tensor_inflation.cpp +./unsupported/test/cxx11_tensor_map.cpp +./unsupported/test/cxx11_tensor_shuffling.cpp +./unsupported/test/cxx11_tensor_padding.cpp +./unsupported/test/cxx11_tensor_argmax_sycl.cpp +./unsupported/test/matrix_square_root.cpp +./unsupported/test/dgmres.cpp +./unsupported/test/cxx11_tensor_custom_op_sycl.cpp +./unsupported/test/cxx11_tensor_reduction_sycl.cpp +./unsupported/test/cxx11_runqueue.cpp +./unsupported/test/cxx11_tensor_const.cpp +./unsupported/test/matrix_power.cpp +./unsupported/test/cxx11_tensor_contraction.cpp +./unsupported/test/cxx11_tensor_random.cpp +./unsupported/test/cxx11_tensor_volume_patch_sycl.cpp +./unsupported/test/cxx11_tensor_contract_sycl.cpp +./unsupported/test/cxx11_tensor_math.cpp +./unsupported/test/splines.cpp +./unsupported/test/cxx11_tensor_ref.cpp +./unsupported/test/cxx11_tensor_concatenation_sycl.cpp +./unsupported/test/gmres.cpp +./unsupported/test/cxx11_tensor_fixed_size.cpp +./unsupported/test/cxx11_tensor_custom_op.cpp +./unsupported/test/cxx11_tensor_generator_sycl.cpp +./unsupported/test/cxx11_tensor_uint128.cpp +./unsupported/test/cxx11_tensor_builtins_sycl.cpp +./unsupported/test/polynomialsolver.cpp +./unsupported/test/cxx11_tensor_concatenation.cpp +./unsupported/test/cxx11_tensor_broadcasting.cpp +./unsupported/test/cxx11_tensor_convolution.cpp +./unsupported/test/cxx11_tensor_forced_eval.cpp +./unsupported/test/levenberg_marquardt.cpp +./unsupported/test/cxx11_tensor_reverse.cpp +./unsupported/test/cxx11_tensor_notification.cpp +./unsupported/test/cxx11_tensor_patch_sycl.cpp +./unsupported/test/cxx11_tensor_image_patch.cpp +./unsupported/test/cxx11_tensor_scan.cpp +./unsupported/test/cxx11_tensor_padding_sycl.cpp +./unsupported/test/cxx11_tensor_index_list.cpp +./unsupported/test/cxx11_tensor_io.cpp +./unsupported/test/cxx11_tensor_mixed_indices.cpp +./unsupported/test/cxx11_tensor_striding_sycl.cpp +./unsupported/test/cxx11_tensor_of_const_values.cpp +./unsupported/test/cxx11_tensor_symmetry.cpp +./unsupported/test/cxx11_tensor_custom_index.cpp +./unsupported/test/cxx11_tensor_chipping_sycl.cpp +./unsupported/test/cxx11_tensor_roundings.cpp +./unsupported/test/matrix_exponential.cpp +./unsupported/test/cxx11_eventcount.cpp +./unsupported/test/special_functions.cpp +./unsupported/test/cxx11_tensor_dimension.cpp +./unsupported/test/cxx11_tensor_layout_swap_sycl.cpp +./lapack/eigenvalues.cpp +./lapack/single.cpp +./lapack/svd.cpp +./lapack/complex_single.cpp +./lapack/lu.cpp +./lapack/double.cpp +./lapack/complex_double.cpp +./lapack/cholesky.cpp +./lapack/lapack_common.h +./blas/level2_impl.h +./blas/PackedTriangularMatrixVector.h +./blas/level3_impl.h +./blas/complex_double.cpp +./blas/common.h +./blas/GeneralRank1Update.h +./blas/double.cpp +./blas/complex_single.cpp +./blas/Rank2Update.h +./blas/level1_impl.h +./blas/level2_real_impl.h +./blas/level1_real_impl.h +./blas/single.cpp +./blas/PackedSelfadjointProduct.h +./blas/BandTriangularSolver.h +./blas/level2_cplx_impl.h +./blas/PackedTriangularSolverVector.h +./blas/level1_cplx_impl.h +./bench/analyze-blocking-sizes.cpp +./bench/BenchTimer.h +./bench/spbench/spbenchsolver.h +./bench/spbench/spbenchstyle.h +./bench/benchFFT.cpp +./bench/eig33.cpp +./bench/benchmark-blocking-sizes.cpp +./demos/opengl/quaternion_demo.cpp +./demos/opengl/camera.h +./demos/opengl/gpuhelper.cpp +./demos/opengl/gpuhelper.h +./demos/opengl/icosphere.cpp +./demos/opengl/quaternion_demo.h +./demos/opengl/trackball.h +./demos/opengl/icosphere.h +./demos/opengl/camera.cpp +./demos/opengl/trackball.cpp +./demos/mix_eigen_and_c/binary_library.h +./demos/mix_eigen_and_c/binary_library.cpp +./demos/mandelbrot/mandelbrot.cpp +./demos/mandelbrot/mandelbrot.h + +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. + +---------------------------------------------------------------------- +Following applies to: +./doc/UsingIntelMKL.dox +./doc/UsingIntelMKL.dox +./Eigen/src/Eigenvalues/ComplexSchur_MKL.h +./Eigen/src/Eigenvalues/ComplexSchur_MKL.h +./Eigen/src/Eigenvalues/SelfAdjointEigenSolver_MKL.h +./Eigen/src/Eigenvalues/SelfAdjointEigenSolver_MKL.h +./Eigen/src/Eigenvalues/RealSchur_MKL.h +./Eigen/src/Eigenvalues/RealSchur_MKL.h +./Eigen/src/LU/arch/Inverse_SSE.h +./Eigen/src/LU/arch/Inverse_SSE.h +./Eigen/src/LU/PartialPivLU_MKL.h +./Eigen/src/LU/PartialPivLU_MKL.h +./Eigen/src/QR/HouseholderQR_MKL.h +./Eigen/src/QR/HouseholderQR_MKL.h +./Eigen/src/QR/ColPivHouseholderQR_MKL.h +./Eigen/src/QR/ColPivHouseholderQR_MKL.h +./Eigen/src/SVD/JacobiSVD_MKL.h +./Eigen/src/SVD/JacobiSVD_MKL.h +./Eigen/src/PardisoSupport/PardisoSupport.h +./Eigen/src/PardisoSupport/PardisoSupport.h +./Eigen/src/Core/Assign_MKL.h +./Eigen/src/Core/Assign_MKL.h +./Eigen/src/Core/products/SelfadjointMatrixVector_MKL.h +./Eigen/src/Core/products/SelfadjointMatrixVector_MKL.h +./Eigen/src/Core/products/GeneralMatrixVector_MKL.h +./Eigen/src/Core/products/GeneralMatrixVector_MKL.h +./Eigen/src/Core/products/SelfadjointMatrixMatrix_MKL.h +./Eigen/src/Core/products/SelfadjointMatrixMatrix_MKL.h +./Eigen/src/Core/products/TriangularMatrixMatrix_MKL.h +./Eigen/src/Core/products/TriangularMatrixMatrix_MKL.h +./Eigen/src/Core/products/GeneralMatrixMatrix_MKL.h +./Eigen/src/Core/products/GeneralMatrixMatrix_MKL.h +./Eigen/src/Core/products/TriangularMatrixVector_MKL.h +./Eigen/src/Core/products/TriangularMatrixVector_MKL.h +./Eigen/src/Core/products/GeneralMatrixMatrixTriangular_MKL.h +./Eigen/src/Core/products/GeneralMatrixMatrixTriangular_MKL.h +./Eigen/src/Core/products/TriangularSolverMatrix_MKL.h +./Eigen/src/Core/products/TriangularSolverMatrix_MKL.h +./Eigen/src/Core/util/MKL_support.h +./Eigen/src/Core/util/MKL_support.h +./Eigen/src/Cholesky/LLT_MKL.h +./Eigen/src/Cholesky/LLT_MKL.h + +/* + Copyright (c) 2011, Intel Corporation. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. * + Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. * Neither the name of Intel Corporation nor the + names of its contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +---------------------------------------------------------------------- +Following applies to: +./unsupported/Eigen/src/LevenbergMarquardt/LevenbergMarquardt.h +./unsupported/Eigen/src/LevenbergMarquardt/LMcovar.h +./unsupported/Eigen/src/LevenbergMarquardt/LMonestep.h +./unsupported/Eigen/src/LevenbergMarquardt/LMpar.h +./unsupported/Eigen/src/LevenbergMarquardt/LMqrsolv.h + +Minpack Copyright Notice (1999) University of Chicago. All rights +reserved + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the +following conditions are met: + +1. Redistributions of source code must retain the above +copyright notice, this list of conditions and the following +disclaimer. + +2. Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials +provided with the distribution. + +3. The end-user documentation included with the +redistribution, if any, must include the following +acknowledgment: + + "This product includes software developed by the + University of Chicago, as Operator of Argonne National + Laboratory. + +Alternately, this acknowledgment may appear in the software +itself, if and wherever such third-party acknowledgments +normally appear. + +4. WARRANTY DISCLAIMER. THE SOFTWARE IS SUPPLIED "AS IS" +WITHOUT WARRANTY OF ANY KIND. THE COPYRIGHT HOLDER, THE +UNITED STATES, THE UNITED STATES DEPARTMENT OF ENERGY, AND +THEIR EMPLOYEES: (1) DISCLAIM ANY WARRANTIES, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO ANY IMPLIED WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE +OR NON-INFRINGEMENT, (2) DO NOT ASSUME ANY LEGAL LIABILITY +OR RESPONSIBILITY FOR THE ACCURACY, COMPLETENESS, OR +USEFULNESS OF THE SOFTWARE, (3) DO NOT REPRESENT THAT USE OF +THE SOFTWARE WOULD NOT INFRINGE PRIVATELY OWNED RIGHTS, (4) +DO NOT WARRANT THAT THE SOFTWARE WILL FUNCTION +UNINTERRUPTED, THAT IT IS ERROR-FREE OR THAT ANY ERRORS WILL +BE CORRECTED. + +5. LIMITATION OF LIABILITY. IN NO EVENT WILL THE COPYRIGHT +HOLDER, THE UNITED STATES, THE UNITED STATES DEPARTMENT OF +ENERGY, OR THEIR EMPLOYEES: BE LIABLE FOR ANY INDIRECT, +INCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES OF +ANY KIND OR NATURE, INCLUDING BUT NOT LIMITED TO LOSS OF +PROFITS OR LOSS OF DATA, FOR ANY REASON WHATSOEVER, WHETHER +SUCH LIABILITY IS ASSERTED ON THE BASIS OF CONTRACT, TORT +(INCLUDING NEGLIGENCE OR STRICT LIABILITY), OR OTHERWISE, +EVEN IF ANY OF SAID PARTIES HAS BEEN WARNED OF THE +POSSIBILITY OF SUCH LOSS OR DAMAGES. + + +Copyright (c) 1992-2013 The University of Tennessee and The University + of Tennessee Research Foundation. All rights + reserved. +Copyright (c) 2000-2013 The University of California Berkeley. All + rights reserved. +Copyright (c) 2006-2013 The University of Colorado Denver. All rights + reserved. + +Following applies to: +./lapack/*.c + +$COPYRIGHT$ + +Additional copyrights may follow + +$HEADER$ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer listed + in this license in the documentation and/or other materials + provided with the distribution. + +- Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +The copyright holders provide no reassurances that the source code +provided does not infringe any patent, copyright, or any other +intellectual property rights of third parties. The copyright holders +disclaim any liability to any recipient for claims brought against +recipient by any third party for infringement of that parties +intellectual property rights. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +FImmutableSortedDictionary +Copyright (c) 2012 Mads Hartmann Jensen + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +FlatBuffers + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2014 Google Inc. + + 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. + +GTMHTTPServer.m +Based a little on HTTPServer, part of the CocoaHTTPServer sample code found at +https://opensource.apple.com/source/HTTPServer/HTTPServer-11/CocoaHTTPServer/ +License for the CocoaHTTPServer sample code: + +Software License Agreement (BSD License) + +Copyright (c) 2011, Deusty, LLC +All rights reserved. + +Redistribution and use of this software in source and binary forms, +with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above +copyright notice, this list of conditions and the +following disclaimer. + +* Neither the name of Deusty nor the names of its +contributors may be used to endorse or promote products +derived from this software without specific prior +written permission of Deusty, LLC. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +GULAppEnvironmentUtil.m +The following copyright from Landon J. Fuller applies to the isAppEncrypted function. + +Copyright (c) 2017 Landon J. Fuller +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Comment from iPhone Dev Wiki +Crack Prevention: +App Store binaries are signed by both their developer and Apple. This encrypts the binary so +that decryption keys are needed in order to make the binary readable. When iOS executes the +binary, the decryption keys are used to decrypt the binary into a readable state where it is +then loaded into memory and executed. iOS can tell the encryption status of a binary via the +cryptid structure member of LC_ENCRYPTION_INFO MachO load command. If cryptid is a non-zero +value then the binary is encrypted. + +'Cracking' works by letting the kernel decrypt the binary then siphoning the decrypted data into +a new binary file, resigning, and repackaging. This will only work on jailbroken devices as +codesignature validation has been removed. Resigning takes place because while the codesignature +doesn't have to be valid thanks to the jailbreak, it does have to be in place unless you have +AppSync or similar to disable codesignature checks. + +More information at Landon Fuller's blog + +Google APIs Client Library for Objective-C for REST + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +Google Runtime Environment + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations +below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. +^L + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it +becomes a de-facto standard. To achieve this, non-free programs must +be allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. +^L + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control +compilation and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. +^L + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. +^L + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least + three years, to give the same user the materials specified in + Subsection 6a, above, for a charge no more than the cost of + performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. +^L + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. +^L + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply, and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License +may add an explicit geographical distribution limitation excluding those +countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. +^L + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS +^L + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms +of the ordinary General Public License). + + To apply these terms, attach the following notices to the library. +It is safest to attach them to the start of each source file to most +effectively convey the exclusion of warranty; and each file should +have at least the "copyright" line and a pointer to where the full +notice is found. + + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or +your school, if any, to sign a "copyright disclaimer" for the library, +if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James + Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + + +Google Toolbox for Mac + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +Google Utilities + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. +Halide +Copyright (c) 2012 MIT CSAIL + +Developed by: + + The Halide team + MIT CSAIL + http://halide-lang.org + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +HighwayHash + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + + +ICU4C +COPYRIGHT AND PERMISSION NOTICE (ICU 58 and later) + +Copyright © 1991-2019 Unicode, Inc. All rights reserved. +Distributed under the Terms of Use in https://www.unicode.org/copyright.html. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Unicode data files and any associated documentation +(the "Data Files") or Unicode software and any associated documentation +(the "Software") to deal in the Data Files or Software +without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, and/or sell copies of +the Data Files or Software, and to permit persons to whom the Data Files +or Software are furnished to do so, provided that either +(a) this copyright and permission notice appear with all copies +of the Data Files or Software, or +(b) this copyright and permission notice appear in associated +Documentation. + +THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS. +IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS +NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THE DATA FILES OR SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, +use or other dealings in these Data Files or Software without prior +written authorization of the copyright holder. + +--------------------- + +Third-Party Software Licenses + +This section contains third-party software notices and/or additional +terms for licensed third-party software components included within ICU +libraries. + +1. ICU License - ICU 1.8.1 to ICU 57.1 + +COPYRIGHT AND PERMISSION NOTICE + +Copyright (c) 1995-2016 International Business Machines Corporation and others +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, provided that the above +copyright notice(s) and this permission notice appear in all copies of +the Software and that both the above copyright notice(s) and this +permission notice appear in supporting documentation. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY +SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +Except as contained in this notice, the name of a copyright holder +shall not be used in advertising or otherwise to promote the sale, use +or other dealings in this Software without prior written authorization +of the copyright holder. + +All trademarks and registered trademarks mentioned herein are the +property of their respective owners. + +2. Chinese/Japanese Word Break Dictionary Data (cjdict.txt) + + # The Google Chrome software developed by Google is licensed under + # the BSD license. Other software included in this distribution is + # provided under other licenses, as set forth below. + # + # The BSD License + # http://opensource.org/licenses/bsd-license.php + # Copyright (C) 2006-2008, Google Inc. + # + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions are met: + # + # Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # Redistributions in binary form must reproduce the above + # copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided with + # the distribution. + # Neither the name of Google Inc. nor the names of its + # contributors may be used to endorse or promote products derived from + # this software without specific prior written permission. + # + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + # + # + # The word list in cjdict.txt are generated by combining three word lists + # listed below with further processing for compound word breaking. The + # frequency is generated with an iterative training against Google web + # corpora. + # + # * Libtabe (Chinese) + # - https://sourceforge.net/project/?group_id=1519 + # - Its license terms and conditions are shown below. + # + # * IPADIC (Japanese) + # - http://chasen.aist-nara.ac.jp/chasen/distribution.html + # - Its license terms and conditions are shown below. + # + # ---------COPYING.libtabe ---- BEGIN-------------------- + # + # /* + # * Copyright (c) 1999 TaBE Project. + # * Copyright (c) 1999 Pai-Hsiang Hsiao. + # * All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the TaBE Project nor the names of its + # * contributors may be used to endorse or promote products derived + # * from this software without specific prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # /* + # * Copyright (c) 1999 Computer Systems and Communication Lab, + # * Institute of Information Science, Academia + # * Sinica. All rights reserved. + # * + # * Redistribution and use in source and binary forms, with or without + # * modification, are permitted provided that the following conditions + # * are met: + # * + # * . Redistributions of source code must retain the above copyright + # * notice, this list of conditions and the following disclaimer. + # * . Redistributions in binary form must reproduce the above copyright + # * notice, this list of conditions and the following disclaimer in + # * the documentation and/or other materials provided with the + # * distribution. + # * . Neither the name of the Computer Systems and Communication Lab + # * nor the names of its contributors may be used to endorse or + # * promote products derived from this software without specific + # * prior written permission. + # * + # * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # * REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + # * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # * OF THE POSSIBILITY OF SUCH DAMAGE. + # */ + # + # Copyright 1996 Chih-Hao Tsai @ Beckman Institute, + # University of Illinois + # c-tsai4@uiuc.edu http://casper.beckman.uiuc.edu/~c-tsai4 + # + # ---------------COPYING.libtabe-----END-------------------------------- + # + # + # ---------------COPYING.ipadic-----BEGIN------------------------------- + # + # Copyright 2000, 2001, 2002, 2003 Nara Institute of Science + # and Technology. All Rights Reserved. + # + # Use, reproduction, and distribution of this software is permitted. + # Any copy of this software, whether in its original form or modified, + # must include both the above copyright notice and the following + # paragraphs. + # + # Nara Institute of Science and Technology (NAIST), + # the copyright holders, disclaims all warranties with regard to this + # software, including all implied warranties of merchantability and + # fitness, in no event shall NAIST be liable for + # any special, indirect or consequential damages or any damages + # whatsoever resulting from loss of use, data or profits, whether in an + # action of contract, negligence or other tortuous action, arising out + # of or in connection with the use or performance of this software. + # + # A large portion of the dictionary entries + # originate from ICOT Free Software. The following conditions for ICOT + # Free Software applies to the current dictionary as well. + # + # Each User may also freely distribute the Program, whether in its + # original form or modified, to any third party or parties, PROVIDED + # that the provisions of Section 3 ("NO WARRANTY") will ALWAYS appear + # on, or be attached to, the Program, which is distributed substantially + # in the same form as set out herein and that such intended + # distribution, if actually made, will neither violate or otherwise + # contravene any of the laws and regulations of the countries having + # jurisdiction over the User or the intended distribution itself. + # + # NO WARRANTY + # + # The program was produced on an experimental basis in the course of the + # research and development conducted during the project and is provided + # to users as so produced on an experimental basis. Accordingly, the + # program is provided without any warranty whatsoever, whether express, + # implied, statutory or otherwise. The term "warranty" used herein + # includes, but is not limited to, any warranty of the quality, + # performance, merchantability and fitness for a particular purpose of + # the program and the nonexistence of any infringement or violation of + # any right of any third party. + # + # Each user of the program will agree and understand, and be deemed to + # have agreed and understood, that there is no warranty whatsoever for + # the program and, accordingly, the entire risk arising from or + # otherwise connected with the program is assumed by the user. + # + # Therefore, neither ICOT, the copyright holder, or any other + # organization that participated in or was otherwise related to the + # development of the program and their respective officials, directors, + # officers and other employees shall be held liable for any and all + # damages, including, without limitation, general, special, incidental + # and consequential damages, arising out of or otherwise in connection + # with the use or inability to use the program or any product, material + # or result produced or otherwise obtained by using the program, + # regardless of whether they have been advised of, or otherwise had + # knowledge of, the possibility of such damages at any time during the + # project or thereafter. Each user will be deemed to have agreed to the + # foregoing by his or her commencement of use of the program. The term + # "use" as used herein includes, but is not limited to, the use, + # modification, copying and distribution of the program and the + # production of secondary products from the program. + # + # In the case where the program, whether in its original form or + # modified, was distributed or delivered to or received by a user from + # any person, organization or entity other than ICOT, unless it makes or + # grants independently of ICOT any specific warranty to the user in + # writing, such person, organization or entity, will also be exempted + # from and not be held liable to the user for any such damages as noted + # above as far as the program is concerned. + # + # ---------------COPYING.ipadic-----END---------------------------------- + +3. Lao Word Break Dictionary Data (laodict.txt) + + # Copyright (c) 2013 International Business Machines Corporation + # and others. All Rights Reserved. + # + # Project: http://code.google.com/p/lao-dictionary/ + # Dictionary: http://lao-dictionary.googlecode.com/git/Lao-Dictionary.txt + # License: http://lao-dictionary.googlecode.com/git/Lao-Dictionary-LICENSE.txt + # (copied below) + # + # This file is derived from the above dictionary, with slight + # modifications. + # ---------------------------------------------------------------------- + # Copyright (C) 2013 Brian Eugene Wilson, Robert Martin Campbell. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, + # are permitted provided that the following conditions are met: + # + # + # Redistributions of source code must retain the above copyright notice, this + # list of conditions and the following disclaimer. Redistributions in + # binary form must reproduce the above copyright notice, this list of + # conditions and the following disclaimer in the documentation and/or + # other materials provided with the distribution. + # + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + # OF THE POSSIBILITY OF SUCH DAMAGE. + # -------------------------------------------------------------------------- + +4. Burmese Word Break Dictionary Data (burmesedict.txt) + + # Copyright (c) 2014 International Business Machines Corporation + # and others. All Rights Reserved. + # + # This list is part of a project hosted at: + # github.com/kanyawtech/myanmar-karen-word-lists + # + # -------------------------------------------------------------------------- + # Copyright (c) 2013, LeRoy Benjamin Sharon + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: Redistributions of source code must retain the above + # copyright notice, this list of conditions and the following + # disclaimer. Redistributions in binary form must reproduce the + # above copyright notice, this list of conditions and the following + # disclaimer in the documentation and/or other materials provided + # with the distribution. + # + # Neither the name Myanmar Karen Word Lists, nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + # CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS + # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED + # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + # TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + # THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + # SUCH DAMAGE. + # -------------------------------------------------------------------------- + +5. Time Zone Database + + ICU uses the public domain data and code derived from Time Zone +Database for its time zone support. The ownership of the TZ database +is explained in BCP 175: Procedure for Maintaining the Time Zone +Database section 7. + + # 7. Database Ownership + # + # The TZ database itself is not an IETF Contribution or an IETF + # document. Rather it is a pre-existing and regularly updated work + # that is in the public domain, and is intended to remain in the + # public domain. Therefore, BCPs 78 [RFC5378] and 79 [RFC3979] do + # not apply to the TZ Database or contributions that individuals make + # to it. Should any claims be made and substantiated against the TZ + # Database, the organization that is providing the IANA + # Considerations defined in this RFC, under the memorandum of + # understanding with the IETF, currently ICANN, may act in accordance + # with all competent court orders. No ownership claims will be made + # by ICANN or the IETF Trust on the database or the code. Any person + # making a contribution to the database or code waives all rights to + # future claims in that contribution or in the TZ Database. + +6. Google double-conversion + +Copyright 2006-2011, the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Immutable +Copyright (c) 2012 Mads Hartmann Jensen + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Khronos OpenGL headers +OpenGL Licenses + +Component Location Primary Author License +---------------------------------------------------------------------------- +standard headers gl/ The Khronos Group, MIT, SGI, and + Brian Paul, Apache 2.0 + Silicon Graphics + +EGL utilities util/ Google Apache 2.0 + +------------------------------------- + +Copyright The Khronos Group Inc. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and/or associated documentation files (the +"Materials"), to deal in the Materials without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Materials, and to +permit persons to whom the Materials are furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Materials. + +THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS. + + +SGI FREE SOFTWARE LICENSE B +(Version 2.0, Sept. 18, 2008) + +Copyright (C) [dates of first publication] Silicon Graphics, Inc. +All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice including the dates of first publication and either +this permission notice or a reference to http://oss.sgi.com/projects/FreeB/ +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL SILICON +GRAPHICS, INC. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of Silicon Graphics, Inc. shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization from Silicon +Graphics, Inc. + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +LAPACK +Copyright (c) 1992-2011 The University of Tennessee and The University + of Tennessee Research Foundation. All rights + reserved. +Copyright (c) 2000-2011 The University of California Berkeley. All + rights reserved. +Copyright (c) 2006-2012 The University of Colorado Denver. All rights + reserved. + +$COPYRIGHT$ + +Additional copyrights may follow + +$HEADER$ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer listed + in this license in the documentation and/or other materials + provided with the distribution. + +- Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +The copyright holders provide no reassurances that the source code +provided does not infringe any patent, copyright, or any other +intellectual property rights of third parties. The copyright holders +disclaim any liability to any recipient for claims brought against +recipient by any third party for infringement of that parties +intellectual property rights. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Leptonica +/*====================================================================* + - Copyright (C) 2001 Leptonica. All rights reserved. + - + - Redistribution and use in source and binary forms, with or without + - modification, are permitted provided that the following conditions + - are met: + - 1. Redistributions of source code must retain the above copyright + - notice, this list of conditions and the following disclaimer. + - 2. Redistributions in binary form must reproduce the above + - copyright notice, this list of conditions and the following + - disclaimer in the documentation and/or other materials + - provided with the distribution. + - + - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ANY + - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + - OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *====================================================================*/ + + +Libxml2 +Libxml2, an XML C Parser + +Except where otherwise noted in the source code (e.g. the files hash.c, +list.c and the trio files, which are covered by a similar licence but +with different Copyright notices) all the files are: + + Copyright (C) 1998-2012 Daniel Veillard. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is fur- +nished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FIT- +NESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-------------------------------------------------------------------- + +Copyright (C) 2000,2012 Bjorn Reese and Daniel Veillard. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE AUTHORS AND +CONTRIBUTORS ACCEPT NO RESPONSIBILITY IN ANY CONCEIVABLE MANNER. + +Author: breese@users.sourceforge.net + +(taken from hash.c) + +-------------------------------------------------------------------- + + Copyright (C) 2000 Gary Pennington and Daniel Veillard. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE AUTHORS AND +CONTRIBUTORS ACCEPT NO RESPONSIBILITY IN ANY CONCEIVABLE MANNER. + +Author: Gary.Pennington@uk.sun.com + +(taken from list.c) + +-------------------------------------------------------------------- + +Copyright (C) 1998 Bjorn Reese and Daniel Stenberg. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE AUTHORS AND +CONTRIBUTORS ACCEPT NO RESPONSIBILITY IN ANY CONCEIVABLE MANNER. + +(taken from trio.h and trio.c) + +-------------------------------------------------------------------- + +Copyright (C) 2001 Bjorn Reese + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE AUTHORS AND +CONTRIBUTORS ACCEPT NO RESPONSIBILITY IN ANY CONCEIVABLE MANNER. + +(taken from triodef.h, trionan.h, and trionan.c) + +-------------------------------------------------------------------- + +Copyright (C) 2000 Bjorn Reese and Daniel Stenberg. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE AUTHORS AND +CONTRIBUTORS ACCEPT NO RESPONSIBILITY IN ANY CONCEIVABLE MANNER. + +(taken from triop.h) + +-------------------------------------------------------------------- + +Copyright (C) 2001 Bjorn Reese and Daniel Stenberg. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF +MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE AUTHORS AND +CONTRIBUTORS ACCEPT NO RESPONSIBILITY IN ANY CONCEIVABLE MANNER. + +(taken from triostr.h and triostr.c) + +************************************************************************* + +http://ctrio.sourceforge.net/ + +************************************************************************* + +Nano Protocol Buffers +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + +Nanopb +Copyright (c) 2011 Petteri Aimonen + +This software is provided 'as-is', without any express or +implied warranty. In no event will the authors be held liable +for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any +purpose, including commercial applications, and to alter it and +redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you use + this software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and + must not be misrepresented as being the original software. + +3. This notice may not be removed or altered from any source + distribution. + +Ooura FFT +Copyright(C) 1997,2001 Takuya OOURA (email: ooura@kurims.kyoto-u.ac.jp). +You may use, copy, modify this code for any purpose and +without fee. You may distribute this ORIGINAL package. + +OpenBLAS +Copyright (c) 2011-2014, The OpenBLAS Project +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + 3. Neither the name of the OpenBLAS project nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +OpenCV +IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. + +By downloading, copying, installing or using the software you agree to this license. +If you do not agree to this license, do not download, install, +copy or use the software. + + + Intel License Agreement + For Open Source Computer Vision Library + +Copyright (C) 2000, 2001, Intel Corporation, all rights reserved. +Copyright (C) 2013, OpenCV Foundation, all rights reserved. +Third party copyrights are property of their respective owners. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistribution's of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistribution's in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * The name of Intel Corporation may not be used to endorse or promote products + derived from this software without specific prior written permission. + +This software is provided by the copyright holders and contributors "as is" and +any express or implied warranties, including, but not limited to, the implied +warranties of merchantability and fitness for a particular purpose are disclaimed. +In no event shall the Intel Corporation or contributors be liable for any direct, +indirect, incidental, special, exemplary, or consequential damages +(including, but not limited to, procurement of substitute goods or services; +loss of use, data, or profits; or business interruption) however caused +and on any theory of liability, whether in contract, strict liability, +or tort (including negligence or otherwise) arising in any way out of +the use of this software, even if advised of the possibility of such damage. + +OpenCVX +By downloading, copying, installing or using the software you agree to this license. +If you do not agree to this license, do not download, install, +copy or use the software. + + + License Agreement + For Open Source Computer Vision Library + (3-clause BSD License) + +Copyright (C) 2000-2016, Intel Corporation, all rights reserved. +Copyright (C) 2009-2011, Willow Garage Inc., all rights reserved. +Copyright (C) 2009-2016, NVIDIA Corporation, all rights reserved. +Copyright (C) 2010-2013, Advanced Micro Devices, Inc., all rights reserved. +Copyright (C) 2015-2016, OpenCV Foundation, all rights reserved. +Copyright (C) 2015-2016, Itseez Inc., all rights reserved. +Third party copyrights are property of their respective owners. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the names of the copyright holders nor the names of the contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as is" and +any express or implied warranties, including, but not limited to, the implied +warranties of merchantability and fitness for a particular purpose are disclaimed. +In no event shall copyright holders or contributors be liable for any direct, +indirect, incidental, special, exemplary, or consequential damages +(including, but not limited to, procurement of substitute goods or services; +loss of use, data, or profits; or business interruption) however caused +and on any theory of liability, whether in contract, strict liability, +or tort (including negligence or otherwise) arising in any way out of +the use of this software, even if advised of the possibility of such damage. + +PCRE +PCRE LICENCE +------------ + +PCRE is a library of functions to support regular expressions whose syntax +and semantics are as close as possible to those of the Perl 5 language. + +Release 8 of PCRE is distributed under the terms of the "BSD" licence, as +specified below. The documentation for PCRE, supplied in the "doc" +directory, is distributed under the same terms as the software itself. The data +in the testdata directory is not copyrighted and is in the public domain. + +The basic library functions are written in C and are freestanding. Also +included in the distribution is a set of C++ wrapper functions, and a +just-in-time compiler that can be used to optimize pattern matching. These +are both optional features that can be omitted when the library is built. + + +THE BASIC LIBRARY FUNCTIONS +--------------------------- + +Written by: Philip Hazel +Email local part: ph10 +Email domain: cam.ac.uk + +University of Cambridge Computing Service, +Cambridge, England. + +Copyright (c) 1997-2017 University of Cambridge +All rights reserved. + + +PCRE JUST-IN-TIME COMPILATION SUPPORT +------------------------------------- + +Written by: Zoltan Herczeg +Email local part: hzmester +Emain domain: freemail.hu + +Copyright(c) 2010-2017 Zoltan Herczeg +All rights reserved. + + +STACK-LESS JUST-IN-TIME COMPILER +-------------------------------- + +Written by: Zoltan Herczeg +Email local part: hzmester +Emain domain: freemail.hu + +Copyright(c) 2009-2017 Zoltan Herczeg +All rights reserved. + + +THE C++ WRAPPER FUNCTIONS +------------------------- + +Contributed by: Google Inc. + +Copyright (c) 2007-2012, Google Inc. +All rights reserved. + + +THE "BSD" LICENCE +----------------- + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * Neither the name of the University of Cambridge nor the name of Google + Inc. nor the names of their contributors may be used to endorse or + promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + +End + +PNG +COPYRIGHT NOTICE, DISCLAIMER, and LICENSE +========================================= + +PNG Reference Library License version 2 +--------------------------------------- + + * Copyright (c) 1995-2019 The PNG Reference Library Authors. + * Copyright (c) 2018-2019 Cosmin Truta. + * Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. + * Copyright (c) 1996-1997 Andreas Dilger. + * Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +The software is supplied "as is", without warranty of any kind, +express or implied, including, without limitation, the warranties +of merchantability, fitness for a particular purpose, title, and +non-infringement. In no event shall the Copyright owners, or +anyone distributing the software, be liable for any damages or +other liability, whether in contract, tort or otherwise, arising +from, out of, or in connection with the software, or the use or +other dealings in the software, even if advised of the possibility +of such damage. + +Permission is hereby granted to use, copy, modify, and distribute +this software, or portions hereof, for any purpose, without fee, +subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you + must not claim that you wrote the original software. If you + use this software in a product, an acknowledgment in the product + documentation would be appreciated, but is not required. + + 2. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + + +PNG Reference Library License version 1 (for libpng 0.5 through 1.6.35) +----------------------------------------------------------------------- + +libpng versions 1.0.7, July 1, 2000, through 1.6.35, July 15, 2018 are +Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson, are +derived from libpng-1.0.6, and are distributed according to the same +disclaimer and license as libpng-1.0.6 with the following individuals +added to the list of Contributing Authors: + + Simon-Pierre Cadieux + Eric S. Raymond + Mans Rullgard + Cosmin Truta + Gilles Vollant + James Yu + Mandar Sahastrabuddhe + Google Inc. + Vadim Barkov + +and with the following additions to the disclaimer: + + There is no warranty against interference with your enjoyment of + the library or against infringement. There is no warranty that our + efforts or the library will fulfill any of your particular purposes + or needs. This library is provided with all faults, and the entire + risk of satisfactory quality, performance, accuracy, and effort is + with the user. + +Some files in the "contrib" directory and some configure-generated +files that are distributed with libpng have other copyright owners, and +are released under other open source licenses. + +libpng versions 0.97, January 1998, through 1.0.6, March 20, 2000, are +Copyright (c) 1998-2000 Glenn Randers-Pehrson, are derived from +libpng-0.96, and are distributed according to the same disclaimer and +license as libpng-0.96, with the following individuals added to the +list of Contributing Authors: + + Tom Lane + Glenn Randers-Pehrson + Willem van Schaik + +libpng versions 0.89, June 1996, through 0.96, May 1997, are +Copyright (c) 1996-1997 Andreas Dilger, are derived from libpng-0.88, +and are distributed according to the same disclaimer and license as +libpng-0.88, with the following individuals added to the list of +Contributing Authors: + + John Bowler + Kevin Bracey + Sam Bushell + Magnus Holmgren + Greg Roelofs + Tom Tanner + +Some files in the "scripts" directory have other copyright owners, +but are released under this license. + +libpng versions 0.5, May 1995, through 0.88, January 1996, are +Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. + +For the purposes of this copyright and license, "Contributing Authors" +is defined as the following set of individuals: + + Andreas Dilger + Dave Martindale + Guy Eric Schalnat + Paul Schmidt + Tim Wegner + +The PNG Reference Library is supplied "AS IS". The Contributing +Authors and Group 42, Inc. disclaim all warranties, expressed or +implied, including, without limitation, the warranties of +merchantability and of fitness for any purpose. The Contributing +Authors and Group 42, Inc. assume no liability for direct, indirect, +incidental, special, exemplary, or consequential damages, which may +result from the use of the PNG Reference Library, even if advised of +the possibility of such damage. + +Permission is hereby granted to use, copy, modify, and distribute this +source code, or portions hereof, for any purpose, without fee, subject +to the following restrictions: + + 1. The origin of this source code must not be misrepresented. + + 2. Altered versions must be plainly marked as such and must not + be misrepresented as being the original source. + + 3. This Copyright notice may not be removed or altered from any + source or altered source distribution. + +The Contributing Authors and Group 42, Inc. specifically permit, +without fee, and encourage the use of this source code as a component +to supporting the PNG file format in commercial products. If you use +this source code in a product, acknowledgment is not required but would +be appreciated. + +Protocol Buffers +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + +RE2 +// Copyright (c) 2009 The RE2 Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +SocketRocket +Copyright 2012 Square Inc. + +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. + +$OpenBSD: base64.c,v 1.5 2006/10/21 09:55:03 otto Exp $ + +Copyright (c) 1996 by Internet Software Consortium. + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS +ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE +CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + +Portions Copyright (c) 1995 by International Business Machines, Inc. + +International Business Machines, Inc. (hereinafter called IBM) grants +permission under its copyrights to use, copy, modify, and distribute this +Software with or without fee, provided that the above copyright notice and +all paragraphs of this notice appear in all copies, and that the name of IBM +not be used in connection with the marketing of any product incorporating +the Software or modifications thereof, without specific, written prior +permission. + +To the extent it has a right to do so, IBM grants an immunity from suit +under its patents, if any, for the use, sale or manufacture of products to +the extent that such products are used for performing Domain Name System +dynamic updates in TCP/IP networks by means of the Software. No immunity is +granted for any product per se or for any other function of any product. + +THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL, +DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING +OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN +IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES. + +SwiftShader + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +TensorFlow +Copyright 2019 The TensorFlow Authors. All rights reserved. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +UTF +UTF-8 Library + +The authors of this software are Rob Pike and Ken Thompson. + Copyright (c) 1998-2002 by Lucent Technologies. +Permission to use, copy, modify, and distribute this software for any +purpose without fee is hereby granted, provided that this entire notice +is included in all copies of any software which is or includes a copy +or modification of this software and in all copies of the supporting +documentation for such software. +THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED +WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR LUCENT TECHNOLOGIES MAKE ANY +REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY +OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. + +Unsmear + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +X.Org +The following is the 'standard copyright' agreed upon by most contributors, +and is currently the canonical license preferred by the X.Org Foundation. +This is a slight variant of the common MIT license form published by the +Open Source Initiative at http://www.opensource.org/licenses/mit-license.php + +Copyright holders of new code should use this license statement where +possible, and insert their name to this list. Please sort by surname +for people, and by the full name for other entities (e.g. Juliusz +Chroboczek sorts before Intel Corporation sorts before Daniel Stone). + +See each individual source file or directory for the license that applies +to that file. + +Copyright (C) 2003-2006,2008 Jamey Sharp, Josh Triplett +Copyright © 2009 Red Hat, Inc. +Copyright 1990-1992,1999,2000,2004,2009,2010 Oracle and/or its affiliates. +All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + ---------------------------------------------------------------------- + +The following licenses are 'legacy' - usually MIT/X11 licenses with the name +of the copyright holder(s) in the license statement: + +Copyright 1984-1994, 1998 The Open Group + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +OPEN GROUP BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of The Open Group shall not be +used in advertising or otherwise to promote the sale, use or other dealings +in this Software without prior written authorization from The Open Group. + +X Window System is a trademark of The Open Group. + + ---------------------------------------- + +Copyright 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1994, 1996 X Consortium +Copyright 2000 The XFree86 Project, Inc. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the X Consortium shall +not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization +from the X Consortium. + +Copyright 1985, 1986, 1987, 1988, 1989, 1990, 1991 by +Digital Equipment Corporation + +Portions Copyright 1990, 1991 by Tektronix, Inc. + +Permission to use, copy, modify and distribute this documentation for +any purpose and without fee is hereby granted, provided that the above +copyright notice appears in all copies and that both that copyright notice +and this permission notice appear in all copies, and that the names of +Digital and Tektronix not be used in in advertising or publicity pertaining +to this documentation without specific, written prior permission. +Digital and Tektronix makes no representations about the suitability +of this documentation for any purpose. +It is provided ``as is'' without express or implied warranty. + + ---------------------------------------- + +Copyright (c) 1999-2000 Free Software Foundation, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +FREE SOFTWARE FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of the Free Software Foundation +shall not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization from the +Free Software Foundation. + + ---------------------------------------- + +Code and supporting documentation (c) Copyright 1990 1991 Tektronix, Inc. + All Rights Reserved + +This file is a component of an X Window System-specific implementation +of Xcms based on the TekColor Color Management System. TekColor is a +trademark of Tektronix, Inc. The term "TekHVC" designates a particular +color space that is the subject of U.S. Patent No. 4,985,853 (equivalent +foreign patents pending). Permission is hereby granted to use, copy, +modify, sell, and otherwise distribute this software and its +documentation for any purpose and without fee, provided that: + +1. This copyright, permission, and disclaimer notice is reproduced in + all copies of this software and any modification thereof and in + supporting documentation; +2. Any color-handling application which displays TekHVC color + cooordinates identifies these as TekHVC color coordinates in any + interface that displays these coordinates and in any associated + documentation; +3. The term "TekHVC" is always used, and is only used, in association + with the mathematical derivations of the TekHVC Color Space, + including those provided in this file and any equivalent pathways and + mathematical derivations, regardless of digital (e.g., floating point + or integer) representation. + +Tektronix makes no representation about the suitability of this software +for any purpose. It is provided "as is" and with all faults. + +TEKTRONIX DISCLAIMS ALL WARRANTIES APPLICABLE TO THIS SOFTWARE, +INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. IN NO EVENT SHALL TEKTRONIX BE LIABLE FOR ANY +SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA, OR PROFITS, WHETHER IN AN ACTION OF +CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR THE PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +(c) Copyright 1995 FUJITSU LIMITED +This is source code modified by FUJITSU LIMITED under the Joint +Development Agreement for the CDE/Motif PST. + + ---------------------------------------- + +Copyright 1992 by Oki Technosystems Laboratory, Inc. +Copyright 1992 by Fuji Xerox Co., Ltd. + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of Oki Technosystems +Laboratory and Fuji Xerox not be used in advertising or publicity +pertaining to distribution of the software without specific, written +prior permission. +Oki Technosystems Laboratory and Fuji Xerox make no representations +about the suitability of this software for any purpose. It is provided +"as is" without express or implied warranty. + +OKI TECHNOSYSTEMS LABORATORY AND FUJI XEROX DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL OKI TECHNOSYSTEMS +LABORATORY AND FUJI XEROX BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE +OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1990, 1991, 1992, 1993, 1994 by FUJITSU LIMITED + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of FUJITSU LIMITED +not be used in advertising or publicity pertaining to distribution +of the software without specific, written prior permission. +FUJITSU LIMITED makes no representations about the suitability of +this software for any purpose. +It is provided "as is" without express or implied warranty. + +FUJITSU LIMITED DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL FUJITSU LIMITED BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + + +Copyright (c) 1995 David E. Wexelblat. All rights reserved + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL DAVID E. WEXELBLAT BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name of David E. Wexelblat shall +not be used in advertising or otherwise to promote the sale, use or +other dealings in this Software without prior written authorization +from David E. Wexelblat. + + ---------------------------------------- + +Copyright 1990, 1991 by OMRON Corporation + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name OMRON not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. OMRON makes no representations +about the suitability of this software for any purpose. It is provided +"as is" without express or implied warranty. + +OMRON DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL OMRON BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTUOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1985, 1986, 1987, 1988, 1989, 1990, 1991 by +Digital Equipment Corporation + +Portions Copyright 1990, 1991 by Tektronix, Inc + +Rewritten for X.org by Chris Lee + +Permission to use, copy, modify, distribute, and sell this documentation +for any purpose and without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. +Chris Lee makes no representations about the suitability for any purpose +of the information in this document. It is provided \`\`as-is'' without +express or implied warranty. + + ---------------------------------------- + +Copyright 1993 by Digital Equipment Corporation, Maynard, Massachusetts, +Copyright 1994 by FUJITSU LIMITED +Copyright 1994 by Sony Corporation + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the names of Digital, FUJITSU +LIMITED and Sony Corporation not be used in advertising or publicity +pertaining to distribution of the software without specific, written +prior permission. + +DIGITAL, FUJITSU LIMITED AND SONY CORPORATION DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL DIGITAL, FUJITSU LIMITED +AND SONY CORPORATION BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + + +Copyright 1991 by the Open Software Foundation + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of Open Software Foundation +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. Open Software +Foundation makes no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +OPEN SOFTWARE FOUNDATION DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL OPEN SOFTWARE FOUNDATIONN BE +LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1990, 1991, 1992,1993, 1994 by FUJITSU LIMITED +Copyright 1993, 1994 by Sony Corporation + +Permission to use, copy, modify, distribute, and sell this software and +its documentation for any purpose is hereby granted without fee, provided +that the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of FUJITSU LIMITED and Sony Corporation +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. FUJITSU LIMITED and +Sony Corporation makes no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +FUJITSU LIMITED AND SONY CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD +TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL FUJITSU LIMITED OR SONY CORPORATION BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER +RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE +USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright (c) 1993, 1995 by Silicon Graphics Computer Systems, Inc. + +Permission to use, copy, modify, and distribute this +software and its documentation for any purpose and without +fee is hereby granted, provided that the above copyright +notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting +documentation, and that the name of Silicon Graphics not be +used in advertising or publicity pertaining to distribution +of the software without specific prior written permission. +Silicon Graphics makes no representation about the suitability +of this software for any purpose. It is provided "as is" +without any express or implied warranty. + +SILICON GRAPHICS DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL SILICON +GRAPHICS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH +THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1991, 1992, 1993, 1994 by FUJITSU LIMITED +Copyright 1993 by Digital Equipment Corporation + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of FUJITSU LIMITED and +Digital Equipment Corporation not be used in advertising or publicity +pertaining to distribution of the software without specific, written +prior permission. FUJITSU LIMITED and Digital Equipment Corporation +makes no representations about the suitability of this software for +any purpose. It is provided "as is" without express or implied +warranty. + +FUJITSU LIMITED AND DIGITAL EQUIPMENT CORPORATION DISCLAIM ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +FUJITSU LIMITED AND DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR +ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1992, 1993 by FUJITSU LIMITED +Copyright 1993 by Fujitsu Open Systems Solutions, Inc. +Copyright 1994 by Sony Corporation + +Permission to use, copy, modify, distribute and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of FUJITSU LIMITED, +Fujitsu Open Systems Solutions, Inc. and Sony Corporation not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. +FUJITSU LIMITED, Fujitsu Open Systems Solutions, Inc. and +Sony Corporation make no representations about the suitability of +this software for any purpose. It is provided "as is" without +express or implied warranty. + +FUJITSU LIMITED, FUJITSU OPEN SYSTEMS SOLUTIONS, INC. AND SONY +CORPORATION DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, +IN NO EVENT SHALL FUJITSU OPEN SYSTEMS SOLUTIONS, INC., FUJITSU LIMITED +AND SONY CORPORATION BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE +OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1987, 1988, 1990, 1993 by Digital Equipment Corporation, +Maynard, Massachusetts, + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Digital not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + ---------------------------------------- + +Copyright 1993 by SunSoft, Inc. +Copyright 1999-2000 by Bruno Haible + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the names of SunSoft, Inc. and +Bruno Haible not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. SunSoft, Inc. and Bruno Haible make no representations +about the suitability of this software for any purpose. It is +provided "as is" without express or implied warranty. + +SunSoft Inc. AND Bruno Haible DISCLAIM ALL WARRANTIES WITH REGARD +TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS, IN NO EVENT SHALL SunSoft, Inc. OR Bruno Haible BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1991 by the Open Software Foundation +Copyright 1993 by the TOSHIBA Corp. + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of Open Software Foundation and TOSHIBA +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. Open Software +Foundation and TOSHIBA make no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +OPEN SOFTWARE FOUNDATION AND TOSHIBA DISCLAIM ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL OPEN SOFTWARE FOUNDATIONN OR TOSHIBA BE +LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1988 by Wyse Technology, Inc., San Jose, Ca., + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name Wyse not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +WYSE DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + ---------------------------------------- + + +Copyright 1991 by the Open Software Foundation +Copyright 1993, 1994 by the Sony Corporation + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of Open Software Foundation and +Sony Corporation not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior permission. +Open Software Foundation and Sony Corporation make no +representations about the suitability of this software for any purpose. +It is provided "as is" without express or implied warranty. + +OPEN SOFTWARE FOUNDATION AND SONY CORPORATION DISCLAIM ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL OPEN +SOFTWARE FOUNDATIONN OR SONY CORPORATION BE LIABLE FOR ANY SPECIAL, +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1992, 1993 by FUJITSU LIMITED +Copyright 1993 by Fujitsu Open Systems Solutions, Inc. + +Permission to use, copy, modify, distribute and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of FUJITSU LIMITED and +Fujitsu Open Systems Solutions, Inc. not be used in advertising or +publicity pertaining to distribution of the software without specific, +written prior permission. +FUJITSU LIMITED and Fujitsu Open Systems Solutions, Inc. makes no +representations about the suitability of this software for any purpose. +It is provided "as is" without express or implied warranty. + +FUJITSU LIMITED AND FUJITSU OPEN SYSTEMS SOLUTIONS, INC. DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL FUJITSU OPEN SYSTEMS +SOLUTIONS, INC. AND FUJITSU LIMITED BE LIABLE FOR ANY SPECIAL, INDIRECT +OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1993, 1994 by Sony Corporation + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of Sony Corporation +not be used in advertising or publicity pertaining to distribution +of the software without specific, written prior permission. +Sony Corporation makes no representations about the suitability of +this software for any purpose. It is provided "as is" without +express or implied warranty. + +SONY CORPORATION DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL SONY CORPORATION BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1986, 1998 The Open Group +Copyright (c) 2000 The XFree86 Project, Inc. + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +X CONSORTIUM OR THE XFREE86 PROJECT BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Except as contained in this notice, the name of the X Consortium or of the +XFree86 Project shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Software without prior written +authorization from the X Consortium and the XFree86 Project. + + ---------------------------------------- + +Copyright 1990, 1991 by OMRON Corporation, NTT Software Corporation, + and Nippon Telegraph and Telephone Corporation +Copyright 1991 by the Open Software Foundation +Copyright 1993 by the FUJITSU LIMITED + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of OMRON, NTT Software, NTT, and +Open Software Foundation not be used in advertising or publicity +pertaining to distribution of the software without specific, +written prior permission. OMRON, NTT Software, NTT, and Open Software +Foundation make no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +OMRON, NTT SOFTWARE, NTT, AND OPEN SOFTWARE FOUNDATION +DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT +SHALL OMRON, NTT SOFTWARE, NTT, OR OPEN SOFTWARE FOUNDATION BE +LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 1988 by Wyse Technology, Inc., San Jose, Ca, +Copyright 1987 by Digital Equipment Corporation, Maynard, Massachusetts, + + All Rights Reserved + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name Digital not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +DIGITAL AND WYSE DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL DIGITAL OR WYSE BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF +USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + + +Copyright 1991, 1992 by Fuji Xerox Co., Ltd. +Copyright 1992, 1993, 1994 by FUJITSU LIMITED + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of Fuji Xerox, +FUJITSU LIMITED not be used in advertising or publicity pertaining +to distribution of the software without specific, written prior +permission. Fuji Xerox, FUJITSU LIMITED make no representations +about the suitability of this software for any purpose. +It is provided "as is" without express or implied warranty. + +FUJI XEROX, FUJITSU LIMITED DISCLAIM ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL FUJI XEROX, +FUJITSU LIMITED BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA +OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 2006 Josh Triplett + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + + ---------------------------------------- + +(c) Copyright 1996 by Sebastien Marineau and Holger Veit + + + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +HOLGER VEIT BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Except as contained in this notice, the name of Sebastien Marineau or Holger Veit +shall not be used in advertising or otherwise to promote the sale, use or other +dealings in this Software without prior written authorization from Holger Veit or +Sebastien Marineau. + + ---------------------------------------- + +Copyright 1990, 1991 by OMRON Corporation, NTT Software Corporation, + and Nippon Telegraph and Telephone Corporation +Copyright 1991 by the Open Software Foundation +Copyright 1993 by the TOSHIBA Corp. +Copyright 1993, 1994 by Sony Corporation +Copyright 1993, 1994 by the FUJITSU LIMITED + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of OMRON, NTT Software, NTT, Open +Software Foundation, and Sony Corporation not be used in advertising +or publicity pertaining to distribution of the software without specific, +written prior permission. OMRON, NTT Software, NTT, Open Software +Foundation, and Sony Corporation make no representations about the +suitability of this software for any purpose. It is provided "as is" +without express or implied warranty. + +OMRON, NTT SOFTWARE, NTT, OPEN SOFTWARE FOUNDATION, AND SONY +CORPORATION DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT +SHALL OMRON, NTT SOFTWARE, NTT, OPEN SOFTWARE FOUNDATION, OR SONY +CORPORATION BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER +IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright 2000 by Bruno Haible + +Permission to use, copy, modify, distribute, and sell this software +and its documentation for any purpose is hereby granted without fee, +provided that the above copyright notice appear in all copies and +that both that copyright notice and this permission notice appear +in supporting documentation, and that the name of Bruno Haible not +be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. Bruno Haible +makes no representations about the suitability of this software for +any purpose. It is provided "as is" without express or implied +warranty. + +Bruno Haible DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN +NO EVENT SHALL Bruno Haible BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE +OR PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright © 2003 Keith Packard + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of Keith Packard not be used in +advertising or publicity pertaining to distribution of the software without +specific, written prior permission. Keith Packard makes no +representations about the suitability of this software for any purpose. It +is provided "as is" without express or implied warranty. + +KEITH PACKARD DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, +INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO +EVENT SHALL KEITH PACKARD BE LIABLE FOR ANY SPECIAL, INDIRECT OR +CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, +DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + ---------------------------------------- + +Copyright (c) 2007-2009, Troy D. Hanson +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER +OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + ---------------------------------------- + +Copyright 1992, 1993 by TOSHIBA Corp. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided +that the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the name of TOSHIBA not be used in advertising +or publicity pertaining to distribution of the software without specific, +written prior permission. TOSHIBA make no representations about the +suitability of this software for any purpose. It is provided "as is" +without express or implied warranty. + +TOSHIBA DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL +TOSHIBA BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + + ---------------------------------------- + +Copyright IBM Corporation 1993 + +All Rights Reserved + +License to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of IBM not be +used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. + +IBM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING +ALL IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS, AND +NONINFRINGEMENT OF THIRD PARTY RIGHTS, IN NO EVENT SHALL +IBM BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR +ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS +SOFTWARE. + + ---------------------------------------- + +Copyright 1990, 1991 by OMRON Corporation, NTT Software Corporation, + and Nippon Telegraph and Telephone Corporation + +Permission to use, copy, modify, distribute, and sell this software and its +documentation for any purpose is hereby granted without fee, provided that +the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation, and that the names of OMRON, NTT Software, and NTT +not be used in advertising or publicity pertaining to distribution of the +software without specific, written prior permission. OMRON, NTT Software, +and NTT make no representations about the suitability of this +software for any purpose. It is provided "as is" without express or +implied warranty. + +OMRON, NTT SOFTWARE, AND NTT, DISCLAIM ALL WARRANTIES WITH REGARD +TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS, IN NO EVENT SHALL OMRON, NTT SOFTWARE, OR NTT, BE +LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +Zippy +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=== + +Some of the benchmark data in util/zippy/testdata is licensed differently: + + - fireworks.jpeg is Copyright 2013 Steinar H. Gunderson, and + is licensed under the Creative Commons Attribution 3.0 license + (CC-BY-3.0). See https://creativecommons.org/licenses/by/3.0/ + for more information. + + - kppkn.gtb is taken from the Gaviota chess tablebase set, and + is licensed under the MIT License. See + https://sites.google.com/site/gaviotachessengine/Home/endgame-tablebases-1 + for more information. + + - paper-100k.pdf is an excerpt (bytes 92160 to 194560) from the paper + “Combinatorial Modeling of Chromatin Features Quantitatively Predicts DNA + Replication Timing in _Drosophila_” by Federico Comoglio and Renato Paro, + which is licensed under the CC-BY license. See + http://www.ploscompbiol.org/static/license for more ifnormation. + + - alice29.txt, asyoulik.txt, plrabn12.txt and lcet10.txt are from Project + Gutenberg. The first three have expired copyrights and are in the public + domain; the latter does not have expired copyright, but is still in the + public domain according to the license information + (http://www.gutenberg.org/ebooks/53). + +Zstandard +BSD License + +For Zstandard software + +Copyright (c) 2016-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +bzip2 + +-------------------------------------------------------------------------- + +This program, "bzip2", the associated library "libbzip2", and all +documentation, are copyright (C) 1996-2010 Julian R Seward. All +rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. The origin of this software must not be misrepresented; you must + not claim that you wrote the original software. If you use this + software in a product, an acknowledgment in the product + documentation would be appreciated but is not required. + +3. Altered source versions must be plainly marked as such, and must + not be misrepresented as being the original software. + +4. The name of the author may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Julian Seward, jseward@bzip.org +bzip2/libbzip2 version 1.0.6 of 6 September 2010 + +-------------------------------------------------------------------------- + +c-ares +Ares + +(extracted from ares.h) + +Copyright 1998 by the Massachusetts Institute of Technology. + +Permission to use, copy, modify, and distribute this +software and its documentation for any purpose and without +fee is hereby granted, provided that the above copyright +notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting +documentation, and that the name of M.I.T. not be used in +advertising or publicity pertaining to distribution of the +software without specific, written prior permission. +M.I.T. makes no representations about the suitability of +this software for any purpose. It is provided "as is" +without express or implied warranty. + + +double-conversion +Copyright 2006-2011, the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +gRPC + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +gemmlowp + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +giflib +The GIFLIB distribution is Copyright (c) 1997 Eric S. Raymond + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +gtm_session_fetcher + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +intrinsics + +LICENSE file has been created for compliance purposes. Not included in original distribution. +License notice found in NEONvsSEE_6.h files. + +---------------------------------------------------------------------------------------------------------------- +//created by Victoria Zhislina, the Senior Application Engineer, Intel Corporation, victoria.zhislina@intel.com + +//*** Copyright (C) 2012-2014 Intel Corporation. All rights reserved. + +//IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. + +//By downloading, copying, installing or using the software you agree to this license. +//If you do not agree to this license, do not download, install, copy or use the software. + +// License Agreement +//Redistribution and use in source and binary forms, with or without modification, +//are permitted provided that the following conditions are met: + +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. + +// * The name of the copyright holders may not be used to endorse or promote products +// derived from this software without specific prior written permission. + +//This software is provided by the copyright holders and contributors "as is" and +//any express or implied warranties, including, but not limited to, the implied +//warranties of merchantability and fitness for a particular purpose are disclaimed. +//In no event shall the Intel Corporation or contributors be liable for any direct, +//indirect, incidental, special, exemplary, or consequential damages +//(including, but not limited to, procurement of substitute goods or services; +//loss of use, data, or profits; or business interruption) however caused +//and on any theory of liability, whether in contract, strict liability, +//or tort (including negligence or otherwise) arising in any way out of +//the use of this software, even if advised of the possibility of such damage. + +//***************************************************************************************** +// This file is intended to simplify ARM->IA32 porting +// It makes the correspondence between ARM NEON intrinsics (as defined in "arm_neon.h") +// and x86 SSE(up to SSE4.2) intrinsic functions as defined in headers files below +//MMX instruction set is not used due to non availability on x64 systems, +//performance overhead and the necessity to use the EMMS instruction (_mm_empty())for mmx-x87 floating point switching +//***************************************************************************************** + +leveldb +Copyright (c) 2011 The LevelDB Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +libcap +Unless otherwise *explicitly* stated, the following text describes the +licensed conditions under which the contents of this libcap release +may be used and distributed: + +------------------------------------------------------------------------- +Redistribution and use in source and binary forms of libcap, with +or without modification, are permitted provided that the following +conditions are met: + +1. Redistributions of source code must retain any existing copyright + notice, and this entire permission notice in its entirety, + including the disclaimer of warranties. + +2. Redistributions in binary form must reproduce all prior and current + copyright notices, this list of conditions, and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +3. The name of any author may not be used to endorse or promote + products derived from this software without their specific prior + written permission. + +ALTERNATIVELY, this product may be distributed under the terms of the +GNU General Public License (v2.0 - see below), in which case the +provisions of the GNU GPL are required INSTEAD OF the above +restrictions. (This clause is necessary due to a potential conflict +between the GNU GPL and the restrictions contained in a BSD-style +copyright.) + +THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED +WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. +------------------------------------------------------------------------- + +------------------------- +Full text of gpl-2.0.txt: +------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. + +libjpeg +libjeg + +(extracted from src/README) + +LEGAL ISSUES +============ + +In plain English: + +1. We don't promise that this software works. (But if you find any bugs, + please let us know!) +2. You can use this software for whatever you want. You don't have to pay us. +3. You may not pretend that you wrote this software. If you use it in a + program, you must acknowledge somewhere in your documentation that + you've used the IJG code. + +In legalese: + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-1998, Thomas G. Lane. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +ansi2knr.c is included in this distribution by permission of L. Peter Deutsch, +sole proprietor of its copyright holder, Aladdin Enterprises of Menlo Park, CA. +ansi2knr.c is NOT covered by the above copyright and conditions, but instead +by the usual distribution terms of the Free Software Foundation; principally, +that you must include source code if you redistribute it. (See the file +ansi2knr.c for full details.) However, since ansi2knr.c is not needed as part +of any program generated from the IJG code, this does not limit you more than +the foregoing paragraphs do. + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltconfig, ltmain.sh). Another support script, install-sh, is copyright +by M.I.T. but is also freely distributable. + +It appears that the arithmetic coding option of the JPEG spec is covered by +patents owned by IBM, AT&T, and Mitsubishi. Hence arithmetic coding cannot +legally be used without obtaining one or more licenses. For this reason, +support for arithmetic coding has been removed from the free JPEG software. +(Since arithmetic coding provides only a marginal gain over the unpatented +Huffman mode, it is unlikely that very many implementations will support it.) +So far as we are aware, there are no patent restrictions on the remaining +code. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent, GIF reading support has +been removed altogether, and the GIF writer has been simplified to produce +"uncompressed GIFs". This technique does not use the LZW algorithm; the +resulting GIF files are larger than usual, but are readable by all standard +GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + +libjpeg-turbo +For a summary of these license terms, see LICENSE.md. + +libjpeg-turbo license +--------------------- + This license covers the TurboJPEG API library and associated programs. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +- Neither the name of the libjpeg-turbo Project nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS", +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. + + +libjpeg license, Independent JPEG Group +--------------------------------------- + This license applies to the libjpeg API library and associated programs + (any code inherited from libjpeg, and any modifications to that code.) + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2016, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. + + +The Unix configuration script "configure" was produced with GNU Autoconf. +It is copyright by the Free Software Foundation but is freely distributable. +The same holds for its supporting scripts (config.guess, config.sub, +ltmain.sh). Another support script, install-sh, is copyright by X Consortium +but is also freely distributable. + +The IJG distribution formerly included code to read and write GIF files. +To avoid entanglement with the Unisys LZW patent (now expired), GIF reading +support has been removed altogether, and the GIF writer has been simplified +to produce "uncompressed GIFs". This technique does not use the LZW +algorithm; the resulting GIF files are larger than usual, but are readable +by all standard GIF decoders. + +We are required to state that + "The Graphics Interchange Format(c) is the Copyright property of + CompuServe Incorporated. GIF(sm) is a Service Mark property of + CompuServe Incorporated." + + +zlib License +------------ + This license is a subset of the other two, and it covers the libjpeg-turbo + SIMD extensions. + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +libwebp +Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +libyuv +Copyright 2011 The LibYuv Project Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +minizip +zlib + +(extracted from README, except for match.S) + +Copyright notice: + + (C) 1995-2004 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + + +(extracted from match.S, for match.S only) + + Copyright (C) 1998, 2007 Brian Raiter + + This software is provided 'as-is', without any express or implied + warranty. In no event will the author be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + +nsync + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +protobuf +Copyright 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + +sequence_lock + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. + +ssziparchive +Copyright (c) 2010-2011 Sam Soffes + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +tiff +Copyright (c) 1988-1997 Sam Leffler +Copyright (c) 1991-1997 Silicon Graphics, Inc. + +Permission to use, copy, modify, distribute, and sell this software and +its documentation for any purpose is hereby granted without fee, provided +that (i) the above copyright notices and this permission notice appear in +all copies of the software and related documentation, and (ii) the names of +Sam Leffler and Silicon Graphics may not be used in any advertising or +publicity relating to the software without the specific, prior written +permission of Sam Leffler and Silicon Graphics. + +THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, +EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY +WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR +ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND, +OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF +LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +OF THIS SOFTWARE. + +zlib +(extracted from README, except for match.S) + +Copyright notice: + + (C) 1995-2013 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +If you use the zlib library in a product, we would appreciate *not* receiving +lengthy legal documents to sign. The sources are provided for free but without +warranty of any kind. The library has been entirely written by Jean-loup +Gailly and Mark Adler; it does not include third-party code. + +If you redistribute modified sources, we would appreciate that you include in +the file ChangeLog history information documenting your changes. Please read +the FAQ for more information on the distribution of modified source versions. + +(extracted from match.S, for match.S only) + +Copyright (C) 1998, 2007 Brian Raiter + +This software is provided 'as-is', without any express or implied +warranty. In no event will the author be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: --- +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/cmake/external/googletest.cmake b/cmake/external/googletest.cmake index 9d4213c23b0..691e3010196 100644 --- a/cmake/external/googletest.cmake +++ b/cmake/external/googletest.cmake @@ -14,19 +14,15 @@ include(ExternalProject) -# Googletest 1.8.0 fails to build on VS 2017: -# https://github.com/google/googletest/issues/1111. -# -# No release has been made since, so just pick a commit since then. -set(commit ba96d0b1161f540656efdaed035b3c062b60e006) # master@{2018-07-10} +set(version 1.8.1) ExternalProject_Add( googletest DOWNLOAD_DIR ${FIREBASE_DOWNLOAD_DIR} - DOWNLOAD_NAME googletest-${commit}.tar.gz - URL https://github.com/google/googletest/archive/${commit}.tar.gz - URL_HASH SHA256=949c556896cf31ed52e53449e17a1276b8b26d3ee5932f5ca49ee929f4b35c51 + DOWNLOAD_NAME googletest-${version}.tar.gz + URL https://github.com/google/googletest/archive/release-${version}.tar.gz + URL_HASH SHA256=9bf1fe5182a604b4135edc1a425ae356c9ad15e9b23f9f12a02e80184c3a249c PREFIX ${PROJECT_BINARY_DIR} diff --git a/scripts/build.sh b/scripts/build.sh index 9f404e75806..95329ae5317 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -18,6 +18,11 @@ # # Builds the given product for the given platform using the given build method +function pod_gen() { + # Call pod gen with a podspec and additional optional arguments. + bundle exec pod gen --local-sources=./ --sources=https://cdn.cocoapods.org/ "$@" +} + set -euo pipefail if [[ $# -lt 1 ]]; then @@ -86,10 +91,22 @@ function RunXcodebuild() { fi } -ios_flags=( - -sdk 'iphonesimulator' - -destination 'platform=iOS Simulator,name=iPhone 7' -) +# Remove each product when it moves up to Xcode 11 +if [[ $product == 'Firestore' || # #3949 + $product == 'GoogleDataTransport' || # #3947 + $product == 'InAppMessaging' # #3948 + ]]; then + ios_flags=( + -sdk 'iphonesimulator' + -destination 'platform=iOS Simulator,name=iPhone 7' + ) +else + ios_flags=( + -sdk 'iphonesimulator' + -destination 'platform=iOS Simulator,name=iPhone 11' + ) +fi + macos_flags=( -sdk 'macosx' -destination 'platform=OS X,arch=x86_64' @@ -193,28 +210,9 @@ case "$product-$method-$platform" in build \ test - RunXcodebuild \ - -workspace 'GoogleUtilities/Example/GoogleUtilities.xcworkspace' \ - -scheme "Example_$platform" \ - "${xcb_flags[@]}" \ - build \ - test - if [[ $platform == 'iOS' ]]; then # Code Coverage collection is only working on iOS currently. ./scripts/collect_metrics.sh 'Example/Firebase.xcworkspace' "AllUnitTests_$platform" - - # Test iOS Objective-C static library build - cd Example - sed -i -e 's/use_frameworks/\#use_frameworks/' Podfile - pod update --no-repo-update - cd .. - RunXcodebuild \ - -workspace 'Example/Firebase.xcworkspace' \ - -scheme "AllUnitTests_$platform" \ - "${xcb_flags[@]}" \ - build \ - test fi ;; @@ -332,94 +330,41 @@ case "$product-$method-$platform" in build ;; - GoogleDataTransport-xcodebuild-*) - RunXcodebuild \ - -workspace 'gen/GoogleDataTransport/GoogleDataTransport.xcworkspace' \ - -scheme "GoogleDataTransport-$platform-Unit-Tests-Unit" \ - "${xcb_flags[@]}" \ - build \ - test - - RunXcodebuild \ - -workspace 'gen/GoogleDataTransport/GoogleDataTransport.xcworkspace' \ - -scheme "GoogleDataTransport-$platform-Unit-Tests-Lifecycle" \ - "${xcb_flags[@]}" \ - build \ - test - ;; - - GoogleDataTransportIntegrationTest-xcodebuild-*) - RunXcodebuild \ - -workspace 'gen/GoogleDataTransport/GoogleDataTransport.xcworkspace' \ - -scheme "GoogleDataTransport-$platform-Unit-Tests-Integration" \ - "${xcb_flags[@]}" \ - build \ - test - ;; - - GoogleDataTransportCCTSupport-xcodebuild-*) - RunXcodebuild \ - -workspace 'gen/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport.xcworkspace' \ - -scheme "GoogleDataTransportCCTSupport-$platform-Unit-Tests-Unit" \ - "${xcb_flags[@]}" \ - build \ - test - - RunXcodebuild \ - -workspace 'gen/GoogleDataTransportCCTSupport/GoogleDataTransportCCTSupport.xcworkspace' \ - -scheme "GoogleDataTransportCCTSupport-$platform-Unit-Tests-Integration" \ - "${xcb_flags[@]}" \ - build \ - test - ;; - Database-xcodebuild-*) + pod_gen FirebaseDatabase.podspec --platforms=ios RunXcodebuild \ -workspace 'gen/FirebaseDatabase/FirebaseDatabase.xcworkspace' \ - -scheme "FirebaseDatabase-iOS-Unit-unit" \ + -scheme "FirebaseDatabase-Unit-unit" \ "${ios_flags[@]}" \ "${xcb_flags[@]}" \ build \ test - RunXcodebuild \ - -workspace 'gen/FirebaseDatabase/FirebaseDatabase.xcworkspace' \ - -scheme "FirebaseDatabase-macOS-Unit-unit" \ - "${macos_flags[@]}" \ - "${xcb_flags[@]}" \ - build \ - test - RunXcodebuild \ - -workspace 'gen/FirebaseDatabase/FirebaseDatabase.xcworkspace' \ - -scheme "FirebaseDatabase-tvOS-Unit-unit" \ - "${tvos_flags[@]}" \ - "${xcb_flags[@]}" \ - build \ - test if [[ "$TRAVIS_PULL_REQUEST" == "false" || "$TRAVIS_PULL_REQUEST_SLUG" == "$TRAVIS_REPO_SLUG" ]]; then # Integration tests are only run on iOS to minimize flake failures. RunXcodebuild \ -workspace 'gen/FirebaseDatabase/FirebaseDatabase.xcworkspace' \ - -scheme "FirebaseDatabase-iOS-Unit-integration" \ + -scheme "FirebaseDatabase-Unit-integration" \ "${ios_flags[@]}" \ "${xcb_flags[@]}" \ build \ test fi - ;; - Messaging-xcodebuild-*) + pod_gen FirebaseDatabase.podspec --platforms=macos --clean RunXcodebuild \ - -workspace 'gen/FirebaseMessaging/FirebaseMessaging.xcworkspace' \ - -scheme "FirebaseMessaging-iOS-Unit-unit" \ - "${ios_flags[@]}" \ + -workspace 'gen/FirebaseDatabase/FirebaseDatabase.xcworkspace' \ + -scheme "FirebaseDatabase-Unit-unit" \ + "${macos_flags[@]}" \ "${xcb_flags[@]}" \ build \ test + + pod_gen FirebaseDatabase.podspec --platforms=tvos --clean RunXcodebuild \ - -workspace 'gen/FirebaseMessaging/FirebaseMessaging.xcworkspace' \ - -scheme "FirebaseMessaging-tvOS-Unit-unit" \ + -workspace 'gen/FirebaseDatabase/FirebaseDatabase.xcworkspace' \ + -scheme "FirebaseDatabase-Unit-unit" \ "${tvos_flags[@]}" \ "${xcb_flags[@]}" \ build \ @@ -427,39 +372,44 @@ case "$product-$method-$platform" in ;; Storage-xcodebuild-*) + pod_gen FirebaseStorage.podspec --platforms=ios RunXcodebuild \ -workspace 'gen/FirebaseStorage/FirebaseStorage.xcworkspace' \ - -scheme "FirebaseStorage-iOS-Unit-unit" \ + -scheme "FirebaseStorage-Unit-unit" \ "${ios_flags[@]}" \ "${xcb_flags[@]}" \ build \ test + + if [[ "$TRAVIS_PULL_REQUEST" == "false" || + "$TRAVIS_PULL_REQUEST_SLUG" == "$TRAVIS_REPO_SLUG" ]]; then + # Integration tests are only run on iOS to minimize flake failures. + RunXcodebuild \ + -workspace 'gen/FirebaseStorage/FirebaseStorage.xcworkspace' \ + -scheme "FirebaseStorage-Unit-integration" \ + "${ios_flags[@]}" \ + "${xcb_flags[@]}" \ + build \ + test + fi + + pod_gen FirebaseStorage.podspec --platforms=macos --clean RunXcodebuild \ -workspace 'gen/FirebaseStorage/FirebaseStorage.xcworkspace' \ - -scheme "FirebaseStorage-macOS-Unit-unit" \ + -scheme "FirebaseStorage-Unit-unit" \ "${macos_flags[@]}" \ "${xcb_flags[@]}" \ build \ test + + pod_gen FirebaseStorage.podspec --platforms=tvos --clean RunXcodebuild \ -workspace 'gen/FirebaseStorage/FirebaseStorage.xcworkspace' \ - -scheme "FirebaseStorage-tvOS-Unit-unit" \ + -scheme "FirebaseStorage-Unit-unit" \ "${tvos_flags[@]}" \ "${xcb_flags[@]}" \ build \ test - - if [[ "$TRAVIS_PULL_REQUEST" == "false" || - "$TRAVIS_PULL_REQUEST_SLUG" == "$TRAVIS_REPO_SLUG" ]]; then - # Integration tests are only run on iOS to minimize flake failures. - RunXcodebuild \ - -workspace 'gen/FirebaseStorage/FirebaseStorage.xcworkspace' \ - -scheme "FirebaseStorage-iOS-Unit-integration" \ - "${ios_flags[@]}" \ - "${xcb_flags[@]}" \ - build \ - test - fi ;; *) echo "Don't know how to build this product-platform-method combination" 1>&2 diff --git a/scripts/if_changed.sh b/scripts/if_changed.sh index 5116e8c97b5..2177cc6ce3f 100755 --- a/scripts/if_changed.sh +++ b/scripts/if_changed.sh @@ -78,6 +78,11 @@ else check_changes '^(Firebase/Core|Firebase/Auth|Example/Auth|GoogleUtilities|FirebaseAuth.podspec)' ;; + CoreDiagnostics-*) + check_changes '^(Firebase/CoreDiagnostics|Example/CoreDiagnostics/Tests|FirebaseCoreDiagnostics.podspec|'\ +'FirebaseCoreDiagnosticsInterop|FirebaseCoreDiagnosticsInterop.podspec)' + ;; + Database-*) check_changes '^(Firebase/Core|Firebase/Database|Example/Database|GoogleUtilities|FirebaseDatabase.podspec)' ;; @@ -113,11 +118,6 @@ else check_changes '^(Firestore/(core|third_party)|cmake|GoogleUtilities)' ;; - FirebaseCoreDiagnostics-*) - check_changes '^(Firebase/CoreDiagnostics|FirebaseCoreDiagnosticsInterop|'\ -'FirebaseCoreDiagnostics.podspec|FirebaseCoreDiagnosticsInterop.podspec)' - ;; - GoogleDataTransport-*) check_changes '^(GoogleDataTransport|GoogleDataTransport.podspec)' ;; @@ -134,6 +134,10 @@ else check_changes '^(Firebase/Core|Firebase/Messaging|Example/Messaging|GoogleUtilities|FirebaseMessaging.podspec|Firebase/InstanceID)' ;; + RemoteConfig-*) + check_changes '^(Firebase/Core|FirebaseRemoteConfig)' + ;; + Storage-*) check_changes '^(Firebase/Core|Firebase/Storage|Example/Storage|GoogleUtilities|FirebaseStorage.podspec)' ;; diff --git a/scripts/install_prereqs.sh b/scripts/install_prereqs.sh index 1c23af11c35..098a595762d 100755 --- a/scripts/install_prereqs.sh +++ b/scripts/install_prereqs.sh @@ -26,7 +26,7 @@ function install_secrets() { # requests from forks. See # https://docs.travis-ci.com/user/pull-requests#pull-requests-and-security-restrictions if [[ ! -z $encrypted_d6a88994a5ab_key ]]; then - openssl aes-256-cbc -K $encrypted_b02643c8c602_key -iv $encrypted_b02643c8c602_iv \ + openssl aes-256-cbc -K $encrypted_3360571ebe7a_key -iv $encrypted_3360571ebe7a_iv \ -in scripts/travis-encrypted/Secrets.tar.enc \ -out scripts/travis-encrypted/Secrets.tar -d @@ -43,20 +43,18 @@ function install_secrets() { cp Secrets/Storage/App/GoogleService-Info.plist Example/Database/App/GoogleService-Info.plist cp Secrets/Metrics/database.config Metrics/database.config - fi -} -function pod_gen() { - # Call pod gen with a podspec and additonal optional arguments. - bundle exec pod gen --local-sources=./ --sources=https://cdn.cocoapods.org/ "$@" + # Firebase Installations + fis_resources_dir=FirebaseInstallations/Source/Tests/Resources/ + mkdir -p "$fis_resources_dir" + cp Secrets/Installations/GoogleService-Info.plist "$fis_resources_dir" + fi } 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 - bundle exec pod install --project-directory=GoogleUtilities/Example install_secrets ;; @@ -74,9 +72,6 @@ case "$PROJECT-$PLATFORM-$METHOD" in ;; Database-*) - # Install the workspace to have better control over test runs than - # pod lib lint, since the integration tests can be flaky. - pod_gen FirebaseDatabase.podspec install_secrets ;; @@ -85,16 +80,11 @@ case "$PROJECT-$PLATFORM-$METHOD" in ./Functions/Backend/start.sh synchronous ;; - Messaging-*) - # Install the workspace to have better control over test runs than - # pod lib lint, since the integration tests can be flaky. - pod_gen FirebaseMessaging.podspec + Storage-*) + install_secrets ;; - Storage-*) - # Install the workspace to have better control over test runs than - # pod lib lint, since the integration tests can be flaky. - pod_gen FirebaseStorage.podspec + Installations-*) install_secrets ;; @@ -105,6 +95,18 @@ case "$PROJECT-$PLATFORM-$METHOD" in ;; Firestore-*-xcodebuild | Firestore-*-fuzz) + if [[ $XCODE_VERSION == "8."* ]]; then + # Firestore still compiles with Xcode 8 to help verify general + # conformance with C++11 by using an older compiler that doesn't have as + # many extensions from later versions of the language. However, Firebase + # as a whole does not support this environment and @available checks in + # GoogleDataTransport would otherwise break this build. + # + # This drops the dependency that adds GoogleDataTransport into + # Firestore's dependencies. + sed -i.bak "/s.dependency 'FirebaseCoreDiagnostics'/d" FirebaseCore.podspec + fi + gem install xcpretty bundle exec pod install --project-directory=Firestore/Example --repo-update ;; @@ -125,25 +127,6 @@ case "$PROJECT-$PLATFORM-$METHOD" in bundle exec pod install --project-directory=SymbolCollisionTest --repo-update ;; - FirebaseCoreDiagnostics-*-xcodebuild) - gem install xcpretty - pod_gen FirebaseCoreDiagnostics.podspec - ;; - - GoogleDataTransport-*-xcodebuild) - gem install xcpretty - pod_gen GoogleDataTransport.podspec - ;; - - GoogleDataTransportIntegrationTest-*-xcodebuild) - gem install xcpretty - pod_gen GoogleDataTransport.podspec - ;; - - GoogleDataTransportCCTSupport-*-xcodebuild) - gem install xcpretty - pod_gen GoogleDataTransportCCTSupport.podspec - ;; *) echo "Unknown project-platform-method combo" 1>&2 echo " PROJECT=$PROJECT" 1>&2 diff --git a/scripts/run_firestore_emulator.sh b/scripts/run_firestore_emulator.sh index 21c8a26d694..365af3a429f 100755 --- a/scripts/run_firestore_emulator.sh +++ b/scripts/run_firestore_emulator.sh @@ -20,7 +20,7 @@ set -euo pipefail -VERSION='1.6.2' +VERSION='1.8.0' FILENAME="cloud-firestore-emulator-v${VERSION}.jar" URL="https://storage.googleapis.com/firebase-preview-drop/emulator/${FILENAME}" diff --git a/scripts/sync_project.rb b/scripts/sync_project.rb index fdc0e9e20fe..1cd193fe703 100755 --- a/scripts/sync_project.rb +++ b/scripts/sync_project.rb @@ -130,6 +130,10 @@ def sync_firestore(test_only) # Protobuf wants to #include '"${PODS_ROOT}/ProtobufCpp/src"', + ], + + 'OTHER_CFLAGS' => [ + '-Werror' ] } @@ -141,7 +145,7 @@ def sync_firestore(test_only) ['iOS', 'macOS', 'tvOS'].each do |platform| s.target "Firestore_Example_#{platform}" do |t| - t.xcconfig = xcconfig_objc + { + t.xcconfig = xcconfig_objc + xcconfig_swift + { # Passing -all_load is required to get all our C++ code into the test # host. # @@ -171,7 +175,7 @@ def sync_firestore(test_only) # These files are integration tests, handled below 'Firestore/Example/Tests/Integration/**', ] - t.xcconfig = xcconfig_objc + t.xcconfig = xcconfig_objc + xcconfig_swift end end diff --git a/scripts/travis-encrypted/Secrets.tar.enc b/scripts/travis-encrypted/Secrets.tar.enc index cb155ad75d7..8302e1cfed6 100644 Binary files a/scripts/travis-encrypted/Secrets.tar.enc and b/scripts/travis-encrypted/Secrets.tar.enc differ