mirror of
https://github.com/google/GoogleUtilities
synced 2026-11-09 22:25:54 +05:45
Merge branch 'main' into app-delegate-main
This commit is contained in:
commit
54b221e66e
35 files changed
+1414
-198
No files matched your search
@@ -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.
|
||||
@@ -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)
|
||||
---
|
||||
<!-- DO NOT DELETE
|
||||
validate_template=true
|
||||
template_path=.github/ISSUE_TEMPLATE/bug_report.md
|
||||
-->
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
+24
-1
@@ -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)
|
||||
|
||||
+12
-4
@@ -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
|
||||
|
||||
@@ -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<NSString *> *paths;
|
||||
#if TARGET_OS_TV
|
||||
NSArray<NSString *> *paths =
|
||||
NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
#else
|
||||
NSArray<NSString *> *paths =
|
||||
NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
|
||||
#endif
|
||||
NSArray<NSString *> *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<Class> *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
|
||||
@@ -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
|
||||
@@ -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 <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* Describes an object that can store and fetch heartbeat dates for given tags.
|
||||
*/
|
||||
@protocol GULHeartbeatDateStorable <NSObject>
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -16,10 +16,15 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#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 <GULHeartbeatDateStorable>
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
|
||||
+51
@@ -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 <Foundation/Foundation.h>
|
||||
|
||||
#import "GULHeartbeatDateStorable.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Stores either a date or a dictionary to a specified file.
|
||||
@interface GULHeartbeatDateStorageUserDefaults : NSObject <GULHeartbeatDateStorable>
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
#import "GoogleUtilities/ISASwizzler/Public/GoogleUtilities/GULObjectSwizzler.h"
|
||||
|
||||
FOUNDATION_EXPORT NSString *kGULSwizzlerAssociatedObjectKey;
|
||||
|
||||
@interface GULObjectSwizzler (Internal)
|
||||
|
||||
- (void)swizzledObjectHasBeenDeallocatedWithGeneratedSubclass:(BOOL)isInstanceOfGeneratedSubclass;
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#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];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/** 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,75 +16,423 @@
|
||||
|
||||
#import <XCTest/XCTest.h>
|
||||
#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<NSString *> *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<NSString *> *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<NSString *> *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
|
||||
@@ -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 <XCTest/XCTest.h>
|
||||
#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
|
||||
@@ -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]
|
||||
|
||||
@@ -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 <Foundation/Foundation.h>
|
||||
|
||||
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
|
||||
@@ -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<NSString *> *paths =
|
||||
NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
#else
|
||||
NSArray<NSString *> *paths =
|
||||
NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
|
||||
#endif
|
||||
NSArray<NSString *> *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
|
||||
@@ -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 <Foundation/Foundation.h>
|
||||
|
||||
#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 <GULHeartbeatDateStorable>
|
||||
|
||||
- (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
|
||||
@@ -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<NSString *> *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<Class> *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
|
||||
@@ -62,7 +62,7 @@
|
||||
#import <CFNetwork/CFNetwork.h>
|
||||
#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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#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];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-5
@@ -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: [
|
||||
|
||||
@@ -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:
|
||||
<details>
|
||||
<summary>Push to <b>SpecsStaging</b></summary>
|
||||
|
||||
```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
|
||||
```
|
||||
</details>
|
||||
* Otherwise:
|
||||
<details>
|
||||
<summary>Push to <b>SpecsDev</b></summary>
|
||||
|
||||
```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
|
||||
```
|
||||
</details>
|
||||
* 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
|
||||
<details>
|
||||
<summary>Clean up <b>SpecsStaging</b></summary>
|
||||
|
||||
```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
|
||||
```
|
||||
</details>
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
|
||||
Reference in new issue
Block a user