diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..29250d1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Google Utilities + url: https://github.com/firebase/firebase-ios-sdk/issues/new/choose + about: Google Utilities is not supported for direct usage. Please file with the product library that depends on Google Utilities. diff --git a/.github/ISSUE_TEMPLATE/issue_report.md b/.github/ISSUE_TEMPLATE/issue_report.md new file mode 100644 index 0000000..31f1511 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue_report.md @@ -0,0 +1,8 @@ +--- +name: 🐞 Internal Issue +about: Googlers may file issues here or to track with [Firebase](https://github.com/firebase/firebase-ios-sdk/issues/new/choose) +--- + diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 7ff802f..f32d2f7 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -30,7 +30,7 @@ jobs: - name: Setup check run: | brew update - brew install clang-format@11 + brew install clang-format@12 brew install mint mint bootstrap diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/test_coverage.yml new file mode 100644 index 0000000..bdeeb27 --- /dev/null +++ b/.github/workflows/test_coverage.yml @@ -0,0 +1,79 @@ +name: test_coverage + +on: + pull_request: + # open will be triggered when a pull request is created. + # synchronize will be triggered when a pull request has new commits. + # closed will be triggered when a pull request is closed. + types: [opened, synchronize, closed] + +env: + METRICS_SERVICE_SECRET: ${{ secrets.GHASecretsGPGPassphrase1 }} + +jobs: + pod_lib_lint: + if: github.repository == 'google/GoogleUtilities' && (github.event.action == 'synchronize' || github.event.action == 'opened' || github.event.pull_request.merged) + runs-on: macos-latest + strategy: + matrix: + target: [ios] + steps: + - uses: actions/checkout@v2 + - name: Setup Scripts Directory + run: ./setup-scripts.sh + - name: Setup Bundler + run: ./scripts/setup_bundler.sh + - name: PodLibLint DataTransport + run: | + scripts/code_coverage_report/pod_test_code_coverage_report.sh --sdk=GoogleUtilities --platform=${{ matrix.target }} --test_spec=unit,unit-swift --output_path="/Users/runner/GoogleUtilities-ios-unit.xcresult" + - uses: actions/upload-artifact@v2 + with: + name: codecoverage + path: /Users/runner/*.xcresult + manage_test_results: + if: github.repository == 'google/GoogleUtilities' && (github.event.action == 'synchronize' || github.event.action == 'opened' || github.event.pull_request.merged) + needs: pod_lib_lint + runs-on: macOS-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/download-artifact@v2 + id: download + with: + path: /Users/runner/test + - name: Setup Scripts Directory + run: ./setup-scripts.sh + - name: Access to Metrics Service + if: ${{ env.METRICS_SERVICE_SECRET }} + run: | + # Install gcloud sdk + curl https://sdk.cloud.google.com > install.sh + bash install.sh --disable-prompts + echo "${HOME}/google-cloud-sdk/bin/" >> $GITHUB_PATH + export PATH="${HOME}/google-cloud-sdk/bin/:${PATH}" + + # Activate the service account for Metrics Service. + scripts/decrypt_gha_secret.sh scripts/gha-encrypted/metrics_service_access.json.gpg \ + metrics-access.json "${{ env.METRICS_SERVICE_SECRET }}" + gcloud auth activate-service-account --key-file metrics-access.json + - name: Build code coverage tool + run: | + cd firebase-ios-sdk/scripts/code_coverage_report/generate_code_coverage_report/ + swift build + - name: Generate report + if: github.event.pull_request.merged != true && env.METRICS_SERVICE_SECRET + env: + pr_branch: ${{ github.event.pull_request.head.ref }} + run: | + common_commit=$(git merge-base remotes/origin/${pr_branch} remotes/origin/main) + GITHUB_SHA=$(cat $GITHUB_EVENT_PATH | jq -r .pull_request.head.sha) + if [ -d "${{steps.download.outputs.download-path}}" ]; then + firebase-ios-sdk/scripts/code_coverage_report/generate_code_coverage_report/.build/debug/CoverageReportGenerator --presubmit "google/GoogleUtilities" --head-commit "${GITHUB_SHA}" --token $(gcloud auth print-identity-token) --xcresult-dir "${{steps.download.outputs.download-path}}/codecoverage" --log-link "https://github.com/google/GoogleUtilities/actions/runs/${GITHUB_RUN_ID}" --pull-request-num ${{github.event.pull_request.number}} --base-commit "$common_commit" + fi + - name: Update New Coverage Data + if: github.event.pull_request.merged == true && env.METRICS_SERVICE_SECRET + run: | + if [ -d "${{steps.download.outputs.download-path}}" ]; then + firebase-ios-sdk/scripts/code_coverage_report/generate_code_coverage_report/.build/debug/CoverageReportGenerator --merge "google/GoogleUtilities" --head-commit "${GITHUB_SHA}" --token $(gcloud auth print-identity-token) --xcresult-dir "${{steps.download.outputs.download-path}}/codecoverage" --log-link "https://github.com/google/GoogleUtilities/actions/runs/${GITHUB_RUN_ID}" --source-branch "${{ github.base_ref }}" + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index d9ad256..81336ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +# 7.5.0 +- Bump Promises dependency. (#8334) + +# 7.4.3 +- Include all object classes when using archiver. (#42) + +# 7.4.2 (Swift PM) +- Fix warnings in Xcode 13 beta 1. (#41) + +# 7.4.1 +- Improve heartbeat date storage's version compatability. (#37) + +# 7.4.0 +- Change heartbeat directory name and refactor file. (#19) +- Introduce user defaults based heartbeat storage. (#23) +- Support iOS 9 with Swift Package Manager. + +# 7.3.1 +- Add explicit dependency on Security framework to Environment subspec. (#12) + +# 7.3.0 +- Fix conflicting internal constant name and move it out of the public header. (#7635) + # 7.2.2 - Fix warnings introduced with Xcode 12.5. (#8) @@ -85,7 +108,7 @@ # 6.0.0 - GULAppDelegateSwizzler - proxy APNS methods separately. (#2835) - Cocoapods 1.7.0 multiproject support. (#2751) -- Bump minimium iOS version to iOS 8. (#2876) +- Bump minimum iOS version to iOS 8. (#2876) # 5.7.0 - Restore to 5.5.0 tag after increased App Store warnings. (#2807) diff --git a/GoogleUtilities.podspec b/GoogleUtilities.podspec index 9e8cd0a..ae8f33d 100644 --- a/GoogleUtilities.podspec +++ b/GoogleUtilities.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'GoogleUtilities' - s.version = '7.2.2' + s.version = '7.5.0' s.summary = 'Google Utilities for Apple platform SDKs' s.description = <<-DESC @@ -38,7 +38,10 @@ other Google CocoaPods. They're not intended for direct public usage. s.subspec 'Environment' do |es| es.source_files = 'GoogleUtilities/Environment/**/*.[mh]' es.public_header_files = 'GoogleUtilities/Environment/Public/GoogleUtilities/*.h' - es.dependency 'PromisesObjC', '~> 1.2' + es.dependency 'PromisesObjC', '>= 1.2', '< 3.0' + es.frameworks = [ + 'Security' + ] end s.subspec 'Logger' do |ls| @@ -126,7 +129,7 @@ other Google CocoaPods. They're not intended for direct public usage. s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } - # All tests require arc except Tests/Network/third_party/GTMHTTPServer.m + unit_tests.platforms = { :ios => ios_deployment_target, :osx => osx_deployment_target, @@ -135,7 +138,12 @@ other Google CocoaPods. They're not intended for direct public usage. unit_tests.source_files = [ 'GoogleUtilities/Tests/Unit/**/*.[mh]', ] - unit_tests.requires_arc = 'GoogleUtilities/Tests/Unit/*/*.[mh]' + + # All tests require arc except Tests/Network/third_party/GTMHTTPServer.m + unit_tests.requires_arc = [ + 'GoogleUtilities/Tests/Unit/*/*.[mh]', + 'GoogleUtilities/Tests/Unit/Environment/**/*.[mh]' + ] unit_tests.requires_app_host = true unit_tests.dependency 'OCMock' end diff --git a/GoogleUtilities/Environment/GULHeartbeatDateStorage.m b/GoogleUtilities/Environment/GULHeartbeatDateStorage.m index d36cfde..25ee5f4 100644 --- a/GoogleUtilities/Environment/GULHeartbeatDateStorage.m +++ b/GoogleUtilities/Environment/GULHeartbeatDateStorage.m @@ -17,6 +17,8 @@ #import "GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorage.h" #import "GoogleUtilities/Environment/Public/GoogleUtilities/GULSecureCoding.h" +NSString *const kGULHeartbeatStorageDirectory = @"Google/FIRApp"; + @interface GULHeartbeatDateStorage () /** The storage to store the date of the last sent heartbeat. */ @property(nonatomic, readonly) NSFileCoordinator *fileCoordinator; @@ -29,9 +31,7 @@ @synthesize fileURL = _fileURL; - (instancetype)initWithFileName:(NSString *)fileName { - if (fileName == nil) { - return nil; - } + if (fileName == nil) return nil; self = [super init]; if (self) { @@ -41,13 +41,13 @@ return self; } -/** Lazy getter for fileURL +/** Lazy getter for fileURL. * @return fileURL where heartbeat information is stored. */ - (NSURL *)fileURL { if (!_fileURL) { - NSURL *directoryURL = [[self class] directoryPathURL]; - [[self class] checkAndCreateDirectory:directoryURL fileCoordinator:_fileCoordinator]; + NSURL *directoryURL = [self directoryPathURL]; + [self checkAndCreateDirectory:directoryURL]; _fileURL = [directoryURL URLByAppendingPathComponent:_fileName]; } return _fileURL; @@ -56,36 +56,32 @@ /** Returns the URL path of the directory for heartbeat storage data. * @return the URL path of the directory for heartbeat storage data. */ -+ (NSURL *)directoryPathURL { +- (NSURL *)directoryPathURL { + NSArray *paths; #if TARGET_OS_TV - NSArray *paths = - NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); #else - NSArray *paths = - NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); -#endif - NSArray *components = @[ paths.lastObject, @"Google/FIRApp" ]; - NSString *directoryString = [NSString pathWithComponents:components]; - NSURL *directoryURL = [NSURL fileURLWithPath:directoryString]; + paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); +#endif // TARGET_OS_TV + NSString *rootPath = [paths lastObject]; + NSURL *rootURL = [NSURL fileURLWithPath:rootPath]; + NSURL *directoryURL = [rootURL URLByAppendingPathComponent:kGULHeartbeatStorageDirectory]; return directoryURL; } -/** Checks and creates a directory for the directory specified by the - * directory url - * @param directoryPathURL The path to the directory which needs to be created. - * @param fileCoordinator The fileCoordinator object to coordinate writes to the directory. +/** Check for the existence of the directory specified by the URL, and create it if it does not + * exist. + * @param directoryPathURL The path to the directory that needs to exist. */ -+ (void)checkAndCreateDirectory:(NSURL *)directoryPathURL - fileCoordinator:(NSFileCoordinator *)fileCoordinator { +- (void)checkAndCreateDirectory:(NSURL *)directoryPathURL { NSError *fileCoordinatorError = nil; - [fileCoordinator + [self.fileCoordinator coordinateWritingItemAtURL:directoryPathURL options:0 error:&fileCoordinatorError byAccessor:^(NSURL *writingDirectoryURL) { NSError *error; if (![writingDirectoryURL checkResourceIsReachableAndReturnError:&error]) { - // If fail creating the Application Support directory, log warning. NSError *error; [[NSFileManager defaultManager] createDirectoryAtURL:writingDirectoryURL withIntermediateDirectories:YES @@ -95,65 +91,78 @@ }]; } -- (nullable NSMutableDictionary *)heartbeatDictionaryWithFileURL:(NSURL *)readingFileURL { - NSError *error; - NSMutableDictionary *dict; - NSData *objectData = [NSData dataWithContentsOfURL:readingFileURL options:0 error:&error]; - if (objectData == nil || error != nil) { - dict = [NSMutableDictionary dictionary]; - } else { - dict = [GULSecureCoding - unarchivedObjectOfClasses:[NSSet setWithArray:@[ NSDictionary.class, NSDate.class ]] - fromData:objectData - error:&error]; - if (dict == nil || error != nil) { - dict = [NSMutableDictionary dictionary]; - } - } - return dict; -} - - (nullable NSDate *)heartbeatDateForTag:(NSString *)tag { - __block NSMutableDictionary *dict; + __block NSDictionary *heartbeatDictionary; NSError *error; [self.fileCoordinator coordinateReadingItemAtURL:self.fileURL options:0 error:&error byAccessor:^(NSURL *readingURL) { - dict = [self heartbeatDictionaryWithFileURL:readingURL]; + heartbeatDictionary = + [self heartbeatDictionaryWithFileURL:readingURL]; }]; - return dict[tag]; + NSDate *heartbeatDate = heartbeatDictionary[tag]; + if (![heartbeatDate isKindOfClass:[NSDate class]]) { + return nil; + } + return heartbeatDate; } - (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag { NSError *error; __block BOOL isSuccess = false; - [self.fileCoordinator coordinateReadingItemAtURL:self.fileURL - options:0 - writingItemAtURL:self.fileURL - options:0 - error:&error - byAccessor:^(NSURL *readingURL, NSURL *writingURL) { - NSMutableDictionary *dictionary = - [self heartbeatDictionaryWithFileURL:readingURL]; - dictionary[tag] = date; - NSError *error; - isSuccess = [self writeDictionary:dictionary - forWritingURL:writingURL - error:&error]; - }]; + [self.fileCoordinator + coordinateReadingItemAtURL:self.fileURL + options:0 + writingItemAtURL:self.fileURL + options:0 + error:&error + byAccessor:^(NSURL *readingURL, NSURL *writingURL) { + NSMutableDictionary *heartbeatDictionary = + [[self heartbeatDictionaryWithFileURL:readingURL] mutableCopy]; + heartbeatDictionary[tag] = date; + NSError *error; + isSuccess = [self writeDictionary:[heartbeatDictionary copy] + forWritingURL:writingURL + error:&error]; + }]; return isSuccess; } -- (BOOL)writeDictionary:(NSMutableDictionary *)dictionary +- (NSDictionary *)heartbeatDictionaryWithFileURL:(NSURL *)readingFileURL { + NSDictionary *heartbeatDictionary; + + NSError *error; + NSData *objectData = [NSData dataWithContentsOfURL:readingFileURL options:0 error:&error]; + + if (objectData.length > 0 && error == nil) { + NSSet *objectClasses = + [NSSet setWithArray:@[ NSDictionary.class, NSDate.class, NSString.class ]]; + heartbeatDictionary = [GULSecureCoding unarchivedObjectOfClasses:objectClasses + fromData:objectData + error:&error]; + } + + if (heartbeatDictionary.count == 0 || error != nil) { + heartbeatDictionary = [NSDictionary dictionary]; + } + + return heartbeatDictionary; +} + +- (BOOL)writeDictionary:(NSDictionary *)dictionary forWritingURL:(NSURL *)writingFileURL error:(NSError **)outError { - NSData *data = [GULSecureCoding archivedDataWithRootObject:dictionary error:outError]; - if (*outError != nil) { - return false; - } else { - return [data writeToURL:writingFileURL atomically:YES]; + // Archive a mutable copy `dictionary` for writing to disk. This is done for + // backwards compatibility. See Google Utilities issue #36 for more context. + // TODO: Remove usage of mutable copy in a future version of Google Utilities. + NSData *data = [GULSecureCoding archivedDataWithRootObject:[dictionary mutableCopy] + error:outError]; + if (data.length == 0) { + return NO; } + + return [data writeToURL:writingFileURL atomically:YES]; } @end diff --git a/GoogleUtilities/Environment/GULHeartbeatDateStorageUserDefaults.m b/GoogleUtilities/Environment/GULHeartbeatDateStorageUserDefaults.m new file mode 100644 index 0000000..f8f1159 --- /dev/null +++ b/GoogleUtilities/Environment/GULHeartbeatDateStorageUserDefaults.m @@ -0,0 +1,68 @@ +/* + * Copyright 2021 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 "GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorageUserDefaults.h" + +@interface GULHeartbeatDateStorageUserDefaults () + +/** The storage to store the date of the last sent heartbeat. */ +@property(nonatomic, readonly) NSUserDefaults *userDefaults; + +/** The key for user defaults to store heartbeat information. */ +@property(nonatomic, readonly) NSString *key; + +@end + +@implementation GULHeartbeatDateStorageUserDefaults + +- (instancetype)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key { + self = [super init]; + if (self) { + _userDefaults = defaults; + _key = key; + } + return self; +} + +- (NSMutableDictionary *)heartbeatDictionaryFromDefaults { + NSDictionary *heartbeatDict = [self.userDefaults objectForKey:self.key]; + if (heartbeatDict != nil) { + return [heartbeatDict mutableCopy]; + } else { + return [NSMutableDictionary dictionary]; + } +} + +- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag { + NSDate *date = nil; + @synchronized(self.userDefaults) { + NSMutableDictionary *dict = [self heartbeatDictionaryFromDefaults]; + date = dict[tag]; + } + + return date; +} + +- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag { + @synchronized(self.userDefaults) { + NSMutableDictionary *dict = [self heartbeatDictionaryFromDefaults]; + dict[tag] = date; + [self.userDefaults setObject:dict forKey:self.key]; + } + return true; +} + +@end diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorable.h b/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorable.h new file mode 100644 index 0000000..43d3740 --- /dev/null +++ b/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorable.h @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * Describes an object that can store and fetch heartbeat dates for given tags. + */ +@protocol GULHeartbeatDateStorable + +/** + * Reads the date from the specified file for the given tag. + * @return Returns date if exists, otherwise `nil`. + */ +- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag; + +/** + * Saves the date for the specified tag in the specified file. + * @return YES on success, NO otherwise. + */ +- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorage.h b/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorage.h index 9432dfc..245b1a2 100644 --- a/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorage.h +++ b/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorage.h @@ -16,10 +16,15 @@ #import +#import "GULHeartbeatDateStorable.h" + NS_ASSUME_NONNULL_BEGIN +/// The name of the directory where the heartbeat data is stored. +extern NSString *const kGULHeartbeatStorageDirectory; + /// Stores either a date or a dictionary to a specified file. -@interface GULHeartbeatDateStorage : NSObject +@interface GULHeartbeatDateStorage : NSObject - (instancetype)init NS_UNAVAILABLE; diff --git a/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorageUserDefaults.h b/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorageUserDefaults.h new file mode 100644 index 0000000..e6c7dda --- /dev/null +++ b/GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorageUserDefaults.h @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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 + +#import "GULHeartbeatDateStorable.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Stores either a date or a dictionary to a specified file. +@interface GULHeartbeatDateStorageUserDefaults : NSObject + +/** + * Default initializer. tvOS can only write to the cache directory and + * there are no guarantees that the directory will persist. User defaults will + * be retained, so that should be used instead. + * @param defaults User defaults instance to store the heartbeat information. + * @param key The key to be used with the user defaults instance. + */ +- (instancetype)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Reads the date from the specified file for the given tag. + * @return Returns date if exists, otherwise `nil`. + */ +- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag; + +/** + * Saves the date for the specified tag in the specified file. + * @return YES on success, NO otherwise. + */ +- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleUtilities/ISASwizzler/GULObjectSwizzler+Internal.h b/GoogleUtilities/ISASwizzler/GULObjectSwizzler+Internal.h index 2b09108..26d8655 100644 --- a/GoogleUtilities/ISASwizzler/GULObjectSwizzler+Internal.h +++ b/GoogleUtilities/ISASwizzler/GULObjectSwizzler+Internal.h @@ -14,6 +14,8 @@ #import "GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULObjectSwizzler.h" +FOUNDATION_EXPORT NSString *kGULSwizzlerAssociatedObjectKey; + @interface GULObjectSwizzler (Internal) - (void)swizzledObjectHasBeenDeallocatedWithGeneratedSubclass:(BOOL)isInstanceOfGeneratedSubclass; diff --git a/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m b/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m index d4bbefd..e147b53 100644 --- a/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m +++ b/GoogleUtilities/ISASwizzler/GULObjectSwizzler.m @@ -16,6 +16,7 @@ #import +#import "GoogleUtilities/ISASwizzler/GULObjectSwizzler+Internal.h" #import "GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULSwizzledObject.h" @implementation GULObjectSwizzler { @@ -80,7 +81,7 @@ } GULObjectSwizzler *existingSwizzler = - [[self class] getAssociatedObject:object key:kSwizzlerAssociatedObjectKey]; + [[self class] getAssociatedObject:object key:kGULSwizzlerAssociatedObjectKey]; if ([existingSwizzler isKindOfClass:[GULObjectSwizzler class]]) { // The object has been swizzled already, no need to swizzle again. return existingSwizzler; @@ -130,7 +131,7 @@ __strong id swizzledObject = _swizzledObject; GULObjectSwizzler *existingSwizzler = - [[self class] getAssociatedObject:swizzledObject key:kSwizzlerAssociatedObjectKey]; + [[self class] getAssociatedObject:swizzledObject key:kGULSwizzlerAssociatedObjectKey]; if (existingSwizzler != nil) { NSAssert(existingSwizzler == self, @"The swizzled object has a different swizzler."); // The object has been swizzled already. @@ -139,7 +140,7 @@ if (swizzledObject) { [GULObjectSwizzler setAssociatedObject:swizzledObject - key:kSwizzlerAssociatedObjectKey + key:kGULSwizzlerAssociatedObjectKey value:self association:GUL_ASSOCIATION_RETAIN]; diff --git a/GoogleUtilities/ISASwizzler/GULSwizzledObject.m b/GoogleUtilities/ISASwizzler/GULSwizzledObject.m index 6ea7028..bf3c707 100644 --- a/GoogleUtilities/ISASwizzler/GULSwizzledObject.m +++ b/GoogleUtilities/ISASwizzler/GULSwizzledObject.m @@ -17,7 +17,7 @@ #import "GoogleUtilities/ISASwizzler/GULObjectSwizzler+Internal.h" #import "GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULSwizzledObject.h" -NSString *kSwizzlerAssociatedObjectKey = @"gul_objectSwizzler"; +NSString *kGULSwizzlerAssociatedObjectKey = @"gul_objectSwizzler"; @interface GULSwizzledObject () @@ -46,7 +46,7 @@ NSString *kSwizzlerAssociatedObjectKey = @"gul_objectSwizzler"; } - (GULObjectSwizzler *)gul_objectSwizzler { - return [GULObjectSwizzler getAssociatedObject:self key:kSwizzlerAssociatedObjectKey]; + return [GULObjectSwizzler getAssociatedObject:self key:kGULSwizzlerAssociatedObjectKey]; } #pragma mark - Donor methods diff --git a/GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULSwizzledObject.h b/GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULSwizzledObject.h index 314ceec..fc07f0a 100644 --- a/GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULSwizzledObject.h +++ b/GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULSwizzledObject.h @@ -18,8 +18,6 @@ @class GULObjectSwizzler; -FOUNDATION_EXPORT NSString *kSwizzlerAssociatedObjectKey; - /** This class exists as a method donor. These methods will be added to all objects that are * swizzled by the object swizzler. This class should not be instantiated. */ diff --git a/GoogleUtilities/MethodSwizzler/GULSwizzler.m b/GoogleUtilities/MethodSwizzler/GULSwizzler.m index 6b17408..7f0c82e 100644 --- a/GoogleUtilities/MethodSwizzler/GULSwizzler.m +++ b/GoogleUtilities/MethodSwizzler/GULSwizzler.m @@ -65,7 +65,7 @@ dispatch_queue_t GetGULSwizzlingQueue(void) { [inv setArgument:&(currentImp) atIndex:2]; [inv setArgument:&(newImp) atIndex:3]; [inv setArgument:&(resolvedClass) atIndex:4]; - [inv setArgument:(void *_Nonnull) & (selector) atIndex:5]; + [inv setArgument:(void *_Nonnull)&(selector) atIndex:5]; [inv invoke]; } } diff --git a/GoogleUtilities/README.md b/GoogleUtilities/README.md deleted file mode 100644 index 75efe77..0000000 --- a/GoogleUtilities/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Google Utilities - -## Overview - -The GoogleUtilities pod is a set of utilities organized into CocoaPods subspecs. -See the [podspec](../GoogleUtilities.podspec) for a summary of what utilities -are currently included. They're used by Firebase and other Google products. - -Direct usage by non-Google products and CocoaPods is **NOT** currently -recommended or supported in general. - -**However**, we do specifically recommend that non-Google SDKs can use the App Delegate Swizzler to hook into app delegate methods as this reduces conflicts with multiple SDKs trying to do the same. - -Instructions on how to adopt the app delegate swizzler for use by your SDK are available [here](./AppDelegateSwizzler/README.md). - -## Development - -Follow the subsequent instructions to develop, debug, unit test, and -integration test FirebaseFunctions: - -### Prereqs - -- At least CocoaPods 1.7.0 - -### To Develop - -- Run `pod gen GoogleUtilities.podspec` -- `open gen/GoogleUtilities/GoogleUtilities.xcworkspace` - -OR these two commands can be combined with - -- `pod gen GoogleUtilities.podspec --auto-open --gen-directory="gen" --clean` - -You're now in an Xcode workspace generate for building, debugging and -testing the GoogleUtilities CocoaPod. - -### Running Unit Tests - -Choose the one of the Tests* schemes and press Command-u. diff --git a/GoogleUtilities/SwizzlerTestHelpers/GULSwizzlingCache.h b/GoogleUtilities/SwizzlerTestHelpers/GULSwizzlingCache.h index 552c58f..9e51b88 100644 --- a/GoogleUtilities/SwizzlerTestHelpers/GULSwizzlingCache.h +++ b/GoogleUtilities/SwizzlerTestHelpers/GULSwizzlingCache.h @@ -16,7 +16,7 @@ #import -/** This class handles the caching and retreival of IMPs as we swizzle and unswizzle them. It uses +/** This class handles the caching and retrieval of IMPs as we swizzle and unswizzle them. It uses * two C++ STL unordered_maps as the underlying data store. This class is NOT thread safe. */ @interface GULSwizzlingCache : NSObject diff --git a/GoogleUtilities/SwizzlerTestHelpers/GULSwizzlingCache_Private.h b/GoogleUtilities/SwizzlerTestHelpers/GULSwizzlingCache_Private.h index 0f2269b..245dfeb 100644 --- a/GoogleUtilities/SwizzlerTestHelpers/GULSwizzlingCache_Private.h +++ b/GoogleUtilities/SwizzlerTestHelpers/GULSwizzlingCache_Private.h @@ -18,7 +18,7 @@ /** Checks if we've swizzled the currentIMP and returns the original IMP that would be invoked if * we hadn't swizzled it in the first place. This method is private because consumers don't need it - * to cache or retrive any IMPs. It is used internally and for certain asserts in GULSwizzler. + * to cache or retrieve any IMPs. It is used internally and for certain asserts in GULSwizzler. * * @param currentIMP The IMP returned by class_getMethodImplementation. * @return The original IMP that would be invoked if we hadn't swizzled at all, and in cases where diff --git a/GoogleUtilities/Tests/Unit/Environment/GULHeartbeatDateStorageTest.m b/GoogleUtilities/Tests/Unit/Environment/GULHeartbeatDateStorageTest.m index 096cf15..5f74423 100644 --- a/GoogleUtilities/Tests/Unit/Environment/GULHeartbeatDateStorageTest.m +++ b/GoogleUtilities/Tests/Unit/Environment/GULHeartbeatDateStorageTest.m @@ -16,75 +16,423 @@ #import #import "GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorage.h" +#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULSecureCoding.h" + +// Import specific version implementations for compatibility testing. +#import "GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.3.1.h" +#import "GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.4.0.h" @interface GULHeartbeatDateStorageTest : XCTestCase @property(nonatomic) GULHeartbeatDateStorage *storage; @end -static NSString *const kTestFileName = @"GULStorageHeartbeatTest"; +static NSString *const kTestFileName = @"GULStorageHeartbeatTestFile"; -@implementation GULHeartbeatDateStorageTest +@implementation GULHeartbeatDateStorageTest { + BOOL _rootDirectoryCreated; +} -- (void)setUp { -#if TARGET_OS_TV - NSArray *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); -#else - NSArray *path = - NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); -#endif - NSString *rootPath = [path firstObject]; - XCTAssertNotNil(rootPath); - NSURL *rootURL = [NSURL fileURLWithPath:rootPath]; +- (BOOL)setUpWithError:(NSError *__autoreleasing _Nullable *)error { + [super setUpWithError:error]; - NSError *error; - if (![rootURL checkResourceIsReachableAndReturnError:&error]) { - XCTAssert([[NSFileManager defaultManager] createDirectoryAtURL:rootURL - withIntermediateDirectories:YES - attributes:nil - error:&error], - @"Error: %@", error); + NSError *directoryError; + if (!_rootDirectoryCreated) { + _rootDirectoryCreated = [self createRootDirectoryWithError:&directoryError]; } - self.storage = [[GULHeartbeatDateStorage alloc] initWithFileName:kTestFileName]; + BOOL success = _rootDirectoryCreated; + if (!success && error) { + *error = [NSError errorWithDomain:@"com.GULHeartbeatDateStorageTest.ErrorDomain" + code:1 + userInfo:@{NSUnderlyingErrorKey : directoryError}]; + } + return success; +} + +- (void)setUp { + [super setUp]; + + self.storage = [[GULHeartbeatDateStorage alloc] initWithFileName:kTestFileName]; [self assertInitializationDoesNotAccessFileSystem]; } - (void)tearDown { - [[NSFileManager defaultManager] removeItemAtURL:[self.storage fileURL] error:nil]; + [super tearDown]; + + // Removes the Heartbeat Storage Directory if it exists. + NSURL *directoryURL = [self pathURLForDirectory:kGULHeartbeatStorageDirectory]; + [[NSFileManager defaultManager] removeItemAtURL:directoryURL error:nil]; + self.storage = nil; } +#pragma mark - Public API Tests + - (void)testHeartbeatDateForTag { - NSDate *now = [NSDate date]; - [self.storage setHearbeatDate:now forTag:@"fire-iid"]; - XCTAssertEqual([now timeIntervalSinceReferenceDate], - [[self.storage heartbeatDateForTag:@"fire-iid"] timeIntervalSinceReferenceDate]); + // 1. Tags and saves heartbeat info, which creates the storage directory & file as side effects. + NSDate *date = [NSDate date]; + NSString *tag = @"tag"; + BOOL successfulSave = [self.storage setHearbeatDate:date forTag:tag]; + XCTAssert(successfulSave); + + // 2. Retrieve saved heartbeat info. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:tag]; + + // 3. Assert that requested heartbeat info matches what was stored. + // This implies the storage directory & file were created, written to, and read from. + XCTAssertEqualObjects(retrievedDate, date); } -#pragma mark - Private Helpers +/// Heartbeat info is requested when the storage directory already exists (i.e. it was created in a +/// previous app launch). +- (void)testHeartbeatDateForTagWhenHeartbeatStorageDirectoryExists { + // 1. Manually create the heartbeat directory. + NSURL *heartbeatStorageDirectoryURL = [self pathURLForDirectory:kGULHeartbeatStorageDirectory]; + NSError *error; + BOOL directoryCreated = [self explicitlyCreateDirectoryForURL:heartbeatStorageDirectoryURL + withError:&error]; + XCTAssert(directoryCreated); + + // 2. Populate the storage file with heartbeat info. + // 2.1 Create a dictionary with heartbeat info. + NSDate *storedDate = [NSDate distantPast]; + NSString *storedTag = @"stored-tag"; + NSDictionary *storedHeartbeatDictionary = @{storedTag : storedDate}; + // 2.2 Encode the dictionary. + NSError *archiveError; + NSData *data = [GULSecureCoding archivedDataWithRootObject:storedHeartbeatDictionary + error:&archiveError]; + XCTAssertNotNil(data); + XCTAssertNil(archiveError); + + // 2.3 Write the encoded dictionary to file. + NSURL *heartbeatStorageFileURL = [self fileURLForDirectory:heartbeatStorageDirectoryURL]; + [data writeToURL:heartbeatStorageFileURL atomically:YES]; + XCTAssertNotNil(data); + + // 3. Retrieve the stored heartbeat info and validate that it matches the info that was stored. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:storedTag]; + + XCTAssertEqualObjects(retrievedDate, storedDate); +} + +- (void)testHeartbeatDateForTagWhenReturnedDateIsNil { + NSString *nonexistentTag = @"missing-tag"; + NSDate *nilDate = [self.storage heartbeatDateForTag:nonexistentTag]; + XCTAssertNil(nilDate); +} + +- (void)testSetHeartbeatDateForTagWhenExpectingFailure { + // The `setHearbeatDate: forTag:` API is expected to return NO if it is unable to write the + // heartbeat info to file. To verify the API successfully fails, an invalid string is used to + // create an invalid instance of `GULHeartbeatDateStorage` by supplying an invalid filename. + // This replicates a file system error that prohibits `setHearbeatDate: forTag:` from successfully + // storing the heartbeat info. + NSString *invalidFileName = @""; + GULHeartbeatDateStorage *invalidStorage = + [[GULHeartbeatDateStorage alloc] initWithFileName:invalidFileName]; + + BOOL success = [invalidStorage setHearbeatDate:NSDate.date forTag:@"tag"]; + XCTAssertFalse(success); +} + +- (void)testSetHeartbeatDateForTag { + NSDate *date = [NSDate date]; + NSString *tag = @"tag"; + + // 1. Retrieve heartbeat info that is not stored and assert the retrieved info is nil. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertNil(retrievedDate); + + // 2. Save the heartbeat info and assert the save was successful. + BOOL successfulSave = [self.storage setHearbeatDate:date forTag:tag]; + XCTAssert(successfulSave); + + // 3. Retrieve heartbeat info that is now stored and assert the retrieved info is accurate. + retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertEqualObjects(retrievedDate, date); +} + +- (void)testHeartbeatDateForTagWhenHeartbeatFileReturnsInvalidData { + // 1. Manually create the heartbeat directory. + NSURL *heartbeatStorageDirectoryURL = [self pathURLForDirectory:kGULHeartbeatStorageDirectory]; + NSError *error; + BOOL directoryCreated = [self explicitlyCreateDirectoryForURL:heartbeatStorageDirectoryURL + withError:&error]; + XCTAssert(directoryCreated); + + // 2. Populate the storage file with invalid data. + NSData *corruptedData = [@"abc" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNotNil(corruptedData); + NSURL *heartbeatStorageFileURL = [self fileURLForDirectory:heartbeatStorageDirectoryURL]; + [corruptedData writeToURL:heartbeatStorageFileURL atomically:YES]; + + // 3. Retrieve saved heartbeat info and assert that it is nil. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:@"tag"]; + XCTAssertNil(retrievedDate); +} + +- (void)testHeartbeatDateForTagWhenHeartbeatFileContainsUnexpectedContent { + // 1. Manually create the heartbeat directory. + NSURL *heartbeatStorageDirectoryURL = [self pathURLForDirectory:kGULHeartbeatStorageDirectory]; + NSError *error; + BOOL directoryCreated = [self explicitlyCreateDirectoryForURL:heartbeatStorageDirectoryURL + withError:&error]; + XCTAssert(directoryCreated); + + // 2. Populate the storage file with unexpected content. + NSArray *array = @[ [NSDate distantPast], [NSDate date], [NSDate distantFuture] ]; + NSError *archiveError; + NSData *data = [GULSecureCoding archivedDataWithRootObject:array error:&archiveError]; + XCTAssertNotNil(data); + XCTAssert(data.length > 0); + XCTAssertNil(archiveError); + NSURL *heartbeatStorageFileURL = [self fileURLForDirectory:heartbeatStorageDirectoryURL]; + [data writeToURL:heartbeatStorageFileURL atomically:YES]; + + // 3. Retrieve saved heartbeat info and assert that it is nil. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:@"tag"]; + XCTAssertNil(retrievedDate); +} + +- (void)testSetHeartbeatDateForTagWhenHeartbeatFileContainsUnexpectedDictionaryContent { + // 1. Manually create the heartbeat directory. + NSURL *heartbeatStorageDirectoryURL = [self pathURLForDirectory:kGULHeartbeatStorageDirectory]; + NSError *error; + BOOL directoryCreated = [self explicitlyCreateDirectoryForURL:heartbeatStorageDirectoryURL + withError:&error]; + XCTAssert(directoryCreated); + + // 2. Populate the storage file with unexpected content. + NSDictionary *heartbeatDict = @{@"tag" : [NSDate distantPast]}; + NSDictionary *nestedHeartbeatDict = @{@"tag" : heartbeatDict}; + NSError *archiveError; + NSData *data = [GULSecureCoding archivedDataWithRootObject:nestedHeartbeatDict + error:&archiveError]; + XCTAssertNotNil(data); + XCTAssert(data.length > 0); + XCTAssertNil(archiveError); + + NSURL *heartbeatStorageFileURL = [self fileURLForDirectory:heartbeatStorageDirectoryURL]; + [data writeToURL:heartbeatStorageFileURL atomically:YES]; + + // 3. Create heartbeat info. + NSDate *date = [NSDate date]; + NSString *tag = @"tag"; + + // 4. Retrieve heartbeat info that is not stored and assert the retrieved info is nil. + // This assertion implies type validation in `heartbeatDateForTag:` has worked correctly. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertNil(retrievedDate); + + // 5. Save the heartbeat info and assert the save was successful. + BOOL successfulSave = [self.storage setHearbeatDate:date forTag:tag]; + XCTAssert(successfulSave); + + // 6. Retrieve heartbeat info that is now stored and assert the retrieved info is accurate. + retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertEqualObjects(retrievedDate, date); +} + +- (void)testSetHeartbeatDateForTagWhenHeartbeatFileContainsUnexpectedContent { + // 1. Manually create the heartbeat directory. + NSURL *heartbeatStorageDirectoryURL = [self pathURLForDirectory:kGULHeartbeatStorageDirectory]; + NSError *error; + BOOL directoryCreated = [self explicitlyCreateDirectoryForURL:heartbeatStorageDirectoryURL + withError:&error]; + XCTAssert(directoryCreated); + + // 2. Populate the storage file with unexpected content. + NSArray *array = @[ [NSDate distantPast], [NSDate date], [NSDate distantFuture] ]; + NSError *archiveError; + NSData *data = [GULSecureCoding archivedDataWithRootObject:array error:&archiveError]; + XCTAssertNotNil(data); + XCTAssert(data.length > 0); + XCTAssertNil(archiveError); + + NSURL *heartbeatStorageFileURL = [self fileURLForDirectory:heartbeatStorageDirectoryURL]; + [data writeToURL:heartbeatStorageFileURL atomically:YES]; + + // 3. Create heartbeat info. + NSDate *date = [NSDate date]; + NSString *tag = @"tag"; + + // 4. Retrieve heartbeat info that is not stored and assert the retrieved info is nil. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertNil(retrievedDate); + + // 5. Save the heartbeat info and assert the save was successful. + BOOL successfulSave = [self.storage setHearbeatDate:date forTag:tag]; + XCTAssert(successfulSave); + + // 6. Retrieve heartbeat info that is now stored and assert the retrieved info is accurate. + retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertEqualObjects(retrievedDate, date); +} + +- (void)testSetHeartbeatDateForTagWhenHeartbeatFileReturnsInvalidData { + // 1. Manually create the heartbeat directory. + NSURL *heartbeatStorageDirectoryURL = [self pathURLForDirectory:kGULHeartbeatStorageDirectory]; + NSError *error; + BOOL directoryCreated = [self explicitlyCreateDirectoryForURL:heartbeatStorageDirectoryURL + withError:&error]; + XCTAssert(directoryCreated); + + // 2. Populate the storage file with invalid data. + NSData *corruptedData = [@"abc" dataUsingEncoding:NSUTF8StringEncoding]; + XCTAssertNotNil(corruptedData); + NSURL *heartbeatStorageFileURL = [self fileURLForDirectory:heartbeatStorageDirectoryURL]; + [corruptedData writeToURL:heartbeatStorageFileURL atomically:YES]; + + // 3. Tag and save heartbeat info. This should overwrite the invalid data with a + // correctly encoded heartbeat dictionary. + NSDate *date = [NSDate date]; + NSString *tag = @"tag"; + BOOL successfulSave = [self.storage setHearbeatDate:date forTag:tag]; + XCTAssert(successfulSave); + + // 4. Retrieve saved heartbeat info. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:tag]; + + // 5. Assert that requested heartbeat info matches what was stored. + // This implies the storage directory & file were created, written to, and read from. + XCTAssertEqualObjects(retrievedDate, date); +} + +- (void)testConformsToHeartbeatStorableProtocol { + XCTAssertTrue([self.storage conformsToProtocol:@protocol(GULHeartbeatDateStorable)]); +} + +#pragma mark - Version Compatibility (#36) + +- (void)testCompatibility_pre7_4_0 { + NSString *tag = @"tag"; + + // 1. Store heartbeat using current heartbeat API. + NSDate *distantPast = [NSDate distantPast]; + BOOL successfulSave = [self.storage setHearbeatDate:distantPast forTag:tag]; + XCTAssert(successfulSave); + [self assertStoredHeartbeatDictionaryIsKindOf:[NSMutableDictionary class]]; + + // 2. Developer downgrades to below 7.4.0. + + // 3. Store heartbeat from pre-7.4.0 API and verify success. + NSDate *storedDate = [NSDate date]; + GULHeartbeatDateStorage7_3_1 *storage7_3_1 = + [[GULHeartbeatDateStorage7_3_1 alloc] initWithFileName:kTestFileName]; + // The following line caused crashes after persistent storage modifications by 7.4.0. + successfulSave = [storage7_3_1 setHearbeatDate:storedDate forTag:tag]; + XCTAssert(successfulSave); + [self assertStoredHeartbeatDictionaryIsKindOf:[NSMutableDictionary class]]; + + NSDate *retrievedDate = [storage7_3_1 heartbeatDateForTag:tag]; + XCTAssertEqualObjects(retrievedDate, storedDate); +} + +- (void)testForwardCompatibility7_4_0 { + NSString *tag = @"tag"; + + // 1. Store heartbeat using heartbeat API from 7.4.0. (Immutable info is written to disk.) + NSDate *storedDate = [NSDate distantPast]; + GULHeartbeatDateStorage7_4_0 *storage7_4_0 = + [[GULHeartbeatDateStorage7_4_0 alloc] initWithFileName:kTestFileName]; + // The following line caused crashes after persistent storage modifications by 7.4.0. + BOOL successfulSave = [storage7_4_0 setHearbeatDate:storedDate forTag:tag]; + XCTAssert(successfulSave); + [self assertStoredHeartbeatDictionaryIsKindOf:[NSDictionary class]]; + + // 2. Developer upgrades to current version. + + // 3. Retrieve and then store heartbeat from current API and verify success. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertNotNil(retrievedDate); + XCTAssertEqualObjects(retrievedDate, storedDate); + + NSDate *date = [NSDate date]; + successfulSave = [self.storage setHearbeatDate:date forTag:tag]; + XCTAssert(successfulSave); + [self assertStoredHeartbeatDictionaryIsKindOf:[NSMutableDictionary class]]; + + retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertNotNil(retrievedDate); + XCTAssertEqualObjects(retrievedDate, date); +} + +#pragma mark - Testing Utilities + +- (BOOL)createRootDirectoryWithError:(NSError *__autoreleasing _Nullable *)outError { + NSArray *paths; +#if TARGET_OS_TV + paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); +#else + paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); +#endif // TARGET_OS_TV + NSString *rootPath = [paths lastObject]; + NSURL *rootURL = [NSURL fileURLWithPath:rootPath]; + + BOOL rootDirectoryExists = [rootURL checkResourceIsReachableAndReturnError:nil]; + if (!rootDirectoryExists) { + rootDirectoryExists = [[NSFileManager defaultManager] createDirectoryAtURL:rootURL + withIntermediateDirectories:YES + attributes:nil + error:outError]; + } + return rootDirectoryExists; +} + +- (BOOL)explicitlyCreateDirectoryForURL:(NSURL *)directoryPathURL withError:(NSError **)outError { + BOOL success = false; + if (![directoryPathURL checkResourceIsReachableAndReturnError:outError]) { + success = [[NSFileManager defaultManager] createDirectoryAtURL:directoryPathURL + withIntermediateDirectories:YES + attributes:nil + error:outError]; + } + return success; +} - (void)assertInitializationDoesNotAccessFileSystem { - NSURL *fileURL = [self heartbeatFileURL]; - NSError *error; - BOOL fileIsReachable = [fileURL checkResourceIsReachableAndReturnError:&error]; - XCTAssertFalse(fileIsReachable, + NSString *directoryURL = [[self pathURLForDirectory:kGULHeartbeatStorageDirectory] path]; + BOOL isDir; + BOOL directoryIsReachable = + [[NSFileManager defaultManager] fileExistsAtPath:directoryURL isDirectory:&isDir] && isDir; + XCTAssertFalse(directoryIsReachable, + @"The Heartbeat Storage Directory already exists. " @"GULHeartbeatDateStorage initialization should not access the file system."); - XCTAssertNotNil(error, @"Error: %@", error); } -- (NSURL *)heartbeatFileURL { +- (NSURL *)pathURLForDirectory:(NSString *)directory { + NSArray *paths; #if TARGET_OS_TV - NSArray *path = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); #else - NSArray *path = - NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); -#endif - NSString *rootPath = [path firstObject]; - NSArray *components = @[ rootPath, @"Google/FIRApp", kTestFileName ]; - NSString *fileString = [NSString pathWithComponents:components]; - NSURL *fileURL = [NSURL fileURLWithPath:fileString]; + paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); +#endif // TARGET_OS_TV + NSString *rootPath = [paths lastObject]; + NSURL *rootURL = [NSURL fileURLWithPath:rootPath]; + NSURL *directoryURL = [rootURL URLByAppendingPathComponent:directory]; + return directoryURL; +} + +- (NSURL *)fileURLForDirectory:(NSURL *)directoryURL { + NSURL *fileURL = [directoryURL URLByAppendingPathComponent:kTestFileName]; return fileURL; } +- (void)assertStoredHeartbeatDictionaryIsKindOf:(Class)class { + NSURL *heartbeatStorageDirectoryURL = [self pathURLForDirectory:kGULHeartbeatStorageDirectory]; + NSURL *heartbeatStorageFileURL = [self fileURLForDirectory:heartbeatStorageDirectoryURL]; + NSData *objectData = [NSData dataWithContentsOfURL:heartbeatStorageFileURL options:0 error:nil]; + __auto_type objectClasses = + [NSSet setWithArray:@[ NSDictionary.class, NSDate.class, NSString.class ]]; + NSMutableDictionary *heartbeatDict = [GULSecureCoding unarchivedObjectOfClasses:objectClasses + fromData:objectData + error:nil]; + if (class == [NSDictionary class]) { + XCTAssertFalse([heartbeatDict isKindOfClass:[NSMutableDictionary class]]); + } + XCTAssertTrue([heartbeatDict isKindOfClass:class]); +} + @end diff --git a/GoogleUtilities/Tests/Unit/Environment/GULHeartbeatDateStorageUserDefaultsTest.m b/GoogleUtilities/Tests/Unit/Environment/GULHeartbeatDateStorageUserDefaultsTest.m new file mode 100644 index 0000000..18c13b2 --- /dev/null +++ b/GoogleUtilities/Tests/Unit/Environment/GULHeartbeatDateStorageUserDefaultsTest.m @@ -0,0 +1,89 @@ +/* + * Copyright 2021 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 +#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorageUserDefaults.h" + +@interface GULHeartbeatDateStorageUserDefaultsTest : XCTestCase +@property(nonatomic) GULHeartbeatDateStorageUserDefaults *storage; +@property(nonatomic) NSUserDefaults *defaults; +@end + +@implementation GULHeartbeatDateStorageUserDefaultsTest + +- (void)setUp { + NSString *suiteName = [self userDefaultsSuiteName]; + self.defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName]; + self.storage = [[GULHeartbeatDateStorageUserDefaults alloc] initWithDefaults:self.defaults + key:@"test_root"]; +} + +- (void)tearDown { + [self.defaults removePersistentDomainForName:[self userDefaultsSuiteName]]; + self.defaults = nil; + + self.storage = nil; +} + +#pragma mark - Public API Tests + +- (void)testHeartbeatDateForTag { + // 1. Tag and save some heartbeat info. + NSDate *storedDate = [NSDate date]; + NSString *tag = @"fire-iid"; + [self.storage setHearbeatDate:storedDate forTag:tag]; + + // 2. Retrieve the stored heartbeat info and assert the retrieved info is accurate. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertEqualObjects(retrievedDate, storedDate); +} + +- (void)testHeartbeatDateForTagWhenReturnedDateIsNil { + NSString *nonexistentTag = @"missing-tag"; + NSDate *nilDate = [self.storage heartbeatDateForTag:nonexistentTag]; + XCTAssertNil(nilDate); +} + +- (void)testSetHeartbeatDateForTag { + NSDate *date = [NSDate date]; + NSString *tag = @"tag"; + + // 1. Retrieve heartbeat info that is not stored and assert the retrieved info is nil. + NSDate *retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertNil(retrievedDate); + + // 2. Save the heartbeat info and assert the save was successful. + BOOL successfulSave = [self.storage setHearbeatDate:date forTag:tag]; + XCTAssert(successfulSave); + + // 3. Retrieve heartbeat info that is now stored and assert the retrieved info is accurate. + retrievedDate = [self.storage heartbeatDateForTag:tag]; + XCTAssertEqualObjects(retrievedDate, date); +} + +- (void)testConformsToHeartbeatStorableProtocol { + XCTAssertTrue([self.storage conformsToProtocol:@protocol(GULHeartbeatDateStorable)]); +} + +#pragma mark - Testing Utilities + +- (NSString *)userDefaultsSuiteName { + NSCharacterSet *lettersToTrim = [[NSCharacterSet letterCharacterSet] invertedSet]; + NSString *nameWithSpaces = [self.name stringByTrimmingCharactersInSet:lettersToTrim]; + return [nameWithSpaces stringByReplacingOccurrencesOfString:@" " withString:@"_"]; +} + +@end diff --git a/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m b/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m index 5fa3486..7e79095 100644 --- a/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m +++ b/GoogleUtilities/Tests/Unit/Environment/GULKeychainStorageTests.m @@ -87,6 +87,8 @@ // 3. Read existing object which is not present in in-memory cache. [self.cache removeAllObjects]; + // TODO: Evaluate if GULKeychainStorage needs an API that takes set of classes. (#42) + // The following method causes an NSKeyedUnarchiver-related runtime warning log. [self assertSuccessReadObject:@{@"key" : @"value"} forKey:@"test-key1" class:[NSDictionary class] diff --git a/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.3.1.h b/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.3.1.h new file mode 100644 index 0000000..ef983da --- /dev/null +++ b/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.3.1.h @@ -0,0 +1,51 @@ +/* + * Copyright 2021 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. + */ + +// MARK: This file is strictly for version compatibility testing. + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// Stores either a date or a dictionary to a specified file. +@interface GULHeartbeatDateStorage7_3_1 : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +@property(nonatomic, readonly) NSURL *fileURL; + +/** + * Default initializer. + * @param fileName The name of the file to store the date information. + * exist, it will be created if needed. + */ +- (instancetype)initWithFileName:(NSString *)fileName; + +/** + * Reads the date from the specified file for the given tag. + * @return Returns date if exists, otherwise `nil`. + */ +- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag; + +/** + * Saves the date for the specified tag in the specified file. + * @return YES on success, NO otherwise. + */ +- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.3.1.m b/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.3.1.m new file mode 100644 index 0000000..136169c --- /dev/null +++ b/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.3.1.m @@ -0,0 +1,161 @@ +/* + * Copyright 2021 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. + */ + +// MARK: This file is strictly for version compatibility testing. + +#import "GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.3.1.h" +#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULSecureCoding.h" + +@interface GULHeartbeatDateStorage7_3_1 () +/** The storage to store the date of the last sent heartbeat. */ +@property(nonatomic, readonly) NSFileCoordinator *fileCoordinator; +/** The name of the file that stores heartbeat information. */ +@property(nonatomic, readonly) NSString *fileName; +@end + +@implementation GULHeartbeatDateStorage7_3_1 + +@synthesize fileURL = _fileURL; + +- (instancetype)initWithFileName:(NSString *)fileName { + if (fileName == nil) { + return nil; + } + + self = [super init]; + if (self) { + _fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; + _fileName = fileName; + } + return self; +} + +/** Lazy getter for fileURL + * @return fileURL where heartbeat information is stored. + */ +- (NSURL *)fileURL { + if (!_fileURL) { + NSURL *directoryURL = [[self class] directoryPathURL]; + [[self class] checkAndCreateDirectory:directoryURL fileCoordinator:_fileCoordinator]; + _fileURL = [directoryURL URLByAppendingPathComponent:_fileName]; + } + return _fileURL; +} + +/** Returns the URL path of the directory for heartbeat storage data. + * @return the URL path of the directory for heartbeat storage data. + */ ++ (NSURL *)directoryPathURL { +#if TARGET_OS_TV + NSArray *paths = + NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); +#else + NSArray *paths = + NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); +#endif + NSArray *components = @[ paths.lastObject, @"Google/FIRApp" ]; + NSString *directoryString = [NSString pathWithComponents:components]; + NSURL *directoryURL = [NSURL fileURLWithPath:directoryString]; + return directoryURL; +} + +/** Checks and creates a directory for the directory specified by the + * directory url + * @param directoryPathURL The path to the directory which needs to be created. + * @param fileCoordinator The fileCoordinator object to coordinate writes to the directory. + */ ++ (void)checkAndCreateDirectory:(NSURL *)directoryPathURL + fileCoordinator:(NSFileCoordinator *)fileCoordinator { + NSError *fileCoordinatorError = nil; + [fileCoordinator + coordinateWritingItemAtURL:directoryPathURL + options:0 + error:&fileCoordinatorError + byAccessor:^(NSURL *writingDirectoryURL) { + NSError *error; + if (![writingDirectoryURL checkResourceIsReachableAndReturnError:&error]) { + // If fail creating the Application Support directory, log warning. + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtURL:writingDirectoryURL + withIntermediateDirectories:YES + attributes:nil + error:&error]; + } + }]; +} + +- (nullable NSMutableDictionary *)heartbeatDictionaryWithFileURL:(NSURL *)readingFileURL { + NSError *error; + NSMutableDictionary *dict; + NSData *objectData = [NSData dataWithContentsOfURL:readingFileURL options:0 error:&error]; + if (objectData == nil || error != nil) { + dict = [NSMutableDictionary dictionary]; + } else { + dict = [GULSecureCoding + unarchivedObjectOfClasses:[NSSet setWithArray:@[ NSDictionary.class, NSDate.class ]] + fromData:objectData + error:&error]; + if (dict == nil || error != nil) { + dict = [NSMutableDictionary dictionary]; + } + } + return dict; +} + +- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag { + __block NSMutableDictionary *dict; + NSError *error; + [self.fileCoordinator coordinateReadingItemAtURL:self.fileURL + options:0 + error:&error + byAccessor:^(NSURL *readingURL) { + dict = [self heartbeatDictionaryWithFileURL:readingURL]; + }]; + return dict[tag]; +} + +- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag { + NSError *error; + __block BOOL isSuccess = false; + [self.fileCoordinator coordinateReadingItemAtURL:self.fileURL + options:0 + writingItemAtURL:self.fileURL + options:0 + error:&error + byAccessor:^(NSURL *readingURL, NSURL *writingURL) { + NSMutableDictionary *dictionary = + [self heartbeatDictionaryWithFileURL:readingURL]; + dictionary[tag] = date; + NSError *error; + isSuccess = [self writeDictionary:dictionary + forWritingURL:writingURL + error:&error]; + }]; + return isSuccess; +} + +- (BOOL)writeDictionary:(NSMutableDictionary *)dictionary + forWritingURL:(NSURL *)writingFileURL + error:(NSError **)outError { + NSData *data = [GULSecureCoding archivedDataWithRootObject:dictionary error:outError]; + if (*outError != nil) { + return false; + } else { + return [data writeToURL:writingFileURL atomically:YES]; + } +} + +@end diff --git a/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.4.0.h b/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.4.0.h new file mode 100644 index 0000000..ce7da7c --- /dev/null +++ b/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.4.0.h @@ -0,0 +1,55 @@ +/* + * Copyright 2021 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. + */ + +// MARK: This file is strictly for version compatibility testing. + +#import + +#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULHeartbeatDateStorable.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const kGULHeartbeatStorageDirectory7_4_0; // Avoids duplicate linker error. + +/// Stores either a date or a dictionary to a specified file. +@interface GULHeartbeatDateStorage7_4_0 : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +@property(nonatomic, readonly) NSURL *fileURL; + +/** + * Default initializer. + * @param fileName The name of the file to store the date information. + * exist, it will be created if needed. + */ +- (instancetype)initWithFileName:(NSString *)fileName; + +/** + * Reads the date from the specified file for the given tag. + * @return Returns date if exists, otherwise `nil`. + */ +- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag; + +/** + * Saves the date for the specified tag in the specified file. + * @return YES on success, NO otherwise. + */ +- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.4.0.m b/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.4.0.m new file mode 100644 index 0000000..2ec621e --- /dev/null +++ b/GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.4.0.m @@ -0,0 +1,162 @@ +/* + * Copyright 2021 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. + */ + +// MARK: This file is strictly for version compatibility testing. + +#import "GoogleUtilities/Tests/Unit/Environment/Sources/GULHeartbeatDateStorageV7.4.0.h" +#import "GoogleUtilities/Environment/Public/GoogleUtilities/GULSecureCoding.h" + +// Add static to avoid linker error. +NSString *const kGULHeartbeatStorageDirectory7_4_0 = @"Google/FIRApp"; + +@interface GULHeartbeatDateStorage7_4_0 () +/** The storage to store the date of the last sent heartbeat. */ +@property(nonatomic, readonly) NSFileCoordinator *fileCoordinator; +/** The name of the file that stores heartbeat information. */ +@property(nonatomic, readonly) NSString *fileName; +@end + +@implementation GULHeartbeatDateStorage7_4_0 + +@synthesize fileURL = _fileURL; + +- (instancetype)initWithFileName:(NSString *)fileName { + if (fileName == nil) return nil; + + self = [super init]; + if (self) { + _fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil]; + _fileName = fileName; + } + return self; +} + +/** Lazy getter for fileURL. + * @return fileURL where heartbeat information is stored. + */ +- (NSURL *)fileURL { + if (!_fileURL) { + NSURL *directoryURL = [self directoryPathURL]; + [self checkAndCreateDirectory:directoryURL]; + _fileURL = [directoryURL URLByAppendingPathComponent:_fileName]; + } + return _fileURL; +} + +/** Returns the URL path of the directory for heartbeat storage data. + * @return the URL path of the directory for heartbeat storage data. + */ +- (NSURL *)directoryPathURL { + NSArray *paths; +#if TARGET_OS_TV + paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); +#else + paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); +#endif // TARGET_OS_TV + NSString *rootPath = [paths lastObject]; + NSURL *rootURL = [NSURL fileURLWithPath:rootPath]; + NSURL *directoryURL = [rootURL URLByAppendingPathComponent:kGULHeartbeatStorageDirectory7_4_0]; + return directoryURL; +} + +/** Check for the existence of the directory specified by the URL, and create it if it does not + * exist. + * @param directoryPathURL The path to the directory that needs to exist. + */ +- (void)checkAndCreateDirectory:(NSURL *)directoryPathURL { + NSError *fileCoordinatorError = nil; + [self.fileCoordinator + coordinateWritingItemAtURL:directoryPathURL + options:0 + error:&fileCoordinatorError + byAccessor:^(NSURL *writingDirectoryURL) { + NSError *error; + if (![writingDirectoryURL checkResourceIsReachableAndReturnError:&error]) { + NSError *error; + [[NSFileManager defaultManager] createDirectoryAtURL:writingDirectoryURL + withIntermediateDirectories:YES + attributes:nil + error:&error]; + } + }]; +} + +- (nullable NSDate *)heartbeatDateForTag:(NSString *)tag { + __block NSDictionary *heartbeatDictionary; + NSError *error; + [self.fileCoordinator coordinateReadingItemAtURL:self.fileURL + options:0 + error:&error + byAccessor:^(NSURL *readingURL) { + heartbeatDictionary = + [self heartbeatDictionaryWithFileURL:readingURL]; + }]; + return heartbeatDictionary[tag]; +} + +- (BOOL)setHearbeatDate:(NSDate *)date forTag:(NSString *)tag { + NSError *error; + __block BOOL isSuccess = false; + [self.fileCoordinator + coordinateReadingItemAtURL:self.fileURL + options:0 + writingItemAtURL:self.fileURL + options:0 + error:&error + byAccessor:^(NSURL *readingURL, NSURL *writingURL) { + NSMutableDictionary *heartbeatDictionary = + [[self heartbeatDictionaryWithFileURL:readingURL] mutableCopy]; + heartbeatDictionary[tag] = date; + NSError *error; + isSuccess = [self writeDictionary:heartbeatDictionary.copy + forWritingURL:writingURL + error:&error]; + }]; + return isSuccess; +} + +- (NSDictionary *)heartbeatDictionaryWithFileURL:(NSURL *)readingFileURL { + NSDictionary *heartbeatDictionary = nil; + + NSError *error; + NSData *objectData = [NSData dataWithContentsOfURL:readingFileURL options:0 error:&error]; + + if (objectData.length > 0 && error == nil) { + NSSet *objectClasses = [NSSet setWithArray:@[ NSDictionary.class, NSDate.class ]]; + heartbeatDictionary = [GULSecureCoding unarchivedObjectOfClasses:objectClasses + fromData:objectData + error:&error]; + } + + if (heartbeatDictionary.count == 0 || error != nil) { + heartbeatDictionary = [NSDictionary dictionary]; + } + + return heartbeatDictionary; +} + +- (BOOL)writeDictionary:(NSDictionary *)dictionary + forWritingURL:(NSURL *)writingFileURL + error:(NSError **)outError { + NSData *data = [GULSecureCoding archivedDataWithRootObject:dictionary error:outError]; + if (data.length == 0) { + return NO; + } + + return [data writeToURL:writingFileURL atomically:YES]; +} + +@end diff --git a/GoogleUtilities/Tests/Unit/Network/third_party/GTMHTTPServer.h b/GoogleUtilities/Tests/Unit/Network/third_party/GTMHTTPServer.h index cc51207..11e2a1c 100644 --- a/GoogleUtilities/Tests/Unit/Network/third_party/GTMHTTPServer.h +++ b/GoogleUtilities/Tests/Unit/Network/third_party/GTMHTTPServer.h @@ -62,7 +62,7 @@ #import #endif // GTM_IPHONE_SDK -// Global contants needed for errors from start +// Global constants needed for errors from start #undef _EXTERN #undef _INITIALIZE_AS @@ -87,7 +87,7 @@ enum { // ---------------------------------------------------------------------------- -// See comment at top of file for the intened use of this class. +// See comment at top of file for the intended use of this class. @interface GTMHTTPServer : NSObject { @private id delegate_; // WEAK diff --git a/GoogleUtilities/Tests/Unit/SecureCoding/GULSecureCodingTests.m b/GoogleUtilities/Tests/Unit/SecureCoding/GULSecureCodingTests.m index 6352da8..d936469 100644 --- a/GoogleUtilities/Tests/Unit/SecureCoding/GULSecureCodingTests.m +++ b/GoogleUtilities/Tests/Unit/SecureCoding/GULSecureCodingTests.m @@ -37,7 +37,7 @@ @implementation GULSecureCodingTests - (void)testArchiveUnarchiveSingleClass { - NSDictionary *objectToArchive = @{@"key1" : @"value1", @"key2" : @(2)}; + NSDictionary *objectToArchive = @{}; NSError *error; NSData *archiveData = [GULSecureCoding archivedDataWithRootObject:objectToArchive error:&error]; @@ -60,7 +60,8 @@ XCTAssertNotNil(archiveData); NSDictionary *unarchivedObject = [GULSecureCoding - unarchivedObjectOfClasses:[NSSet setWithArray:@[ NSDictionary.class, NSDate.class ]] + unarchivedObjectOfClasses: + [NSSet setWithArray:@[ NSDictionary.class, NSDate.class, NSString.class, NSNumber.class ]] fromData:archiveData error:&error]; XCTAssertNil(error); diff --git a/GoogleUtilities/Tests/Unit/Swizzler/GULAppDelegateSwizzlerTest.m b/GoogleUtilities/Tests/Unit/Swizzler/GULAppDelegateSwizzlerTest.m index 67d18ca..03e763a 100644 --- a/GoogleUtilities/Tests/Unit/Swizzler/GULAppDelegateSwizzlerTest.m +++ b/GoogleUtilities/Tests/Unit/Swizzler/GULAppDelegateSwizzlerTest.m @@ -562,7 +562,7 @@ static BOOL gRespondsToHandleBackgroundSession; XCTAssertEqualObjects(descriptionAfter, descriptionString); } -/** Tests that methods that are not overriden by the App Delegate Proxy still work as expected. */ +/** Tests that methods that are not overridden by the App Delegate Proxy still work as expected. */ - (void)testNotOverriddenMethods { GULTestAppDelegate *realAppDelegate = [[GULTestAppDelegate alloc] init]; OCMStub([self.mockSharedApplication delegate]).andReturn(realAppDelegate); @@ -587,7 +587,7 @@ static BOOL gRespondsToHandleBackgroundSession; * handles it correctly. */ - (void)testAppDelegateInstance { - // The test logic involves using KVC on the UIApplication.delegate propery. This does not really + // The test logic involves using KVC on the UIApplication.delegate property. This does not really // work well with OCMPartialMock([GULApplication sharedApplication]) and triggers issue // https://github.com/erikdoe/ocmock/issues/346. // Let's stop mocking the shared application for this particular test. @@ -776,12 +776,12 @@ static BOOL gRespondsToHandleBackgroundSession; [GULAppDelegateSwizzler proxyOriginalDelegate]; NSUserActivity *testUserActivity = [[NSUserActivity alloc] initWithActivityType:@"test"]; - BOOL shouldContinueUserActvitiy = [testAppDelegate application:[GULApplication sharedApplication] + BOOL shouldContinueUserActivity = [testAppDelegate application:[GULApplication sharedApplication] continueUserActivity:testUserActivity restorationHandler:^(NSArray *restorableObjects){ }]; // Verify that it is NO when there are no interceptors. - XCTAssertFalse(shouldContinueUserActvitiy); + XCTAssertFalse(shouldContinueUserActivity); id interceptor = OCMProtocolMock(@protocol(GULApplicationDelegate)); OCMExpect([interceptor application:OCMOCK_ANY @@ -789,12 +789,12 @@ static BOOL gRespondsToHandleBackgroundSession; restorationHandler:OCMOCK_ANY]) .andReturn(NO); [GULAppDelegateSwizzler registerAppDelegateInterceptor:interceptor]; - shouldContinueUserActvitiy = [testAppDelegate application:[GULApplication sharedApplication] + shouldContinueUserActivity = [testAppDelegate application:[GULApplication sharedApplication] continueUserActivity:testUserActivity restorationHandler:^(NSArray *restorableObjects){ }]; // Verify that it is NO when the only interceptor returns a NO. - XCTAssertFalse(shouldContinueUserActvitiy); + XCTAssertFalse(shouldContinueUserActivity); id interceptor2 = OCMProtocolMock(@protocol(GULApplicationDelegate)); OCMExpect([interceptor2 application:OCMOCK_ANY @@ -807,13 +807,13 @@ static BOOL gRespondsToHandleBackgroundSession; continueUserActivity:OCMOCK_ANY restorationHandler:OCMOCK_ANY]) .andReturn(NO); - shouldContinueUserActvitiy = [testAppDelegate application:[GULApplication sharedApplication] + shouldContinueUserActivity = [testAppDelegate application:[GULApplication sharedApplication] continueUserActivity:testUserActivity restorationHandler:^(NSArray *restorableObjects){ }]; // The result is YES if one of the interceptors returns YES. - XCTAssertTrue(shouldContinueUserActvitiy); + XCTAssertTrue(shouldContinueUserActivity); } - (void)testApplicationDidRegisterForRemoteNotificationsIsInvokedOnInterceptors { diff --git a/GoogleUtilities/Tests/Unit/Swizzler/GULObjectSwizzlerTest.m b/GoogleUtilities/Tests/Unit/Swizzler/GULObjectSwizzlerTest.m index a6a2c4d..29b6150 100644 --- a/GoogleUtilities/Tests/Unit/Swizzler/GULObjectSwizzlerTest.m +++ b/GoogleUtilities/Tests/Unit/Swizzler/GULObjectSwizzlerTest.m @@ -15,7 +15,7 @@ #import #import -#import "GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULObjectSwizzler.h" +#import "GoogleUtilities/ISASwizzler/GULObjectSwizzler+Internal.h" #import "GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULSwizzledObject.h" #import "GoogleUtilities/SwizzlerTestHelpers/GULProxy.h" @@ -390,7 +390,7 @@ // Release GULObjectSwizzler [GULObjectSwizzler setAssociatedObject:object - key:kSwizzlerAssociatedObjectKey + key:kGULSwizzlerAssociatedObjectKey value:nil association:GUL_ASSOCIATION_RETAIN]; @@ -434,7 +434,7 @@ // Release GULObjectSwizzler [GULObjectSwizzler setAssociatedObject:object - key:kSwizzlerAssociatedObjectKey + key:kGULSwizzlerAssociatedObjectKey value:nil association:GUL_ASSOCIATION_RETAIN]; diff --git a/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlerInheritedMethodsSwizzlingTest.m b/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlerInheritedMethodsSwizzlingTest.m index b2ba248..5d171fe 100644 --- a/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlerInheritedMethodsSwizzlingTest.m +++ b/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlerInheritedMethodsSwizzlingTest.m @@ -126,7 +126,7 @@ SEL swizzledSelector = @selector(description); - // Observe that swizzling the superclass IMP propogates to its subclasses when we haven't yet + // Observe that swizzling the superclass IMP propagates to its subclasses when we haven't yet // swizzled and unsiwzzled the subclasses. [GULSwizzler swizzleClass:[PollutedTestObject class] selector:swizzledSelector diff --git a/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlerTest.m b/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlerTest.m index a285109..f46c05f 100644 --- a/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlerTest.m +++ b/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlerTest.m @@ -410,7 +410,7 @@ selector:testSelector2 isClassSelector:NO]; XCTAssertNotEqual(origImp2, NULL, @"Original IMP is NULL after swizzle."); - XCTAssertNotEqual(origImp, origImp2, @"Implementations are the same when they should't be."); + XCTAssertNotEqual(origImp, origImp2, @"Implementations are the same when they shouldn't be."); [GULSwizzler unswizzleClass:testClass selector:testSelector isClassSelector:NO]; [GULSwizzler unswizzleClass:testClass2 selector:testSelector2 isClassSelector:NO]; diff --git a/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlingCacheTest.m b/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlingCacheTest.m index 3dcd001..34f606f 100644 --- a/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlingCacheTest.m +++ b/GoogleUtilities/Tests/Unit/Swizzler/GULSwizzlingCacheTest.m @@ -149,7 +149,7 @@ XCTAssert([[GULSwizzlingCache sharedInstance] cachedIMPForClass:swizzledClass withSelector:swizzledSelector] != NULL); XCTAssertEqual([GULSwizzlingCache originalIMPOfCurrentIMP:newIMP], originalIMP, - @"New to original IMP cache was not correctly poplated."); + @"New to original IMP cache was not correctly populated."); [[GULSwizzlingCache sharedInstance] clearCacheForSwizzledIMP:newIMP selector:swizzledSelector diff --git a/Package.swift b/Package.swift index 28c22f7..3316c85 100644 --- a/Package.swift +++ b/Package.swift @@ -19,7 +19,7 @@ import PackageDescription let package = Package( name: "GoogleUtilities", - platforms: [.iOS(.v10), .macOS(.v10_12), .tvOS(.v10), .watchOS(.v6)], + platforms: [.iOS(.v9), .macOS(.v10_12), .tvOS(.v10), .watchOS(.v6)], products: [ .library( name: "GULAppDelegateSwizzler", @@ -63,7 +63,7 @@ let package = Package( ), ], dependencies: [ - .package(name: "Promises", url: "https://github.com/google/promises.git", "1.2.8" ..< "1.3.0"), + .package(name: "Promises", url: "https://github.com/google/promises.git", "1.2.8" ..< "3.0.0"), .package( name: "OCMock", url: "https://github.com/firebase/ocmock.git", @@ -79,7 +79,6 @@ let package = Package( "GoogleUtilities_Network"], path: "GoogleUtilities", exclude: [ - "README.md", "AppDelegateSwizzler/README.md", "Environment/", "Network/", @@ -94,8 +93,7 @@ let package = Package( ], sources: [ "AppDelegateSwizzler/", - "SceneDelegateSwizzler/", - "Common/*.h", + "Common/", ], publicHeadersPath: "AppDelegateSwizzler/Public", cSettings: [ diff --git a/README.md b/README.md index c34a73c..d3ee9d2 100644 --- a/README.md +++ b/README.md @@ -16,22 +16,113 @@ These instructions apply to minor and patch version updates. Major versions need a customized adaptation. After the CI is green: - * Update the version in the podspec - * Add the CocoaPods tag - * `git tag CocoaPods-{version}` - * `git push origin CocoaPods-{version}` - * Push the podspec to SpecsStaging - * `pod repo push staging GoogleUtilities.podspec` - * Run Firebase CI by waiting until next nightly or adding a PR that touches `Gemfile` - * On google3, copybara and run a global TAP - * `third_party/firebase/ios/Releases/run_copy_bara.py --directory GoogleUtilities` +* Determine the next version for release by checking the + [tagged releases](https://github.com/google/GoogleUtilities/tags). + Ensure that the next release version keeps the Swift PM and CocoaPods versions in sync. +* Verify that the releasing version is the latest entry in the [CHANGELOG.md](CHANGELOG.md), + updating it if necessary. +* Update the version in the podspec to match the latest entry in the [CHANGELOG.md](CHANGELOG.md) +* Checkout the `main` branch and ensure it is up to date + ```console + git checkout main + git pull + ``` +* Add the CocoaPods tag (`{version}` will be the latest version in the [podspec](GoogleUtilities.podspec#L3)) + ```console + git tag CocoaPods-{version} + git push origin CocoaPods-{version} + ``` +* Push the podspec to the designated repo + * If this version of GoogleUtilities is intended to launch **before or with** the next Firebase release: +
+ Push to SpecsStaging + + ```console + pod repo push --skip-tests staging GoogleUtilities.podspec + ``` + + If the command fails with `Unable to find the 'staging' repo.`, add the staging repo with: + ```console + pod repo add staging git@github.com:firebase/SpecsStaging.git + ``` +
+ * Otherwise: +
+ Push to SpecsDev + + ```console + pod repo push --skip-tests dev GoogleUtilities.podspec + ``` + + If the command fails with `Unable to find the 'dev' repo.`, add the dev repo with: + ```console + pod repo add dev git@github.com:firebase/SpecsDev.git + ``` +
+* Run Firebase CI by waiting until next nightly or adding a PR that touches `Gemfile`. +* On google3, run copybara using the command below. Then, start a global TAP on the generated CL. Deflake as needed. + ```console + third_party/firebase/ios/Releases/run_copy_bara.py --directory GoogleUtilities --branch main + ``` ## Publishing +The release process is as follows: +1. [Tag and release for Swift PM](#swift-package-manager) +2. [Publish to CocoaPods](#cocoapods) +3. [Create GitHub Release](#create-github-release) +4. [Perform post release cleanup](#post-release-cleanup) + +### Swift Package Manager + By creating and [pushing a tag](https://github.com/google/GoogleUtilities/tags) + for Swift PM, the newly tagged version will be immediately released for public use. + Given this, please verify the intended time of release for Swift PM. * Add a version tag for Swift PM - * `git tag {version}` - * `git push origin {version}` - * `pod trunk push GoogleUtilities.podspec` - * Clean up SpecsStaging + ```console + git tag {version} + git push origin {version} + ``` + *Note: Ensure that any inflight PRs that depend on the new `GoogleUtilities` version are updated to point to the + newly tagged version rather than a checksum.* + +### CocoaPods +* Publish the newly versioned pod to CocoaPods + + It's recommended to point to the `GoogleUtilities.podspec` in `staging` to make sure the correct spec is being published. + ```console + pod trunk push ~/.cocoapods/repos/staging/GoogleUtilities/{version}/GoogleUtilities.podspec + ``` + *Note: In some cases, it may be acceptable to `pod trunk push` with the `--skip-tests` flag. Please double check with + the maintainers before doing so.* + + The pod push was successful if the above command logs: `🚀 GoogleUtilities ({version}) successfully published`. + In addition, a new commit that publishes the new version (co-authored by [CocoaPodsAtGoogle](https://github.com/CocoaPodsAtGoogle)) + should appear in the [CocoaPods specs repo](https://github.com/CocoaPods/Specs). Last, the latest version should be displayed + on [GoogleUtilities's CocoaPods page](https://cocoapods.org/pods/GoogleUtilities). + +### [Create GitHub Release](https://github.com/google/GoogleUtilities/releases/new/) + Update the [release template](https://github.com/google/GoogleUtilities/releases/new/)'s **Tag version** and **Release title** + fields with the latest version. In addition, reference the [Release Notes](./CHANGELOG.md) in the release's description. + + See [this release](https://github.com/google/GoogleUtilities/releases/edit/9.0.1) for an example. + + *Don't forget to perform the [post release cleanup](#post-release-cleanup)!* + +### Post Release Cleanup +
+ Clean up SpecsStaging + + ```console + pwd=$(pwd) + mkdir -p /tmp/release-cleanup && cd $_ + git clone git@github.com:firebase/SpecsStaging.git + cd SpecsStaging/ + git rm -rf GoogleUtilities/ + git commit -m "Post publish cleanup" + git push origin master + rm -rf /tmp/release-cleanup + cd $pwd + ``` +
## Development @@ -76,8 +167,8 @@ before creating a PR. GitHub Actions will verify that any code changes are done in a style compliant way. Install `clang-format` and `mint`: -``` -brew install clang-format@11 +```console +brew install clang-format@12 brew install mint ```