diff --git a/CHANGELOG.md b/CHANGELOG.md index 691d4ef355..e98afc79dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,25 @@ # Mobile Center SDK for iOS Change Log +## Version 0.11.1 + +This version contains bug fixes and an improvement that changes the current behavior. + +## MobileCenter + +* **[Fix]** Fix bugs that sent multiple or empty service start logs at launch time. + +## MobileCenterAnalytics + +* **[Improvement]** Send truncated event name and properties instead of skipping it if its lengths are beyond the limits. + +## MobileCenterCrashes + +* **[Fix]** Fixes two bugs that caused error logs to be assiciated with wrong session information. +___ + ## Version 0.11.0 -This version has a **breaking change** in the Crashes module and contains other bugfixes and improvements. +This version has a **breaking change** in the Crashes module and contains other bug fixes and improvements. ### MobileCenter @@ -21,7 +38,7 @@ ___ ## Version 0.10.1 -This version contains a bugfix for crash attachments. +This version contains a bug fix for crash attachments. ### MobileCenterCrashes @@ -173,7 +190,7 @@ ___ ### MobileCenterAnalytics -* **[Bug]** Fix session Id's tOffset matching. +* **[Bug]** Fix session Id's toffset matching. ### MobileCenterCrashes diff --git a/CrashLibIOS/CrashLibIOS.xcodeproj/project.pbxproj b/CrashLibIOS/CrashLibIOS.xcodeproj/project.pbxproj index 89b2c7dd92..78a389052e 100644 --- a/CrashLibIOS/CrashLibIOS.xcodeproj/project.pbxproj +++ b/CrashLibIOS/CrashLibIOS.xcodeproj/project.pbxproj @@ -341,8 +341,12 @@ B21A71E11DD2A38C0044BB1C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = ( + "$(ARCHS_STANDARD)", + armv7s, + ); CLANG_ANALYZER_NONNULL = YES_NONAGGRESSIVE; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; @@ -424,8 +428,12 @@ B21A71E21DD2A38C0044BB1C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = ( + "$(ARCHS_STANDARD)", + armv7s, + ); CLANG_ANALYZER_NONNULL = YES_NONAGGRESSIVE; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ANALYZER_SECURITY_INSECUREAPI_RAND = YES; diff --git a/Documentation/MobileCenter/.jazzy.yaml b/Documentation/MobileCenter/.jazzy.yaml index fa9ca3006f..06fa500017 100644 --- a/Documentation/MobileCenter/.jazzy.yaml +++ b/Documentation/MobileCenter/.jazzy.yaml @@ -5,7 +5,7 @@ sdk: iphonesimulator theme: ../Themes/apple module: MobileCenter -module_version: 0.11.0 +module_version: 0.11.1 author: Microsoft Corp author_url: http://www.microsoft.com diff --git a/Documentation/MobileCenterAnalytics/.jazzy.yaml b/Documentation/MobileCenterAnalytics/.jazzy.yaml index f907de6577..7a1801303f 100644 --- a/Documentation/MobileCenterAnalytics/.jazzy.yaml +++ b/Documentation/MobileCenterAnalytics/.jazzy.yaml @@ -5,7 +5,7 @@ sdk: iphonesimulator theme: ../Themes/apple module: MobileCenterAnalytics -module_version: 0.11.0 +module_version: 0.11.1 author: Microsoft Corp author_url: http://www.microsoft.com diff --git a/Documentation/MobileCenterCrashes/.jazzy.yaml b/Documentation/MobileCenterCrashes/.jazzy.yaml index 7233bd9e6a..63d73736c5 100644 --- a/Documentation/MobileCenterCrashes/.jazzy.yaml +++ b/Documentation/MobileCenterCrashes/.jazzy.yaml @@ -5,7 +5,7 @@ sdk: iphonesimulator theme: ../Themes/apple module: Crashes -module_version: 0.11.0 +module_version: 0.11.1 author: Microsoft Corp author_url: http://www.microsoft.com diff --git a/Documentation/MobileCenterDistribute/.jazzy.yaml b/Documentation/MobileCenterDistribute/.jazzy.yaml index 7946fa247c..d66c2e5dcc 100644 --- a/Documentation/MobileCenterDistribute/.jazzy.yaml +++ b/Documentation/MobileCenterDistribute/.jazzy.yaml @@ -5,7 +5,7 @@ sdk: iphonesimulator theme: ../Themes/apple module: Distribute -module_version: 0.11.0 +module_version: 0.11.1 author: Microsoft Corp author_url: http://www.microsoft.com diff --git a/Documentation/MobileCenterPush/.jazzy.yaml b/Documentation/MobileCenterPush/.jazzy.yaml index a1e5bae41a..ced18e81f1 100644 --- a/Documentation/MobileCenterPush/.jazzy.yaml +++ b/Documentation/MobileCenterPush/.jazzy.yaml @@ -5,7 +5,7 @@ sdk: iphonesimulator theme: ../Themes/apple module: MobileCenterPush -module_version: 0.11.0 +module_version: 0.11.1 author: Microsoft Corp author_url: http://www.microsoft.com diff --git a/Global.xcconfig b/Global.xcconfig index 009c30221a..b28b873b4b 100644 --- a/Global.xcconfig +++ b/Global.xcconfig @@ -1,5 +1,5 @@ BUILD_NUMBER = 1 -VERSION_STRING = 0.11.0 +VERSION_STRING = 0.11.1 SDK_NAME = mobilecenter.ios diff --git a/MobileCenter.podspec b/MobileCenter.podspec index c7b3194618..2e3c76d43f 100644 --- a/MobileCenter.podspec +++ b/MobileCenter.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'MobileCenter' - s.version = '0.11.0' + s.version = '0.11.1' s.summary = 'Mobile Center is mission control for mobile apps. Get faster release cycles, higher-quality apps, and the insights to build what users want.' s.description = <<-DESC diff --git a/MobileCenter/MobileCenter.xcodeproj/project.pbxproj b/MobileCenter/MobileCenter.xcodeproj/project.pbxproj index 131a96f691..ea959581f1 100644 --- a/MobileCenter/MobileCenter.xcodeproj/project.pbxproj +++ b/MobileCenter/MobileCenter.xcodeproj/project.pbxproj @@ -111,6 +111,7 @@ 6EB1F40E1D2443B7005F9F99 /* MSChannelDefaultTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6EB1F40D1D2443B7005F9F99 /* MSChannelDefaultTests.m */; }; 6EF628F41D371B1600CAFF64 /* MSChannelConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EF628F21D371B1600CAFF64 /* MSChannelConfiguration.h */; }; 6EF628F51D371B1600CAFF64 /* MSChannelConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 6EF628F31D371B1600CAFF64 /* MSChannelConfiguration.m */; }; + 805F3F6B1F209C9D00B489E4 /* MSMockService.m in Sources */ = {isa = PBXBuildFile; fileRef = 805F3F6A1F209C9D00B489E4 /* MSMockService.m */; }; 842C131E1E7FCF5300F16EA3 /* MSUtility.h in Headers */ = {isa = PBXBuildFile; fileRef = 842C13151E7FCF5300F16EA3 /* MSUtility.h */; }; 842C131F1E7FCF5300F16EA3 /* MSUtility.m in Sources */ = {isa = PBXBuildFile; fileRef = 842C13161E7FCF5300F16EA3 /* MSUtility.m */; }; 842C13201E7FCF5300F16EA3 /* MSUtility+Application.h in Headers */ = {isa = PBXBuildFile; fileRef = 842C13171E7FCF5300F16EA3 /* MSUtility+Application.h */; }; @@ -123,8 +124,8 @@ B21E29911E83521A00F9A22D /* MSUtility+StringFormatting.h in Headers */ = {isa = PBXBuildFile; fileRef = B21E298F1E83521A00F9A22D /* MSUtility+StringFormatting.h */; }; B21E29921E83521A00F9A22D /* MSUtility+StringFormatting.m in Sources */ = {isa = PBXBuildFile; fileRef = B21E29901E83521A00F9A22D /* MSUtility+StringFormatting.m */; }; B24F3F171D93A3FF00827213 /* MSLoggerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B24F3F161D93A3FF00827213 /* MSLoggerTests.m */; }; + B289FF881F291F9D00AA2E06 /* MSDeviceInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2C3C1831DB83A3E00CB83F7 /* MSDeviceInternal.h */; }; B2B9441E1DF0DF23009117D1 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2B9441D1DF0DF23009117D1 /* OHHTTPStubs.framework */; }; - B2C3C1841DB83A3E00CB83F7 /* MSDeviceInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = B2C3C1831DB83A3E00CB83F7 /* MSDeviceInternal.h */; }; B2FD53621E56501B0050F909 /* MSDeviceHistoryInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = B2FD53601E56501B0050F909 /* MSDeviceHistoryInfo.h */; }; B2FD53631E56501B0050F909 /* MSDeviceHistoryInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = B2FD53611E56501B0050F909 /* MSDeviceHistoryInfo.m */; }; B2FD53651E567BCF0050F909 /* MSDeviceHistoryInfoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B2FD53641E567BCF0050F909 /* MSDeviceHistoryInfoTests.m */; }; @@ -294,6 +295,8 @@ 6EB1F40D1D2443B7005F9F99 /* MSChannelDefaultTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSChannelDefaultTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 6EF628F21D371B1600CAFF64 /* MSChannelConfiguration.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSChannelConfiguration.h; sourceTree = ""; }; 6EF628F31D371B1600CAFF64 /* MSChannelConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSChannelConfiguration.m; sourceTree = ""; }; + 805F3F691F209C8A00B489E4 /* MSMockService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSMockService.h; sourceTree = ""; }; + 805F3F6A1F209C9D00B489E4 /* MSMockService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSMockService.m; sourceTree = ""; }; 842C13151E7FCF5300F16EA3 /* MSUtility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSUtility.h; sourceTree = ""; }; 842C13161E7FCF5300F16EA3 /* MSUtility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSUtility.m; sourceTree = ""; }; 842C13171E7FCF5300F16EA3 /* MSUtility+Application.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MSUtility+Application.h"; sourceTree = ""; }; @@ -344,8 +347,8 @@ E88D17051D35B6B500A5EA57 /* MSMockLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSMockLog.m; sourceTree = ""; }; E88EBBEB1D2C612E007E7785 /* MSAbstractLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSAbstractLog.m; sourceTree = ""; }; E88EBBFA1D2C8CC7007E7785 /* MSLogContainerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = MSLogContainerTests.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; - E8A8D1E91D3057A90022931E /* MSUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSUserDefaults.h; path = ../Model/Utils/MSUserDefaults.h; sourceTree = ""; }; - E8A8D1EA1D3057A90022931E /* MSUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSUserDefaults.m; path = ../Model/Utils/MSUserDefaults.m; sourceTree = ""; }; + E8A8D1E91D3057A90022931E /* MSUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSUserDefaults.h; path = ../Model/Util/MSUserDefaults.h; sourceTree = ""; }; + E8A8D1EA1D3057A90022931E /* MSUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSUserDefaults.m; path = ../Model/Util/MSUserDefaults.m; sourceTree = ""; }; E8AEE9811D5970A400C0FF6C /* MSLogManagerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSLogManagerDelegate.h; sourceTree = ""; }; E8E48F941D50FF4300A8C1B0 /* MSSenderCallDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSSenderCallDelegate.h; sourceTree = ""; }; F803BBF11E8E3677004B1E7A /* MSCustomProperties.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCustomProperties.h; sourceTree = ""; }; @@ -506,7 +509,7 @@ 045BC3181E3FD8AC00B6C960 /* MSKeychainUtil.h */, ); name = Util; - path = Utils; + path = Util; sourceTree = ""; }; 6E0401821D1CAD810051BCFA /* Support */ = { @@ -632,6 +635,8 @@ 3805DDA61EB7A6A6001DB846 /* MSMockOriginalAppDelegate.m */, 3872CA961ECA0B04006B2E3B /* MSMockCustomAppDelegate.h */, 3872CA971ECA0B04006B2E3B /* MSMockCustomAppDelegate.m */, + 805F3F691F209C8A00B489E4 /* MSMockService.h */, + 805F3F6A1F209C9D00B489E4 /* MSMockService.m */, ); path = Util; sourceTree = ""; @@ -713,7 +718,7 @@ E8753D6D1D4BE53F00241513 /* MSSenderUtil.m */, ); name = Util; - path = Utils; + path = Util; sourceTree = ""; }; /* End PBXGroup section */ @@ -752,9 +757,8 @@ 384772A61DA5691F009365DE /* MSSenderDelegate.h in Headers */, D38023E91E6EFC7C00466558 /* MSStartServiceLog.h in Headers */, 387C76FD1D6C9CF100D68CC1 /* MSServiceAbstract.h in Headers */, - B2C3C1841DB83A3E00CB83F7 /* MSDeviceInternal.h in Headers */, + B289FF881F291F9D00AA2E06 /* MSDeviceInternal.h in Headers */, 383481731EA7FF6100787F56 /* MSLogDBStoragePrivate.h in Headers */, - B2C3C1841DB83A3E00CB83F7 /* MSDeviceInternal.h in Headers */, B21E29911E83521A00F9A22D /* MSUtility+StringFormatting.h in Headers */, 842C13251E7FCF5300F16EA3 /* MSUtility+Environment.h in Headers */, 6E3E2C9E1D35701000B1EE50 /* MSLog.h in Headers */, @@ -841,7 +845,7 @@ isa = PBXProject; attributes = { CLASSPREFIX = MS; - LastUpgradeCheck = 0820; + LastUpgradeCheck = 0830; ORGANIZATIONNAME = Microsoft; TargetAttributes = { 6E0401021D1C98220051BCFA = { @@ -980,6 +984,7 @@ 380A4DCB1DD6908A00E99219 /* MSUtilityTests.m in Sources */, 3849BA7E1EF3489D0072E3E0 /* MSDBStorageTests.m in Sources */, E829E4231D25C8BA00F19DA1 /* MSIngestionSenderTests.m in Sources */, + 805F3F6B1F209C9D00B489E4 /* MSMockService.m in Sources */, E88EBBFB1D2C8CC7007E7785 /* MSLogContainerTests.m in Sources */, 04FD126B1E4103CC007ABFE7 /* MSKeychainUtilTests.m in Sources */, D38024121E7130C700466558 /* MSStartServiceLogTests.m in Sources */, diff --git a/MobileCenter/MobileCenter/Internals/Channel/MSChannelDefault.m b/MobileCenter/MobileCenter/Internals/Channel/MSChannelDefault.m index 45023d7a19..f6e3a2e694 100644 --- a/MobileCenter/MobileCenter/Internals/Channel/MSChannelDefault.m +++ b/MobileCenter/MobileCenter/Internals/Channel/MSChannelDefault.m @@ -204,6 +204,12 @@ - (void)flushQueue { // Remove from pending logs. [self.pendingBatchIds removeObject:senderBatchId]; [self.storage deleteLogsWithBatchId:senderBatchId groupId:self.configuration.groupId]; + + // Update pending batch queue state. + if (self.pendingBatchQueueFull && + self.pendingBatchIds.count < self.configuration.pendingBatchesLimit) { + self.pendingBatchQueueFull = NO; + } } } else MSLogWarning([MSMobileCenter logTag], @"Batch Id %@ not expected, ignore.", senderBatchId); diff --git a/MobileCenter/MobileCenter/Internals/Channel/MSLogManagerDefault.m b/MobileCenter/MobileCenter/Internals/Channel/MSLogManagerDefault.m index 40db50e9b2..7a47af00ca 100644 --- a/MobileCenter/MobileCenter/Internals/Channel/MSLogManagerDefault.m +++ b/MobileCenter/MobileCenter/Internals/Channel/MSLogManagerDefault.m @@ -149,11 +149,14 @@ - (void)processLog:(id)log forGroupId:(NSString *)groupId { [delegate onEnqueuingLog:log withInternalId:internalLogId]; }]; - // Set common log info. - log.toffset = [NSNumber numberWithLongLong:(long long)([MSUtility nowInMilliseconds])]; - - // Only add device info in case the log doesn't have one. In case the log is restored after a crash or for crashes, - // We don't want the device information to be updated but want the old one preserved. + /* + * Set common log info. + * Only add toffset and device info in case the log doesn't have one. In case the log is restored after a crash or for + * crashes, we don't want the toffset and the device information to be updated but want the old one preserved. + */ + if (!log.toffset) { + log.toffset = [NSNumber numberWithLongLong:(long long)([MSUtility nowInMilliseconds])]; + } if (!log.device) { log.device = [[MSDeviceTracker sharedInstance] device]; } diff --git a/MobileCenter/MobileCenter/Internals/MobileCenter+Internal.h b/MobileCenter/MobileCenter/Internals/MobileCenter+Internal.h index e4de61e4c7..7ddbecac58 100644 --- a/MobileCenter/MobileCenter/Internals/MobileCenter+Internal.h +++ b/MobileCenter/MobileCenter/Internals/MobileCenter+Internal.h @@ -10,7 +10,7 @@ #import "Model/MSLog.h" #import "Model/MSLogContainer.h" #import "Model/MSLogWithPropertiesInternal.h" -#import "Model/Utils/MSUserDefaults.h" +#import "Model/Util/MSUserDefaults.h" // Channel #import "Channel/MSChannelDelegate.h" diff --git a/MobileCenter/MobileCenter/Internals/Model/Utils/MSUserDefaults.h b/MobileCenter/MobileCenter/Internals/Model/Util/MSUserDefaults.h similarity index 100% rename from MobileCenter/MobileCenter/Internals/Model/Utils/MSUserDefaults.h rename to MobileCenter/MobileCenter/Internals/Model/Util/MSUserDefaults.h diff --git a/MobileCenter/MobileCenter/Internals/Model/Utils/MSUserDefaults.m b/MobileCenter/MobileCenter/Internals/Model/Util/MSUserDefaults.m similarity index 100% rename from MobileCenter/MobileCenter/Internals/Model/Utils/MSUserDefaults.m rename to MobileCenter/MobileCenter/Internals/Model/Util/MSUserDefaults.m diff --git a/MobileCenter/MobileCenter/Internals/Sender/MSSenderCall.m b/MobileCenter/MobileCenter/Internals/Sender/MSSenderCall.m index 94f8448344..3d6cab299e 100644 --- a/MobileCenter/MobileCenter/Internals/Sender/MSSenderCall.m +++ b/MobileCenter/MobileCenter/Internals/Sender/MSSenderCall.m @@ -71,11 +71,14 @@ - (void)sender:(id)sender callCompletedWithStatus:(NSUInteger)statusCode data:(NSData *)data error:(NSError *)error { - if ([MSSenderUtil isNoInternetConnectionError:error]) { + BOOL internetIsDown = [MSSenderUtil isNoInternetConnectionError:error]; + BOOL couldNotEstablishSecureConnection = [MSSenderUtil isSSLConnectionError:error]; + if (internetIsDown || couldNotEstablishSecureConnection) { - // Reset the retry count, will retry once the connection is established again. + // Reset the retry count, will retry once the (secure) connection is established again. [self resetRetry]; - MSLogInfo([MSMobileCenter logTag], @"Internet connection is down."); + NSString *logMessage = internetIsDown ? @"Internet connection is down." : @"Could not establish secure connection."; + MSLogInfo([MSMobileCenter logTag],logMessage); [sender suspend]; } diff --git a/MobileCenter/MobileCenter/Internals/Sender/Utils/MSSenderUtil.h b/MobileCenter/MobileCenter/Internals/Sender/Util/MSSenderUtil.h similarity index 73% rename from MobileCenter/MobileCenter/Internals/Sender/Utils/MSSenderUtil.h rename to MobileCenter/MobileCenter/Internals/Sender/Util/MSSenderUtil.h index 957df7e75a..baa020164e 100644 --- a/MobileCenter/MobileCenter/Internals/Sender/Utils/MSSenderUtil.h +++ b/MobileCenter/MobileCenter/Internals/Sender/Util/MSSenderUtil.h @@ -30,10 +30,21 @@ static NSString *const kMSHidingStringForAppSecret = @"*"; * * @param error http error. * - * @return is no network connection error. + * @return YES if it is a no network connection error, NO otherwise. */ + (BOOL)isNoInternetConnectionError:(NSError *)error; +/** + * Indicate if error is because a secure connection could not be established, e.g. when using a public network that + * is open but requires accepting terms and conditions, and the user hasn't done that, yet. + * + * @param error http error. + * + * @return YES if it is an SSL connection error, NO otherwise. + + */ ++ (BOOL)isSSLConnectionError:(NSError *)error; + /** * Indicate if error is due to cancelation of the request. * diff --git a/MobileCenter/MobileCenter/Internals/Sender/Utils/MSSenderUtil.m b/MobileCenter/MobileCenter/Internals/Sender/Util/MSSenderUtil.m similarity index 79% rename from MobileCenter/MobileCenter/Internals/Sender/Utils/MSSenderUtil.m rename to MobileCenter/MobileCenter/Internals/Sender/Util/MSSenderUtil.m index 60e3509012..89582ef405 100644 --- a/MobileCenter/MobileCenter/Internals/Sender/Utils/MSSenderUtil.m +++ b/MobileCenter/MobileCenter/Internals/Sender/Util/MSSenderUtil.m @@ -15,6 +15,13 @@ + (BOOL)isNoInternetConnectionError:(NSError *)error { return ([error.domain isEqualToString:NSURLErrorDomain] && (error.code == NSURLErrorNotConnectedToInternet)); } ++ (BOOL)isSSLConnectionError:(NSError *)error { + + // Check for error domain and if the error.code falls in the range of SSL connection errors (between -2000 and -1200). + return ([error.domain isEqualToString:NSURLErrorDomain] && + ((error.code >= NSURLErrorCannotLoadFromNetwork) && (error.code <= NSURLErrorSecureConnectionFailed))); +} + + (BOOL)isRequestCanceledError:(NSError *)error { return ([error.domain isEqualToString:NSURLErrorDomain] && (error.code == NSURLErrorCancelled)); } diff --git a/MobileCenter/MobileCenter/Internals/Storage/MSDBStorage.m b/MobileCenter/MobileCenter/Internals/Storage/MSDBStorage.m index 9032209b92..eefd11f8ad 100644 --- a/MobileCenter/MobileCenter/Internals/Storage/MSDBStorage.m +++ b/MobileCenter/MobileCenter/Internals/Storage/MSDBStorage.m @@ -1,5 +1,5 @@ - + #import #import "MSDBStoragePrivate.h" @@ -95,7 +95,7 @@ - (BOOL)executeNonSelectionQuery:(NSString *)query { [[NSString alloc] initWithUTF8String:errMsg]); } } else { - MSLogError([MSMobileCenter logTag], @"Failed to open database."); + MSLogError([MSMobileCenter logTag], @"Failed to open database for non-selection query with result: %d.", result); } sqlite3_close(db); return SQLITE_OK == result; @@ -144,7 +144,7 @@ - (BOOL)executeNonSelectionQuery:(NSString *)query { [[NSString alloc] initWithUTF8String:sqlite3_errmsg(db)]); } } else { - MSLogError([MSMobileCenter logTag], @"Failed to open database."); + MSLogError([MSMobileCenter logTag], @"Failed to open database for non-selection query with result: %d.", result); } sqlite3_close(db); return entries; diff --git a/MobileCenter/MobileCenter/Internals/Storage/MSLogDBStorage.m b/MobileCenter/MobileCenter/Internals/Storage/MSLogDBStorage.m index f3083c335d..fab6c29219 100644 --- a/MobileCenter/MobileCenter/Internals/Storage/MSLogDBStorage.m +++ b/MobileCenter/MobileCenter/Internals/Storage/MSLogDBStorage.m @@ -47,12 +47,12 @@ - (BOOL)saveLog:(id)log withGroupId:(NSString *)groupId { [NSString stringWithFormat:@"INSERT INTO \"%@\" (\"%@\", \"%@\") VALUES ('%@', '%@')", kMSLogTableName, kMSGroupIdColumnName, kMSLogColumnName, groupId, base64Data]; BOOL succeeded = [self executeNonSelectionQuery:addLogQuery]; - NSUInteger logCount = [self countLogsWithGroupId:groupId]; + NSUInteger logCount = [self countLogs]; // Max out DB. if (succeeded && logCount > self.capacity) { NSUInteger overflowCount = logCount - self.capacity; - [self deleteOldestLogsWithGroupId:groupId count:overflowCount]; + [self deleteOldestLogsWithCount:overflowCount]; MSLogDebug([MSMobileCenter logTag], @"Log storage was over capacity, %ld oldest log(s) deleted.", (long)overflowCount); } @@ -188,14 +188,19 @@ - (void)deleteLogsWithBatchId:(NSString *)batchId groupId:(NSString *)groupId { NSData *logData = [[NSData alloc] initWithBase64EncodedString:row[self.logColumnIndex] options:NSDataBase64DecodingIgnoreUnknownCharacters]; id log; + NSException *exception; // Deserialize the log. @try { log = [NSKeyedUnarchiver unarchiveObjectWithData:logData]; - } @catch (NSException *exception) { + } @catch (NSException *e) { + exception = e; + } + if (!log || exception) { // The archived log is not valid. - MSLogError([MSMobileCenter logTag], @"Deserialization failed for log with Id %@: %@", dbId, exception); + MSLogError([MSMobileCenter logTag], @"Deserialization failed for log with Id %@: %@", dbId, + exception ? exception.reason : @"The log deserialized to NULL."); [self deleteLogFromDBWithId:dbId]; continue; } @@ -236,18 +241,16 @@ - (void)deleteLogsFromDBWithColumnValues:(NSArray *)columnValues columnName:(NSS } } -- (void)deleteOldestLogsWithGroupId:(NSString *)groupId count:(NSInteger)count { - NSString *deleteLogQuery = - [NSString stringWithFormat:@"DELETE FROM \"%@\" WHERE \"%@\" = '%@' ORDER BY \"%@\" ASC LIMIT %ld", - kMSLogTableName, kMSGroupIdColumnName, groupId, kMSIdColumnName, (long)count]; +- (void)deleteOldestLogsWithCount:(NSInteger)count { + NSString *deleteLogQuery = [NSString stringWithFormat:@"DELETE FROM \"%@\" ORDER BY \"%@\" ASC LIMIT %ld", + kMSLogTableName, kMSIdColumnName, (long)count]; [self executeNonSelectionQuery:deleteLogQuery]; } #pragma mark - DB count -- (NSUInteger)countLogsWithGroupId:(NSString *)groupId { - NSString *condition = [NSString stringWithFormat:@"\"%@\" = '%@'", kMSGroupIdColumnName, groupId]; - return [self countEntriesForTable:kMSLogTableName condition:condition]; +- (NSUInteger)countLogs { + return [self countEntriesForTable:kMSLogTableName condition:nil]; } @end diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSConstants+Internal.h b/MobileCenter/MobileCenter/Internals/Util/MSConstants+Internal.h similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSConstants+Internal.h rename to MobileCenter/MobileCenter/Internals/Util/MSConstants+Internal.h diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSKeychainUtil.h b/MobileCenter/MobileCenter/Internals/Util/MSKeychainUtil.h similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSKeychainUtil.h rename to MobileCenter/MobileCenter/Internals/Util/MSKeychainUtil.h diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSKeychainUtil.m b/MobileCenter/MobileCenter/Internals/Util/MSKeychainUtil.m similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSKeychainUtil.m rename to MobileCenter/MobileCenter/Internals/Util/MSKeychainUtil.m diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+Application.h b/MobileCenter/MobileCenter/Internals/Util/MSUtility+Application.h similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+Application.h rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+Application.h diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+Application.m b/MobileCenter/MobileCenter/Internals/Util/MSUtility+Application.m similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+Application.m rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+Application.m diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+ApplicationPrivate.h b/MobileCenter/MobileCenter/Internals/Util/MSUtility+ApplicationPrivate.h similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+ApplicationPrivate.h rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+ApplicationPrivate.h diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+Date.h b/MobileCenter/MobileCenter/Internals/Util/MSUtility+Date.h similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+Date.h rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+Date.h diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+Date.m b/MobileCenter/MobileCenter/Internals/Util/MSUtility+Date.m similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+Date.m rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+Date.m diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+Environment.h b/MobileCenter/MobileCenter/Internals/Util/MSUtility+Environment.h similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+Environment.h rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+Environment.h diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+Environment.m b/MobileCenter/MobileCenter/Internals/Util/MSUtility+Environment.m similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+Environment.m rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+Environment.m diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+File.h b/MobileCenter/MobileCenter/Internals/Util/MSUtility+File.h similarity index 65% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+File.h rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+File.h index 3590311585..d4e4040218 100644 --- a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+File.h +++ b/MobileCenter/MobileCenter/Internals/Util/MSUtility+File.h @@ -13,7 +13,7 @@ extern NSString *MSUtilityFileCategory; @interface MSUtility (File) /** - * Creates a file at the given location, intermediate directories are also create if nonexistant. + * Creates a file at the given location, intermediate directories are also create if nonexistent. * * @param fileURL URL representing the absolute path of the file to create. * @@ -33,4 +33,16 @@ extern NSString *MSUtilityFileCategory; */ + (BOOL)removeItemAtURL:(NSURL *)itemURL; +/** + * Creates a directory at the given location, intermediate directories are also created if nonexistent. + * + * @param directoryURL URL representing the absolute path of the directory to create. + * + * @return `YES` if the operation was successful or if the item already exists, otherwise `NO`. + * + * @discussion SDK files should not be backed up in iCloud. Thus, iCloud backup is explicitely + * deactivated on every folder created. + */ ++ (BOOL)createDirectoryAtURL:(NSURL *)directoryURL; + @end diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+File.m b/MobileCenter/MobileCenter/Internals/Util/MSUtility+File.m similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+File.m rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+File.m diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+StringFormatting.h b/MobileCenter/MobileCenter/Internals/Util/MSUtility+StringFormatting.h similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+StringFormatting.h rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+StringFormatting.h diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility+StringFormatting.m b/MobileCenter/MobileCenter/Internals/Util/MSUtility+StringFormatting.m similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility+StringFormatting.m rename to MobileCenter/MobileCenter/Internals/Util/MSUtility+StringFormatting.m diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility.h b/MobileCenter/MobileCenter/Internals/Util/MSUtility.h similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility.h rename to MobileCenter/MobileCenter/Internals/Util/MSUtility.h diff --git a/MobileCenter/MobileCenter/Internals/Utils/MSUtility.m b/MobileCenter/MobileCenter/Internals/Util/MSUtility.m similarity index 100% rename from MobileCenter/MobileCenter/Internals/Utils/MSUtility.m rename to MobileCenter/MobileCenter/Internals/Util/MSUtility.m diff --git a/MobileCenter/MobileCenter/MSMobileCenter.m b/MobileCenter/MobileCenter/MSMobileCenter.m index f6ed6d085c..c1312f4b0b 100644 --- a/MobileCenter/MobileCenter/MSMobileCenter.m +++ b/MobileCenter/MobileCenter/MSMobileCenter.m @@ -50,7 +50,7 @@ + (void)start:(NSString *)appSecret withServices:(NSArray *)services { } + (void)startService:(Class)service { - [[self sharedInstance] startService:service]; + [[self sharedInstance] startService:service andSendLog:YES]; } + (BOOL)isConfigured { @@ -94,7 +94,7 @@ + (MSLogLevel)logLevel { + (void)setLogLevel:(MSLogLevel)logLevel { MSLogger.currentLogLevel = logLevel; - + // The logger is not set at the time of swizzling but now may be a good time to flush the traces. [MSAppDelegateForwarder flushTraceBuffer]; } @@ -202,16 +202,20 @@ - (BOOL)configure:(NSString *)appSecret { - (void)start:(NSString *)appSecret withServices:(NSArray *)services { @synchronized(self) { BOOL configured = [self configure:appSecret]; - if (configured) { + if (configured && services) { NSArray *sortedServices = [self sortServices:services]; NSMutableArray *servicesNames = [NSMutableArray arrayWithCapacity:sortedServices.count]; for (Class service in sortedServices) { - if ([self startService:service]) { + if ([self startService:service andSendLog:NO]) { [servicesNames addObject:[service serviceName]]; } } - [self sendStartServiceLog:servicesNames]; + if ([servicesNames count] > 0) { + [self sendStartServiceLog:servicesNames]; + } else { + MSLogDebug([MSMobileCenter logTag], @"No services have been started."); + } } } } @@ -236,10 +240,15 @@ - (NSArray *)sortServices:(NSArray *)services { } } -- (BOOL)startService:(Class)clazz { +- (BOOL)startService:(Class)clazz andSendLog:(BOOL)sendLog { @synchronized(self) { - id service = [clazz sharedInstance]; + // Check if clazz is valid class + if (![clazz conformsToProtocol:@protocol(MSServiceCommon)]) { + MSLogError([MSMobileCenter logTag], @"Cannot start service %@. Provided value is nil or invalid.", clazz); + return NO; + } + id service = [clazz sharedInstance]; if (service.isAvailable) { // Service already works, we shouldn't send log with this service name @@ -252,7 +261,12 @@ - (BOOL)startService:(Class)clazz { // Start service with log manager. [service startWithLogManager:self.logManager appSecret:self.appSecret]; - // Service started + // Send start service log. + if (sendLog) { + [self sendStartServiceLog:@[ [clazz serviceName] ]]; + } + + // Service started. return YES; } } @@ -385,7 +399,7 @@ - (void)sendStartServiceLog:(NSArray *)servicesNames { - (void)sendCustomPropertiesLog:(NSDictionary *)properties { MSCustomPropertiesLog *customPropertiesLog = [MSCustomPropertiesLog new]; customPropertiesLog.properties = properties; - + // FIXME: withPriority parameter need to be removed on merge. [self.logManager processLog:customPropertiesLog forGroupId:kMSGroupId]; } diff --git a/MobileCenter/MobileCenterTests/MSChannelDefaultTests.m b/MobileCenter/MobileCenterTests/MSChannelDefaultTests.m index 94bf07a4f1..9de1b1620a 100644 --- a/MobileCenter/MobileCenterTests/MSChannelDefaultTests.m +++ b/MobileCenter/MobileCenterTests/MSChannelDefaultTests.m @@ -9,6 +9,7 @@ #import "MSChannelDefaultPrivate.h" #import "MSChannelDelegate.h" #import "MSHttpSender.h" +#import "MSLogContainer.h" #import "MSMobileCenterErrors.h" #import "MSUtility.h" @@ -68,6 +69,184 @@ - (void)testNewInstanceWasInitialisedCorrectly { assertThatUnsignedLong(self.sut.itemsCount, equalToInt(0)); } +- (void)testLogsSentWithSuccess { + + /* + * If + */ + [self initChannelEndJobExpectation]; + id delegateMock = OCMProtocolMock(@protocol(MSChannelDelegate)); + __block MSSendAsyncCompletionHandler senderBlock; + __block MSLogContainer *logContainer; + __block NSString *expectedBatchId = @"1"; + int batchSizeLimit = 1; + id expectedLog = [MSAbstractLog new]; + expectedLog.sid = MS_UUID_STRING; + + // Init mocks. + id senderMock = OCMProtocolMock(@protocol(MSSender)); + OCMStub([senderMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + + // Get sender bloc for later call. + [invocation retainArguments]; + [invocation getArgument:&senderBlock atIndex:3]; + [invocation getArgument:&logContainer atIndex:2]; + }); + + // Stub the storage load for that log. + id storageMock = OCMProtocolMock(@protocol(MSStorage)); + OCMStub([storageMock loadLogsWithGroupId:kMSTestGroupId limit:batchSizeLimit withCompletion:(OCMOCK_ANY)]) + .andDo(^(NSInvocation *invocation) { + MSLoadDataCompletionBlock loadCallback; + + // Get sender bloc for later call. + [invocation getArgument:&loadCallback atIndex:4]; + + // Mock load. + loadCallback(((NSArray> *)@[ expectedLog ]), expectedBatchId); + }); + + // Configure channel. + MSChannelConfiguration *config = [[MSChannelConfiguration alloc] initWithGroupId:kMSTestGroupId + priority:MSPriorityDefault + flushInterval:0.0 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:1]; + self.sut.configuration = config; + MSChannelDefault *sut = [[MSChannelDefault alloc] initWithSender:senderMock + storage:storageMock + configuration:config + logsDispatchQueue:dispatch_get_main_queue()]; + [sut addDelegate:delegateMock]; + OCMReject([delegateMock channel:sut didFailSendingLog:OCMOCK_ANY withError:OCMOCK_ANY]); + OCMExpect([delegateMock channel:sut didSucceedSendingLog:expectedLog]); + OCMExpect([storageMock deleteLogsWithBatchId:expectedBatchId groupId:kMSTestGroupId]); + + /* + * When + */ + [sut enqueueItem:[self getValidMockLog] withCompletion:nil]; + + // Try to release one batch. + dispatch_async(self.logsDispatchQueue, ^{ + senderBlock([@(1) stringValue], 200, nil, nil); + + /* + * Then + */ + dispatch_async(self.logsDispatchQueue, ^{ + [self enqueueChannelEndJobExpectation]; + }); + }); + + /* + * Then + */ + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + + // Get sure it has been sent. + assertThat(logContainer.batchId, is(expectedBatchId)); + assertThat(logContainer.logs, is(@[ expectedLog ])); + assertThatBool(sut.pendingBatchQueueFull, isFalse()); + assertThatUnsignedInt(sut.pendingBatchIds.count, equalToInt(0)); + OCMVerifyAll(delegateMock); + OCMVerifyAll(storageMock); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + +- (void)testLogsSentWithFailure { + + /* + * If + */ + [self initChannelEndJobExpectation]; + id delegateMock = OCMProtocolMock(@protocol(MSChannelDelegate)); + __block MSSendAsyncCompletionHandler senderBlock; + __block MSLogContainer *logContainer; + __block NSString *expectedBatchId = @"1"; + int batchSizeLimit = 1; + id expectedLog = [MSAbstractLog new]; + expectedLog.sid = MS_UUID_STRING; + + // Init mocks. + id senderMock = OCMProtocolMock(@protocol(MSSender)); + OCMStub([senderMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { + + // Get sender bloc for later call. + [invocation retainArguments]; + [invocation getArgument:&senderBlock atIndex:3]; + [invocation getArgument:&logContainer atIndex:2]; + }); + + // Stub the storage load for that log. + id storageMock = OCMProtocolMock(@protocol(MSStorage)); + OCMStub([storageMock loadLogsWithGroupId:kMSTestGroupId limit:batchSizeLimit withCompletion:(OCMOCK_ANY)]) + .andDo(^(NSInvocation *invocation) { + MSLoadDataCompletionBlock loadCallback; + + // Get sender bloc for later call. + [invocation getArgument:&loadCallback atIndex:4]; + + // Mock load. + loadCallback(((NSArray> *)@[ expectedLog ]), expectedBatchId); + }); + + // Configure channel. + MSChannelConfiguration *config = [[MSChannelConfiguration alloc] initWithGroupId:kMSTestGroupId + priority:MSPriorityDefault + flushInterval:0.0 + batchSizeLimit:batchSizeLimit + pendingBatchesLimit:1]; + self.sut.configuration = config; + MSChannelDefault *sut = [[MSChannelDefault alloc] initWithSender:senderMock + storage:storageMock + configuration:config + logsDispatchQueue:dispatch_get_main_queue()]; + [sut addDelegate:delegateMock]; + OCMExpect([delegateMock channel:sut didFailSendingLog:expectedLog withError:OCMOCK_ANY]); + OCMReject([delegateMock channel:sut didSucceedSendingLog:OCMOCK_ANY]); + OCMExpect([storageMock deleteLogsWithBatchId:expectedBatchId groupId:kMSTestGroupId]); + + /* + * When + */ + [sut enqueueItem:[self getValidMockLog] withCompletion:nil]; + + // Try to release one batch. + dispatch_async(self.logsDispatchQueue, ^{ + senderBlock([@(1) stringValue], 300, nil, nil); + + /* + * Then + */ + dispatch_async(self.logsDispatchQueue, ^{ + [self enqueueChannelEndJobExpectation]; + }); + }); + + /* + * Then + */ + [self waitForExpectationsWithTimeout:1 + handler:^(NSError *error) { + + // Get sure it has been sent. + assertThat(logContainer.batchId, is(expectedBatchId)); + assertThat(logContainer.logs, is(@[ expectedLog ])); + assertThatBool(sut.pendingBatchQueueFull, isFalse()); + assertThatUnsignedInt(sut.pendingBatchIds.count, equalToInt(0)); + OCMVerifyAll(delegateMock); + OCMVerifyAll(storageMock); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } + }]; +} + - (void)testEnqueuingItemsWillIncreaseCounter { // If @@ -142,7 +321,7 @@ - (void)testBatchQueueLimit { // Set up mock and stubs. id senderMock = OCMProtocolMock(@protocol(MSSender)); - OCMStub([senderMock sendAsync:[OCMArg any] completionHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + OCMStub([senderMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { MSLogContainer *container; [invocation getArgument:&container atIndex:2]; if (container) { @@ -150,7 +329,7 @@ - (void)testBatchQueueLimit { } }); id storageMock = OCMProtocolMock(@protocol(MSStorage)); - OCMStub([storageMock loadLogsWithGroupId:kMSTestGroupId limit:batchSizeLimit withCompletion:([OCMArg any])]) + OCMStub([storageMock loadLogsWithGroupId:kMSTestGroupId limit:batchSizeLimit withCompletion:(OCMOCK_ANY)]) .andDo(^(NSInvocation *invocation) { MSLoadDataCompletionBlock loadCallback; @@ -193,7 +372,7 @@ - (void)testBatchQueueLimit { - (void)testNextBatchSentIfPendingQueueGotRoomAgain { - /** + /* * If */ [self initChannelEndJobExpectation]; @@ -205,7 +384,7 @@ - (void)testNextBatchSentIfPendingQueueGotRoomAgain { // Init mocks. id senderMock = OCMProtocolMock(@protocol(MSSender)); - OCMStub([senderMock sendAsync:[OCMArg any] completionHandler:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + OCMStub([senderMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { // Get sender bloc for later call. [invocation retainArguments]; @@ -215,7 +394,7 @@ - (void)testNextBatchSentIfPendingQueueGotRoomAgain { // Stub the storage load for that log. id storageMock = OCMProtocolMock(@protocol(MSStorage)); - OCMStub([storageMock loadLogsWithGroupId:kMSTestGroupId limit:batchSizeLimit withCompletion:([OCMArg any])]) + OCMStub([storageMock loadLogsWithGroupId:kMSTestGroupId limit:batchSizeLimit withCompletion:(OCMOCK_ANY)]) .andDo(^(NSInvocation *invocation) { MSLoadDataCompletionBlock loadCallback; @@ -238,7 +417,7 @@ - (void)testNextBatchSentIfPendingQueueGotRoomAgain { configuration:config logsDispatchQueue:dispatch_get_main_queue()]; - /** + /* * When */ [sut enqueueItem:[self getValidMockLog] withCompletion:nil]; @@ -247,7 +426,7 @@ - (void)testNextBatchSentIfPendingQueueGotRoomAgain { dispatch_async(self.logsDispatchQueue, ^{ senderBlock([@(1) stringValue], 200, nil, nil); - /** + /* * Then */ dispatch_async(self.logsDispatchQueue, ^{ @@ -256,7 +435,7 @@ - (void)testNextBatchSentIfPendingQueueGotRoomAgain { assertThatBool(sut.pendingBatchQueueFull, isFalse()); [oneLogSentExpectation fulfill]; - /** + /* * When */ @@ -267,7 +446,7 @@ - (void)testNextBatchSentIfPendingQueueGotRoomAgain { }); }); - /** + /* * Then */ [self waitForExpectationsWithTimeout:1 @@ -288,7 +467,8 @@ - (void)testDontForwardLogsToSenderOnDisabled { int batchSizeLimit = 1; id mockLog = [self getValidMockLog]; id senderMock = OCMProtocolMock(@protocol(MSSender)); - OCMStub([senderMock sendAsync:[OCMArg any] completionHandler:[OCMArg any]]); + OCMReject([senderMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]); + OCMStub([senderMock sendAsync:OCMOCK_ANY completionHandler:OCMOCK_ANY]); id storageMock = OCMProtocolMock(@protocol(MSStorage)); OCMStub([storageMock loadLogsWithGroupId:kMSTestGroupId @@ -304,21 +484,19 @@ - (void)testDontForwardLogsToSenderOnDisabled { storage:storageMock configuration:config logsDispatchQueue:dispatch_get_main_queue()]; - /** + /* * When */ [sut setEnabled:NO andDeleteDataOnDisabled:NO]; [sut enqueueItem:mockLog withCompletion:nil]; [self enqueueChannelEndJobExpectation]; - /** + /* * Then */ [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) { - - // Get sure it hasn't been sent. - OCMReject([senderMock sendAsync:[OCMArg any] completionHandler:[OCMArg any]]); + OCMVerifyAll(senderMock); if (error) { XCTFail(@"Expectation Failed with error: %@", error); } @@ -365,17 +543,18 @@ - (void)testDeleteDataOnDisabled { } - (void)testDontSaveLogsWhileDisabledWithDataDeletion { - + // If [self initChannelEndJobExpectation]; id mockLog = [self getValidMockLog]; - OCMReject([self.storageMock saveLog:[OCMArg any] withGroupId:[OCMArg any]]); - + OCMReject([self.storageMock saveLog:OCMOCK_ANY withGroupId:OCMOCK_ANY]); + // When [self.sut setEnabled:NO andDeleteDataOnDisabled:YES]; - [self.sut enqueueItem:mockLog withCompletion:^(__attribute__((unused)) BOOL success) { - [self enqueueChannelEndJobExpectation]; - }]; + [self.sut enqueueItem:mockLog + withCompletion:^(__attribute__((unused)) BOOL success) { + [self enqueueChannelEndJobExpectation]; + }]; // Then [self waitForExpectationsWithTimeout:1 @@ -388,50 +567,51 @@ - (void)testDontSaveLogsWhileDisabledWithDataDeletion { } - (void)testSaveLogsAfterReEnabled { - + // If [self initChannelEndJobExpectation]; [self.sut setEnabled:NO andDeleteDataOnDisabled:YES]; id mockLog = [self getValidMockLog]; - + // When [self.sut setEnabled:YES andDeleteDataOnDisabled:NO]; - [self.sut enqueueItem:mockLog withCompletion:^(__attribute__((unused)) BOOL success) { - [self enqueueChannelEndJobExpectation]; - }]; - + [self.sut enqueueItem:mockLog + withCompletion:^(__attribute__((unused)) BOOL success) { + [self enqueueChannelEndJobExpectation]; + }]; + // Then [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) { assertThatBool(self.sut.discardLogs, isFalse()); - OCMVerify([self.storageMock saveLog:mockLog withGroupId:[OCMArg any]]); + OCMVerify([self.storageMock saveLog:mockLog withGroupId:OCMOCK_ANY]); if (error) { XCTFail(@"Expectation Failed with error: %@", error); } }]; - + // If [self initChannelEndJobExpectation]; [self.sut setEnabled:NO andDeleteDataOnDisabled:NO]; - + // When [self.sut setEnabled:YES andDeleteDataOnDisabled:NO]; - [self.sut enqueueItem:mockLog withCompletion:^(__attribute__((unused)) BOOL success) { - [self enqueueChannelEndJobExpectation]; - }]; - + [self.sut enqueueItem:mockLog + withCompletion:^(__attribute__((unused)) BOOL success) { + [self enqueueChannelEndJobExpectation]; + }]; + // Then [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) { assertThatBool(self.sut.discardLogs, isFalse()); - OCMVerify([self.storageMock saveLog:mockLog withGroupId:[OCMArg any]]); + OCMVerify([self.storageMock saveLog:mockLog withGroupId:OCMOCK_ANY]); if (error) { XCTFail(@"Expectation Failed with error: %@", error); } }]; } - - (void)testDisableAndDeleteDataOnSenderFatalError { // If diff --git a/MobileCenter/MobileCenterTests/MSIngestionSenderTests.m b/MobileCenter/MobileCenterTests/MSIngestionSenderTests.m index 896b9f17c6..865733b04d 100644 --- a/MobileCenter/MobileCenterTests/MSIngestionSenderTests.m +++ b/MobileCenter/MobileCenterTests/MSIngestionSenderTests.m @@ -352,8 +352,8 @@ - (void)testRetryStoppedWhileSuspended { OCMStub([mockedCall sender:self.sut callCompletedWithStatus:MSHTTPCodesNo500InternalServerError - data:[OCMArg any] - error:[OCMArg any]]) + data:OCMOCK_ANY + error:OCMOCK_ANY]) .andForwardToRealObject() .andDo(^(__attribute__((unused)) NSInvocation *invocation) { [responseReceivedExcpectation fulfill]; diff --git a/MobileCenter/MobileCenterTests/MSMobileCenterTests.m b/MobileCenter/MobileCenterTests/MSMobileCenterTests.m index 6c1b98b505..f218f2876a 100644 --- a/MobileCenter/MobileCenterTests/MSMobileCenterTests.m +++ b/MobileCenter/MobileCenterTests/MSMobileCenterTests.m @@ -7,10 +7,12 @@ #import "MSMobileCenterPrivate.h" #import "MSMockCustomAppDelegate.h" #import "MSMockOriginalAppDelegate.h" +#import "MSMockService.h" #import "MSMockUserDefaults.h" -#import "MSLogManager.h" +#import "MSLogManagerDefault.h" #import "MSCustomProperties.h" #import "MSCustomPropertiesLog.h" +#import "MSStartServiceLog.h" static NSString *const kMSInstallIdStringExample = @"F18499DA-5C3D-4F05-B4E8-D8C9C06A6F09"; @@ -152,7 +154,7 @@ - (void)testSetCustomProperties { // If [MSMobileCenter start:MS_UUID_STRING withServices:nil]; id logManager = OCMProtocolMock(@protocol(MSLogManager)); - OCMStub([logManager processLog:[OCMArg isKindOfClass:[MSCustomPropertiesLog class]] forGroupId:[OCMArg any]]) + OCMStub([logManager processLog:[OCMArg isKindOfClass:[MSCustomPropertiesLog class]] forGroupId:OCMOCK_ANY]) .andDo(nil); [MSMobileCenter sharedInstance].logManager = logManager; @@ -162,13 +164,16 @@ - (void)testSetCustomProperties { [MSMobileCenter setCustomProperties:customProperties]; // Then - OCMVerify([logManager processLog:[OCMArg isKindOfClass:[MSCustomPropertiesLog class]] forGroupId:[OCMArg any]]); + OCMVerify([logManager processLog:[OCMArg isKindOfClass:[MSCustomPropertiesLog class]] forGroupId:OCMOCK_ANY]); // When // Not allow processLog more - OCMReject([logManager processLog:[OCMArg isKindOfClass:[MSCustomPropertiesLog class]] forGroupId:[OCMArg any]]); + OCMReject([logManager processLog:[OCMArg isKindOfClass:[MSCustomPropertiesLog class]] forGroupId:OCMOCK_ANY]); [MSMobileCenter setCustomProperties:nil]; [MSMobileCenter setCustomProperties:[MSCustomProperties new]]; + + // Then + OCMVerifyAll(logManager); } - (void)testConfigureWithAppSecret { @@ -176,6 +181,50 @@ - (void)testConfigureWithAppSecret { XCTAssertTrue([MSMobileCenter isConfigured]); } +- (void)testStartServiceWithInvalidValues { + NSUInteger servicesCount = [[MSMobileCenter sharedInstance] services].count; + [MSMobileCenter startService:[MSMobileCenter class]]; + [MSMobileCenter startService:[NSString class]]; + [MSMobileCenter startService:nil]; + XCTAssertEqual(servicesCount, [[MSMobileCenter sharedInstance] services].count); +} + +- (void)testStartWithoutServices { + + // If + id logManager = OCMClassMock([MSLogManagerDefault class]); + OCMStub([logManager alloc]).andReturn(logManager); + OCMStub([logManager initWithAppSecret:[OCMArg any] installId:[OCMArg any] logUrl:[OCMArg any]]).andReturn(logManager); + + // Not allow processLog + OCMReject([logManager processLog:[OCMArg isKindOfClass:[MSStartServiceLog class]] forGroupId:[OCMArg any]]); + + // When + [MSMobileCenter start:MS_UUID_STRING withServices:nil]; + + // Then + OCMVerifyAll(logManager); + + // Clear + [logManager stopMocking]; +} + +- (void)testStartServiceLogIsSentAfterStartService { + + // If + [MSMobileCenter start:MS_UUID_STRING withServices:nil]; + id logManager = OCMProtocolMock(@protocol(MSLogManager)); + OCMStub([logManager processLog:[OCMArg isKindOfClass:[MSStartServiceLog class]] forGroupId:OCMOCK_ANY]) + .andDo(nil); + [MSMobileCenter sharedInstance].logManager = logManager; + + // When + [MSMobileCenter startService:MSMockService.class]; + + // Then + OCMVerify([logManager processLog:[OCMArg isKindOfClass:[MSStartServiceLog class]] forGroupId:OCMOCK_ANY]); +} + - (void)testSortingServicesWorks { // If diff --git a/MobileCenter/MobileCenterTests/MSSenderUtilTests.m b/MobileCenter/MobileCenterTests/MSSenderUtilTests.m index fc97ff268d..460ffc72bc 100644 --- a/MobileCenter/MobileCenterTests/MSSenderUtilTests.m +++ b/MobileCenter/MobileCenterTests/MSSenderUtilTests.m @@ -44,4 +44,82 @@ - (void)testShortSecret { assertThat(hiddenSecret, is(fullyHiddenSecret)); } +- (void)testIsNoInternetConnectionError { + + // When. + NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorNotConnectedToInternet userInfo:nil]; + + // Then. + XCTAssertTrue([MSSenderUtil isNoInternetConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateHasBadDate userInfo:nil]; + + // Then. + XCTAssertFalse([MSSenderUtil isNoInternetConnectionError:error]); +} + +- (void)testSSLConnectionErrorDetected { + + // When. + NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorSecureConnectionFailed userInfo:nil]; + + // Then. + XCTAssertTrue([MSSenderUtil isSSLConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateHasBadDate userInfo:nil]; + + // Then. + XCTAssertTrue([MSSenderUtil isSSLConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateUntrusted userInfo:nil]; + + // Then. + XCTAssertTrue([MSSenderUtil isSSLConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateHasUnknownRoot userInfo:nil]; + + // Then. + XCTAssertTrue([MSSenderUtil isSSLConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorServerCertificateNotYetValid userInfo:nil]; + + // Then. + XCTAssertTrue([MSSenderUtil isSSLConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorClientCertificateRejected userInfo:nil]; + + // Then. + XCTAssertTrue([MSSenderUtil isSSLConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorClientCertificateRequired userInfo:nil]; + + // Then. + XCTAssertTrue([MSSenderUtil isSSLConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:NSURLErrorCannotLoadFromNetwork userInfo:nil]; + + // Then. + XCTAssertTrue([MSSenderUtil isSSLConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorFailingURLErrorKey code:NSURLErrorCannotLoadFromNetwork userInfo:nil]; + + // Then. + XCTAssertFalse([MSSenderUtil isSSLConnectionError:error]); + + // When. + error = [[NSError alloc] initWithDomain:NSURLErrorDomain code:10 userInfo:nil]; + + // Then. + XCTAssertFalse([MSSenderUtil isSSLConnectionError:error]); +} + @end diff --git a/MobileCenter/MobileCenterTests/MSServiceAbstractTests.m b/MobileCenter/MobileCenterTests/MSServiceAbstractTests.m index f13fe3c4f3..2e49c2d470 100644 --- a/MobileCenter/MobileCenterTests/MSServiceAbstractTests.m +++ b/MobileCenter/MobileCenterTests/MSServiceAbstractTests.m @@ -165,7 +165,7 @@ - (void)testIsEnabledToPersistence { assertThat([NSNumber numberWithBool:self.abstractService.isEnabled], is([NSNumber numberWithBool:expected])); // Also check that the sut did access the persistence. - OCMVerify([self.settingsMock setObject:[OCMArg any] forKey:[OCMArg any]]); + OCMVerify([self.settingsMock setObject:OCMOCK_ANY forKey:OCMOCK_ANY]); } - (void)testIsEnabledFromPersistence { @@ -187,7 +187,7 @@ - (void)testIsEnabledFromPersistence { assertThat(@(isEnabled), is(expected)); // Also check that the sut did access the persistence. - OCMVerify([self.settingsMock objectForKey:[OCMArg any]]); + OCMVerify([self.settingsMock objectForKey:OCMOCK_ANY]); } - (void)testCanBeUsed { diff --git a/MobileCenter/MobileCenterTests/MSUtilityTests.m b/MobileCenter/MobileCenterTests/MSUtilityTests.m index 0e559e5a65..bfcacd2fbd 100644 --- a/MobileCenter/MobileCenterTests/MSUtilityTests.m +++ b/MobileCenter/MobileCenterTests/MSUtilityTests.m @@ -42,6 +42,7 @@ - (void)testMSAppReturnsUnknownOnAppExtensions { id bundleMock = OCMClassMock([NSBundle class]); OCMStub([bundleMock executablePath]).andReturn(@"/apath/coolappext.appex/coolappext"); OCMStub([bundleMock mainBundle]).andReturn(bundleMock); + OCMReject([self.utils sharedAppState]); /** * Then @@ -49,7 +50,6 @@ - (void)testMSAppReturnsUnknownOnAppExtensions { assertThat(@([MSUtility applicationState]), is(@(MSApplicationStateUnknown))); // Make sure the sharedApplication as not been called, it's forbidden within app extensions - OCMReject([self.utils sharedAppState]); [bundleMock stopMocking]; } diff --git a/MobileCenter/MobileCenterTests/Util/MSMockService.h b/MobileCenter/MobileCenterTests/Util/MSMockService.h new file mode 100644 index 0000000000..c2a8cae487 --- /dev/null +++ b/MobileCenter/MobileCenterTests/Util/MSMockService.h @@ -0,0 +1,5 @@ +#import "MSServiceAbstract.h" +#import "MSServiceInternal.h" + +@interface MSMockService : MSServiceAbstract +@end diff --git a/MobileCenter/MobileCenterTests/Util/MSMockService.m b/MobileCenter/MobileCenterTests/Util/MSMockService.m new file mode 100644 index 0000000000..b8dac0326d --- /dev/null +++ b/MobileCenter/MobileCenterTests/Util/MSMockService.m @@ -0,0 +1,46 @@ +#import "MSMockService.h" +#import "MSChannelConfiguration.h" + +static NSString *const kMSServiceName = @"MSMockService"; +static NSString *const kMSGroupId = @"MSMock"; +static MSMockService *sharedInstance = nil; + +@implementation MSMockService + +@synthesize appSecret; +@synthesize available; +@synthesize initializationPriority; +@synthesize logManager; +@synthesize channelConfiguration; + +- (instancetype)init { + if ((self = [super init])) { + // Init channel configuration. + channelConfiguration = [[MSChannelConfiguration alloc] initDefaultConfigurationWithGroupId:[self groupId]]; + } + return self; +} + ++ (instancetype)sharedInstance { + if (sharedInstance == nil) { + sharedInstance = [[self alloc] init]; + } + return sharedInstance; +} + ++ (NSString *)serviceName { + return kMSServiceName; +} + +- (NSString *)groupId { + return kMSGroupId; +} + ++ (NSString *)logTag { + return @"MobileCenterTest"; +} + +- (void)applyEnabledState:(BOOL) __unused isEnabled { +} + +@end diff --git a/MobileCenter/MobileCenterTests/Util/MSMockUserDefaults.m b/MobileCenter/MobileCenterTests/Util/MSMockUserDefaults.m index ce3a337de8..32b24dafcd 100644 --- a/MobileCenter/MobileCenterTests/Util/MSMockUserDefaults.m +++ b/MobileCenter/MobileCenterTests/Util/MSMockUserDefaults.m @@ -16,9 +16,9 @@ - (instancetype)init { if (self) { _dictionary = [NSMutableDictionary new]; _mockUserDefaults = OCMClassMock([NSUserDefaults class]); - OCMStub([_mockUserDefaults objectForKey:[OCMArg any]]).andCall(self,@selector(objectForKey:)); - OCMStub([_mockUserDefaults setObject:[OCMArg any] forKey:[OCMArg any]]).andCall(self,@selector(setObject:forKey:)); - OCMStub([_mockUserDefaults removeObjectForKey:[OCMArg any]]).andCall(self,@selector(removeObjectForKey:)); + OCMStub([_mockUserDefaults objectForKey:OCMOCK_ANY]).andCall(self,@selector(objectForKey:)); + OCMStub([_mockUserDefaults setObject:OCMOCK_ANY forKey:OCMOCK_ANY]).andCall(self,@selector(setObject:forKey:)); + OCMStub([_mockUserDefaults removeObjectForKey:OCMOCK_ANY]).andCall(self,@selector(removeObjectForKey:)); OCMStub([_mockUserDefaults standardUserDefaults]).andReturn(self.mockUserDefaults); // Mock MSUserDefaults shared method to return this instance. diff --git a/MobileCenterAnalytics/MobileCenterAnalytics.xcodeproj/project.pbxproj b/MobileCenterAnalytics/MobileCenterAnalytics.xcodeproj/project.pbxproj index b45feec110..f0f39696e2 100644 --- a/MobileCenterAnalytics/MobileCenterAnalytics.xcodeproj/project.pbxproj +++ b/MobileCenterAnalytics/MobileCenterAnalytics.xcodeproj/project.pbxproj @@ -240,7 +240,7 @@ E81591311D526C3B003D5F3C /* MSSessionTrackerUtil.m */, ); name = Util; - path = Utils; + path = Util; sourceTree = ""; }; E83E81351D3ECEF600421B13 /* Util */ = { @@ -250,7 +250,7 @@ E83E81371D3ECF5900421B13 /* MSAnalyticsCategory.m */, ); name = Util; - path = Utils; + path = Util; sourceTree = ""; }; E85547B71D2D6253002DF6E2 = { diff --git a/MobileCenterAnalytics/MobileCenterAnalytics/Internals/MSAnalyticsInternal.h b/MobileCenterAnalytics/MobileCenterAnalytics/Internals/MSAnalyticsInternal.h index 92641fd2b6..8e9f00122a 100644 --- a/MobileCenterAnalytics/MobileCenterAnalytics/Internals/MSAnalyticsInternal.h +++ b/MobileCenterAnalytics/MobileCenterAnalytics/Internals/MSAnalyticsInternal.h @@ -42,7 +42,7 @@ NS_ASSUME_NONNULL_BEGIN * * @return YES if event name is valid, NO otherwise. */ -- (BOOL)validateEventName:(NSString *)eventName forLogType:(NSString *)logType; +- (NSString *)validateEventName:(NSString *)eventName forLogType:(NSString *)logType; /** * Validate keys and values of properties. diff --git a/MobileCenterAnalytics/MobileCenterAnalytics/Internals/Session/MSSessionTracker.m b/MobileCenterAnalytics/MobileCenterAnalytics/Internals/Session/MSSessionTracker.m index da67fde3f6..80994f6e25 100644 --- a/MobileCenterAnalytics/MobileCenterAnalytics/Internals/Session/MSSessionTracker.m +++ b/MobileCenterAnalytics/MobileCenterAnalytics/Internals/Session/MSSessionTracker.m @@ -1,8 +1,8 @@ #import "MSAnalyticsInternal.h" -#import "MSUtility+Date.h" #import "MSSessionTracker.h" #import "MSStartSessionLog.h" #import "MSStartServiceLog.h" +#import "MSUtility+Date.h" static NSTimeInterval const kMSSessionTimeOut = 20; static NSString *const kMSPastSessionsKey = @"pastSessionsKey"; @@ -67,12 +67,18 @@ - (NSString *)sessionId { sessionInfo.sessionId = _sessionId; sessionInfo.toffset = [NSNumber numberWithDouble:[MSUtility nowInMilliseconds]]; - // Insert at the beginning of the list. - [self.pastSessions insertObject:sessionInfo atIndex:0]; + // Insert new MSSessionHistoryInfo at the proper index to keep pastSessions sorted. + NSUInteger newIndex = [self.pastSessions indexOfObject:sessionInfo + inSortedRange:(NSRange) { 0, [self.pastSessions count] } + options:NSBinarySearchingInsertionIndex + usingComparator:^(id a, id b) { + return [((MSSessionHistoryInfo *)a).toffset compare:((MSSessionHistoryInfo *)b).toffset]; + }]; + [self.pastSessions insertObject:sessionInfo atIndex:newIndex]; - // Remove last item if reached max limit. + // Remove first (the oldest) item if reached max limit. if ([self.pastSessions count] > kMSMaxSessionHistoryCount) - [self.pastSessions removeLastObject]; + [self.pastSessions removeObjectAtIndex:0]; // Persist the session history in NSData format. [MS_USER_DEFAULTS setObject:[NSKeyedArchiver archivedDataWithRootObject:self.pastSessions] @@ -181,7 +187,8 @@ - (void)onEnqueuingLog:(id)log withInternalId:(NSString *)internalId { * Start session log is created in this method, therefore, skip in order to avoid infinite loop. * Also skip start service log as it's always sent and should not trigger a session. */ - if ([((NSObject *)log) isKindOfClass:[MSStartSessionLog class]] || [((NSObject *)log) isKindOfClass:[MSStartServiceLog class]]) + if ([((NSObject *)log) isKindOfClass:[MSStartSessionLog class]] || + [((NSObject *)log) isKindOfClass:[MSStartServiceLog class]]) return; // Attach corresponding session id. diff --git a/MobileCenterAnalytics/MobileCenterAnalytics/Internals/Utils/MSAnalyticsCategory.h b/MobileCenterAnalytics/MobileCenterAnalytics/Internals/Util/MSAnalyticsCategory.h similarity index 100% rename from MobileCenterAnalytics/MobileCenterAnalytics/Internals/Utils/MSAnalyticsCategory.h rename to MobileCenterAnalytics/MobileCenterAnalytics/Internals/Util/MSAnalyticsCategory.h diff --git a/MobileCenterAnalytics/MobileCenterAnalytics/Internals/Utils/MSAnalyticsCategory.m b/MobileCenterAnalytics/MobileCenterAnalytics/Internals/Util/MSAnalyticsCategory.m similarity index 100% rename from MobileCenterAnalytics/MobileCenterAnalytics/Internals/Utils/MSAnalyticsCategory.m rename to MobileCenterAnalytics/MobileCenterAnalytics/Internals/Util/MSAnalyticsCategory.m diff --git a/MobileCenterAnalytics/MobileCenterAnalytics/MSAnalytics.m b/MobileCenterAnalytics/MobileCenterAnalytics/MSAnalytics.m index 0c549dd7cb..0eae31c8e6 100644 --- a/MobileCenterAnalytics/MobileCenterAnalytics/MSAnalytics.m +++ b/MobileCenterAnalytics/MobileCenterAnalytics/MSAnalytics.m @@ -152,18 +152,18 @@ + (BOOL)isAutoPageTrackingEnabled { #pragma mark - Private methods -- (BOOL)validateEventName:(NSString *)eventName forLogType:(NSString *)logType { +- (NSString *)validateEventName:(NSString *)eventName forLogType:(NSString *)logType { if (!eventName || [eventName length] < minEventNameLength) { MSLogError([MSAnalytics logTag], @"%@ name cannot be null or empty", logType); - return NO; + return nil; } if ([eventName length] > maxEventNameLength) { - MSLogError([MSAnalytics logTag], - @"%@ '%@' : name length cannot be longer than %d characters", logType, eventName, maxEventNameLength); - return NO; + MSLogWarning([MSAnalytics logTag], + @"%@ '%@' : name length cannot be longer than %d characters. Name will be truncated.", logType, eventName, maxEventNameLength); + eventName = [eventName substringToIndex:maxEventNameLength]; } - return YES; + return eventName; } - (NSDictionary *)validateProperties:(NSDictionary *)properties @@ -196,30 +196,28 @@ - (BOOL)validateEventName:(NSString *)eventName forLogType:(NSString *)logType { } if ([strKey length] > maxPropertyKeyLength) { MSLogWarning([MSAnalytics logTag], - @"%@ '%@' : property %@ : property key length cannot be longer than %d characters. Property %@ will be skipped.", + @"%@ '%@' : property %@ : property key length cannot be longer than %d characters. Property key will be truncated.", logType, logName, strKey, - maxPropertyKeyLength, - strKey); - continue; + maxPropertyKeyLength); + strKey = [strKey substringToIndex:maxPropertyKeyLength]; } // Validate value. NSString *value = properties[key]; if([value length] > maxPropertyValueLength) { MSLogWarning([MSAnalytics logTag], - @"%@ '%@' : property '%@' : property value cannot be longer than %d characters. Property %@ will be skipped.", + @"%@ '%@' : property '%@' : property value cannot be longer than %d characters. Property value will be truncated.", logType, logName, strKey, - maxPropertyValueLength, - strKey); - continue; + maxPropertyValueLength); + value = [value substringToIndex:maxPropertyValueLength]; } // Save valid properties. - [validProperties setObject:value forKey:key]; + [validProperties setObject:value forKey:strKey]; } return validProperties; } @@ -232,12 +230,13 @@ - (void)trackEvent:(NSString *)eventName withProperties:(NSDictionary 0) { @@ -257,12 +256,13 @@ - (void)trackPage:(NSString *)pageName withProperties:(NSDictionary 0) { // Send only valid properties. diff --git a/MobileCenterAnalytics/MobileCenterAnalyticsTests/MSAnalyticsTests.m b/MobileCenterAnalytics/MobileCenterAnalyticsTests/MSAnalyticsTests.m index e12e7f7705..c7e89045eb 100644 --- a/MobileCenterAnalytics/MobileCenterAnalyticsTests/MSAnalyticsTests.m +++ b/MobileCenterAnalytics/MobileCenterAnalyticsTests/MSAnalyticsTests.m @@ -49,6 +49,7 @@ - (void)tearDown { #pragma mark - Tests - (void)testValidateEventName { + const int maxEventNameLength = 256; // If NSString *validEventName = @"validEventName"; @@ -65,26 +66,27 @@ - (void)testValidateEventName { @"tooLongEventNametooLongEventNametooLongEventNametooLongEventNametooLongEventNametooLongEventNametooLongEventNametooLongEventName"]; // When - BOOL valid = [[MSAnalytics sharedInstance] validateEventName:validEventName forLogType:kMSTypeEvent]; - BOOL validShortEventName = [[MSAnalytics sharedInstance] validateEventName:shortEventName forLogType:kMSTypeEvent]; - BOOL validEventName256 = [[MSAnalytics sharedInstance] validateEventName:eventName256 forLogType:kMSTypeEvent]; - BOOL validNullableEventName = - [[MSAnalytics sharedInstance] validateEventName:nullableEventName forLogType:kMSTypeEvent]; - BOOL validEmptyEventName = [[MSAnalytics sharedInstance] validateEventName:emptyEventName forLogType:kMSTypeEvent]; - BOOL validTooLongEventName = - [[MSAnalytics sharedInstance] validateEventName:tooLongEventName forLogType:kMSTypeEvent]; + NSString* valid = [[MSAnalytics sharedInstance] validateEventName:validEventName forLogType:kMSTypeEvent]; + NSString* validShortEventName = [[MSAnalytics sharedInstance] validateEventName:shortEventName forLogType:kMSTypeEvent]; + NSString* validEventName256 = [[MSAnalytics sharedInstance] validateEventName:eventName256 forLogType:kMSTypeEvent]; + NSString* validNullableEventName = [[MSAnalytics sharedInstance] validateEventName:nullableEventName forLogType:kMSTypeEvent]; + NSString* validEmptyEventName = [[MSAnalytics sharedInstance] validateEventName:emptyEventName forLogType:kMSTypeEvent]; + NSString* validTooLongEventName = [[MSAnalytics sharedInstance] validateEventName:tooLongEventName forLogType:kMSTypeEvent]; // Then - XCTAssertTrue(valid); - XCTAssertTrue(validShortEventName); - XCTAssertTrue(validEventName256); - XCTAssertFalse(validNullableEventName); - XCTAssertFalse(validEmptyEventName); - XCTAssertFalse(validTooLongEventName); + XCTAssertNotNil(valid); + XCTAssertNotNil(validShortEventName); + XCTAssertNotNil(validEventName256); + XCTAssertNil(validNullableEventName); + XCTAssertNil(validEmptyEventName); + XCTAssertNotNil(validTooLongEventName); + XCTAssertEqual([validTooLongEventName length], maxEventNameLength); } - (void)testValidatePropertyType { const int maxPropertriesPerEvent = 5; + const int maxPropertyKeyLength = 64; + const int maxPropertyValueLength = 64; NSString *longStringValue = [NSString stringWithFormat:@"%@", @"valueValueValueValueValueValueValueValueValueValueValueValueValue"]; NSString *stringValue64 = @@ -130,7 +132,6 @@ - (void)testValidatePropertyType { NSDictionary *invalidKeysInProperties = @{ @"Key1" : @"Value1", @(2) : @"Value2", - longStringValue : @"Value3", @"" : @"Value4" }; // When @@ -143,7 +144,7 @@ - (void)testValidatePropertyType { // Test invalid values // If - NSDictionary *invalidValuesInProperties = @{ @"Key1" : @"Value1", @"Key2" : @(2), @"Key3" : longStringValue }; + NSDictionary *invalidValuesInProperties = @{ @"Key1" : @"Value1", @"Key2" : @(2)}; // When validatedProperties = [[MSAnalytics sharedInstance] validateProperties:invalidValuesInProperties @@ -153,6 +154,22 @@ - (void)testValidatePropertyType { // Then XCTAssertTrue([validatedProperties count] == 1); + // Test long keys and values are truncated. + // If + NSDictionary *tooLongKeysAndValuesInProperties = @{longStringValue:longStringValue}; + + // When + validatedProperties = [[MSAnalytics sharedInstance] validateProperties:tooLongKeysAndValuesInProperties + forLogName:kMSTypeEvent + andType:kMSTypeEvent]; + + // Then + NSString *truncatedKey = (NSString *)[[validatedProperties allKeys] firstObject]; + NSString *truncatedValue = (NSString *)[[validatedProperties allValues] firstObject]; + XCTAssertTrue([validatedProperties count] == 1); + XCTAssertEqual([truncatedKey length], maxPropertyKeyLength); + XCTAssertEqual([truncatedValue length], maxPropertyValueLength); + // Test mixed variant // If NSDictionary *mixedEventProperties = @{ @@ -163,7 +180,6 @@ - (void)testValidatePropertyType { @"Key5" : @"Value5", @"Key6" : @(2), @"Key7" : longStringValue, - @"Key8" : @"" }; // When @@ -177,9 +193,8 @@ - (void)testValidatePropertyType { XCTAssertNotNil([validatedProperties objectForKey:stringValue64]); XCTAssertNotNil([validatedProperties objectForKey:@"Key4"]); XCTAssertNotNil([validatedProperties objectForKey:@"Key5"]); - XCTAssertNotNil([validatedProperties objectForKey:@"Key8"]); XCTAssertNil([validatedProperties objectForKey:@"Key6"]); - XCTAssertNil([validatedProperties objectForKey:@"Key7"]); + XCTAssertNotNil([validatedProperties objectForKey:@"Key7"]); } - (void)testApplyEnabledStateWorks { @@ -209,13 +224,17 @@ - (void)testAnalyticsDelegateWithoutImplementations { // If NSString *groupId = [[MSAnalytics sharedInstance] groupId]; - id delegateMock = OCMProtocolMock(@protocol(MSAnalyticsDelegate)); + MSEventLog *eventLog = OCMClassMock([MSEventLog class]); + id delegateMock = OCMProtocolMock(@protocol(MSAnalyticsDelegate)); + OCMReject([delegateMock analytics:[MSAnalytics sharedInstance] willSendEventLog:eventLog]); + OCMReject([delegateMock analytics:[MSAnalytics sharedInstance] didSucceedSendingEventLog:eventLog]); + OCMReject([delegateMock analytics:[MSAnalytics sharedInstance] didFailSendingEventLog:eventLog withError:nil]); [MSMobileCenter sharedInstance].sdkConfigured = NO; [MSMobileCenter start:kMSTestAppSecret withServices:@[ [MSAnalytics class] ]]; NSMutableDictionary *channelsInLogManager = ((MSLogManagerDefault *)([MSAnalytics sharedInstance].logManager)).channels; MSChannelDefault *channelMock = channelsInLogManager[groupId] = OCMPartialMock(channelsInLogManager[groupId]); - OCMStub([channelMock enqueueItem:[OCMArg any] withCompletion:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + OCMStub([channelMock enqueueItem:OCMOCK_ANY withCompletion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { id log = nil; [invocation getArgument:&log atIndex:2]; for (id delegate in channelMock.delegates) { @@ -228,13 +247,10 @@ - (void)testAnalyticsDelegateWithoutImplementations { }); // When - MSEventLog *eventLog = OCMClassMock([MSEventLog class]); [[MSAnalytics sharedInstance].logManager processLog:eventLog forGroupId:groupId]; - + // Then - OCMReject([delegateMock analytics:[MSAnalytics sharedInstance] willSendEventLog:eventLog]); - OCMReject([delegateMock analytics:[MSAnalytics sharedInstance] didSucceedSendingEventLog:eventLog]); - OCMReject([delegateMock analytics:[MSAnalytics sharedInstance] didFailSendingEventLog:eventLog withError:nil]); + OCMVerifyAll(delegateMock); } - (void)testAnalyticsDelegateMethodsAreCalled { @@ -248,7 +264,7 @@ - (void)testAnalyticsDelegateMethodsAreCalled { NSMutableDictionary *channelsInLogManager = ((MSLogManagerDefault *)([MSAnalytics sharedInstance].logManager)).channels; MSChannelDefault *channelMock = channelsInLogManager[groupId] = OCMPartialMock(channelsInLogManager[groupId]); - OCMStub([channelMock enqueueItem:[OCMArg any] withCompletion:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + OCMStub([channelMock enqueueItem:OCMOCK_ANY withCompletion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { id log = nil; [invocation getArgument:&log atIndex:2]; for (id delegate in channelMock.delegates) { @@ -278,7 +294,7 @@ - (void)testTrackEventWithoutProperties { __block NSString *type; NSString *expectedName = @"gotACoffee"; id logManagerMock = OCMProtocolMock(@protocol(MSLogManager)); - OCMStub([logManagerMock processLog:[OCMArg isKindOfClass:[MSLogWithProperties class]] forGroupId:[OCMArg any]]) + OCMStub([logManagerMock processLog:[OCMArg isKindOfClass:[MSLogWithProperties class]] forGroupId:OCMOCK_ANY]) .andDo(^(NSInvocation *invocation) { MSEventLog *log; [invocation getArgument:&log atIndex:2]; @@ -305,7 +321,7 @@ - (void)testTrackEventWithProperties { NSString *expectedName = @"gotACoffee"; NSDictionary *expectedProperties = @{ @"milk" : @"yes", @"cookie" : @"of course" }; id logManagerMock = OCMProtocolMock(@protocol(MSLogManager)); - OCMStub([logManagerMock processLog:[OCMArg isKindOfClass:[MSLogWithProperties class]] forGroupId:[OCMArg any]]) + OCMStub([logManagerMock processLog:[OCMArg isKindOfClass:[MSLogWithProperties class]] forGroupId:OCMOCK_ANY]) .andDo(^(NSInvocation *invocation) { MSEventLog *log; [invocation getArgument:&log atIndex:2]; @@ -332,7 +348,7 @@ - (void)testTrackPageWithoutProperties { __block NSString *type; NSString *expectedName = @"HomeSweetHome"; id logManagerMock = OCMProtocolMock(@protocol(MSLogManager)); - OCMStub([logManagerMock processLog:[OCMArg isKindOfClass:[MSLogWithProperties class]] forGroupId:[OCMArg any]]) + OCMStub([logManagerMock processLog:[OCMArg isKindOfClass:[MSLogWithProperties class]] forGroupId:OCMOCK_ANY]) .andDo(^(NSInvocation *invocation) { MSEventLog *log; [invocation getArgument:&log atIndex:2]; @@ -359,7 +375,7 @@ - (void)testTrackPageWithProperties { NSString *expectedName = @"HomeSweetHome"; NSDictionary *expectedProperties = @{ @"Sofa" : @"yes", @"TV" : @"of course" }; id logManagerMock = OCMProtocolMock(@protocol(MSLogManager)); - OCMStub([logManagerMock processLog:[OCMArg isKindOfClass:[MSLogWithProperties class]] forGroupId:[OCMArg any]]) + OCMStub([logManagerMock processLog:[OCMArg isKindOfClass:[MSLogWithProperties class]] forGroupId:OCMOCK_ANY]) .andDo(^(NSInvocation *invocation) { MSEventLog *log; [invocation getArgument:&log atIndex:2]; diff --git a/MobileCenterAnalytics/MobileCenterAnalyticsTests/MSSessionTrackerTests.m b/MobileCenterAnalytics/MobileCenterAnalyticsTests/MSSessionTrackerTests.m index daf4d1b728..c4be997300 100644 --- a/MobileCenterAnalytics/MobileCenterAnalyticsTests/MSSessionTrackerTests.m +++ b/MobileCenterAnalytics/MobileCenterAnalyticsTests/MSSessionTrackerTests.m @@ -215,7 +215,7 @@ - (void)testOnProcessingLog { // Then XCTAssertEqual(0, log.toffset.integerValue); - XCTAssertEqual(log.sid, [self.sut.pastSessions firstObject].sessionId); + XCTAssertEqual(log.sid, [self.sut.pastSessions lastObject].sessionId); // When log.toffset = [NSNumber numberWithUnsignedLongLong:UINT64_MAX]; diff --git a/MobileCenterAnalytics/MobileCenterAnalyticsTests/Utils/MSSessionTrackerUtil.h b/MobileCenterAnalytics/MobileCenterAnalyticsTests/Util/MSSessionTrackerUtil.h similarity index 100% rename from MobileCenterAnalytics/MobileCenterAnalyticsTests/Utils/MSSessionTrackerUtil.h rename to MobileCenterAnalytics/MobileCenterAnalyticsTests/Util/MSSessionTrackerUtil.h diff --git a/MobileCenterAnalytics/MobileCenterAnalyticsTests/Utils/MSSessionTrackerUtil.m b/MobileCenterAnalytics/MobileCenterAnalyticsTests/Util/MSSessionTrackerUtil.m similarity index 100% rename from MobileCenterAnalytics/MobileCenterAnalyticsTests/Utils/MSSessionTrackerUtil.m rename to MobileCenterAnalytics/MobileCenterAnalyticsTests/Util/MSSessionTrackerUtil.m diff --git a/MobileCenterCrashes/MobileCenterCrashes.xcodeproj/project.pbxproj b/MobileCenterCrashes/MobileCenterCrashes.xcodeproj/project.pbxproj index aae6602142..9bf15d6ed5 100644 --- a/MobileCenterCrashes/MobileCenterCrashes.xcodeproj/project.pbxproj +++ b/MobileCenterCrashes/MobileCenterCrashes.xcodeproj/project.pbxproj @@ -35,9 +35,15 @@ /* Begin PBXBuildFile section */ 3507AE3D1DD14C240030878F /* MSException.h in Headers */ = {isa = PBXBuildFile; fileRef = 3507AE3A1DD14C240030878F /* MSException.h */; }; 3507AE3E1DD14C240030878F /* MSStackFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 3507AE3B1DD14C240030878F /* MSStackFrame.h */; }; + 350B29F21F1D67EE009B91CF /* MSWrapperCrashesHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 350B29F11F1D67EE009B91CF /* MSWrapperCrashesHelper.m */; }; + 350B29F41F1E6F1D009B91CF /* MSWrapperExceptionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 350B29F31F1E6F1D009B91CF /* MSWrapperExceptionTests.m */; }; 3515F9C11DD63EC9005E6E27 /* MSCrashesInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 3515F9C01DD63EC9005E6E27 /* MSCrashesInternal.h */; }; + 352B1D6F1F27C36300684A7F /* MSWrapperCrashesHelperTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 352B1D6E1F27C36300684A7F /* MSWrapperCrashesHelperTests.m */; }; + 353FD1611F29209000E1DF78 /* MSWrapperException.h in Headers */ = {isa = PBXBuildFile; fileRef = 353FD15E1F29209000E1DF78 /* MSWrapperException.h */; }; + 353FD1621F29209000E1DF78 /* MSWrapperException.m in Sources */ = {isa = PBXBuildFile; fileRef = 353FD15F1F29209000E1DF78 /* MSWrapperException.m */; }; + 353FD1631F29209000E1DF78 /* MSWrapperExceptionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 353FD1601F29209000E1DF78 /* MSWrapperExceptionInternal.h */; }; 35B7D87A1DE4D4A300C846CD /* MSWrapperExceptionManagerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 35B7D8791DE4CB6D00C846CD /* MSWrapperExceptionManagerInternal.h */; }; - 35D504CF1DDD140500D58B40 /* MSWrapperExceptionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D504CD1DDD140500D58B40 /* MSWrapperExceptionManager.h */; }; + 35D504CF1DDD140500D58B40 /* MSWrapperExceptionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D504CD1DDD140500D58B40 /* MSWrapperExceptionManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35D504D01DDD140500D58B40 /* MSWrapperExceptionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35D504CE1DDD140500D58B40 /* MSWrapperExceptionManager.m */; }; 35EF18E01DDBCF6C00731CA8 /* MSWrapperExceptionManagerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 35EF18DF1DDBCF6C00731CA8 /* MSWrapperExceptionManagerTests.m */; }; 380B81321E8C540E001C76C9 /* MSLogWithProperties.h in Headers */ = {isa = PBXBuildFile; fileRef = 380B81311E8C540E001C76C9 /* MSLogWithProperties.h */; }; @@ -163,7 +169,15 @@ /* Begin PBXFileReference section */ 3507AE3A1DD14C240030878F /* MSException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSException.h; sourceTree = ""; }; 3507AE3B1DD14C240030878F /* MSStackFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSStackFrame.h; sourceTree = ""; }; + 350B29E71F192554009B91CF /* MSCrashHandlerSetupDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSCrashHandlerSetupDelegate.h; sourceTree = ""; }; + 350B29E81F1929A1009B91CF /* MSWrapperCrashesHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSWrapperCrashesHelper.h; sourceTree = ""; }; + 350B29F11F1D67EE009B91CF /* MSWrapperCrashesHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSWrapperCrashesHelper.m; sourceTree = ""; }; + 350B29F31F1E6F1D009B91CF /* MSWrapperExceptionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSWrapperExceptionTests.m; sourceTree = ""; }; 3515F9C01DD63EC9005E6E27 /* MSCrashesInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSCrashesInternal.h; sourceTree = ""; }; + 352B1D6E1F27C36300684A7F /* MSWrapperCrashesHelperTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSWrapperCrashesHelperTests.m; sourceTree = ""; }; + 353FD15E1F29209000E1DF78 /* MSWrapperException.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSWrapperException.h; sourceTree = ""; }; + 353FD15F1F29209000E1DF78 /* MSWrapperException.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSWrapperException.m; sourceTree = ""; }; + 353FD1601F29209000E1DF78 /* MSWrapperExceptionInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSWrapperExceptionInternal.h; sourceTree = ""; }; 35B7D8791DE4CB6D00C846CD /* MSWrapperExceptionManagerInternal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MSWrapperExceptionManagerInternal.h; sourceTree = ""; }; 35D504CD1DDD140500D58B40 /* MSWrapperExceptionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MSWrapperExceptionManager.h; sourceTree = ""; }; 35D504CE1DDD140500D58B40 /* MSWrapperExceptionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MSWrapperExceptionManager.m; sourceTree = ""; }; @@ -301,6 +315,19 @@ name = Model; sourceTree = ""; }; + 350B29E91F192A5C009B91CF /* WrapperSdkUtilities */ = { + isa = PBXGroup; + children = ( + 350B29E81F1929A1009B91CF /* MSWrapperCrashesHelper.h */, + 350B29F11F1D67EE009B91CF /* MSWrapperCrashesHelper.m */, + 350B29E71F192554009B91CF /* MSCrashHandlerSetupDelegate.h */, + 35B7D8791DE4CB6D00C846CD /* MSWrapperExceptionManagerInternal.h */, + 35D504CE1DDD140500D58B40 /* MSWrapperExceptionManager.m */, + 35D504CD1DDD140500D58B40 /* MSWrapperExceptionManager.h */, + ); + name = WrapperSdkUtilities; + sourceTree = ""; + }; 6E04012A1D1C98690051BCFA = { isa = PBXGroup; children = ( @@ -343,9 +370,7 @@ 6E0401511D1C9A4F0051BCFA /* Internals */ = { isa = PBXGroup; children = ( - 35D504CD1DDD140500D58B40 /* MSWrapperExceptionManager.h */, - 35D504CE1DDD140500D58B40 /* MSWrapperExceptionManager.m */, - 35B7D8791DE4CB6D00C846CD /* MSWrapperExceptionManagerInternal.h */, + 350B29E91F192A5C009B91CF /* WrapperSdkUtilities */, 3515F9C01DD63EC9005E6E27 /* MSCrashesInternal.h */, 6E73FE711D4059E7008CDC15 /* MSCrashesCXXExceptionWrapperException.h */, 6E73FE721D4059E7008CDC15 /* MSCrashesCXXExceptionWrapperException.m */, @@ -362,6 +387,7 @@ isa = PBXGroup; children = ( 6E7D5C811D3EC06C009EC9AC /* MSConstants.h */, + 350B29F31F1E6F1D009B91CF /* MSWrapperExceptionTests.m */, B2FF130B1DD12F61003DC677 /* MSAppleErrorLogTests.m */, B2F120D41D6546740060DED7 /* MSErrorAttachmentLogTests.m */, 6E7D5C7F1D3EAEB5009EC9AC /* MSBinaryTests.m */, @@ -375,6 +401,7 @@ 6EC99A2D1D4166C50016C325 /* MSCrashesTests.mm */, B24F3F0E1D93368F00827213 /* MSErrorLogFormatterTests.mm */, 6E171AE51D22F781000DC480 /* Info.plist */, + 352B1D6E1F27C36300684A7F /* MSWrapperCrashesHelperTests.m */, 6EC99A2F1D416CCF0016C325 /* Fixtures */, 6E171AEC1D22F7AB000DC480 /* Frameworks */, 0444496D1DD3D26D00D33BAE /* Model */, @@ -441,12 +468,15 @@ 6E73FE6E1D4032AB008CDC15 /* MSCrashesUtil.m */, ); name = Util; - path = Internals/Utils; + path = Internals/Util; sourceTree = ""; }; 6E7D5C641D3E92B1009EC9AC /* Model */ = { isa = PBXGroup; children = ( + 353FD15E1F29209000E1DF78 /* MSWrapperException.h */, + 353FD15F1F29209000E1DF78 /* MSWrapperException.m */, + 353FD1601F29209000E1DF78 /* MSWrapperExceptionInternal.h */, 382346411E8B3DAD001C3A76 /* MSErrorAttachmentLogInternal.h */, 3507AE3A1DD14C240030878F /* MSException.h */, 6E7D5C6E1D3E9346009EC9AC /* MSException.m */, @@ -540,7 +570,6 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - 35D504CF1DDD140500D58B40 /* MSWrapperExceptionManager.h in Headers */, 3507AE3E1DD14C240030878F /* MSStackFrame.h in Headers */, E8AEE9861D59744D00C0FF6C /* MSLogManager.h in Headers */, 6E7D5C671D3E9321009EC9AC /* MSBinary.h in Headers */, @@ -549,17 +578,20 @@ 35B7D87A1DE4D4A300C846CD /* MSWrapperExceptionManagerInternal.h in Headers */, 6E0401751D1C9E750051BCFA /* MobileCenterCrashes.h in Headers */, 38BD86521E8499EF004E8D7A /* MSErrorAttachmentLog.h in Headers */, + 35D504CF1DDD140500D58B40 /* MSWrapperExceptionManager.h in Headers */, B2F375091D41AD5100F07032 /* MSErrorLogFormatter.h in Headers */, + 3507AE3D1DD14C240030878F /* MSException.h in Headers */, B2F120E71D657F4F0060DED7 /* MSErrorReportPrivate.h in Headers */, + 353FD1631F29209000E1DF78 /* MSWrapperExceptionInternal.h in Headers */, 380B81341E8C565E001C76C9 /* MSAbstractLog.h in Headers */, 6E7D5C6B1D3E9332009EC9AC /* MSAppleErrorLog.h in Headers */, 6E7D5C731D3E9381009EC9AC /* MSThread.h in Headers */, 382346421E8B3DAD001C3A76 /* MSErrorAttachmentLogInternal.h in Headers */, + 353FD1611F29209000E1DF78 /* MSWrapperException.h in Headers */, 6E73FE6F1D4032AB008CDC15 /* MSCrashesUtil.h in Headers */, B2CD3BF91D80EE49000A8A91 /* MSAbstractErrorLog.h in Headers */, 3515F9C11DD63EC9005E6E27 /* MSCrashesInternal.h in Headers */, 3858A2181E93F37E00535A69 /* MSErrorAttachmentLog+Utility.h in Headers */, - 3507AE3D1DD14C240030878F /* MSException.h in Headers */, 387C77071D6CC41100D68CC1 /* MSServiceAbstract.h in Headers */, B24F3F121D9341BC00827213 /* MSErrorLogFormatterPrivate.h in Headers */, 6E0401771D1C9EBE0051BCFA /* MobileCenter+Internal.h in Headers */, @@ -729,11 +761,13 @@ B2CD3BFA1D80EE49000A8A91 /* MSAbstractErrorLog.m in Sources */, 3858A21A1E93F3B400535A69 /* MSErrorAttachmentLog+Utility.m in Sources */, 6E7D5C701D3E9346009EC9AC /* MSException.m in Sources */, + 350B29F21F1D67EE009B91CF /* MSWrapperCrashesHelper.m in Sources */, 6E73FE741D4059E7008CDC15 /* MSCrashesCXXExceptionWrapperException.m in Sources */, 6E7D5C6C1D3E9332009EC9AC /* MSAppleErrorLog.m in Sources */, 6E7D5C781D3E9395009EC9AC /* MSStackFrame.m in Sources */, 8024743B1EAE077800AEC284 /* MSErrorAttachmentLog.m in Sources */, 6E7D5C741D3E9381009EC9AC /* MSThread.m in Sources */, + 353FD1621F29209000E1DF78 /* MSWrapperException.m in Sources */, 6E0401541D1C9A4F0051BCFA /* MSCrashes.mm in Sources */, B2F3750A1D41AD5100F07032 /* MSErrorLogFormatter.m in Sources */, 35D504D01DDD140500D58B40 /* MSWrapperExceptionManager.m in Sources */, @@ -748,8 +782,10 @@ files = ( B2FF130C1DD12F61003DC677 /* MSAppleErrorLogTests.m in Sources */, 6EC99A2E1D4166C50016C325 /* MSCrashesTests.mm in Sources */, + 350B29F41F1E6F1D009B91CF /* MSWrapperExceptionTests.m in Sources */, 6E7D5C8A1D3EC504009EC9AC /* MSExceptionTests.m in Sources */, B2F120D71D65469D0060DED7 /* MSErrorReportTests.m in Sources */, + 352B1D6F1F27C36300684A7F /* MSWrapperCrashesHelperTests.m in Sources */, 6E7D5C871D3EC332009EC9AC /* MSStackFrameTests.m in Sources */, F851DAED1E81867D00525570 /* MSCrashesCXXExceptionTests.mm in Sources */, 6E7D5C841D3EC0F7009EC9AC /* MSThreadTests.m in Sources */, diff --git a/MobileCenterCrashes/MobileCenterCrashes.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/MobileCenterCrashes/MobileCenterCrashes.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 94b2795e22..0000000000 --- a/MobileCenterCrashes/MobileCenterCrashes.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashHandlerSetupDelegate.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashHandlerSetupDelegate.h new file mode 100644 index 0000000000..6b2dcc53cf --- /dev/null +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashHandlerSetupDelegate.h @@ -0,0 +1,33 @@ +#import + +/** + * This is required for Wrapper SDKs that need to provide custom behavior + * surrounding the setup of crash handlers. + */ +@protocol MSCrashHandlerSetupDelegate + +@optional + +/** + * Callback method that will be called immediately before crash handlers are set up. + */ +- (void)willSetUpCrashHandlers; + +/** + * Callback method that will be called immediately after crash handlers are set up. + */ +- (void)didSetUpCrashHandlers; + +/** + * Callback method that gets a value indicating whether the SDK should enable + * an uncaught exception handler. + * + * @return YES if SDK should enable uncaught exception handler, otherwise NO. + * + * @discussion Do not register an UncaughtExceptionHandler for Xamarin as we rely + * on the Xamarin runtime to report NSExceptions. Registering our own UncaughtExceptionHandler + * will cause the Xamarin debugger to not work properly (it will not stop for NSExceptions). + */ +- (BOOL)shouldEnableUncaughtExceptionHandler; + +@end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashesInternal.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashesInternal.h index 57cdf19b26..807fa7cd8a 100644 --- a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashesInternal.h +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashesInternal.h @@ -1,6 +1,8 @@ #import "MSCrashes.h" #import "MSServiceInternal.h" +@class MSException; + @interface MSCrashes () /** diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashesPrivate.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashesPrivate.h index 4e2b8cf759..e277bc70db 100644 --- a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashesPrivate.h +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSCrashesPrivate.h @@ -151,11 +151,6 @@ typedef struct MSCrashesCallbacks { */ - (BOOL)delegateImplementsAttachmentCallback; -/** - * Save the managed exception information in the event of a crash from a wrapper sdk. - */ -+ (void)wrapperCrashCallback; - /** * Creates log buffer to buffer logs which will be saved in an async-safe manner * at crash time. The buffer makes sure we don't loose any logs at crashtime. diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperCrashesHelper.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperCrashesHelper.h new file mode 100644 index 0000000000..f85bb58a76 --- /dev/null +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperCrashesHelper.h @@ -0,0 +1,20 @@ +#import +#import "MSCrashHandlerSetupDelegate.h" + +/** + * This general class allows wrappers to supplement the Crashes SDK with their own + * behavior. + */ +@interface MSWrapperCrashesHelper : NSObject + +/** + * Sets the crash handler setup delegate. + */ ++ (void)setCrashHandlerSetupDelegate:(id)delegate; + +/** + * Gets the crash handler setup delegate. + */ ++ (id)getCrashHandlerSetupDelegate; + +@end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperCrashesHelper.m b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperCrashesHelper.m new file mode 100644 index 0000000000..ba75163070 --- /dev/null +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperCrashesHelper.m @@ -0,0 +1,31 @@ +#import "MSWrapperCrashesHelper.h" + +@interface MSWrapperCrashesHelper () + +@property(weak, nonatomic) id crashHandlerSetupDelegate; + +@end + +@implementation MSWrapperCrashesHelper + +/** + * Gets the singleton instance. + */ ++ (instancetype)sharedInstance { + static MSWrapperCrashesHelper *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + ++ (void)setCrashHandlerSetupDelegate:(id)delegate { + [[self sharedInstance] setCrashHandlerSetupDelegate:delegate]; +} + ++ (id)getCrashHandlerSetupDelegate { + return [[self sharedInstance] crashHandlerSetupDelegate]; +} + +@end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManager.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManager.h index 0741437af0..791f2e0cad 100644 --- a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManager.h +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManager.h @@ -1,111 +1,42 @@ #import -@class MSException; - -/** - * This is required for Wrapper SDKs that need to install their own - * signal handlers for certain signals. - */ -@protocol MSWrapperCrashesInitializationDelegate - -/** - * Implement this function to override the default behavior for - * setting up the crash handlers (i.e., configuring PLCrashReporter) +@class MSWrapperException; + +/** + * This class helps wrapper SDKs augment crash reports with additional data. + * + * HOW IT WORKS: + * 1. Application is crashing from a wrapper SDK, but before propagating + * crash to the native code, it calls "saveWrapperException" + * which saves the MSWrapperException to a file called + * "last_saved_wrapper_exception". + * 2. On startup, the native SDK must find that file if it exists, + * and use the MSWrapperException's "pid" to correlate the file to a + * PLCrashReport on disk. If a match is found, the file is renamed + * to the UUID of the PLCrashReport. + * 3. When an MSAppleErrorLog must be generated, a corresponding + * MSWrapperException file is searched for, and if found, its + * MSException property is added to the MSAppleErrorLog. The file + * is not deleted because there is an additional "data" property + * that contains information that the wrapper SDK may want. + * 4. When an MSErrorReport equivalent needs to be generated by a + * wrapper SDK, it is identical to the actual MSErrorReport, but also + * includes the additional data that was saved. This data is accessed + * by "loadWrapperExceptionWithUUID". Thus, the file on disk can only + * be deleted after all crash callbacks with an MSErrorReport parameter + * have completed. */ -@optional -- (BOOL) setUpCrashHandlers; - -@end - @interface MSWrapperExceptionManager : NSObject /** - * Check if a wrapper SDK has stored an exception in memory. - * - * @return YES if there is an exception from a wrapper SDK - */ -+ (BOOL)hasException; - -/** - * Load an MSException from a wrapper SDK into memory - * - * @param uuidRef The UUID of the associated incident - * @return The wrapper exception corresponding to uuidRef - */ -+ (MSException*)loadWrapperException:(CFUUIDRef)uuidRef; - -/** - * Save the wrapper exception in memory to disk - * - * @param uuidRef The UUID of the associated incident - */ -+ (void)saveWrapperException:(CFUUIDRef)uuidRef; - -/** - * Remove the wrapper exception corresponding to uuidRef from disk - * - * @param uuidRef The UUID of the associated incident - */ -+ (void)deleteWrapperExceptionWithUUID:(CFUUIDRef)uuidRef; - -/** - * Remove all saved wrapper exception files from disk. + * Save the MSWrapperException to the file "last_saved_wrapper_exception". + * This should only be used by a wrapper SDK; native code has no use for it. */ -+ (void)deleteAllWrapperExceptions; ++ (void)saveWrapperException:(MSWrapperException *)wrapperException; /** - * Save a wrapper exception in memory. This should only be used by - * a wrapper SDK. - * - * @param exception The exception to be stored - */ -+ (void)setWrapperException:(MSException*)exception; - -/** - * Save (in memory) any additional data that a wrapper SDK needs to - * generate an error report. This should only be used by a wrapper - * SDK. - * - * @param data The exception data to be stored - */ -+ (void)setWrapperExceptionData:(NSData*)data; - -/** - * Load exception data from a wrapper SDK into memory. - * - * @param uuidString String representation of the UUID of the incident - * @return The wrapper exception data corresponding to uuidString - */ -+ (NSData*)loadWrapperExceptionDataWithUUIDString:(NSString*)uuidString; - -/** - * Delete a particular wrapper exception data file from disk and store it - * in memory - * - * @param uuidString String representation of the UUID of the incident - */ -+ (void)deleteWrapperExceptionDataWithUUIDString:(NSString*)uuidString; - -/** - * Remove all saved wrapper exception data files from disk. - */ -+ (void)deleteAllWrapperExceptionData; - -/** - * Configure the crash reporting libraries. This should only be used in the delegate method - * for MSWrapperCrashesInitializationDelegate, and only by a wrapper SDK. - */ -+ (void)startCrashReportingFromWrapperSdk; - -/** - * Set a delegate for intercepting the point at which the crash libraries are - * set up. This should only be used by a wrapper SDK. + * Load a wrapper exception from disk with a given UUID. */ -+ (void)setDelegate:(id) delegate; ++(MSWrapperException *)loadWrapperExceptionWithUUIDString:(NSString *)uuidString; -/** - * Get the previously set delegate for intercepting the point at which the crash libraries - * are set up. - */ -+ (id)getDelegate; @end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManager.m b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManager.m index 453411e603..9134d4d2b6 100644 --- a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManager.m +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManager.m @@ -1,295 +1,134 @@ -#import "MSCrashes.h" #import "MSCrashesInternal.h" +#import "MSCrashesUtil.h" +#import "MSErrorReport.h" #import "MSException.h" +#import "MSUtility+File.h" +#import "MSWrapperExceptionInternal.h" #import "MSWrapperExceptionManagerInternal.h" @implementation MSWrapperExceptionManager : NSObject -#pragma mark - Public methods +static NSString* const kMSLastWrapperExceptionFileName = @"last_saved_wrapper_exception"; +static NSMutableDictionary* unprocessedWrapperExceptions; -+ (BOOL)hasException { - return [[self sharedInstance] hasException]; ++ (void) load { + unprocessedWrapperExceptions = [NSMutableDictionary new]; } -+ (void)setWrapperException:(MSException *)wrapperException { - [self sharedInstance].wrapperException = wrapperException; -} - -+ (void)saveWrapperExceptionData:(CFUUIDRef)uuidRef { - [[self sharedInstance] saveWrapperExceptionData:uuidRef]; -} - -+ (NSData *)loadWrapperExceptionDataWithUUIDString:(NSString *)uuidString { - return [[self sharedInstance] loadWrapperExceptionDataWithUUIDString:uuidString]; -} +#pragma mark Public Methods -+ (MSException *)loadWrapperException:(CFUUIDRef)uuidRef { - return [[self sharedInstance] loadWrapperException:uuidRef]; +/** + * Gets a wrapper exception with a given UUID. + */ ++ (MSWrapperException *)loadWrapperExceptionWithUUIDString:(NSString *)uuidString { + MSWrapperException *foundException = [unprocessedWrapperExceptions objectForKey:uuidString]; + return foundException ? foundException : [self loadWrapperExceptionWithBaseFilename:uuidString]; } -+ (void)saveWrapperException:(CFUUIDRef)uuidRef { - [[self sharedInstance] saveWrapperException:uuidRef]; +/** + * Saves a wrapper exception to disk. Should only be used by wrapper SDK. + */ ++ (void)saveWrapperException:(MSWrapperException *)wrapperException { + [self saveWrapperException:wrapperException withBaseFilename:kMSLastWrapperExceptionFileName]; } -+ (void)setWrapperExceptionData:(NSData *)data { - [self sharedInstance].unsavedWrapperExceptionData = data; -} +#pragma mark Internal Methods -+ (void)deleteWrapperExceptionWithUUID:(CFUUIDRef)uuidRef { - [[self sharedInstance] deleteWrapperExceptionWithUUID:uuidRef]; +/** + * Deletes a wrapper exception with a given UUID. + */ ++ (void)deleteWrapperExceptionWithUUIDString:(NSString *)uuidString { + [self deleteWrapperExceptionWithBaseFilename:uuidString]; } +/** + * Deletes all wrapper exceptions on disk. + */ + (void)deleteAllWrapperExceptions { - [[self sharedInstance] deleteAllWrapperExceptions]; -} - -+ (void)deleteWrapperExceptionDataWithUUIDString:(NSString *)uuidString { - [[self sharedInstance] deleteWrapperExceptionDataWithUUIDString:uuidString]; -} -+ (void)deleteAllWrapperExceptionData { - [[self sharedInstance] deleteAllWrapperExceptionData]; -} - -+ (void)setDelegate:(id)delegate { - [self sharedInstance].crashesDelegate = delegate; -} - -+ (id)getDelegate { - return [self sharedInstance].crashesDelegate; -} - -+ (void)startCrashReportingFromWrapperSdk { - [[self sharedInstance] startCrashReportingFromWrapperSdk]; -} - -#pragma mark - Private methods - -- (instancetype)init { - if ((self = [super init])) { - - _unsavedWrapperExceptionData = nil; - _wrapperException = nil; - _wrapperExceptionData = [[NSMutableDictionary alloc] init]; - _currentUUIDRef = nil; - - // Create the directory if it doesn't exist - NSFileManager *defaultManager = [NSFileManager defaultManager]; - - if (![defaultManager fileExistsAtPath:[[self class] directoryPath]]) { - NSError *error = nil; - [defaultManager createDirectoryAtPath:[[self class] directoryPath] - withIntermediateDirectories:NO - attributes:nil - error:&error]; - if (error) { - MSLogError([MSCrashes logTag], @"Failed to create directory %@: %@", [[self class] directoryPath], - error.localizedDescription); - } - } + NSString *directoryPath = [[MSCrashesUtil wrapperExceptionsDir] absoluteString]; + for (NSString* path in [[NSFileManager defaultManager] enumeratorAtPath:directoryPath]) { + [MSUtility removeItemAtURL:[NSURL URLWithString:path]]; } - - return self; -} - -+ (instancetype)sharedInstance { - static MSWrapperExceptionManager *sharedInstance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sharedInstance = [[self alloc] init]; - }); - return sharedInstance; } -- (BOOL)hasException { - return self.wrapperException != nil; -} +/** + * Renames the last saved wrapper exception with the error ID of the + * corresponding report in the given array. Pairing is based on the process + * id of the error report. + */ ++ (void)correlateLastSavedWrapperExceptionToReport:(NSArray *)reports { + MSWrapperException *lastSavedWrapperException = [self loadWrapperExceptionWithBaseFilename:kMSLastWrapperExceptionFileName]; -- (MSException *)loadWrapperException:(CFUUIDRef)uuidRef { - if (self.wrapperException && [[self class] isCurrentUUIDRef:uuidRef]) { - return self.wrapperException; - } - NSString *filename = [[self class] getFilenameWithUUIDRef:uuidRef]; - if (![[NSFileManager defaultManager] fileExistsAtPath:filename]) { - return nil; + // Delete the last saved exception from disk if it exists. + if (lastSavedWrapperException) { + [self deleteWrapperExceptionWithBaseFilename:kMSLastWrapperExceptionFileName]; } - MSException *loadedException = [NSKeyedUnarchiver unarchiveObjectWithFile:filename]; - - if (!loadedException) { - MSLogError([MSCrashes logTag], @"Could not load wrapper exception from file %@", filename); - return nil; - } - - self.wrapperException = loadedException; - self.currentUUIDRef = uuidRef; - - return self.wrapperException; -} - -- (void)saveWrapperException:(CFUUIDRef)uuidRef { - NSString *filename = [[self class] getFilenameWithUUIDRef:uuidRef]; - [self saveWrapperExceptionData:uuidRef]; - BOOL success = [NSKeyedArchiver archiveRootObject:self.wrapperException toFile:filename]; - if (!success) { - MSLogError([MSCrashes logTag], @"Failed to save file %@", filename); - } -} - -- (void)deleteWrapperExceptionWithUUID:(CFUUIDRef)uuidRef { - NSString *path = [MSWrapperExceptionManager getFilenameWithUUIDRef:uuidRef]; - [[self class] deleteFile:path]; - - if ([[self class] isCurrentUUIDRef:uuidRef]) { - self.currentUUIDRef = nil; - self.wrapperException = nil; - } -} - -- (void)deleteAllWrapperExceptions { - self.currentUUIDRef = nil; - self.wrapperException = nil; - - NSFileManager *fileManager = [NSFileManager defaultManager]; - - for (NSString *filePath in [fileManager enumeratorAtPath:[[self class] directoryPath]]) { - if (![[self class] isDataFile:filePath]) { - NSString *path = [[[self class] directoryPath] stringByAppendingPathComponent:filePath]; - [[self class] deleteFile:path]; + MSErrorReport *correspondingReport = nil; + for (MSErrorReport *report in reports) { + if ([lastSavedWrapperException.processId unsignedLongValue] == report.appProcessIdentifier) { + correspondingReport = report; + break; } } -} - -- (void)saveWrapperExceptionData:(CFUUIDRef)uuidRef { - if (!self.unsavedWrapperExceptionData) { - return; - } - NSString *dataFilename = [[self class] getDataFilenameWithUUIDRef:uuidRef]; - [self.unsavedWrapperExceptionData writeToFile:dataFilename atomically:YES]; -} + if (correspondingReport) { -- (NSData *)loadWrapperExceptionDataWithUUIDString:(NSString *)uuidString { - NSString *dataFilename = [[self class] getDataFilename:uuidString]; - NSData *data = self.wrapperExceptionData[dataFilename]; - if (data) { - return data; - } - NSError *error = nil; - data = [NSData dataWithContentsOfFile:dataFilename options:NSDataReadingMappedIfSafe error:&error]; - if (error) { - MSLogError([MSCrashes logTag], @"Error loading file %@: %@", dataFilename, error.localizedDescription); + // As soon as the wrapper exception is correlated, store it in memory and save it to disk + unprocessedWrapperExceptions[correspondingReport.incidentIdentifier] = lastSavedWrapperException; + [self saveWrapperException:lastSavedWrapperException withBaseFilename:correspondingReport.incidentIdentifier]; } - return data; } -- (void)deleteWrapperExceptionDataWithUUIDString:(NSString *)uuidString { - NSString *dataFilename = [[self class] getDataFilename:uuidString]; - NSData *data = [self loadWrapperExceptionDataWithUUIDString:uuidString]; - if (data) { - self.wrapperExceptionData[dataFilename] = data; - } - [[self class] deleteFile:dataFilename]; -} +#pragma mark Helper methods -- (void)deleteAllWrapperExceptionData { - NSFileManager *fileManager = [NSFileManager defaultManager]; - for (NSString *filePath in [fileManager enumeratorAtPath:[[self class] directoryPath]]) { - if ([[self class] isDataFile:filePath]) { - NSString *path = [[[self class] directoryPath] stringByAppendingPathComponent:filePath]; - [[self class] deleteFile:path]; - } - } -} +/** + * Saves a wrapper exception to disk with the given file name. + */ ++ (void)saveWrapperException:(MSWrapperException *)wrapperException withBaseFilename:(NSString *)baseFilename { + NSURL *exceptionFileURL = [self getAbsoluteFileURL:baseFilename]; + BOOL success = [MSUtility createFileAtURL:exceptionFileURL]; + if (success) { -+ (void)deleteFile:(NSString *)path { - if (![[NSFileManager defaultManager] fileExistsAtPath:path]) { - return; - } - NSError *error = nil; - [[NSFileManager defaultManager] removeItemAtPath:path error:&error]; - if (error) { - MSLogError([MSCrashes logTag], @"Error deleting file %@: %@", path, error.localizedDescription); + // For some reason, archiving directly to a file fails in some cases, so archive + // to NSData and write that to the file + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:wrapperException]; + [data writeToURL:exceptionFileURL atomically:YES]; } } -+ (NSString *)uuidRefToString:(CFUUIDRef)uuidRef { - if (!uuidRef) { - return nil; - } - CFStringRef uuidStringRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); - return (__bridge_transfer NSString *)uuidStringRef; +/** + * Deletes a wrapper exception with a given file name. + */ ++ (void)deleteWrapperExceptionWithBaseFilename:(NSString *)baseFilename +{ + NSURL *exceptionFileURL = [self getAbsoluteFileURL:baseFilename]; + [MSUtility removeItemAtURL:exceptionFileURL]; } -+ (BOOL)isCurrentUUIDRef:(CFUUIDRef)uuidRef { - CFUUIDRef currentUUIDRef = [self sharedInstance].currentUUIDRef; +/** + * Loads a wrapper exception with a given filename. + */ ++ (MSWrapperException *)loadWrapperExceptionWithBaseFilename:(NSString *)baseFilename { + NSURL *exceptionFileURL = [self getAbsoluteFileURL:baseFilename]; - BOOL currentUUIDRefIsNull = (currentUUIDRef == nil); - BOOL uuidRefIsNull = (uuidRef == nil); - - if (currentUUIDRefIsNull && uuidRefIsNull) { - return true; + // For some reason, unarchiving directly from a file fails in some cases, so load + // data from a file and unarchive it after + NSData *data = [NSData dataWithContentsOfURL:exceptionFileURL]; + MSWrapperException *wrapperException = nil; + @try { + wrapperException = [NSKeyedUnarchiver unarchiveObjectWithData:data]; } - if (currentUUIDRefIsNull || uuidRefIsNull) { - return false; + @catch (__attribute__((unused)) NSException *exception) { + MSLogError([MSCrashes logTag], @"Could not read exception data stored on disk with file name %@", baseFilename); + [self deleteWrapperExceptionWithBaseFilename:baseFilename]; } - - // For whatever reason, CFEqual causes a crash, so we compare strings - NSString *uuidString = [self uuidRefToString:uuidRef]; - NSString *currentUUIDString = [self uuidRefToString:currentUUIDRef]; - - return [uuidString isEqualToString:currentUUIDString]; -} - -- (void)startCrashReportingFromWrapperSdk { - - /** - * Do not register an UncaughtExceptionHandler for Xamarin as we rely on the xamarin runtime to report NSExceptions. - * Registering our own UncaughtExceptionHandler will cause the Xamarin debugger to not work properly (it will not stop - * for NSExceptions). - */ - [[MSCrashes sharedInstance] configureCrashReporterWithUncaughtExceptionHandlerEnabled:NO]; -} - -+ (NSString *)dataFileExtension { - return @"ms"; -} - -+ (NSString *)directoryName { - return @"wrapper_exceptions"; -} - -+ (NSString *)directoryPath { - - static NSString *path = nil; - - if (!path) { - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); - NSString *documentsDirectory = paths[0]; - path = [documentsDirectory stringByAppendingPathComponent:[self directoryName]]; - } - - return path; -} - -+ (NSString *)getFilename:(NSString *)uuidString { - return [[self directoryPath] stringByAppendingPathComponent:uuidString]; -} - -+ (NSString *)getDataFilename:(NSString *)uuidString { - NSString *filename = [MSWrapperExceptionManager getFilename:uuidString]; - return [filename stringByAppendingPathExtension:[self dataFileExtension]]; -} - -+ (NSString *)getFilenameWithUUIDRef:(CFUUIDRef)uuidRef { - NSString *uuidString = [MSWrapperExceptionManager uuidRefToString:uuidRef]; - return [MSWrapperExceptionManager getFilename:uuidString]; -} - -+ (NSString *)getDataFilenameWithUUIDRef:(CFUUIDRef)uuidRef { - NSString *uuidString = [MSWrapperExceptionManager uuidRefToString:uuidRef]; - return [MSWrapperExceptionManager getDataFilename:uuidString]; + return wrapperException; } -+ (BOOL)isDataFile:(NSString *)path { - return [path hasSuffix:[@"." stringByAppendingString:[self dataFileExtension]]]; +/** + * Gets the full path for a given file name that should be in the wrapper crashes directory. + */ ++ (NSURL *)getAbsoluteFileURL:(NSString *)filename { + return [[MSCrashesUtil wrapperExceptionsDir] URLByAppendingPathComponent:filename]; } @end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManagerInternal.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManagerInternal.h index feb4438e5b..68b8bcf2e9 100644 --- a/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManagerInternal.h +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/MSWrapperExceptionManagerInternal.h @@ -1,39 +1,24 @@ #import "MSWrapperExceptionManager.h" -@interface MSWrapperExceptionManager () - -@property MSException *wrapperException; -@property NSMutableDictionary *wrapperExceptionData; -@property NSData *unsavedWrapperExceptionData; -@property CFUUIDRef currentUUIDRef; -@property(weak, nonatomic) id crashesDelegate; - -@property(copy, readonly) NSString *dataFileExtension; -@property(copy, readonly) NSString *directoryName; -@property(copy, readonly) NSString *directoryPath; +@class MSWrapperException; +@class MSErrorReport; -+ (MSWrapperExceptionManager*)sharedInstance; -- (BOOL)hasException; -- (MSException*)loadWrapperException:(CFUUIDRef)uuidRef; -- (void)saveWrapperException:(CFUUIDRef)uuidRef; -- (void)deleteWrapperExceptionWithUUID:(CFUUIDRef)uuidRef; -- (void)deleteAllWrapperExceptions; -- (void)deleteAllWrapperExceptionData; -- (void)saveWrapperExceptionData:(CFUUIDRef)uuidRef; - -- (NSData*)loadWrapperExceptionDataWithUUIDString:(NSString*)uuidString; -- (void)deleteWrapperExceptionDataWithUUIDString:(NSString*)uuidString; - -+ (NSString*)directoryPath; +@interface MSWrapperExceptionManager () -+ (NSString*)getFilename:(NSString*)uuidString; -+ (NSString*)getDataFilename:(NSString*)uuidString; -+ (NSString*)getFilenameWithUUIDRef:(CFUUIDRef)uuidRef; -+ (NSString*)getDataFilenameWithUUIDRef:(CFUUIDRef)uuidRef; -+ (void)deleteFile:(NSString*)path; -+ (BOOL)isDataFile:(NSString*)path; -+ (NSString*)uuidRefToString:(CFUUIDRef)uuidRef; -+ (BOOL)isCurrentUUIDRef:(CFUUIDRef)uuidRef; -- (void)startCrashReportingFromWrapperSdk; +/** + * Delete all wrapper exception files on disk. + */ ++ (void)deleteAllWrapperExceptions; + +/** + * Find the PLCrashReport with a matching process id to the MSWrapperException that + * was last saved on disk, and update the filename to the report's UUID. + */ ++ (void)correlateLastSavedWrapperExceptionToReport:(NSArray *)reports; + +/** + * Delete a wrapper exception with a given UUID. + */ ++ (void)deleteWrapperExceptionWithUUIDString:(NSString *)uuidString; @end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/Model/MSWrapperException.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/Model/MSWrapperException.h new file mode 100644 index 0000000000..1698d4b94c --- /dev/null +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/Model/MSWrapperException.h @@ -0,0 +1,26 @@ +#import + +@class MSException; + +/** + * This class represents a wrapper exception that augments the data + * recorded when the application crashes. + */ +@interface MSWrapperException : NSObject + +/** + * The model exception for the corresponding crash. + */ +@property(nonatomic) MSException* modelException; + +/** + * Additional data that the wrapper SDK needs to save. + */ +@property(nonatomic) NSData* exceptionData; + +/** + * Id of the crashed process; used for correlation to a PLCrashReport. + */ +@property(nonatomic, copy) NSNumber* processId; + +@end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/Model/MSWrapperException.m b/MobileCenterCrashes/MobileCenterCrashes/Internals/Model/MSWrapperException.m new file mode 100644 index 0000000000..8206174ed6 --- /dev/null +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/Model/MSWrapperException.m @@ -0,0 +1,42 @@ +#import "MSWrapperExceptionInternal.h" +#import "MSException.h" + +@implementation MSWrapperException + +static NSString* const kMSModelException = @"model_exception"; +static NSString* const kMSExceptionData = @"exception_data"; +static NSString* const KMSProcessId = @"process_id"; + +- (NSMutableDictionary *)serializeToDictionary { + NSMutableDictionary *dict = [NSMutableDictionary new]; + if (self.modelException) { + dict[kMSModelException] = [self.modelException serializeToDictionary]; + } + if (self.processId) { + dict[KMSProcessId] = self.processId; + } + if (self.exceptionData) { + dict[kMSExceptionData] = self.exceptionData; + } + return dict; +} + +#pragma mark - NSCoding + +- (instancetype)initWithCoder:(NSCoder *)coder { + self = [super init]; + if (self) { + self.modelException = [coder decodeObjectForKey:kMSModelException]; + self.exceptionData = [coder decodeObjectForKey:kMSExceptionData]; + self.processId = [coder decodeObjectForKey:KMSProcessId]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeObject:self.modelException forKey:kMSModelException]; + [coder encodeObject:self.exceptionData forKey:kMSExceptionData]; + [coder encodeObject:self.processId forKey:KMSProcessId]; +} + +@end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/Model/MSWrapperExceptionInternal.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/Model/MSWrapperExceptionInternal.h new file mode 100644 index 0000000000..6bf2d9bc69 --- /dev/null +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/Model/MSWrapperExceptionInternal.h @@ -0,0 +1,9 @@ +#import "MSSerializableObject.h" +#import "MSWrapperException.h" + +/** + * MSWrapperException must be serializable, but only internally (so that + * MSSerializableObject does not need to be bound for wrapper SDKs) + */ +@interface MSWrapperException () +@end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSCrashesUtil.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSCrashesUtil.h similarity index 73% rename from MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSCrashesUtil.h rename to MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSCrashesUtil.h index aba081016e..4e4dbf5e57 100644 --- a/MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSCrashesUtil.h +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSCrashesUtil.h @@ -17,4 +17,11 @@ */ + (NSURL *)logBufferDir; +/** + * Returns the directory for storing and reading wrapper exception data. + * + * @return The directory containing wrapper exception data. + */ ++ (NSURL *)wrapperExceptionsDir; + @end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSCrashesUtil.m b/MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSCrashesUtil.m similarity index 63% rename from MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSCrashesUtil.m rename to MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSCrashesUtil.m index 856879d47e..dd84d70a61 100644 --- a/MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSCrashesUtil.m +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSCrashesUtil.m @@ -5,6 +5,7 @@ static NSString *const kMSCrashesDirectory = @"com.microsoft.azure.mobile.mobilecenter/crashes"; static NSString *const kMSLogBufferDirectory = @"com.microsoft.azure.mobile.mobilecenter/crasheslogbuffer"; +static NSString *const kMSWrapperExceptionsDirectory = @"com.microsoft.azure.mobile.mobilecenter/crasheswrapperexceptions"; @interface MSCrashesUtil () @@ -28,7 +29,7 @@ + (NSURL *)crashesDir { NSError *error = nil; NSFileManager *fileManager = [[NSFileManager alloc] init]; - // temporary directory for crashes grabbed from PLCrashReporter + // Temporary directory for crashes grabbed from PLCrashReporter. NSURL *cachesDirectory = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject]; crashesDir = [cachesDirectory URLByAppendingPathComponent:kMSCrashesDirectory]; @@ -54,7 +55,7 @@ + (NSURL *)logBufferDir { NSError *error = nil; NSFileManager *fileManager = [[NSFileManager alloc] init]; - // temporary directory for crashes grabbed from PLCrashReporter + // Temporary directory for crashes grabbed from PLCrashReporter. NSURL *cachesDirectory = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject]; logBufferDir = [cachesDirectory URLByAppendingPathComponent:kMSLogBufferDirectory]; @@ -72,4 +73,31 @@ + (NSURL *)logBufferDir { return logBufferDir; } ++ (NSURL *)wrapperExceptionsDir { + static NSURL *wrapperExceptionsDir = nil; + static dispatch_once_t predSettingsDir; + + dispatch_once(&predSettingsDir, ^{ + NSError *error = nil; + NSFileManager *fileManager = [[NSFileManager alloc] init]; + + // Temporary directory for crashes grabbed from PLCrashReporter. + NSURL *cachesDirectory = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject]; + wrapperExceptionsDir = [cachesDirectory URLByAppendingPathComponent:kMSWrapperExceptionsDirectory]; + + if (![wrapperExceptionsDir checkResourceIsReachableAndReturnError:&error]) { + NSDictionary *attributes = @{ NSFilePosixPermissions : @0755 }; + NSError *theError = nil; + + [fileManager createDirectoryAtURL:wrapperExceptionsDir + withIntermediateDirectories:YES + attributes:attributes + error:&theError]; + } + }); + + return wrapperExceptionsDir; +} + + @end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSErrorLogFormatter.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSErrorLogFormatter.h similarity index 100% rename from MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSErrorLogFormatter.h rename to MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSErrorLogFormatter.h diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSErrorLogFormatter.m b/MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSErrorLogFormatter.m similarity index 97% rename from MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSErrorLogFormatter.m rename to MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSErrorLogFormatter.m index d33f49ac48..7b75ba16aa 100644 --- a/MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSErrorLogFormatter.m +++ b/MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSErrorLogFormatter.m @@ -54,7 +54,8 @@ #import "MSStackFrame.h" #import "MSThread.h" #import "MSDeviceTrackerPrivate.h" -#import "MSWrapperExceptionManager.h" +#import "MSWrapperExceptionManagerInternal.h" +#import "MSWrapperException.h" static NSString *unknownString = @"???"; @@ -256,9 +257,9 @@ + (MSAppleErrorLog *)errorLogFromCrashReport:(MSPLCrashReport *)report { // The registers of the crashed thread might contain the last method call, this can be very helpful. errorLog.selectorRegisterValue = - [self selectorRegisterValueFromReport:report ofCrashedThread:crashedThread is64bit:is64bit]; + [self selectorRegisterValueFromReport:report ofCrashedThread:crashedThread is64bit:is64bit]; - // Extract all threads and registers, + // Extract all threads and registers. errorLog.threads = [self extractThreadsFromReport:report crashedThread:crashedThread is64bit:is64bit]; errorLog.registers = [self extractRegistersFromCrashedThread:crashedThread is64bit:is64bit]; @@ -266,16 +267,17 @@ + (MSAppleErrorLog *)errorLogFromCrashReport:(MSPLCrashReport *)report { NSArray *addresses = [self addressesFromReport:report]; errorLog.binaries = [self extractBinaryImagesFromReport:report addresses:addresses codeType:codeType is64bit:is64bit]; - // Set the exception from the wrapper sdk - errorLog.exception = [MSWrapperExceptionManager loadWrapperException:report.uuidRef]; - /* * Set the device here to make sure we don't use the current device information but the one from history that matches * the time of our crash. */ errorLog.device = [[MSDeviceTracker new] deviceForToffset:errorLog.toffset]; - // Finally done with transforming PLCrashReport to MSAppleErrorReport. + // Set the exception from the wrapper SDK. + MSWrapperException* wrapperException = [MSWrapperExceptionManager loadWrapperExceptionWithUUIDString:[self uuidRefToString:report.uuidRef]]; + if (wrapperException) { + errorLog.exception = wrapperException.modelException; + } return errorLog; } @@ -813,4 +815,12 @@ + (NSArray *)addressesFromReport:(MSPLCrashReport *)report { return addresses; } ++ (NSString *)uuidRefToString:(CFUUIDRef)uuidRef { + if (!uuidRef) { + return nil; + } + CFStringRef uuidStringRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); + return (__bridge_transfer NSString *)uuidStringRef; +} + @end diff --git a/MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSErrorLogFormatterPrivate.h b/MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSErrorLogFormatterPrivate.h similarity index 100% rename from MobileCenterCrashes/MobileCenterCrashes/Internals/Utils/MSErrorLogFormatterPrivate.h rename to MobileCenterCrashes/MobileCenterCrashes/Internals/Util/MSErrorLogFormatterPrivate.h diff --git a/MobileCenterCrashes/MobileCenterCrashes/MSCrashes.mm b/MobileCenterCrashes/MobileCenterCrashes/MSCrashes.mm index 506ff417f4..678f19dc02 100644 --- a/MobileCenterCrashes/MobileCenterCrashes/MSCrashes.mm +++ b/MobileCenterCrashes/MobileCenterCrashes/MSCrashes.mm @@ -1,6 +1,7 @@ #import "MSAppleErrorLog.h" #import "MSCrashesCXXExceptionWrapperException.h" #import "MSCrashesDelegate.h" +#import "MSCrashHandlerSetupDelegate.h" #import "MSCrashesInternal.h" #import "MSCrashesPrivate.h" #import "MSCrashesUtil.h" @@ -9,7 +10,8 @@ #import "MSErrorLogFormatter.h" #import "MSMobileCenterInternal.h" #import "MSServiceAbstractProtected.h" -#import "MSWrapperExceptionManager.h" +#import "MSWrapperExceptionManagerInternal.h" +#import "MSWrapperCrashesHelper.h" /** * Service name for initialization. @@ -69,8 +71,6 @@ static void ms_save_log_buffer_callback(__attribute__((unused)) siginfo_t *info, // Proxy implementation for PLCrashReporter to keep our interface stable while this can change. static void plcr_post_crash_callback(siginfo_t *info, ucontext_t *uap, void *context) { ms_save_log_buffer_callback(info, uap, context); - [MSCrashes wrapperCrashCallback]; - if (msCrashesCallbacks.handleSignal != NULL) { msCrashesCallbacks.handleSignal(context); } @@ -125,6 +125,7 @@ @implementation MSCrashes #pragma mark - Public Methods + (void)generateTestCrash { + @synchronized([self sharedInstance]) { if ([[self sharedInstance] canBeUsed]) { if ([MSUtility currentAppEnvironment] != MSEnvironmentAppStore) { @@ -166,7 +167,7 @@ + (void)notifyWithUserConfirmation:(MSUserConfirmation)userConfirmation { NSURL *fileURL = crashes.unprocessedFilePaths[i]; MSErrorReport *report = crashes.unprocessedReports[i]; [crashes deleteCrashReportWithFileURL:fileURL]; - [MSWrapperExceptionManager deleteWrapperExceptionDataWithUUIDString:report.incidentIdentifier]; + [MSWrapperExceptionManager deleteWrapperExceptionWithUUIDString:report.incidentIdentifier]; [crashes.crashFiles removeObject:fileURL]; } @@ -214,7 +215,7 @@ + (void)notifyWithUserConfirmation:(MSUserConfirmation)userConfirmation { // Clean up. [crashes deleteCrashReportWithFileURL:fileURL]; - [MSWrapperExceptionManager deleteWrapperExceptionDataWithUUIDString:report.incidentIdentifier]; + [MSWrapperExceptionManager deleteWrapperExceptionWithUUIDString:report.incidentIdentifier]; [crashes.crashFiles removeObject:fileURL]; } } @@ -247,7 +248,7 @@ - (instancetype)init { priority:MSPriorityHigh flushInterval:1.0 batchSizeLimit:10 - pendingBatchesLimit:6]; + pendingBatchesLimit:1]; /** * Using our own queue with high priority as the default main queue is slower and we want the files to be created @@ -264,14 +265,27 @@ - (instancetype)init { - (void)applyEnabledState:(BOOL)isEnabled { [super applyEnabledState:isEnabled]; - // Enabling + // Enabling. if (isEnabled) { + id crashSetupDelegate = [MSWrapperCrashesHelper getCrashHandlerSetupDelegate]; - // Check if there is a wrapper SDK that needs to do some custom handler setup. If there is, - // then the wrapper SDK will call [self configureCrashReporter]. - if (![[MSWrapperExceptionManager getDelegate] respondsToSelector:@selector(setUpCrashHandlers)] || - ![[MSWrapperExceptionManager getDelegate] setUpCrashHandlers]) { - [self configureCrashReporterWithUncaughtExceptionHandlerEnabled:YES]; + // Check if a wrapper SDK has a preference for uncaught exception handling. + BOOL enableUncaughtExceptionHandler = YES; + if ([crashSetupDelegate respondsToSelector:@selector(shouldEnableUncaughtExceptionHandler)]) { + enableUncaughtExceptionHandler = [crashSetupDelegate shouldEnableUncaughtExceptionHandler]; + } + + // Allow a wrapper SDK to perform custom behavior before setting up crash handlers. + if ([crashSetupDelegate respondsToSelector:@selector(willSetUpCrashHandlers)]) { + [crashSetupDelegate willSetUpCrashHandlers]; + } + + // Set up crash handlers. + [self configureCrashReporterWithUncaughtExceptionHandlerEnabled:YES]; + + // Allow a wrapper SDK to perform custom behavior after setting up crash handlers. + if ([crashSetupDelegate respondsToSelector:@selector(didSetUpCrashHandlers)]) { + [crashSetupDelegate didSetUpCrashHandlers]; } // PLCrashReporter keeps collecting crash reports even when the SDK is disabled, @@ -307,7 +321,6 @@ - (void)applyEnabledState:(BOOL)isEnabled { // Don't set PLCrashReporter to nil! MSLogDebug([MSCrashes logTag], @"Cleaning up all crash files."); [MSWrapperExceptionManager deleteAllWrapperExceptions]; - [MSWrapperExceptionManager deleteAllWrapperExceptionData]; [self deleteAllFromCrashesDirectory]; [self emptyLogBufferFiles]; [self removeAnalyzerFile]; @@ -615,47 +628,55 @@ - (void)startCrashProcessing { } - (void)processCrashReports { + + // Handle 'disabled' state all at once to simplify the logic that follows. + if (!self.isEnabled) { + MSLogDebug([MSCrashes logTag], @"Crashes service is disabled; discard all crash reports"); + [self deleteAllFromCrashesDirectory]; + [MSWrapperExceptionManager deleteAllWrapperExceptions]; + return; + } NSError *error = NULL; - self.unprocessedLogs = [[NSMutableArray alloc] init]; self.unprocessedReports = [[NSMutableArray alloc] init]; + self.unprocessedLogs = [[NSMutableArray alloc] init]; self.unprocessedFilePaths = [[NSMutableArray alloc] init]; - // Start crash processing for real. - NSArray *tempCrashesFiles = [NSArray arrayWithArray:self.crashFiles]; - for (NSURL *fileURL in tempCrashesFiles) { - NSString *uuidString; - - // We always start sending with the oldest pending one. + // First save all found crash reports for use in correlation step. + NSMutableDictionary *foundCrashReports = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *foundErrorReports = [[NSMutableDictionary alloc] init]; + for (NSURL *fileURL in self.crashFiles) { NSData *crashFileData = [NSData dataWithContentsOfURL:fileURL]; if ([crashFileData length] > 0) { - MSLogVerbose([MSCrashes logTag], @"Crash report found"); - if (self.isEnabled) { - MSPLCrashReport *report = [[MSPLCrashReport alloc] initWithData:crashFileData error:&error]; - MSAppleErrorLog *log = [MSErrorLogFormatter errorLogFromCrashReport:report]; - MSErrorReport *errorReport = [MSErrorLogFormatter errorReportFromLog:(log)]; - uuidString = errorReport.incidentIdentifier; - if ([self shouldProcessErrorReport:errorReport]) { - MSLogDebug([MSCrashes logTag], - @"shouldProcessErrorReport is not implemented or returned YES, processing the crash report: %@", - report.debugDescription); - - // Put the log to temporary space for next callbacks. - [self.unprocessedLogs addObject:log]; - [self.unprocessedReports addObject:errorReport]; - [self.unprocessedFilePaths addObject:fileURL]; - - continue; + MSPLCrashReport *report = [[MSPLCrashReport alloc] initWithData:crashFileData error:&error]; + foundCrashReports[fileURL] = report; + foundErrorReports[fileURL] = [MSErrorLogFormatter errorReportFromCrashReport:report]; + } + } - } else { - MSLogDebug([MSCrashes logTag], @"shouldProcessErrorReport returned NO, discard the crash report: %@", - report.debugDescription); - } - } else { - MSLogDebug([MSCrashes logTag], @"Crashes service is disabled, discard the crash report"); - } + // Correlation step. + [MSWrapperExceptionManager correlateLastSavedWrapperExceptionToReport:[foundErrorReports allValues]]; + + // Processing step. + for (NSURL *fileURL in [foundCrashReports allKeys]) { + MSLogVerbose([MSCrashes logTag], @"Crash report found"); + MSPLCrashReport *report = foundCrashReports[fileURL]; + MSErrorReport *errorReport = foundErrorReports[fileURL]; + MSAppleErrorLog *log = [MSErrorLogFormatter errorLogFromCrashReport:report]; + if ([self shouldProcessErrorReport:errorReport]) { + MSLogDebug([MSCrashes logTag], + @"shouldProcessErrorReport is not implemented or returned YES, processing the crash report: %@", + report.debugDescription); - // Cleanup. - [MSWrapperExceptionManager deleteWrapperExceptionDataWithUUIDString:uuidString]; + // Put the log to temporary space for next callbacks. + [self.unprocessedLogs addObject:log]; + [self.unprocessedReports addObject:errorReport]; + [self.unprocessedFilePaths addObject:fileURL]; + } else { + MSLogDebug([MSCrashes logTag], @"shouldProcessErrorReport returned NO, discard the crash report: %@", + report.debugDescription); + + // Discard the crash report. + [MSWrapperExceptionManager deleteWrapperExceptionWithUUIDString:errorReport.incidentIdentifier]; [self deleteCrashReportWithFileURL:fileURL]; [self.crashFiles removeObject:fileURL]; } @@ -754,6 +775,7 @@ - (void)handleLatestCrashReport { NSURL *cacheURL = [self.crashesDir URLByAppendingPathComponent:cacheFilename]; [crashData writeToURL:cacheURL atomically:YES]; self.lastSessionCrashReport = [MSErrorLogFormatter errorReportFromCrashReport:report]; + [MSWrapperExceptionManager correlateLastSavedWrapperExceptionToReport:@[self.lastSessionCrashReport]]; } else { MSLogWarning([MSCrashes logTag], @"Could not parse crash report"); } @@ -913,29 +935,6 @@ - (BOOL)delegateImplementsAttachmentCallback { return strongDelegate && [strongDelegate respondsToSelector:@selector(attachmentsWithCrashes:forErrorReport:)]; } -+ (void)wrapperCrashCallback { - if (![MSWrapperExceptionManager hasException]) { - return; - } - - // If a wrapper SDK has passed an exception, save it to disk. - NSError *error = NULL; - NSData *crashData = [[NSData alloc] - initWithData:[[[MSCrashes sharedInstance] plCrashReporter] loadPendingCrashReportDataAndReturnError:&error]]; - - // This shouldn't happen because the callback should only happen once plCrashReporter has written the report to - // disk. - if (!crashData) { - MSLogError([MSCrashes logTag], @"Could not load crash data: %@", error.localizedDescription); - } - MSPLCrashReport *report = [[MSPLCrashReport alloc] initWithData:crashData error:&error]; - if (report) { - [MSWrapperExceptionManager saveWrapperException:report.uuidRef]; - } else { - MSLogError([MSCrashes logTag], @"Could not load crash report: %@", error.localizedDescription); - } -} - // We need to override setter, because it's default behavior creates an NSArray, and some tests fail. - (void)setCrashFiles:(NSMutableArray *)crashFiles { _crashFiles = [[NSMutableArray alloc] initWithArray:crashFiles]; diff --git a/MobileCenterCrashes/MobileCenterCrashesTests/MSCrashesTests.mm b/MobileCenterCrashes/MobileCenterCrashesTests/MSCrashesTests.mm index a8a95eb127..2896176d7d 100644 --- a/MobileCenterCrashes/MobileCenterCrashesTests/MSCrashesTests.mm +++ b/MobileCenterCrashes/MobileCenterCrashesTests/MSCrashesTests.mm @@ -10,6 +10,7 @@ #import "MSCrashesPrivate.h" #import "MSCrashesTestUtil.h" #import "MSCrashesUtil.h" +#import "MSCrashHandlerSetupDelegate.h" #import "MSErrorAttachmentLogInternal.h" #import "MSErrorLogFormatter.h" #import "MSException.h" @@ -20,6 +21,7 @@ #import "MSServiceAbstractPrivate.h" #import "MSServiceAbstractProtected.h" #import "MSWrapperExceptionManagerInternal.h" +#import "MSWrapperCrashesHelper.h" @class MSMockCrashesDelegate; @@ -123,7 +125,7 @@ - (void)testDelegateMethodsAreCalled { NSMutableDictionary *channelsInLogManager = (static_cast([MSCrashes sharedInstance].logManager)).channels; MSChannelDefault *channelMock = channelsInLogManager[groupId] = OCMPartialMock(channelsInLogManager[groupId]); - OCMStub([channelMock enqueueItem:[OCMArg any] withCompletion:[OCMArg any]]).andDo(^(NSInvocation *invocation) { + OCMStub([channelMock enqueueItem:OCMOCK_ANY withCompletion:OCMOCK_ANY]).andDo(^(NSInvocation *invocation) { id log = nil; [invocation getArgument:&log atIndex:2]; for (id delegate in channelMock.delegates) { @@ -149,6 +151,21 @@ - (void)testDelegateMethodsAreCalled { OCMVerify([delegateMock crashes:[MSCrashes sharedInstance] didFailSendingErrorReport:errorReport withError:nil]); } +- (void)testCrashHandlerSetupDelegateMethodsAreCalled { + + // If + id delegateMock = OCMProtocolMock(@protocol(MSCrashHandlerSetupDelegate)); + [MSWrapperCrashesHelper setCrashHandlerSetupDelegate:delegateMock]; + + // When + [self.sut applyEnabledState:YES]; + + // Then + OCMVerify([delegateMock willSetUpCrashHandlers]); + OCMVerify([delegateMock didSetUpCrashHandlers]); + OCMVerify([delegateMock shouldEnableUncaughtExceptionHandler]); +} + - (void)testSettingUserConfirmationHandler { // When @@ -247,19 +264,19 @@ - (void)testProcessCrashesWithErrorAttachments { [self attachmentWithAttachmentId:validString attachmentData:validData contentType:nil], [self attachmentWithAttachmentId:validString attachmentData:validData contentType:@""] ]; + for(NSUInteger i = 0; i < invalidLogs.count; i++) { + OCMReject([logManagerMock processLog:invalidLogs[i] forGroupId:OCMOCK_ANY]); + } MSErrorAttachmentLog *validLog = [self attachmentWithAttachmentId:validString attachmentData:validData contentType:validString]; NSMutableArray *logs = invalidLogs.mutableCopy; [logs addObject:validLog]; id crashesDelegateMock = OCMProtocolMock(@protocol(MSCrashesDelegate)); - OCMStub([crashesDelegateMock attachmentsWithCrashes:[OCMArg any] forErrorReport:[OCMArg any]]).andReturn(logs); - OCMStub([crashesDelegateMock crashes:[OCMArg any] shouldProcessErrorReport:[OCMArg any]]).andReturn(YES); + OCMStub([crashesDelegateMock attachmentsWithCrashes:OCMOCK_ANY forErrorReport:OCMOCK_ANY]).andReturn(logs); + OCMStub([crashesDelegateMock crashes:OCMOCK_ANY shouldProcessErrorReport:OCMOCK_ANY]).andReturn(YES); [[MSCrashes sharedInstance] setDelegate:crashesDelegateMock]; //Then - for(NSUInteger i = 0; i < invalidLogs.count; i++) { - OCMReject([logManagerMock processLog:invalidLogs[i] forGroupId:[OCMArg any]]); - } - OCMExpect([logManagerMock processLog:validLog forGroupId:[OCMArg any]]); + OCMExpect([logManagerMock processLog:validLog forGroupId:OCMOCK_ANY]); [[MSCrashes sharedInstance] startCrashProcessing]; OCMVerifyAll(logManagerMock); } @@ -283,7 +300,7 @@ - (void)testDeleteCrashReportsOnDisabled { // If id settingsMock = OCMClassMock([NSUserDefaults class]); - OCMStub([settingsMock objectForKey:[OCMArg any]]).andReturn(@YES); + OCMStub([settingsMock objectForKey:OCMOCK_ANY]).andReturn(@YES); self.sut.storage = settingsMock; assertThatBool([MSCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); [self.sut startWithLogManager:OCMProtocolMock(@protocol(MSLogManager)) appSecret:kMSTestAppSecret]; @@ -301,7 +318,7 @@ - (void)testDeleteCrashReportsFromDisabledToEnabled { // If id settingsMock = OCMClassMock([NSUserDefaults class]); - OCMStub([settingsMock objectForKey:[OCMArg any]]).andReturn(@NO); + OCMStub([settingsMock objectForKey:OCMOCK_ANY]).andReturn(@NO); self.sut.storage = settingsMock; assertThatBool([MSCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); [self.sut startWithLogManager:OCMProtocolMock(@protocol(MSLogManager)) appSecret:kMSTestAppSecret]; @@ -498,27 +515,6 @@ - (void)testDisableMachExceptionWorks { XCTAssertFalse([self.sut isMachExceptionHandlerEnabled]); } -- (void)testWrapperCrashCallback { - - // If - MSException *exception = [[MSException alloc] init]; - exception.message = @"a message"; - exception.type = @"a type"; - - // When - [[MSCrashes sharedInstance] startWithLogManager:OCMProtocolMock(@protocol(MSLogManager)) appSecret:kMSTestAppSecret]; - MSWrapperExceptionManager *manager = [MSWrapperExceptionManager sharedInstance]; - manager.wrapperException = exception; - [MSCrashesTestUtil deleteAllFilesInDirectory:[MSWrapperExceptionManager directoryPath]]; - assertThatBool([MSCrashesTestUtil copyFixtureCrashReportWithFileName:@"live_report_exception"], isTrue()); - [MSCrashes wrapperCrashCallback]; - - // Then - NSArray *first = - [[NSFileManager defaultManager] contentsOfDirectoryAtPath:[MSWrapperExceptionManager directoryPath] error:NULL]; - XCTAssertTrue(first.count == 1); -} - - (void)testAbstractErrorLogSerialization { MSAbstractErrorLog *log = [MSAbstractErrorLog new]; diff --git a/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperCrashesHelperTests.m b/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperCrashesHelperTests.m new file mode 100644 index 0000000000..b1fd4b6b22 --- /dev/null +++ b/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperCrashesHelperTests.m @@ -0,0 +1,23 @@ +#import +#import +#import +#import + +#import "MSCrashHandlerSetupDelegate.h" +#import "MSWrapperCrashesHelper.h" + +@interface MSWrapperCrashesHelperTests : XCTestCase +@end + +@implementation MSWrapperCrashesHelperTests + +#pragma mark - Test + +- (void)testSettingAndGettingDelegateWorks { + id delegateMock = OCMProtocolMock(@protocol(MSCrashHandlerSetupDelegate)); + [MSWrapperCrashesHelper setCrashHandlerSetupDelegate:delegateMock]; + id retrievedDelegate = [MSWrapperCrashesHelper getCrashHandlerSetupDelegate]; + assertThat(delegateMock, equalTo(retrievedDelegate)); +} + +@end diff --git a/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperExceptionManagerTests.m b/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperExceptionManagerTests.m index d5ca407ce0..d629218a77 100644 --- a/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperExceptionManagerTests.m +++ b/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperExceptionManagerTests.m @@ -1,152 +1,133 @@ #import #import +#import #import + #import "MSCrashes.h" +#import "MSCrashesUtil.h" +#import "MSErrorReport.h" #import "MSException.h" +#import "MSUtility+File.h" +#import "MSWrapperException.h" #import "MSWrapperExceptionManagerInternal.h" -@interface MSWrapperExceptionManagerTests : XCTestCase +// Copied from MSWrapperExceptionManager.m +static NSString* const kMSLastWrapperExceptionFileName = @"last_saved_wrapper_exception"; + +@interface MSWrapperExceptionManagerTests : XCTestCase +@end -@property(nonatomic) MSWrapperExceptionManager *manager; -@property BOOL handlersWereSetUp; +// Expose private methods for use in tests +@interface MSWrapperExceptionManager () + ++ (MSWrapperException *)loadWrapperExceptionWithBaseFilename:(NSString *)baseFilename; @end + @implementation MSWrapperExceptionManagerTests #pragma mark - Housekeeping -- (void)setUp { - [super setUp]; - self.manager = [MSWrapperExceptionManager new]; -} - -- (void)tearDown { +-(void)tearDown { [super tearDown]; + [MSWrapperExceptionManager deleteAllWrapperExceptions]; } #pragma mark - Helper -- (MSException*)anException { +- (MSException*)getModelException { MSException *exception = [[MSException alloc] init]; exception.message = @"a message"; exception.type = @"a type"; return exception; } -- (NSData*)someData { - NSString *string = @"some string"; - NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; - return data; -} - -- (NSString*)uuidRefToString:(CFUUIDRef)uuidRef { - if (!uuidRef) { - return nil; - } - CFStringRef uuidStringRef = CFUUIDCreateString(kCFAllocatorDefault, uuidRef); - return (__bridge_transfer NSString*)uuidStringRef; +- (NSData*)getData { + return [@"some string" dataUsingEncoding:NSUTF8StringEncoding]; } -#pragma mark - Test - -- (void)testHasExceptionWorks { - assertThatBool([self.manager hasException], isFalse()); - self.manager.unsavedWrapperExceptionData = [self someData]; - assertThatBool([self.manager hasException], isFalse()); - self.manager.wrapperException = [self anException]; - assertThatBool([self.manager hasException], isTrue()); +- (MSWrapperException*)getWrapperException { + MSWrapperException *wrapperException = [[MSWrapperException alloc] init]; + wrapperException.modelException = [self getModelException]; + wrapperException.exceptionData = [self getData]; + wrapperException.processId = [NSNumber numberWithInteger:rand()]; + return wrapperException; } -- (void)testWrapperExceptionDiskOperations { - self.manager.wrapperException = [self anException]; - - CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); - - [self.manager saveWrapperException:uuidRef]; - MSException *exception = [self.manager loadWrapperException:uuidRef]; - assertThat(exception, equalTo([self anException])); - - [self.manager deleteWrapperExceptionWithUUID:uuidRef]; - exception = [self.manager loadWrapperException:uuidRef]; +- (void)assertWrapperException:(MSWrapperException*)wrapperException isEqualToOther:(MSWrapperException*)other { - assertThat(exception, nilValue()); + // Test that the exceptions are the same. + assertThat(other.processId, equalTo(wrapperException.processId)); + assertThat(other.exceptionData, equalTo(wrapperException.exceptionData)); + assertThat(other.modelException, equalTo(wrapperException.modelException)); - CFRelease(uuidRef); + // The exception field. + assertThat(other.modelException.type, equalTo(wrapperException.modelException.type)); + assertThat(other.modelException.message, equalTo(wrapperException.modelException.message)); + assertThat(other.modelException.wrapperSdkName, equalTo(wrapperException.modelException.wrapperSdkName)); } -- (void)testWrapperExceptionDataDiskOperations { - // Setup - self.manager.wrapperException = [self anException]; - self.manager.unsavedWrapperExceptionData = [self someData]; - CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); - [self.manager saveWrapperException:uuidRef]; - - // Test that data was saved and loaded properly - NSData* data = [self.manager loadWrapperExceptionDataWithUUIDString:[self uuidRefToString:uuidRef]]; - assertThat(data, equalTo([self someData])); - - // Even after deleting wrapper exception data, we should be able to read it from memory - data = nil; - [self.manager deleteWrapperExceptionDataWithUUIDString:[self uuidRefToString:uuidRef]]; - data = [self.manager loadWrapperExceptionDataWithUUIDString:[self uuidRefToString:uuidRef]]; - assertThat(data, equalTo([self someData])); - CFRelease(uuidRef); -} +#pragma mark - Test --(void)testDeleteAllMethods { - // Setup - self.manager.wrapperException = [self anException]; - self.manager.unsavedWrapperExceptionData = [self someData]; - CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); - [self.manager saveWrapperException:uuidRef]; - - // Delete everything - [self.manager deleteAllWrapperExceptions]; - [self.manager deleteAllWrapperExceptionData]; - - // We should no longer be able to read the data or exception from memory - NSData *data = [self.manager loadWrapperExceptionDataWithUUIDString:[self uuidRefToString:uuidRef]]; - MSException *exception = [self.manager loadWrapperException:uuidRef]; - assertThat(data, nilValue()); - assertThat(exception, nilValue()); - CFRelease(uuidRef); -} +- (void)testSaveAndLoadWrapperExceptionWorks { --(void)testSaveAndLoadErrors { - CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault); - - // Save/load when the wrapper exception has not been set - [self.manager saveWrapperException:uuidRef]; - assertThatBool([self.manager hasException], isFalse()); - assertThat([self.manager loadWrapperException:uuidRef], nilValue()); + // If + MSWrapperException *wrapperException = [self getWrapperException]; - // Load when the wrapper exception has been set to nil - self.manager.wrapperException = nil; - assertThatBool([self.manager hasException], isFalse()); - assertThat([self.manager loadWrapperException:uuidRef], nilValue()); + // When + [MSWrapperExceptionManager saveWrapperException:wrapperException]; + MSWrapperException *loadedException = [MSWrapperExceptionManager loadWrapperExceptionWithBaseFilename:kMSLastWrapperExceptionFileName]; - CFRelease(uuidRef); + // Then + XCTAssertNotNil(loadedException); + [self assertWrapperException:wrapperException isEqualToOther:loadedException]; } -// This particular test case (and the corresponding delegate method) must use the shared instance -// of MSWrapperExceptionManager because of logic in MSCrashes that relies on it. All other test -// cases *must* use self.manager. +- (void) testSaveCorrelateWrapperExceptionWhenExists { -- (void)testStartingFromWrapperSdk { - self.handlersWereSetUp = NO; - [MSWrapperExceptionManager setDelegate:self]; - assertThat([MSWrapperExceptionManager getDelegate], equalTo(self)); - [[MSCrashes sharedInstance] applyEnabledState:YES]; - assertThatBool(self.handlersWereSetUp, isTrue()); + // If + int numReports = 4; + NSMutableArray *mockReports = [NSMutableArray new]; + for (int i = 0; i < numReports; ++i) { + id reportMock = OCMPartialMock([MSErrorReport new]); + OCMStub([reportMock appProcessIdentifier]).andReturn(i); + OCMStub([reportMock incidentIdentifier]).andReturn([[NSUUID UUID] UUIDString]); + [mockReports addObject:reportMock]; + } + MSErrorReport *report = [mockReports objectAtIndex:(rand() % numReports)]; + MSWrapperException *wrapperException = [self getWrapperException]; + wrapperException.processId = [NSNumber numberWithInteger:[report appProcessIdentifier]]; + + // When + [MSWrapperExceptionManager saveWrapperException:wrapperException]; + [MSWrapperExceptionManager correlateLastSavedWrapperExceptionToReport:mockReports]; + MSWrapperException *loadedException = [MSWrapperExceptionManager loadWrapperExceptionWithUUIDString:[report incidentIdentifier]]; + + // Then + XCTAssertNotNil(loadedException); + [self assertWrapperException:wrapperException isEqualToOther:loadedException]; } -#pragma mark - Delegate - -- (BOOL)setUpCrashHandlers { - self.handlersWereSetUp = YES; - [MSWrapperExceptionManager startCrashReportingFromWrapperSdk]; - return true; +- (void) testSaveCorrelateWrapperExceptionWhenNotExists { + + // If + MSWrapperException *wrapperException = [self getWrapperException]; + wrapperException.processId = [NSNumber numberWithInteger:4]; + NSMutableArray *mockReports = [NSMutableArray new]; + id reportMock = OCMPartialMock([MSErrorReport new]); + OCMStub([reportMock appProcessIdentifier]).andReturn(9); + NSString* uuidString = [[NSUUID UUID] UUIDString]; + OCMStub([reportMock incidentIdentifier]).andReturn(uuidString); + [mockReports addObject:reportMock]; + + // When + [MSWrapperExceptionManager saveWrapperException:wrapperException]; + [MSWrapperExceptionManager correlateLastSavedWrapperExceptionToReport:mockReports]; + MSWrapperException *loadedException = [MSWrapperExceptionManager loadWrapperExceptionWithUUIDString:uuidString]; + + // Then + XCTAssertNil(loadedException); } @end diff --git a/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperExceptionTests.m b/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperExceptionTests.m new file mode 100644 index 0000000000..fb34723595 --- /dev/null +++ b/MobileCenterCrashes/MobileCenterCrashesTests/MSWrapperExceptionTests.m @@ -0,0 +1,78 @@ +#import +#import +#import +#import + +#import "MSException.h" +#import "MSWrapperExceptionInternal.h" + +@interface MSWrapperExceptionTests : XCTestCase + +@property(nonatomic) MSWrapperException *sut; + +@end + +@implementation MSWrapperExceptionTests + +#pragma mark - Housekeeping + +- (void)setUp { + [super setUp]; + self.sut = [self wrapperException]; +} + +#pragma mark - Helper + +- (MSWrapperException *)wrapperException { + MSWrapperException *exception = [MSWrapperException new]; + exception.processId = [NSNumber numberWithInteger:4]; + exception.exceptionData = [@"data string" dataUsingEncoding:NSUTF8StringEncoding]; + exception.modelException = [[MSException alloc] init]; + exception.modelException.type = @"type"; + exception.modelException.message = @"message"; + exception.modelException.wrapperSdkName = @"wrapper sdk name"; + return exception; +} + +#pragma mark - Tests + +- (void)testInitializationWorks { + XCTAssertNotNil(self.sut); +} + +- (void)testSerializationToDictionaryWorks { + NSDictionary *actual = [self.sut serializeToDictionary]; + XCTAssertNotNil(actual); + assertThat(actual[@"process_id"], equalTo(self.sut.processId)); + assertThat(actual[@"exception_data"], equalTo(self.sut.exceptionData)); + + // Exception fields. + NSDictionary *exceptionDictionary = actual[@"model_exception"]; + XCTAssertNotNil(exceptionDictionary); + assertThat(exceptionDictionary[@"type"], equalTo(self.sut.modelException.type)); + assertThat(exceptionDictionary[@"message"], equalTo(self.sut.modelException.message)); + assertThat(exceptionDictionary[@"wrapper_sdk_name"], equalTo(self.sut.modelException.wrapperSdkName)); +} + +- (void)testNSCodingSerializationAndDeserializationWorks { + + // When + NSData *serializedWrapperException = [NSKeyedArchiver archivedDataWithRootObject:self.sut]; + id actual = [NSKeyedUnarchiver unarchiveObjectWithData:serializedWrapperException]; + + // Then + assertThat(actual, notNilValue()); + assertThat(actual, instanceOf([MSWrapperException class])); + + // The MSAppleErrorLog. + MSWrapperException *actualWrapperException = actual; + assertThat(actualWrapperException.processId, equalTo(self.sut.processId)); + assertThat(actualWrapperException.exceptionData, equalTo(self.sut.exceptionData)); + + // The exception field. + assertThat(actualWrapperException.modelException.type, equalTo(self.sut.modelException.type)); + assertThat(actualWrapperException.modelException.message, equalTo(self.sut.modelException.message)); + assertThat(actualWrapperException.modelException.wrapperSdkName, equalTo(self.sut.modelException.wrapperSdkName)); +} + +@end diff --git a/MobileCenterDistribute/MobileCenterDistribute.xcodeproj/project.pbxproj b/MobileCenterDistribute/MobileCenterDistribute.xcodeproj/project.pbxproj index 3bbcecf2bc..8f33a748ee 100644 --- a/MobileCenterDistribute/MobileCenterDistribute.xcodeproj/project.pbxproj +++ b/MobileCenterDistribute/MobileCenterDistribute.xcodeproj/project.pbxproj @@ -209,6 +209,7 @@ D377A3101E83C16400B2C97A /* MSMockUserDefaults.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MSMockUserDefaults.h; path = ../../../MobileCenter/MobileCenterTests/Util/MSMockUserDefaults.h; sourceTree = ""; }; D377A3111E83C16400B2C97A /* MSMockUserDefaults.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MSMockUserDefaults.m; path = ../../../MobileCenter/MobileCenterTests/Util/MSMockUserDefaults.m; sourceTree = ""; }; F861639A1E76A04A00E3F06C /* Tests.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Tests.xcconfig; path = ../../../Tests.xcconfig; sourceTree = ""; }; + F8646E651F349C6D006080AC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = Resources/ru.lproj/MobileCenterDistribute.strings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -594,6 +595,7 @@ en, de, fr, + ru, ); mainGroup = 04FD4A131E451E12009B4468; productRefGroup = 04FD4A1D1E451E12009B4468 /* Products */; @@ -747,6 +749,7 @@ B29CE3481E5E5F9A00CEB04D /* en */, B29CE3491E5E5FBB00CEB04D /* fr */, B2C070A21E5E61830076D6A9 /* de */, + F8646E651F349C6D006080AC /* ru */, ); name = MobileCenterDistribute.strings; path = ../MobileCenterDistribute; @@ -964,7 +967,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; - ENABLE_BITCODE = YES; + ENABLE_BITCODE = NO; INFOPLIST_FILE = MobileCenterDistributeResources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; MACOSX_DEPLOYMENT_TARGET = 10.11; @@ -983,7 +986,7 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; - ENABLE_BITCODE = YES; + ENABLE_BITCODE = NO; INFOPLIST_FILE = MobileCenterDistributeResources/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; MACOSX_DEPLOYMENT_TARGET = 10.11; diff --git a/MobileCenterDistribute/MobileCenterDistribute/Resources/ru.lproj/MobileCenterDistribute.strings b/MobileCenterDistribute/MobileCenterDistribute/Resources/ru.lproj/MobileCenterDistribute.strings new file mode 100644 index 0000000000..4f089399eb --- /dev/null +++ b/MobileCenterDistribute/MobileCenterDistribute/Resources/ru.lproj/MobileCenterDistribute.strings @@ -0,0 +1,15 @@ +/* + * Update dialog buttons + */ +"MSDistributeUpdateNow" = "Обновить сейчас"; +"MSDistributeAskMeInADay" = "Спросить завтра"; +"MSDistributeViewReleaseNotes" = "Просмотреть примечания к обновлению"; +"MSDistributeClose" = "Закрыть"; + +/* + * Update dialog titles/messages + */ +"MSDistributeAppUpdateAvailable" = "Доступно обновление"; +"MSDistributeAppUpdateAvailableMandatoryUpdateMessage" = "Вышло обязательное обновление. Вам необходимо обновиться до %@ %@ (%@)."; +"MSDistributeAppUpdateAvailableOptionalUpdateMessage" = "%@ %@ (%@) доступно для загрузки и установки."; +"MSDistributeInAppUpdatesAreDisabled" = "Обновления из приложения отключены."; diff --git a/MobileCenterDistribute/MobileCenterDistributeTests/MSDistributeTests.m b/MobileCenterDistribute/MobileCenterDistributeTests/MSDistributeTests.m index b96797033a..ce15bd9660 100644 --- a/MobileCenterDistribute/MobileCenterDistributeTests/MSDistributeTests.m +++ b/MobileCenterDistribute/MobileCenterDistributeTests/MSDistributeTests.m @@ -70,6 +70,7 @@ @interface MSDistributeTests : XCTestCase @property(nonatomic) id parserMock; @property(nonatomic) id settingsMock; @property(nonatomic) id bundleMock; +@property(nonatomic) id alertControllerMock; @end @@ -94,14 +95,29 @@ - (void)setUp { OCMStub([parserMock machOParserForMainBundle]).andReturn(self.parserMock); OCMStub([self.parserMock uuid]) .andReturn([[NSUUID alloc] initWithUUIDString:@"CD55E7A9-7AD1-4CA6-B722-3D133F487DA9"]); + + // Mock alert. + self.alertControllerMock = OCMClassMock([MSAlertController class]); + OCMStub([self.alertControllerMock alertControllerWithTitle:OCMOCK_ANY message:OCMOCK_ANY]) + .andReturn(self.alertControllerMock); } - (void)tearDown { [super tearDown]; + + // Wait all tasks in tests. + XCTestExpectation *expection = [self expectationWithDescription:@"tearDown"]; + dispatch_async(dispatch_get_main_queue(), ^{ + [expection fulfill]; + }); + [self waitForExpectations:@[expection] timeout:1]; + + // Clear [MSKeychainUtil clear]; [self.parserMock stopMocking]; [self.settingsMock stopMocking]; [self.bundleMock stopMocking]; + [self.alertControllerMock stopMocking]; [MSDistributeTestUtil unMockUpdatesAllowedConditions]; } @@ -115,7 +131,7 @@ - (void)testInstallURL { OCMStub([self.bundleMock objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(bundleArray); OCMStub([self.bundleMock objectForInfoDictionaryKey:@"MSAppName"]).andReturn(@"Something"); id distributeMock = OCMPartialMock(self.sut); - OCMStub([distributeMock openURLInEmbeddedSafari:[OCMArg any] fromClass:[OCMArg any]]).andDo(nil); + OCMStub([distributeMock openURLInEmbeddedSafari:OCMOCK_ANY fromClass:OCMOCK_ANY]).andDo(nil); // Disable for now to bypass initializing sender. [distributeMock setEnabled:NO]; @@ -155,6 +171,7 @@ - (void)testInstallURL { XCTFail(@"Expectation Failed with error: %@", error); } }]; + [distributeMock stopMocking]; } - (void)testMalformedUpdateURL { @@ -180,7 +197,7 @@ - (void)testOpenURLInSafariApp { SEL selector = NSSelectorFromString(@"openURL:options:completionHandler:"); BOOL newOpenURL = [appMock respondsToSelector:selector]; if (newOpenURL) { - OCMStub([appMock openURL:url options:[OCMArg any] completionHandler:[OCMArg any]]).andDo(nil); + OCMStub([appMock openURL:url options:OCMOCK_ANY completionHandler:OCMOCK_ANY]).andDo(nil); } else { OCMStub([appMock openURL:url]).andDo(nil); } @@ -195,7 +212,7 @@ - (void)testOpenURLInSafariApp { [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) { if (newOpenURL) { - OCMVerify([appMock openURL:url options:[OCMArg any] completionHandler:[OCMArg any]]); + OCMVerify([appMock openURL:url options:OCMOCK_ANY completionHandler:OCMOCK_ANY]); } else { OCMVerify([appMock openURL:url]); } @@ -203,6 +220,7 @@ - (void)testOpenURLInSafariApp { XCTFail(@"Expectation Failed with error: %@", error); } }]; + [appMock stopMocking]; } - (void)testOpenURLInEmbeddedSafari { @@ -284,14 +302,12 @@ - (void)testHandleInvalidUpdate { // If MSReleaseDetails *details = [MSReleaseDetails new]; id distributeMock = OCMPartialMock(self.sut); - OCMStub([distributeMock showConfirmationAlert:[OCMArg any]]).andDo(nil); + OCMReject([distributeMock showConfirmationAlert:OCMOCK_ANY]); + OCMStub([distributeMock showConfirmationAlert:OCMOCK_ANY]).andDo(nil); // When [distributeMock handleUpdate:details]; - // Then - OCMReject([distributeMock showConfirmationAlert:[OCMArg any]]); - // If details.id = @1; details.downloadUrl = [NSURL URLWithString:@"https://contoso.com/valid/url"]; @@ -299,9 +315,6 @@ - (void)testHandleInvalidUpdate { // When [distributeMock handleUpdate:details]; - // Then - OCMReject([distributeMock showConfirmationAlert:[OCMArg any]]); - // If details.status = @"available"; details.minOs = @"1000.0"; @@ -309,18 +322,18 @@ - (void)testHandleInvalidUpdate { // When [distributeMock handleUpdate:details]; - // Then - OCMReject([distributeMock showConfirmationAlert:[OCMArg any]]); - // If details.minOs = @"1.0"; - OCMStub([distributeMock isNewerVersion:[OCMArg any]]).andReturn(NO); + OCMStub([distributeMock isNewerVersion:OCMOCK_ANY]).andReturn(NO); // When [distributeMock handleUpdate:details]; - + // Then - OCMReject([distributeMock showConfirmationAlert:[OCMArg any]]); + OCMVerifyAll(distributeMock); + + // Clear + [distributeMock stopMocking]; } - (void)testHandleValidUpdate { @@ -329,10 +342,10 @@ - (void)testHandleValidUpdate { MSReleaseDetails *details = [MSReleaseDetails new]; id distributeMock = OCMPartialMock(self.sut); __block int showConfirmationAlertCounter = 0; - OCMStub([distributeMock showConfirmationAlert:[OCMArg any]]).andDo(^(__attribute((unused)) NSInvocation *invocation) { + OCMStub([distributeMock showConfirmationAlert:OCMOCK_ANY]).andDo(^(__attribute((unused)) NSInvocation *invocation) { showConfirmationAlertCounter++; }); - OCMStub([distributeMock isNewerVersion:[OCMArg any]]).andReturn(YES); + OCMStub([distributeMock isNewerVersion:OCMOCK_ANY]).andReturn(YES); details.id = @1; details.downloadUrl = [NSURL URLWithString:@"https://contoso.com/valid/url"]; details.status = @"available"; @@ -350,6 +363,9 @@ - (void)testHandleValidUpdate { * more explict checks. */ XCTAssertEqual(showConfirmationAlertCounter, 1); + + // Clear + [distributeMock stopMocking]; } /** @@ -361,7 +377,7 @@ - (void)testHandleUpdateAfterPostpone { // If id distributeMock = OCMPartialMock(self.sut); __block int isNewerVersionCounter = 0; - OCMStub([distributeMock isNewerVersion:[OCMArg any]]).andDo(^(__attribute((unused)) NSInvocation *invocation) { + OCMStub([distributeMock isNewerVersion:OCMOCK_ANY]).andDo(^(__attribute((unused)) NSInvocation *invocation) { isNewerVersionCounter++; }); int actualCounter = 0; @@ -378,7 +394,6 @@ - (void)testHandleUpdateAfterPostpone { // Then XCTAssertFalse(result); - OCMReject([distributeMock isNewerVersion:[OCMArg any]]); XCTAssertEqual(isNewerVersionCounter, 0); // If @@ -388,7 +403,6 @@ - (void)testHandleUpdateAfterPostpone { [distributeMock handleUpdate:details]; // Then - OCMVerify([distributeMock isNewerVersion:[OCMArg any]]); XCTAssertEqual(isNewerVersionCounter, ++actualCounter); // If @@ -399,7 +413,6 @@ - (void)testHandleUpdateAfterPostpone { [distributeMock handleUpdate:details]; // Then - OCMVerify([distributeMock isNewerVersion:[OCMArg any]]); XCTAssertEqual(isNewerVersionCounter, ++actualCounter); // If @@ -410,7 +423,6 @@ - (void)testHandleUpdateAfterPostpone { [distributeMock handleUpdate:details]; // Then - OCMVerify([distributeMock isNewerVersion:[OCMArg any]]); XCTAssertEqual(isNewerVersionCounter, ++actualCounter); // If @@ -423,7 +435,6 @@ - (void)testHandleUpdateAfterPostpone { [distributeMock handleUpdate:details]; // Then - OCMVerify([distributeMock isNewerVersion:[OCMArg any]]); XCTAssertEqual(isNewerVersionCounter, ++actualCounter); // If @@ -436,8 +447,10 @@ - (void)testHandleUpdateAfterPostpone { [distributeMock handleUpdate:details]; // Then - OCMVerify([distributeMock isNewerVersion:[OCMArg any]]); XCTAssertEqual(isNewerVersionCounter, ++actualCounter); + + // Clear + [distributeMock stopMocking]; } - (void)testShowConfirmationAlert { @@ -446,10 +459,7 @@ - (void)testShowConfirmationAlert { NSString *appName = @"Test App"; OCMStub([self.bundleMock objectForInfoDictionaryKey:@"CFBundleDisplayName"]).andReturn(appName); id mobileCenterMock = OCMPartialMock(self.sut); - id alertControllerMock = OCMClassMock([MSAlertController class]); MSReleaseDetails *details = [MSReleaseDetails new]; - OCMStub([alertControllerMock alertControllerWithTitle:[OCMArg any] message:[OCMArg any]]) - .andReturn(alertControllerMock); details.shortVersion = @"2.5"; details.version = @"11"; details.releaseNotes = @"Release notes"; @@ -470,20 +480,24 @@ - (void)testShowConfirmationAlert { }); [self waitForExpectationsWithTimeout:1 - handler:^(__attribute__((unused)) NSError *error) { + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } // Then - OCMVerify([alertControllerMock alertControllerWithTitle:[OCMArg any] message:message]); - OCMVerify([alertControllerMock + OCMVerify([self.alertControllerMock alertControllerWithTitle:OCMOCK_ANY message:message]); + OCMVerify([self.alertControllerMock addDefaultActionWithTitle:MSDistributeLocalizedString(@"MSDistributeAskMeInADay") - handler:[OCMArg any]]); - OCMVerify([alertControllerMock + handler:OCMOCK_ANY]); + OCMVerify([self.alertControllerMock addDefaultActionWithTitle:MSDistributeLocalizedString( @"MSDistributeViewReleaseNotes") - handler:[OCMArg any]]); - OCMVerify([alertControllerMock addPreferredActionWithTitle:[OCMArg any] - handler:[OCMArg any]]); + handler:OCMOCK_ANY]); + OCMVerify([self.alertControllerMock addPreferredActionWithTitle:OCMOCK_ANY + handler:OCMOCK_ANY]); }]; + [mobileCenterMock stopMocking]; } - (void)testShowConfirmationAlertWithoutViewReleaseNotesButton { @@ -492,10 +506,11 @@ - (void)testShowConfirmationAlertWithoutViewReleaseNotesButton { NSString *appName = @"Test App"; OCMStub([self.bundleMock objectForInfoDictionaryKey:@"CFBundleDisplayName"]).andReturn(appName); id mobileCenterMock = OCMPartialMock(self.sut); - id alertControllerMock = OCMClassMock([MSAlertController class]); + OCMReject([self.alertControllerMock + addDefaultActionWithTitle:MSDistributeLocalizedString( + @"MSDistributeViewReleaseNotes") + handler:OCMOCK_ANY]); MSReleaseDetails *details = [MSReleaseDetails new]; - OCMStub([alertControllerMock alertControllerWithTitle:[OCMArg any] message:[OCMArg any]]) - .andReturn(alertControllerMock); details.shortVersion = @"2.5"; details.version = @"11"; details.mandatoryUpdate = false; @@ -514,20 +529,21 @@ - (void)testShowConfirmationAlertWithoutViewReleaseNotesButton { }); [self waitForExpectationsWithTimeout:1 - handler:^(__attribute__((unused)) NSError *error) { + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } // Then - OCMVerify([alertControllerMock alertControllerWithTitle:[OCMArg any] message:message]); - OCMVerify([alertControllerMock + OCMVerify([self.alertControllerMock alertControllerWithTitle:OCMOCK_ANY message:message]); + OCMVerify([self.alertControllerMock addDefaultActionWithTitle:MSDistributeLocalizedString(@"MSDistributeAskMeInADay") - handler:[OCMArg any]]); - OCMReject([alertControllerMock - addDefaultActionWithTitle:MSDistributeLocalizedString( - @"MSDistributeViewReleaseNotes") - handler:[OCMArg any]]); - OCMVerify([alertControllerMock addPreferredActionWithTitle:[OCMArg any] - handler:[OCMArg any]]); + handler:OCMOCK_ANY]); + OCMVerify([self.alertControllerMock addPreferredActionWithTitle:OCMOCK_ANY + handler:OCMOCK_ANY]); + OCMVerifyAll(self.alertControllerMock); }]; + [mobileCenterMock stopMocking]; } - (void)testShowConfirmationAlertForMandatoryUpdate { @@ -536,10 +552,10 @@ - (void)testShowConfirmationAlertForMandatoryUpdate { NSString *appName = @"Test App"; OCMStub([self.bundleMock objectForInfoDictionaryKey:@"CFBundleDisplayName"]).andReturn(appName); id mobileCenterMock = OCMPartialMock(self.sut); - id alertControllerMock = OCMClassMock([MSAlertController class]); + OCMReject([self.alertControllerMock + addDefaultActionWithTitle:MSDistributeLocalizedString(@"MSDistributeAskMeInADay") + handler:OCMOCK_ANY]); MSReleaseDetails *details = [MSReleaseDetails new]; - OCMStub([alertControllerMock alertControllerWithTitle:[OCMArg any] message:[OCMArg any]]) - .andReturn(alertControllerMock); details.shortVersion = @"2.5"; details.version = @"11"; details.releaseNotes = @"Release notes"; @@ -560,20 +576,22 @@ - (void)testShowConfirmationAlertForMandatoryUpdate { }); [self waitForExpectationsWithTimeout:1 - handler:^(__attribute__((unused)) NSError *error) { + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } // Then - OCMVerify([alertControllerMock alertControllerWithTitle:[OCMArg any] message:message]); - OCMReject([alertControllerMock - addDefaultActionWithTitle:MSDistributeLocalizedString(@"MSDistributeAskMeInADay") - handler:[OCMArg any]]); - OCMVerify([alertControllerMock + OCMVerify([self.alertControllerMock alertControllerWithTitle:OCMOCK_ANY message:message]); + OCMVerify([self.alertControllerMock addDefaultActionWithTitle:MSDistributeLocalizedString( @"MSDistributeViewReleaseNotes") - handler:[OCMArg any]]); - OCMVerify([alertControllerMock addPreferredActionWithTitle:[OCMArg any] - handler:[OCMArg any]]); + handler:OCMOCK_ANY]); + OCMVerify([self.alertControllerMock addPreferredActionWithTitle:OCMOCK_ANY + handler:OCMOCK_ANY]); + OCMVerifyAll(self.alertControllerMock); }]; + [mobileCenterMock stopMocking]; } - (void)testShowConfirmationAlertWithoutViewReleaseNotesButtonForMandatoryUpdate { @@ -582,10 +600,14 @@ - (void)testShowConfirmationAlertWithoutViewReleaseNotesButtonForMandatoryUpdate NSString *appName = @"Test App"; OCMStub([self.bundleMock objectForInfoDictionaryKey:@"CFBundleDisplayName"]).andReturn(appName); id mobileCenterMock = OCMPartialMock(self.sut); - id alertControllerMock = OCMClassMock([MSAlertController class]); + OCMReject([self.alertControllerMock + addDefaultActionWithTitle:MSDistributeLocalizedString(@"MSDistributeAskMeInADay") + handler:OCMOCK_ANY]); + OCMReject([self.alertControllerMock + addDefaultActionWithTitle:MSDistributeLocalizedString( + @"MSDistributeViewReleaseNotes") + handler:OCMOCK_ANY]); MSReleaseDetails *details = [MSReleaseDetails new]; - OCMStub([alertControllerMock alertControllerWithTitle:[OCMArg any] message:[OCMArg any]]) - .andReturn(alertControllerMock); details.shortVersion = @"2.5"; details.version = @"11"; details.mandatoryUpdate = true; @@ -604,20 +626,18 @@ - (void)testShowConfirmationAlertWithoutViewReleaseNotesButtonForMandatoryUpdate }); [self waitForExpectationsWithTimeout:1 - handler:^(__attribute__((unused)) NSError *error) { + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } // Then - OCMVerify([alertControllerMock alertControllerWithTitle:[OCMArg any] message:message]); - OCMReject([alertControllerMock - addDefaultActionWithTitle:MSDistributeLocalizedString(@"MSDistributeAskMeInADay") - handler:[OCMArg any]]); - OCMReject([alertControllerMock - addDefaultActionWithTitle:MSDistributeLocalizedString( - @"MSDistributeViewReleaseNotes") - handler:[OCMArg any]]); - OCMVerify([alertControllerMock addPreferredActionWithTitle:[OCMArg any] - handler:[OCMArg any]]); + OCMVerify([self.alertControllerMock alertControllerWithTitle:OCMOCK_ANY message:message]); + OCMVerify([self.alertControllerMock addPreferredActionWithTitle:OCMOCK_ANY + handler:OCMOCK_ANY]); + OCMVerifyAll(self.alertControllerMock); }]; + [mobileCenterMock stopMocking]; } - (void)testShowConfirmationAlertForMandatoryUpdateWhileNoNetwork { @@ -628,9 +648,9 @@ - (void)testShowConfirmationAlertForMandatoryUpdateWhileNoNetwork { XCTestExpectation *expection = [self expectationWithDescription:@"Confirmation alert has been displayed"]; // Mock alert. - id alertControllerMock = OCMClassMock([MSAlertController class]); - OCMStub([alertControllerMock alertControllerWithTitle:[OCMArg any] message:[OCMArg any]]) - .andReturn(alertControllerMock); + OCMReject([self.alertControllerMock + addDefaultActionWithTitle:MSDistributeLocalizedString(@"MSDistributeAskMeInADay") + handler:OCMOCK_ANY]); // Mock Bundle. NSString *appName = @"Test App"; @@ -657,7 +677,7 @@ - (void)testShowConfirmationAlertForMandatoryUpdateWhileNoNetwork { // Mock MSDistribute isNewerVersion to return YES. id distributeMock = OCMPartialMock(self.sut); - OCMStub([distributeMock isNewerVersion:[OCMArg any]]).andReturn(YES); + OCMStub([distributeMock isNewerVersion:OCMOCK_ANY]).andReturn(YES); // Mock reachability. id reachabilityMock = OCMClassMock([MS_Reachability class]); @@ -678,22 +698,25 @@ - (void)testShowConfirmationAlertForMandatoryUpdateWhileNoNetwork { [expection fulfill]; }); [self waitForExpectationsWithTimeout:1 - handler:^(__attribute__((unused)) NSError *error) { + handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } /* * Then */ - OCMVerify([alertControllerMock alertControllerWithTitle:[OCMArg any] message:message]); - OCMReject([alertControllerMock - addDefaultActionWithTitle:MSDistributeLocalizedString(@"MSDistributeAskMeInADay") - handler:[OCMArg any]]); - OCMVerify([alertControllerMock + OCMVerify([self.alertControllerMock alertControllerWithTitle:OCMOCK_ANY message:message]); + OCMVerify([self.alertControllerMock addDefaultActionWithTitle:MSDistributeLocalizedString( @"MSDistributeViewReleaseNotes") - handler:[OCMArg any]]); - OCMVerify([alertControllerMock addPreferredActionWithTitle:[OCMArg any] - handler:[OCMArg any]]); + handler:OCMOCK_ANY]); + OCMVerify([self.alertControllerMock addPreferredActionWithTitle:OCMOCK_ANY + handler:OCMOCK_ANY]); + OCMVerifyAll(self.alertControllerMock); }]; + [distributeMock stopMocking]; + [reachabilityMock stopMocking]; } - (void)testDontShowConfirmationAlertIfNoMandatoryReleaseWhileNoNetwork { @@ -704,9 +727,9 @@ - (void)testDontShowConfirmationAlertIfNoMandatoryReleaseWhileNoNetwork { XCTestExpectation *expection = [self expectationWithDescription:@"Confirmation alert has been displayed"]; // Mock alert. - id alertControllerMock = OCMClassMock([MSAlertController class]); - OCMStub([alertControllerMock alertControllerWithTitle:[OCMArg any] message:[OCMArg any]]) - .andReturn(alertControllerMock); + OCMReject([self.alertControllerMock alertControllerWithTitle:OCMOCK_ANY message:OCMOCK_ANY]); + OCMReject([self.alertControllerMock addDefaultActionWithTitle:OCMOCK_ANY handler:OCMOCK_ANY]); + OCMReject([self.alertControllerMock addCancelActionWithTitle:OCMOCK_ANY handler:OCMOCK_ANY]); // Mock reachability. id reachabilityMock = OCMClassMock([MS_Reachability class]); @@ -715,7 +738,7 @@ - (void)testDontShowConfirmationAlertIfNoMandatoryReleaseWhileNoNetwork { NetworkStatus test = NotReachable; [invocation setReturnValue:&test]; }); - + /* * When */ @@ -724,18 +747,15 @@ - (void)testDontShowConfirmationAlertIfNoMandatoryReleaseWhileNoNetwork { [expection fulfill]; }); [self waitForExpectationsWithTimeout:1 - handler:^(__attribute__((unused)) NSError *error) { - - /* - * Then - */ - OCMReject( - [alertControllerMock alertControllerWithTitle:[OCMArg any] message:[OCMArg any]]); - OCMReject( - [alertControllerMock addDefaultActionWithTitle:[OCMArg any] handler:[OCMArg any]]); - OCMReject( - [alertControllerMock addCancelActionWithTitle:[OCMArg any] handler:[OCMArg any]]); + handler:^(NSError *error) { + + // Then + OCMVerifyAll(self.alertControllerMock); + if (error) { + XCTFail(@"Expectation Failed with error: %@", error); + } }]; + [reachabilityMock stopMocking]; } - (void)testPersistLastestMandatoryUpdate { @@ -750,7 +770,7 @@ - (void)testPersistLastestMandatoryUpdate { // Mock MSDistribute isNewerVersion to return YES. id distributeMock = OCMPartialMock(self.sut); - OCMStub([distributeMock isNewerVersion:[OCMArg any]]).andReturn(YES); + OCMStub([distributeMock isNewerVersion:OCMOCK_ANY]).andReturn(YES); // When [self.sut handleUpdate:details]; @@ -760,6 +780,9 @@ - (void)testPersistLastestMandatoryUpdate { MSReleaseDetails *persistedRelease = [[MSReleaseDetails alloc] initWithDictionary:persistedDict]; assertThat(persistedRelease, notNilValue()); assertThat([details serializeToDictionary], is(persistedDict)); + + // Clear + [distributeMock stopMocking]; } - (void)testDontPersistLastestReleaseIfNotMandatory { @@ -783,13 +806,15 @@ - (void)testDontPersistLastestReleaseIfNotMandatory { - (void)testOpenUrl { // If + NSString *requestId = @"FIRST-REQUEST"; + NSString *token = @"TOKEN"; NSString *scheme = [NSString stringWithFormat:kMSDefaultCustomSchemeFormat, kMSTestAppSecret]; id distributeMock = OCMPartialMock(self.sut); + OCMReject([distributeMock checkLatestRelease:OCMOCK_ANY releaseHash:OCMOCK_ANY]); OCMStub([distributeMock sharedInstance]).andReturn(distributeMock); - OCMStub([distributeMock checkLatestRelease:[OCMArg any] releaseHash:kMSTestReleaseHash]).andDo(nil); id mobileCeneterMock = OCMClassMock([MSMobileCenter class]); OCMStub([mobileCeneterMock isConfigured]).andReturn(YES); - [self mockMSPackageHash]; + id utilityMock = [self mockMSPackageHash]; // When NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://?", scheme]]; @@ -797,7 +822,6 @@ - (void)testOpenUrl { // Then assertThatBool(result, isFalse()); - OCMReject([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]); // Disable for now to bypass initializing sender. [distributeMock setEnabled:NO]; @@ -813,7 +837,6 @@ - (void)testOpenUrl { // Then assertThatBool(result, isFalse()); - OCMReject([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]); // If url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://?", scheme]]; @@ -823,10 +846,8 @@ - (void)testOpenUrl { // Then assertThatBool(result, isTrue()); - OCMReject([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]); // If - NSString *requestId = @"FIRST-REQUEST"; url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://?request_id=%@", scheme, requestId]]; // When @@ -834,10 +855,8 @@ - (void)testOpenUrl { // Then assertThatBool(result, isTrue()); - OCMReject([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]); // If - NSString *token = @"TOKEN"; url = [NSURL URLWithString:[NSString stringWithFormat:@"%@://?request_id=%@&update_token=%@", scheme, requestId, token]]; @@ -846,7 +865,6 @@ - (void)testOpenUrl { // Then assertThatBool(result, isTrue()); - OCMReject([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]); // If [MS_USER_DEFAULTS setObject:requestId forKey:kMSUpdateTokenRequestIdKey]; @@ -860,36 +878,70 @@ - (void)testOpenUrl { // Then assertThatBool(result, isFalse()); - OCMReject([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]); - // If - url = [NSURL - URLWithString:[NSString stringWithFormat:@"%@://?request_id=%@&update_token=%@", scheme, requestId, token]]; + // Clear + [distributeMock stopMocking]; + [mobileCeneterMock stopMocking]; + [utilityMock stopMocking]; +} +- (void)testOpenUrlWithCheckLatestRelease { + + // If + NSString *requestId = @"FIRST-REQUEST"; + NSString *token = @"TOKEN"; + NSString *scheme = [NSString stringWithFormat:kMSDefaultCustomSchemeFormat, kMSTestAppSecret]; + id distributeMock = OCMPartialMock(self.sut); + OCMStub([distributeMock checkLatestRelease:OCMOCK_ANY releaseHash:kMSTestReleaseHash]).andDo(nil); + OCMStub([distributeMock sharedInstance]).andReturn(distributeMock); + id mobileCeneterMock = OCMClassMock([MSMobileCenter class]); + OCMStub([mobileCeneterMock isConfigured]).andReturn(YES); + id utilityMock = [self mockMSPackageHash]; + + + // Disable for now to bypass initializing sender. + [distributeMock setEnabled:NO]; + [distributeMock startWithLogManager:OCMProtocolMock(@protocol(MSLogManager)) appSecret:kMSTestAppSecret]; + + // Enable again. + [distributeMock setEnabled:YES]; + + // If + [MS_USER_DEFAULTS setObject:requestId forKey:kMSUpdateTokenRequestIdKey]; + NSURL *url = [NSURL + URLWithString:[NSString stringWithFormat:@"%@://?request_id=%@&update_token=%@", scheme, requestId, token]]; + // When - result = [MSDistribute openURL:url]; - + BOOL result = [MSDistribute openURL:url]; + // Then assertThatBool(result, isTrue()); OCMVerify([distributeMock checkLatestRelease:token releaseHash:kMSTestReleaseHash]); - + + // Not allow checkLatestRelease more. + OCMReject([distributeMock checkLatestRelease:OCMOCK_ANY releaseHash:OCMOCK_ANY]); + // If [distributeMock setEnabled:NO]; - + // When [MSDistribute openURL:url]; - + // Then assertThatBool(result, isTrue()); - OCMReject([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]); + + // Clear + [distributeMock stopMocking]; + [mobileCeneterMock stopMocking]; + [utilityMock stopMocking]; } - (void)testApplyEnabledStateTrueForDebugConfig { // If id distributeMock = OCMPartialMock(self.sut); - OCMStub([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]).andDo(nil); - OCMStub([distributeMock requestUpdateToken:[OCMArg any]]).andDo(nil); + OCMStub([distributeMock checkLatestRelease:OCMOCK_ANY releaseHash:OCMOCK_ANY]).andDo(nil); + OCMStub([distributeMock requestUpdateToken:OCMOCK_ANY]).andDo(nil); // When [distributeMock applyEnabledState:YES]; @@ -902,6 +954,9 @@ - (void)testApplyEnabledStateTrueForDebugConfig { // Then XCTAssertNil([self.settingsMock objectForKey:kMSUpdateTokenRequestIdKey]); + + // Clear + [distributeMock stopMocking]; } - (void)testApplyEnabledStateTrue { @@ -910,9 +965,9 @@ - (void)testApplyEnabledStateTrue { NSDictionary *plist = @{ @"CFBundleShortVersionString" : @"1.0", @"CFBundleVersion" : @"1" }; OCMStub([self.bundleMock infoDictionary]).andReturn(plist); id distributeMock = OCMPartialMock(self.sut); - OCMStub([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]).andDo(nil); - OCMStub([distributeMock requestUpdateToken:[OCMArg any]]).andDo(nil); - [self mockMSPackageHash]; + OCMStub([distributeMock checkLatestRelease:OCMOCK_ANY releaseHash:OCMOCK_ANY]).andDo(nil); + OCMStub([distributeMock requestUpdateToken:OCMOCK_ANY]).andDo(nil); + id utilityMock = [self mockMSPackageHash]; // When [distributeMock applyEnabledState:YES]; @@ -927,7 +982,7 @@ - (void)testApplyEnabledStateTrue { [distributeMock applyEnabledState:YES]; // Then - OCMVerify([distributeMock checkLatestRelease:[OCMArg any] releaseHash:kMSTestReleaseHash]); + OCMVerify([distributeMock checkLatestRelease:OCMOCK_ANY releaseHash:kMSTestReleaseHash]); // If [self.settingsMock setObject:@"RequestID" forKey:kMSUpdateTokenRequestIdKey]; @@ -940,6 +995,10 @@ - (void)testApplyEnabledStateTrue { // Then XCTAssertNil([self.settingsMock objectForKey:kMSUpdateTokenRequestIdKey]); + + // Clear + [distributeMock stopMocking]; + [utilityMock stopMocking]; } - (void)testCheckForUpdatesAllConditionsMet { @@ -948,8 +1007,8 @@ - (void)testCheckForUpdatesAllConditionsMet { [MSDistributeTestUtil unMockUpdatesAllowedConditions]; id mobileCenterMock = OCMClassMock([MSMobileCenter class]); id distributeMock = OCMPartialMock(self.sut); - OCMStub([distributeMock checkLatestRelease:[OCMArg any] releaseHash:[OCMArg any]]).andDo(nil); - OCMStub([distributeMock requestUpdateToken:[OCMArg any]]).andDo(nil); + OCMStub([distributeMock checkLatestRelease:OCMOCK_ANY releaseHash:OCMOCK_ANY]).andDo(nil); + OCMStub([distributeMock requestUpdateToken:OCMOCK_ANY]).andDo(nil); id utilityMock = [self mockMSPackageHash]; // When @@ -964,6 +1023,11 @@ - (void)testCheckForUpdatesAllConditionsMet { // Then OCMVerify([distributeMock requestUpdateToken:kMSTestReleaseHash]); + + // Clear + [distributeMock stopMocking]; + [mobileCenterMock stopMocking]; + [utilityMock stopMocking]; } - (void)testCheckForUpdatesDebuggerAttached { @@ -977,6 +1041,9 @@ - (void)testCheckForUpdatesDebuggerAttached { // Then XCTAssertFalse([self.sut checkForUpdatesAllowed]); + + // Clear + [mobileCenterMock stopMocking]; } - (void)testCheckForUpdatesInvalidEnvironment { @@ -990,6 +1057,9 @@ - (void)testCheckForUpdatesInvalidEnvironment { // Then XCTAssertFalse([self.sut checkForUpdatesAllowed]); + + // Clear + [mobileCenterMock stopMocking]; } - (void)testNotDeleteUpdateToken { @@ -997,12 +1067,13 @@ - (void)testNotDeleteUpdateToken { // If [MS_USER_DEFAULTS setObject:@1 forKey:kMSSDKHasLaunchedWithDistribute]; id keychainMock = OCMClassMock([MSKeychainUtil class]); + OCMReject([keychainMock deleteStringForKey:kMSUpdateTokenKey]); // When [MSDistribute new]; - - // Then - OCMReject([keychainMock deleteStringForKey:kMSUpdateTokenKey]); + + // Clear + [keychainMock stopMocking]; } - (void)testDeleteUpdateTokenAfterReinstall { @@ -1016,6 +1087,9 @@ - (void)testDeleteUpdateTokenAfterReinstall { // Then OCMVerify([keychainMock deleteStringForKey:kMSUpdateTokenKey]); OCMVerify([self.settingsMock setObject:@(1) forKey:kMSSDKHasLaunchedWithDistribute]); + + // Clear + [keychainMock stopMocking]; } - (void)testWithoutNetwork { @@ -1025,15 +1099,17 @@ - (void)testWithoutNetwork { OCMStub([reachabilityMock reachabilityForInternetConnection]).andReturn(reachabilityMock); OCMStub([reachabilityMock currentReachabilityStatus]).andReturn(NotReachable); id distributeMock = OCMPartialMock(self.sut); + OCMReject([distributeMock buildTokenRequestURLWithAppSecret:OCMOCK_ANY releaseHash:kMSTestReleaseHash]); // We should not touch UI in a unit testing environment. - OCMStub([distributeMock openURLInEmbeddedSafari:[OCMArg any] fromClass:[OCMArg any]]).andDo(nil); + OCMStub([distributeMock openURLInEmbeddedSafari:OCMOCK_ANY fromClass:OCMOCK_ANY]).andDo(nil); // When [distributeMock requestUpdateToken:kMSTestReleaseHash]; - - // Then - OCMReject([distributeMock buildTokenRequestURLWithAppSecret:[OCMArg any] releaseHash:kMSTestReleaseHash]); + + // Clear + [distributeMock stopMocking]; + [reachabilityMock stopMocking]; } - (void)testPackageHash { @@ -1055,6 +1131,8 @@ - (void)testDismissEmbeddedSafari { // If XCTestExpectation *safariDismissedExpectation = [self expectationWithDescription:@"Safari dismissed processed"]; id viewControllerMock = OCMClassMock([UIViewController class]); + OCMReject([viewControllerMock dismissViewControllerAnimated:(BOOL)OCMOCK_ANY + completion:OCMOCK_ANY]); self.sut.safariHostingViewController = nil; // When @@ -1066,12 +1144,14 @@ - (void)testDismissEmbeddedSafari { // Then [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) { - OCMReject([viewControllerMock dismissViewControllerAnimated:(BOOL)OCMOCK_ANY - completion:OCMOCK_ANY]); + + // Then + OCMVerifyAll(viewControllerMock); if (error) { XCTFail(@"Expectation Failed with error: %@", error); } }]; + [viewControllerMock stopMocking]; } - (void)testDismissEmbeddedSafariWithNilVC { @@ -1146,6 +1226,7 @@ - (void)testDismissEmbeddedSafariWithValidVC { XCTFail(@"Expectation Failed with error: %@", error); } }]; + [viewControllerMock stopMocking]; } - (void)testDismissEmbeddedSafariWhenOpenURL { @@ -1180,6 +1261,8 @@ - (void)testDismissEmbeddedSafariWhenOpenURL { XCTFail(@"Expectation Failed with error: %@", error); } }]; + [distributeMock stopMocking]; + [viewControllerMock stopMocking]; } - (void)testDismissEmbeddedSafariWhenDisabling { @@ -1204,15 +1287,13 @@ - (void)testDismissEmbeddedSafariWhenDisabling { XCTFail(@"Expectation Failed with error: %@", error); } }]; + [viewControllerMock stopMocking]; } - (void)testShowDistributeDisabledAlert { // If id mobileCenterMock = OCMPartialMock(self.sut); - id alertControllerMock = OCMClassMock([MSAlertController class]); - OCMStub([alertControllerMock alertControllerWithTitle:[OCMArg any] message:[OCMArg any]]) - .andReturn(alertControllerMock); // When XCTestExpectation *expection = [self expectationWithDescription:@"Distribute disabled alert has been displayed"]; @@ -1225,10 +1306,11 @@ - (void)testShowDistributeDisabledAlert { handler:^(__attribute__((unused)) NSError *error) { // Then - OCMVerify([alertControllerMock alertControllerWithTitle:[OCMArg any] message:nil]); + OCMVerify([self.alertControllerMock alertControllerWithTitle:OCMOCK_ANY message:nil]); OCMVerify( - [alertControllerMock addCancelActionWithTitle:[OCMArg any] handler:[OCMArg any]]); + [self.alertControllerMock addCancelActionWithTitle:OCMOCK_ANY handler:OCMOCK_ANY]); }]; + [mobileCenterMock stopMocking]; } - (void)testStartDownload { @@ -1238,7 +1320,7 @@ - (void)testStartDownload { id distributeMock = OCMPartialMock(self.sut); OCMStub([distributeMock closeApp]).andDo(nil); id utilityMock = OCMClassMock([MSUtility class]); - OCMStub(ClassMethod([utilityMock sharedAppOpenUrl:[OCMArg any] options:[OCMArg any] completionHandler:[OCMArg any]])) + OCMStub(ClassMethod([utilityMock sharedAppOpenUrl:OCMOCK_ANY options:OCMOCK_ANY completionHandler:OCMOCK_ANY])) .andDo(^(NSInvocation *invocation) { void (^handler)(MSOpenURLState); [invocation getArgument:&handler atIndex:4]; @@ -1251,6 +1333,10 @@ - (void)testStartDownload { // Then OCMVerify([distributeMock closeApp]); + + // Clear + [distributeMock stopMocking]; + [utilityMock stopMocking]; } - (void)testStartDownloadFailed { @@ -1258,9 +1344,10 @@ - (void)testStartDownloadFailed { // If MSReleaseDetails *details = [MSReleaseDetails new]; id distributeMock = OCMPartialMock(self.sut); + OCMReject([distributeMock closeApp]); OCMStub([distributeMock closeApp]).andDo(nil); id utilityMock = OCMClassMock([MSUtility class]); - OCMStub(ClassMethod([utilityMock sharedAppOpenUrl:[OCMArg any] options:[OCMArg any] completionHandler:[OCMArg any]])) + OCMStub(ClassMethod([utilityMock sharedAppOpenUrl:OCMOCK_ANY options:OCMOCK_ANY completionHandler:OCMOCK_ANY])) .andDo(^(NSInvocation *invocation) { void (^handler)(MSOpenURLState); [invocation getArgument:&handler atIndex:4]; @@ -1270,9 +1357,10 @@ - (void)testStartDownloadFailed { // When details.mandatoryUpdate = YES; [distributeMock startDownload:details]; - - // Then - OCMReject([distributeMock closeApp]); + + // Clear + [distributeMock stopMocking]; + [utilityMock stopMocking]; } - (void)testServiceNameIsCorrect { @@ -1284,10 +1372,9 @@ - (void)testUpdateURLWithUnregisteredScheme { // If NSArray *bundleArray = @[ @{ @"CFBundleURLSchemes" : @[ @"mobilecenter-IAMSUPERSECRET" ] } ]; OCMStub([self.bundleMock objectForInfoDictionaryKey:@"CFBundleURLTypes"]).andReturn(bundleArray); - id distributeMock = OCMPartialMock(self.sut); // When - NSURL *url = [distributeMock buildTokenRequestURLWithAppSecret:kMSTestAppSecret releaseHash:kMSTestReleaseHash]; + NSURL *url = [self.sut buildTokenRequestURLWithAppSecret:kMSTestAppSecret releaseHash:kMSTestReleaseHash]; // Then assertThat(url, nilValue()); @@ -1343,14 +1430,12 @@ - (void)testStartUpdateWhenApplicationEnterForeground { // Then OCMVerify([distributeMock isEnabled]); - OCMReject([distributeMock startUpdate]); XCTAssertEqual(startUpdateCounter, 0); // When [distributeMock setEnabled:YES]; // Then - OCMVerify([distributeMock startUpdate]); XCTAssertEqual(startUpdateCounter, 1); // When @@ -1358,8 +1443,11 @@ - (void)testStartUpdateWhenApplicationEnterForeground { // Then OCMVerify([distributeMock isEnabled]); - OCMVerify([distributeMock startUpdate]); XCTAssertEqual(startUpdateCounter, 2); + + // Clear + [notificationCenterMock stopMocking]; + [distributeMock stopMocking]; } - (void)testNotifyUpdateAction { @@ -1376,6 +1464,10 @@ - (void)testNotifyUpdateAction { // Then assertThat([self.settingsMock objectForKey:kMSPostponedTimestampKey], equalToLongLong((long long)time)); + + // Clear + [distributeMock stopMocking]; + [utilityMock stopMocking]; } - (void)testNotifyUpdateActionTwice { @@ -1401,6 +1493,10 @@ - (void)testNotifyUpdateActionTwice { // Then XCTAssertNil([self.settingsMock objectForKey:kMSPostponedTimestampKey]); + + // Clear + [distributeMock stopMocking]; + [utilityMock stopMocking]; } - (void)testSetDelegate { @@ -1447,6 +1543,8 @@ - (void)testDefaultUpdateAlert { } OCMVerify([distributeMock showConfirmationAlert:detailsMock]); }]; + [detailsMock stopMocking]; + [distributeMock stopMocking]; } - (void)testDefaultUpdateAlertWithDelegate { @@ -1465,7 +1563,7 @@ - (void)testDefaultUpdateAlertWithDelegate { OCMStub([distributeMock showConfirmationAlert:detailsMock]).andDo(nil); // When - OCMStub([delegateMock distribute:distributeMock releaseAvailableWithDetails:[OCMArg any]]).andReturn(NO); + OCMStub([delegateMock distribute:distributeMock releaseAvailableWithDetails:OCMOCK_ANY]).andReturn(NO); [distributeMock setDelegate:delegateMock]; [distributeMock handleUpdate:detailsMock]; dispatch_async(dispatch_get_main_queue(), ^{ @@ -1482,6 +1580,8 @@ - (void)testDefaultUpdateAlertWithDelegate { releaseAvailableWithDetails:detailsMock]); OCMVerify([distributeMock showConfirmationAlert:detailsMock]); }]; + [detailsMock stopMocking]; + [distributeMock stopMocking]; } - (void)testCustomizedUpdateAlert { @@ -1500,7 +1600,7 @@ - (void)testCustomizedUpdateAlert { OCMStub([distributeMock showConfirmationAlert:detailsMock]).andDo(nil); // When - OCMStub([delegateMock distribute:distributeMock releaseAvailableWithDetails:[OCMArg any]]).andReturn(YES); + OCMStub([delegateMock distribute:distributeMock releaseAvailableWithDetails:OCMOCK_ANY]).andReturn(YES); [distributeMock setDelegate:delegateMock]; [distributeMock handleUpdate:detailsMock]; dispatch_async(dispatch_get_main_queue(), ^{ @@ -1515,8 +1615,9 @@ - (void)testCustomizedUpdateAlert { } OCMVerify([[distributeMock delegate] distribute:distributeMock releaseAvailableWithDetails:detailsMock]); - OCMReject([distributeMock showConfirmationAlert:detailsMock]); }]; + [detailsMock stopMocking]; + [distributeMock stopMocking]; } #pragma mark - Helper @@ -1532,7 +1633,7 @@ - (id)mockMSPackageHash { id utilityMock = OCMClassMock([MSUtility class]); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wcast-qual" - OCMStub(ClassMethod([utilityMock sha256:[OCMArg any]])).andReturn(kMSTestReleaseHash); + OCMStub(ClassMethod([utilityMock sha256:OCMOCK_ANY])).andReturn(kMSTestReleaseHash); #pragma GCC diagnostic pop NSDictionary *plist = @{ @"CFBundleShortVersionString" : @"1.0", @"CFBundleVersion" : @"1" }; diff --git a/MobileCenterPush/MobileCenterPush.xcodeproj/xcshareddata/xcschemes/MobileCenterPush.xcscheme b/MobileCenterPush/MobileCenterPush.xcodeproj/xcshareddata/xcschemes/MobileCenterPush.xcscheme index 4e66c76f3a..2cc71abc47 100644 --- a/MobileCenterPush/MobileCenterPush.xcodeproj/xcshareddata/xcschemes/MobileCenterPush.xcscheme +++ b/MobileCenterPush/MobileCenterPush.xcodeproj/xcshareddata/xcschemes/MobileCenterPush.xcscheme @@ -1,6 +1,6 @@