diff --git a/.swiftlint.yml b/.swiftlint.yml index 762af4a7..292e2e15 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -11,6 +11,7 @@ opt_in_rules: # some rules are only opt-in # swiftlint rules included: # paths to include during linting. `--path` is ignored if present. - Sources + - Tests excluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage - Pods diff --git a/Backtrace-macOS/BacktraceCrashExceptionApplication.swift b/Backtrace-macOS/BacktraceCrashExceptionApplication.swift new file mode 100644 index 00000000..45fc2239 --- /dev/null +++ b/Backtrace-macOS/BacktraceCrashExceptionApplication.swift @@ -0,0 +1,11 @@ +import Cocoa + +/// `NSApplication` subclass to catch additional exceptions on macOS +@objc public class BacktraceCrashExceptionApplication: NSApplication { + + /// Catch all exceptions and send them to `Backtrace` + public override func reportException(_ exception: NSException) { + super.reportException(exception) + BacktraceClient.shared?.send(exception: exception, attachmentPaths: [], completion: {_ in }) + } +} diff --git a/Backtrace.podspec b/Backtrace.podspec index f2e8b97f..37276f82 100644 --- a/Backtrace.podspec +++ b/Backtrace.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.name = "Backtrace" - s.version = "1.3.0" + s.version = "1.4.0" s.summary = "Backtrace's integration with iOS and macOS" s.description = "Backtrace's integration with iOS and macOS for handling crashes" s.homepage = "https://backtrace.io/" @@ -20,8 +20,8 @@ Pod::Spec.new do |s| s.ios.deployment_target = "10.0" s.osx.deployment_target = "10.10" - s.ios.source_files = ["Sources/**/*.{swift}", "Backtrace-iOS/**/*.h*"] - s.osx.source_files = ["Sources/**/*.{swift}", "Backtrace-macOS/**/*.h*"] + s.ios.source_files = ["Sources/**/*.{swift}", "Backtrace-iOS/**/*.{h*,swift}"] + s.osx.source_files = ["Sources/**/*.{swift}", "Backtrace-macOS/**/*.{h*,swift}"] s.ios.public_header_files = ["Backtrace-iOS/**/*.h*"] s.osx.public_header_files = ["Backtrace-macOS/**/*.h*"] diff --git a/Backtrace.xcodeproj/project.pbxproj b/Backtrace.xcodeproj/project.pbxproj index 1eb7a7fb..5010bcff 100644 --- a/Backtrace.xcodeproj/project.pbxproj +++ b/Backtrace.xcodeproj/project.pbxproj @@ -9,6 +9,18 @@ /* Begin PBXBuildFile section */ 18ABC4BAF017FF456E5E5186 /* Pods_Example_iOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0FD3E3727B2ED6C3130FFE5 /* Pods_Example_iOS.framework */; }; 27F936A322CFBB6FE46EDEFE /* Pods_Example_macOS_ObjC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70856A4F8005B8D4E85C332B /* Pods_Example_macOS_ObjC.framework */; }; + 282C85E7223FD8E70014FE75 /* BacktraceCrashExceptionApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282C85E6223FD8E70014FE75 /* BacktraceCrashExceptionApplication.swift */; }; + 282C85EA22419C560014FE75 /* BacktraceWatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282C85E822419BB10014FE75 /* BacktraceWatcherTests.swift */; }; + 282C85EB22419C600014FE75 /* BacktraceWatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 282C85E822419BB10014FE75 /* BacktraceWatcherTests.swift */; }; + 2846E1F8222F1DE60035F98C /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2846E1F7222F1DE50035F98C /* NetworkReachability.swift */; }; + 2846E1F9222F1DE60035F98C /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2846E1F7222F1DE50035F98C /* NetworkReachability.swift */; }; + 2846E1FB222F2C850035F98C /* BluetoothStatusListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2846E1FA222F2C850035F98C /* BluetoothStatusListener.swift */; }; + 2846E1FC222F2C850035F98C /* BluetoothStatusListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2846E1FA222F2C850035F98C /* BluetoothStatusListener.swift */; }; + 2846E1FE223070CB0035F98C /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2846E1FD223070CB0035F98C /* Attachment.swift */; }; + 2846E1FF223070CB0035F98C /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2846E1FD223070CB0035F98C /* Attachment.swift */; }; + 2846E201223818550035F98C /* test.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2846E200223818550035F98C /* test.txt */; }; + 2846E203223818920035F98C /* test.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2846E202223818920035F98C /* test.txt */; }; + 2846E2052238189A0035F98C /* test.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2846E2042238189A0035F98C /* test.txt */; }; 28614F9E220B6D7C00D35EFB /* DefaultAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28614F9D220B6D7C00D35EFB /* DefaultAttributes.swift */; }; 28614F9F220B900300D35EFB /* DefaultAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28614F9D220B6D7C00D35EFB /* DefaultAttributes.swift */; }; 28966EFA2214BBD200E6E891 /* AttributesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28966EF92214BBD200E6E891 /* AttributesStorage.swift */; }; @@ -29,17 +41,25 @@ F21771BE21E341CA0059896E /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2CC8ACA21CF8D8400A68CAC /* Dispatcher.swift */; }; F21771C021E344C10059896E /* SendReportRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21771BF21E344C10059896E /* SendReportRequest.swift */; }; F21771C121E344C10059896E /* SendReportRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21771BF21E344C10059896E /* SendReportRequest.swift */; }; + F21D302B224A18D60013B5D7 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21D302A224A18D50013B5D7 /* Store.swift */; }; + F21D302C224A18D60013B5D7 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = F21D302A224A18D50013B5D7 /* Store.swift */; }; + F229D7852239A172008EC851 /* BacktraceClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F229D7842239A172008EC851 /* BacktraceClientTests.swift */; }; + F229D7862239A172008EC851 /* BacktraceClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F229D7842239A172008EC851 /* BacktraceClientTests.swift */; }; + F229D7872239B888008EC851 /* test.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2846E2042238189A0035F98C /* test.txt */; }; + F229D7882239B889008EC851 /* test.txt in Resources */ = {isa = PBXBuildFile; fileRef = 2846E2042238189A0035F98C /* test.txt */; }; + F229D78C223A591F008EC851 /* UrlSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F229D789223A56ED008EC851 /* UrlSessionMock.swift */; }; + F229D78D223A5920008EC851 /* UrlSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F229D789223A56ED008EC851 /* UrlSessionMock.swift */; }; + F229D78F223A5F6A008EC851 /* BacktraceApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F229D78E223A5F6A008EC851 /* BacktraceApiTests.swift */; }; + F229D790223A5F6A008EC851 /* BacktraceApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F229D78E223A5F6A008EC851 /* BacktraceApiTests.swift */; }; F22EB87721BBD36800DEE94E /* BacktraceClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22EB87621BBD36800DEE94E /* BacktraceClient.swift */; }; F240531E21C5680600FC9394 /* CrashReporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F240531D21C5680600FC9394 /* CrashReporting.swift */; }; F240532121C578AA00FC9394 /* BacktraceLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F240532021C578AA00FC9394 /* BacktraceLogger.swift */; }; F259E4D62229A40C00F282C7 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = F259E4D52229A40C00F282C7 /* Result.swift */; }; F259E4D72229A41400F282C7 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = F259E4D52229A40C00F282C7 /* Result.swift */; }; - F259E4DB2229A72D00F282C7 /* AttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F259E4DA2229A72D00F282C7 /* AttributesTests.swift */; }; - F259E4DC2229A7E600F282C7 /* AttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F259E4DA2229A72D00F282C7 /* AttributesTests.swift */; }; + F259E4DB2229A72D00F282C7 /* AttributesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F259E4DA2229A72D00F282C7 /* AttributesProviderTests.swift */; }; + F259E4DC2229A7E600F282C7 /* AttributesProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F259E4DA2229A72D00F282C7 /* AttributesProviderTests.swift */; }; F259E4E22229C29A00F282C7 /* AttributesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F259E4E12229C29A00F282C7 /* AttributesProvider.swift */; }; F259E4E3222AD9F100F282C7 /* AttributesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F259E4E12229C29A00F282C7 /* AttributesProvider.swift */; }; - F25C0FC421C797D50083029C /* BacktraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F266B85421C77D8B00D14417 /* BacktraceTests.swift */; }; - F25C0FC521C797D60083029C /* BacktraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F266B85421C77D8B00D14417 /* BacktraceTests.swift */; }; F25C0FC621C79EF50083029C /* Backtrace_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C2FA5E21BBD26300934744 /* Backtrace_iOSTests.swift */; }; F25C0FC721C79EFB0083029C /* Backtrace_macOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F266B82B21C77B4D00D14417 /* Backtrace_macOSTests.swift */; }; F25F9E9721EE84AF00236E04 /* BacktraceReportStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F25F9E9621EE84AF00236E04 /* BacktraceReportStatus.swift */; }; @@ -50,8 +70,6 @@ F266B82221C77AC800D14417 /* Backtrace.h in Headers */ = {isa = PBXBuildFile; fileRef = F266B81421C77AC800D14417 /* Backtrace.h */; settings = {ATTRIBUTES = (Public, ); }; }; F266B83321C77B9600D14417 /* BacktraceClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = F22EB87621BBD36800DEE94E /* BacktraceClient.swift */; }; F266B83421C77B9600D14417 /* BacktraceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE3721BD7894007CFEFA /* BacktraceError.swift */; }; - F266B83521C77B9600D14417 /* BacktraceApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE3921BD78A9007CFEFA /* BacktraceApi.swift */; }; - F266B83621C77B9600D14417 /* CrashReporter+StringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE3B21BD78D8007CFEFA /* CrashReporter+StringConvertible.swift */; }; F266B83721C77B9600D14417 /* CrashReporting.swift in Sources */ = {isa = PBXBuildFile; fileRef = F240531D21C5680600FC9394 /* CrashReporting.swift */; }; F266B83821C77B9600D14417 /* BacktraceLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F240532021C578AA00FC9394 /* BacktraceLogger.swift */; }; F28162FA21EFD6AD00A12B7A /* BacktraceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = F28162F921EFD6AD00A12B7A /* BacktraceResponse.swift */; }; @@ -60,7 +78,6 @@ F282075921CEA31F0017367F /* BacktraceReport.swift in Sources */ = {isa = PBXBuildFile; fileRef = F282075721CEA31F0017367F /* BacktraceReport.swift */; }; F282075B21CEA37A0017367F /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F282075A21CEA37A0017367F /* Repository.swift */; }; F282075C21CEA37A0017367F /* Repository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F282075A21CEA37A0017367F /* Repository.swift */; }; - F282076021CEABBC0017367F /* BacktraceApiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F282075F21CEABBC0017367F /* BacktraceApiProtocol.swift */; }; F282076121CEABBC0017367F /* BacktraceApiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F282075F21CEABBC0017367F /* BacktraceApiProtocol.swift */; }; F28F164621E28441008E4B96 /* BacktraceReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F28F164521E28441008E4B96 /* BacktraceReporter.swift */; }; F28F164721E28441008E4B96 /* BacktraceReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F28F164521E28441008E4B96 /* BacktraceReporter.swift */; }; @@ -78,22 +95,44 @@ F29CD79221FCC25600216C59 /* BacktraceWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F29CD79021FCC25600216C59 /* BacktraceWatcher.swift */; }; F29CD79421FDD5E900216C59 /* BacktraceClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F29CD79321FDD5E900216C59 /* BacktraceClientDelegate.swift */; }; F29CD79621FDD9DC00216C59 /* BacktraceClientDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F29CD79321FDD5E900216C59 /* BacktraceClientDelegate.swift */; }; + F2AB636A2244243500939BC9 /* Quick+Throws.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63692244243500939BC9 /* Quick+Throws.swift */; }; + F2AB636B2244243500939BC9 /* Quick+Throws.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63692244243500939BC9 /* Quick+Throws.swift */; }; + F2AB636D22442B5100939BC9 /* DebuggerChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB636C22442B5100939BC9 /* DebuggerChecker.swift */; }; + F2AB636E22442B5100939BC9 /* DebuggerChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB636C22442B5100939BC9 /* DebuggerChecker.swift */; }; + F2AB63722246481100939BC9 /* AttachmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63712246481100939BC9 /* AttachmentTests.swift */; }; + F2AB63732246481100939BC9 /* AttachmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63712246481100939BC9 /* AttachmentTests.swift */; }; + F2AB63752246484100939BC9 /* WatcherRepositoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63742246484100939BC9 /* WatcherRepositoryMock.swift */; }; + F2AB63762246484100939BC9 /* WatcherRepositoryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63742246484100939BC9 /* WatcherRepositoryMock.swift */; }; + F2AB637922464AD000939BC9 /* BacktraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB637722464ACF00939BC9 /* BacktraceTests.swift */; }; + F2AB637A22464AD000939BC9 /* BacktraceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB637722464ACF00939BC9 /* BacktraceTests.swift */; }; + F2AB637B22464AD000939BC9 /* BacktraceApiMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB637822464AD000939BC9 /* BacktraceApiMock.swift */; }; + F2AB637C22464AD000939BC9 /* BacktraceApiMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB637822464AD000939BC9 /* BacktraceApiMock.swift */; }; + F2AB637E22464FD500939BC9 /* DebuggerCheckerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB637D22464FD500939BC9 /* DebuggerCheckerMock.swift */; }; + F2AB637F22464FD500939BC9 /* DebuggerCheckerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB637D22464FD500939BC9 /* DebuggerCheckerMock.swift */; }; + F2AB63812246E16400939BC9 /* ReportingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63802246E16400939BC9 /* ReportingPolicy.swift */; }; + F2AB63822246E16400939BC9 /* ReportingPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63802246E16400939BC9 /* ReportingPolicy.swift */; }; + F2AB63842246E1A000939BC9 /* ReportingPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63832246E1A000939BC9 /* ReportingPolicyTests.swift */; }; + F2AB63852246E1A000939BC9 /* ReportingPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63832246E1A000939BC9 /* ReportingPolicyTests.swift */; }; + F2AB63872247075100939BC9 /* DispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63862247075100939BC9 /* DispatcherTests.swift */; }; + F2AB63882247075100939BC9 /* DispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB63862247075100939BC9 /* DispatcherTests.swift */; }; + F2AB638A22470CD700939BC9 /* CrashReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB638922470CD700939BC9 /* CrashReporterTests.swift */; }; + F2AB638B22470CD800939BC9 /* CrashReporterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB638922470CD700939BC9 /* CrashReporterTests.swift */; }; + F2AB638D22470E3500939BC9 /* BacktraceFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB638C22470E3500939BC9 /* BacktraceFileManagerTests.swift */; }; + F2AB638E22470E3500939BC9 /* BacktraceFileManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AB638C22470E3500939BC9 /* BacktraceFileManagerTests.swift */; }; + F2AB639C22479A3500939BC9 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F2AB639A22479A3200939BC9 /* Model.xcdatamodeld */; }; + F2AB639D22479A3600939BC9 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F2AB639A22479A3200939BC9 /* Model.xcdatamodeld */; }; + F2AB639F22479F7E00939BC9 /* BacktraceApiProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F282075F21CEABBC0017367F /* BacktraceApiProtocol.swift */; }; + F2AB63A022479FDF00939BC9 /* BacktraceApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE3921BD78A9007CFEFA /* BacktraceApi.swift */; }; F2AFB5912225E5D000AAA1D7 /* Foundation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AFB5902225E5D000AAA1D7 /* Foundation+Extensions.swift */; }; F2AFB5922225E5D000AAA1D7 /* Foundation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AFB5902225E5D000AAA1D7 /* Foundation+Extensions.swift */; }; - F2AFB5942225E9D400AAA1D7 /* Annotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AFB5932225E9D400AAA1D7 /* Annotations.swift */; }; - F2AFB5952225E9D400AAA1D7 /* Annotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AFB5932225E9D400AAA1D7 /* Annotations.swift */; }; F2AFB59A22274E5400AAA1D7 /* BacktraceClientCustomizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AFB59922274E5400AAA1D7 /* BacktraceClientCustomizing.swift */; }; F2AFB59B22274E5400AAA1D7 /* BacktraceClientCustomizing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AFB59922274E5400AAA1D7 /* BacktraceClientCustomizing.swift */; }; F2AFB59D22274EDA00AAA1D7 /* Dispatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AFB59C22274EDA00AAA1D7 /* Dispatching.swift */; }; F2AFB59E22274EDA00AAA1D7 /* Dispatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2AFB59C22274EDA00AAA1D7 /* Dispatching.swift */; }; - F2C1514021F7D8E30014F1B3 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F2C1513D21F7D8E30014F1B3 /* Model.xcdatamodeld */; }; - F2C1514121F7D8E30014F1B3 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F2C1513D21F7D8E30014F1B3 /* Model.xcdatamodeld */; }; F2C1514221F7D8E30014F1B3 /* PersistentRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2C1513F21F7D8E30014F1B3 /* PersistentRepository.swift */; }; F2C2FA5A21BBD26300934744 /* Backtrace.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2C2FA5021BBD26300934744 /* Backtrace.framework */; }; F2C2FA6121BBD26300934744 /* Backtrace.h in Headers */ = {isa = PBXBuildFile; fileRef = F2C2FA5321BBD26300934744 /* Backtrace.h */; settings = {ATTRIBUTES = (Public, ); }; }; F2CC8ACB21CF8D8400A68CAC /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2CC8ACA21CF8D8400A68CAC /* Dispatcher.swift */; }; - F2D7121E21F10461002D2A26 /* BacktraceNetworkClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D7121D21F10461002D2A26 /* BacktraceNetworkClientMock.swift */; }; - F2D7121F21F10461002D2A26 /* BacktraceNetworkClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D7121D21F10461002D2A26 /* BacktraceNetworkClientMock.swift */; }; F2D7122121F10C45002D2A26 /* BacktraceClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D7122021F10C45002D2A26 /* BacktraceClientConfiguration.swift */; }; F2D7122221F10C45002D2A26 /* BacktraceClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D7122021F10C45002D2A26 /* BacktraceClientConfiguration.swift */; }; F2D7122421F10E78002D2A26 /* BacktraceCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D7122321F10E78002D2A26 /* BacktraceCredentials.swift */; }; @@ -115,7 +154,6 @@ F2D8BE3121BC5F98007CFEFA /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE3021BC5F98007CFEFA /* main.m */; }; F2D8BE3821BD7894007CFEFA /* BacktraceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE3721BD7894007CFEFA /* BacktraceError.swift */; }; F2D8BE3A21BD78A9007CFEFA /* BacktraceApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE3921BD78A9007CFEFA /* BacktraceApi.swift */; }; - F2D8BE3C21BD78D8007CFEFA /* CrashReporter+StringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE3B21BD78D8007CFEFA /* CrashReporter+StringConvertible.swift */; }; F2D8BE4621BDA7CF007CFEFA /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE4521BDA7CF007CFEFA /* AppDelegate.m */; }; F2D8BE4921BDA7CF007CFEFA /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F2D8BE4821BDA7CF007CFEFA /* ViewController.m */; }; F2D8BE4B21BDA7D0007CFEFA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F2D8BE4A21BDA7D0007CFEFA /* Assets.xcassets */; }; @@ -200,6 +238,14 @@ /* Begin PBXFileReference section */ 080261C779321096E82CBEA0 /* Pods-Backtrace-iOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Backtrace-iOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Backtrace-iOS/Pods-Backtrace-iOS.release.xcconfig"; sourceTree = ""; }; 0CAEB5685940F7A87F1D5269 /* Pods-Backtrace-macOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Backtrace-macOS.release.xcconfig"; path = "Pods/Target Support Files/Pods-Backtrace-macOS/Pods-Backtrace-macOS.release.xcconfig"; sourceTree = ""; }; + 282C85E6223FD8E70014FE75 /* BacktraceCrashExceptionApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BacktraceCrashExceptionApplication.swift; sourceTree = ""; }; + 282C85E822419BB10014FE75 /* BacktraceWatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceWatcherTests.swift; sourceTree = ""; }; + 2846E1F7222F1DE50035F98C /* NetworkReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkReachability.swift; sourceTree = ""; }; + 2846E1FA222F2C850035F98C /* BluetoothStatusListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothStatusListener.swift; sourceTree = ""; }; + 2846E1FD223070CB0035F98C /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; + 2846E200223818550035F98C /* test.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = test.txt; sourceTree = ""; }; + 2846E202223818920035F98C /* test.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = test.txt; sourceTree = ""; }; + 2846E2042238189A0035F98C /* test.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = test.txt; sourceTree = ""; }; 28614F9D220B6D7C00D35EFB /* DefaultAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultAttributes.swift; sourceTree = ""; }; 28966EF92214BBD200E6E891 /* AttributesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributesStorage.swift; sourceTree = ""; }; 28AC773B21FA5A8400FED661 /* BacktraceDatabaseSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceDatabaseSettings.swift; sourceTree = ""; }; @@ -226,11 +272,15 @@ F21211A4222348AC000B3692 /* CrashReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporter.swift; sourceTree = ""; }; F21211A7222348C2000B3692 /* SignalContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalContext.swift; sourceTree = ""; }; F21771BF21E344C10059896E /* SendReportRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendReportRequest.swift; sourceTree = ""; }; + F21D302A224A18D50013B5D7 /* Store.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; + F229D7842239A172008EC851 /* BacktraceClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceClientTests.swift; sourceTree = ""; }; + F229D789223A56ED008EC851 /* UrlSessionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlSessionMock.swift; sourceTree = ""; }; + F229D78E223A5F6A008EC851 /* BacktraceApiTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceApiTests.swift; sourceTree = ""; }; F22EB87621BBD36800DEE94E /* BacktraceClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceClient.swift; sourceTree = ""; }; F240531D21C5680600FC9394 /* CrashReporting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporting.swift; sourceTree = ""; }; F240532021C578AA00FC9394 /* BacktraceLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceLogger.swift; sourceTree = ""; }; F259E4D52229A40C00F282C7 /* Result.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; - F259E4DA2229A72D00F282C7 /* AttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributesTests.swift; sourceTree = ""; }; + F259E4DA2229A72D00F282C7 /* AttributesProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributesProviderTests.swift; sourceTree = ""; }; F259E4E12229C29A00F282C7 /* AttributesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributesProvider.swift; sourceTree = ""; }; F25F9E9621EE84AF00236E04 /* BacktraceReportStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceReportStatus.swift; sourceTree = ""; }; F25F9E9921EE84EA00236E04 /* BacktraceResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceResult.swift; sourceTree = ""; }; @@ -240,7 +290,6 @@ F266B81A21C77AC800D14417 /* Backtrace-macOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Backtrace-macOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; F266B82B21C77B4D00D14417 /* Backtrace_macOSTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Backtrace_macOSTests.swift; sourceTree = ""; }; F266B82C21C77B4D00D14417 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F266B85421C77D8B00D14417 /* BacktraceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceTests.swift; sourceTree = ""; }; F28162F921EFD6AD00A12B7A /* BacktraceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceResponse.swift; sourceTree = ""; }; F282075721CEA31F0017367F /* BacktraceReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceReport.swift; sourceTree = ""; }; F282075A21CEA37A0017367F /* Repository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Repository.swift; sourceTree = ""; }; @@ -253,11 +302,23 @@ F29CD78C21FC6BC700216C59 /* BacktraceFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceFileManager.swift; sourceTree = ""; }; F29CD79021FCC25600216C59 /* BacktraceWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceWatcher.swift; sourceTree = ""; }; F29CD79321FDD5E900216C59 /* BacktraceClientDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceClientDelegate.swift; sourceTree = ""; }; + F2AB63692244243500939BC9 /* Quick+Throws.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Quick+Throws.swift"; sourceTree = ""; }; + F2AB636C22442B5100939BC9 /* DebuggerChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebuggerChecker.swift; sourceTree = ""; }; + F2AB63712246481100939BC9 /* AttachmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentTests.swift; sourceTree = ""; }; + F2AB63742246484100939BC9 /* WatcherRepositoryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatcherRepositoryMock.swift; sourceTree = ""; }; + F2AB637722464ACF00939BC9 /* BacktraceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BacktraceTests.swift; sourceTree = ""; }; + F2AB637822464AD000939BC9 /* BacktraceApiMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BacktraceApiMock.swift; sourceTree = ""; }; + F2AB637D22464FD500939BC9 /* DebuggerCheckerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebuggerCheckerMock.swift; sourceTree = ""; }; + F2AB63802246E16400939BC9 /* ReportingPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportingPolicy.swift; sourceTree = ""; }; + F2AB63832246E1A000939BC9 /* ReportingPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportingPolicyTests.swift; sourceTree = ""; }; + F2AB63862247075100939BC9 /* DispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatcherTests.swift; sourceTree = ""; }; + F2AB638922470CD700939BC9 /* CrashReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashReporterTests.swift; sourceTree = ""; }; + F2AB638C22470E3500939BC9 /* BacktraceFileManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceFileManagerTests.swift; sourceTree = ""; }; + F2AB639B22479A3200939BC9 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; F2AFB5902225E5D000AAA1D7 /* Foundation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Foundation+Extensions.swift"; sourceTree = ""; }; F2AFB5932225E9D400AAA1D7 /* Annotations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Annotations.swift; sourceTree = ""; }; F2AFB59922274E5400AAA1D7 /* BacktraceClientCustomizing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceClientCustomizing.swift; sourceTree = ""; }; F2AFB59C22274EDA00AAA1D7 /* Dispatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dispatching.swift; sourceTree = ""; }; - F2C1513E21F7D8E30014F1B3 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; F2C1513F21F7D8E30014F1B3 /* PersistentRepository.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistentRepository.swift; sourceTree = ""; }; F2C2FA5021BBD26300934744 /* Backtrace.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Backtrace.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F2C2FA5321BBD26300934744 /* Backtrace.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Backtrace.h; sourceTree = ""; }; @@ -266,7 +327,6 @@ F2C2FA5E21BBD26300934744 /* Backtrace_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backtrace_iOSTests.swift; sourceTree = ""; }; F2C2FA6021BBD26300934744 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F2CC8ACA21CF8D8400A68CAC /* Dispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dispatcher.swift; sourceTree = ""; }; - F2D7121D21F10461002D2A26 /* BacktraceNetworkClientMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceNetworkClientMock.swift; sourceTree = ""; }; F2D7122021F10C45002D2A26 /* BacktraceClientConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceClientConfiguration.swift; sourceTree = ""; }; F2D7122321F10E78002D2A26 /* BacktraceCredentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceCredentials.swift; sourceTree = ""; }; F2D8BE0421BC065E007CFEFA /* Example-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -288,7 +348,6 @@ F2D8BE3021BC5F98007CFEFA /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; F2D8BE3721BD7894007CFEFA /* BacktraceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceError.swift; sourceTree = ""; }; F2D8BE3921BD78A9007CFEFA /* BacktraceApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BacktraceApi.swift; sourceTree = ""; }; - F2D8BE3B21BD78D8007CFEFA /* CrashReporter+StringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CrashReporter+StringConvertible.swift"; sourceTree = ""; }; F2D8BE4221BDA7CF007CFEFA /* Example-macOS-ObjC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Example-macOS-ObjC.app"; sourceTree = BUILT_PRODUCTS_DIR; }; F2D8BE4421BDA7CF007CFEFA /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; F2D8BE4521BDA7CF007CFEFA /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -374,6 +433,8 @@ 28966EF92214BBD200E6E891 /* AttributesStorage.swift */, F2AFB5932225E9D400AAA1D7 /* Annotations.swift */, F259E4E12229C29A00F282C7 /* AttributesProvider.swift */, + 2846E1F7222F1DE50035F98C /* NetworkReachability.swift */, + 2846E1FA222F2C850035F98C /* BluetoothStatusListener.swift */, ); path = Attributes; sourceTree = ""; @@ -402,6 +463,7 @@ F266B81321C77AC800D14417 /* Backtrace-macOS */ = { isa = PBXGroup; children = ( + 282C85E6223FD8E70014FE75 /* BacktraceCrashExceptionApplication.swift */, F266B81421C77AC800D14417 /* Backtrace.h */, F266B81521C77AC800D14417 /* Info.plist */, ); @@ -420,10 +482,19 @@ F266B85321C77D5D00D14417 /* Tests */ = { isa = PBXGroup; children = ( - F266B85421C77D8B00D14417 /* BacktraceTests.swift */, - F2D7121D21F10461002D2A26 /* BacktraceNetworkClientMock.swift */, + F2AB6370224647F000939BC9 /* Helpers */, + F2AB636F224647DE00939BC9 /* Mocks */, + F2AB637722464ACF00939BC9 /* BacktraceTests.swift */, F29CD78721FC5F6500216C59 /* BacktraceDatabaseTests.swift */, - F259E4DA2229A72D00F282C7 /* AttributesTests.swift */, + F259E4DA2229A72D00F282C7 /* AttributesProviderTests.swift */, + F229D7842239A172008EC851 /* BacktraceClientTests.swift */, + F229D78E223A5F6A008EC851 /* BacktraceApiTests.swift */, + 282C85E822419BB10014FE75 /* BacktraceWatcherTests.swift */, + F2AB63712246481100939BC9 /* AttachmentTests.swift */, + F2AB63832246E1A000939BC9 /* ReportingPolicyTests.swift */, + F2AB63862247075100939BC9 /* DispatcherTests.swift */, + F2AB638922470CD700939BC9 /* CrashReporterTests.swift */, + F2AB638C22470E3500939BC9 /* BacktraceFileManagerTests.swift */, ); path = Tests; sourceTree = ""; @@ -433,7 +504,6 @@ children = ( F28F164E21E28823008E4B96 /* Model */, F2C1513F21F7D8E30014F1B3 /* PersistentRepository.swift */, - F2C1513D21F7D8E30014F1B3 /* Model.xcdatamodeld */, F29CD78C21FC6BC700216C59 /* BacktraceFileManager.swift */, ); path = Repository; @@ -453,7 +523,6 @@ F282076521CEAC230017367F /* Crash Reporting */ = { isa = PBXGroup; children = ( - F2D8BE3B21BD78D8007CFEFA /* CrashReporter+StringConvertible.swift */, F21211A4222348AC000B3692 /* CrashReporter.swift */, ); path = "Crash Reporting"; @@ -472,6 +541,7 @@ isa = PBXGroup; children = ( F282075721CEA31F0017367F /* BacktraceReport.swift */, + 2846E1FD223070CB0035F98C /* Attachment.swift */, ); path = Model; sourceTree = ""; @@ -501,6 +571,33 @@ path = Watcher; sourceTree = ""; }; + F2AB636F224647DE00939BC9 /* Mocks */ = { + isa = PBXGroup; + children = ( + F2AB637822464AD000939BC9 /* BacktraceApiMock.swift */, + F2AB63742246484100939BC9 /* WatcherRepositoryMock.swift */, + F229D789223A56ED008EC851 /* UrlSessionMock.swift */, + F2AB637D22464FD500939BC9 /* DebuggerCheckerMock.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + F2AB6370224647F000939BC9 /* Helpers */ = { + isa = PBXGroup; + children = ( + F2AB63692244243500939BC9 /* Quick+Throws.swift */, + ); + path = Helpers; + sourceTree = ""; + }; + F2AB6395224799A200939BC9 /* Resources */ = { + isa = PBXGroup; + children = ( + F2AB639A22479A3200939BC9 /* Model.xcdatamodeld */, + ); + path = Resources; + sourceTree = ""; + }; F2AFB59622274E1400AAA1D7 /* Public */ = { isa = PBXGroup; children = ( @@ -520,6 +617,7 @@ F2AFB59722274E1C00AAA1D7 /* Features */ = { isa = PBXGroup; children = ( + F2AB6395224799A200939BC9 /* Resources */, F28F165621E2A0BD008E4B96 /* Extensions */, F2CC8AC921CF8D6C00A68CAC /* Dispatching */, F29CD78F21FCC23800216C59 /* Watcher */, @@ -536,6 +634,8 @@ F2AFB5A022274F1000AAA1D7 /* Internal */ = { isa = PBXGroup; children = ( + F21D302A224A18D50013B5D7 /* Store.swift */, + F2AB636C22442B5100939BC9 /* DebuggerChecker.swift */, F240531D21C5680600FC9394 /* CrashReporting.swift */, F2AFB59C22274EDA00AAA1D7 /* Dispatching.swift */, F282075F21CEABBC0017367F /* BacktraceApiProtocol.swift */, @@ -544,6 +644,7 @@ F21211A7222348C2000B3692 /* SignalContext.swift */, F259E4D52229A40C00F282C7 /* Result.swift */, F25F9E9621EE84AF00236E04 /* BacktraceReportStatus.swift */, + F2AB63802246E16400939BC9 /* ReportingPolicy.swift */, ); path = Internal; sourceTree = ""; @@ -640,6 +741,7 @@ F2D8BE0D21BC065F007CFEFA /* Assets.xcassets */, F2D8BE0F21BC065F007CFEFA /* LaunchScreen.storyboard */, F2D8BE1221BC065F007CFEFA /* Info.plist */, + 2846E200223818550035F98C /* test.txt */, ); path = "Example-iOS"; sourceTree = ""; @@ -656,6 +758,7 @@ F2D8BE2C21BC5F98007CFEFA /* LaunchScreen.storyboard */, F2D8BE2F21BC5F98007CFEFA /* Info.plist */, F2D8BE3021BC5F98007CFEFA /* main.m */, + 2846E202223818920035F98C /* test.txt */, ); path = "Example-iOS-ObjC"; sourceTree = ""; @@ -672,6 +775,7 @@ F2D8BE4F21BDA7D0007CFEFA /* Info.plist */, F2D8BE5021BDA7D0007CFEFA /* main.m */, F2D8BE5221BDA7D0007CFEFA /* Example_macOS_ObjC.entitlements */, + 2846E2042238189A0035F98C /* test.txt */, ); path = "Example-macOS-ObjC"; sourceTree = ""; @@ -910,6 +1014,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F229D7882239B889008EC851 /* test.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -924,6 +1029,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + F229D7872239B888008EC851 /* test.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -933,6 +1039,7 @@ files = ( F2D8BE1121BC065F007CFEFA /* LaunchScreen.storyboard in Resources */, F2D8BE0E21BC065F007CFEFA /* Assets.xcassets in Resources */, + 2846E201223818550035F98C /* test.txt in Resources */, F2D8BE0C21BC065E007CFEFA /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -943,6 +1050,7 @@ files = ( F2D8BE2E21BC5F98007CFEFA /* LaunchScreen.storyboard in Resources */, F2D8BE2B21BC5F98007CFEFA /* Assets.xcassets in Resources */, + 2846E203223818920035F98C /* test.txt in Resources */, F2D8BE2921BC5F97007CFEFA /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -952,6 +1060,7 @@ buildActionMask = 2147483647; files = ( F2D8BE4B21BDA7D0007CFEFA /* Assets.xcassets in Resources */, + 2846E2052238189A0035F98C /* test.txt in Resources */, F2D8BE4E21BDA7D0007CFEFA /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1274,6 +1383,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F2AB63A022479FDF00939BC9 /* BacktraceApi.swift in Sources */, F266B83721C77B9600D14417 /* CrashReporting.swift in Sources */, F28162FB21EFD6AD00A12B7A /* BacktraceResponse.swift in Sources */, F29CD79621FDD9DC00216C59 /* BacktraceClientDelegate.swift in Sources */, @@ -1281,27 +1391,29 @@ 28AC773D21FA5A8900FED661 /* BacktraceDatabaseSettings.swift in Sources */, F266B83821C77B9600D14417 /* BacktraceLogger.swift in Sources */, F2AFB59E22274EDA00AAA1D7 /* Dispatching.swift in Sources */, + 2846E1F9222F1DE60035F98C /* NetworkReachability.swift in Sources */, F21211A9222348C2000B3692 /* SignalContext.swift in Sources */, + F2AB639D22479A3600939BC9 /* Model.xcdatamodeld in Sources */, F259E4E3222AD9F100F282C7 /* AttributesProvider.swift in Sources */, F266B83321C77B9600D14417 /* BacktraceClient.swift in Sources */, F25F9E9821EE84AF00236E04 /* BacktraceReportStatus.swift in Sources */, F2AFB5922225E5D000AAA1D7 /* Foundation+Extensions.swift in Sources */, + 2846E1FC222F2C850035F98C /* BluetoothStatusListener.swift in Sources */, 28AC7740220A2A3300FED661 /* MultipartRequestType.swift in Sources */, - F2AFB5952225E9D400AAA1D7 /* Annotations.swift in Sources */, F21771C121E344C10059896E /* SendReportRequest.swift in Sources */, + F2AB63822246E16400939BC9 /* ReportingPolicy.swift in Sources */, F25F9E9B21EE84EA00236E04 /* BacktraceResult.swift in Sources */, F282076121CEABBC0017367F /* BacktraceApiProtocol.swift in Sources */, + F21D302C224A18D60013B5D7 /* Store.swift in Sources */, F266B83421C77B9600D14417 /* BacktraceError.swift in Sources */, 28966EFB2214BBDC00E6E891 /* AttributesStorage.swift in Sources */, F259E4D72229A41400F282C7 /* Result.swift in Sources */, - F2C1514121F7D8E30014F1B3 /* Model.xcdatamodeld in Sources */, + 2846E1FF223070CB0035F98C /* Attachment.swift in Sources */, F2D7122521F10E78002D2A26 /* BacktraceCredentials.swift in Sources */, F28F164721E28441008E4B96 /* BacktraceReporter.swift in Sources */, F21211A6222348AC000B3692 /* CrashReporter.swift in Sources */, F282075921CEA31F0017367F /* BacktraceReport.swift in Sources */, 28AC773A21F8C29800FED661 /* PersistentRepository.swift in Sources */, - F266B83521C77B9600D14417 /* BacktraceApi.swift in Sources */, - F266B83621C77B9600D14417 /* CrashReporter+StringConvertible.swift in Sources */, 28614F9F220B900300D35EFB /* DefaultAttributes.swift in Sources */, F282075C21CEA37A0017367F /* Repository.swift in Sources */, F28F165221E2A08F008E4B96 /* HttpMethod.swift in Sources */, @@ -1309,8 +1421,10 @@ F2D7122221F10C45002D2A26 /* BacktraceClientConfiguration.swift in Sources */, F28F165521E2A097008E4B96 /* RequestType.swift in Sources */, F29CD79221FCC25600216C59 /* BacktraceWatcher.swift in Sources */, + 282C85E7223FD8E70014FE75 /* BacktraceCrashExceptionApplication.swift in Sources */, F2AFB59B22274E5400AAA1D7 /* BacktraceClientCustomizing.swift in Sources */, F28F165921E2A0DA008E4B96 /* URLSession+Sync.swift in Sources */, + F2AB636E22442B5100939BC9 /* DebuggerChecker.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1318,11 +1432,23 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F2AB63852246E1A000939BC9 /* ReportingPolicyTests.swift in Sources */, + F2AB637A22464AD000939BC9 /* BacktraceTests.swift in Sources */, + F2AB637F22464FD500939BC9 /* DebuggerCheckerMock.swift in Sources */, + F2AB638E22470E3500939BC9 /* BacktraceFileManagerTests.swift in Sources */, + F2AB636B2244243500939BC9 /* Quick+Throws.swift in Sources */, F25C0FC721C79EFB0083029C /* Backtrace_macOSTests.swift in Sources */, - F25C0FC521C797D60083029C /* BacktraceTests.swift in Sources */, - F2D7121F21F10461002D2A26 /* BacktraceNetworkClientMock.swift in Sources */, + F2AB63762246484100939BC9 /* WatcherRepositoryMock.swift in Sources */, + F229D7862239A172008EC851 /* BacktraceClientTests.swift in Sources */, + 282C85EB22419C600014FE75 /* BacktraceWatcherTests.swift in Sources */, + F229D78C223A591F008EC851 /* UrlSessionMock.swift in Sources */, F29CD78B21FC5F8600216C59 /* BacktraceDatabaseTests.swift in Sources */, - F259E4DC2229A7E600F282C7 /* AttributesTests.swift in Sources */, + F2AB638B22470CD800939BC9 /* CrashReporterTests.swift in Sources */, + F2AB637C22464AD000939BC9 /* BacktraceApiMock.swift in Sources */, + F2AB63882247075100939BC9 /* DispatcherTests.swift in Sources */, + F259E4DC2229A7E600F282C7 /* AttributesProviderTests.swift in Sources */, + F2AB63732246481100939BC9 /* AttachmentTests.swift in Sources */, + F229D790223A5F6A008EC851 /* BacktraceApiTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1330,6 +1456,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F2AB639F22479F7E00939BC9 /* BacktraceApiProtocol.swift in Sources */, F240531E21C5680600FC9394 /* CrashReporting.swift in Sources */, F28162FA21EFD6AD00A12B7A /* BacktraceResponse.swift in Sources */, F29CD79421FDD5E900216C59 /* BacktraceClientDelegate.swift in Sources */, @@ -1337,34 +1464,37 @@ 28AC773C21FA5A8400FED661 /* BacktraceDatabaseSettings.swift in Sources */, F2CC8ACB21CF8D8400A68CAC /* Dispatcher.swift in Sources */, F2AFB59D22274EDA00AAA1D7 /* Dispatching.swift in Sources */, + 2846E1F8222F1DE60035F98C /* NetworkReachability.swift in Sources */, F21211A8222348C2000B3692 /* SignalContext.swift in Sources */, F259E4E22229C29A00F282C7 /* AttributesProvider.swift in Sources */, + F2AB63812246E16400939BC9 /* ReportingPolicy.swift in Sources */, F25F9E9721EE84AF00236E04 /* BacktraceReportStatus.swift in Sources */, F21771C021E344C10059896E /* SendReportRequest.swift in Sources */, F2AFB5912225E5D000AAA1D7 /* Foundation+Extensions.swift in Sources */, + 2846E1FB222F2C850035F98C /* BluetoothStatusListener.swift in Sources */, 28AC773F220A2A2900FED661 /* MultipartRequestType.swift in Sources */, - F2AFB5942225E9D400AAA1D7 /* Annotations.swift in Sources */, F25F9E9A21EE84EA00236E04 /* BacktraceResult.swift in Sources */, + F2AB639C22479A3500939BC9 /* Model.xcdatamodeld in Sources */, F28F165121E2A08F008E4B96 /* HttpMethod.swift in Sources */, F22EB87721BBD36800DEE94E /* BacktraceClient.swift in Sources */, - F2C1514021F7D8E30014F1B3 /* Model.xcdatamodeld in Sources */, 28966EFA2214BBD200E6E891 /* AttributesStorage.swift in Sources */, F259E4D62229A40C00F282C7 /* Result.swift in Sources */, + 2846E1FE223070CB0035F98C /* Attachment.swift in Sources */, F2D7122421F10E78002D2A26 /* BacktraceCredentials.swift in Sources */, F28F165421E2A097008E4B96 /* RequestType.swift in Sources */, F28F164621E28441008E4B96 /* BacktraceReporter.swift in Sources */, F21211A5222348AC000B3692 /* CrashReporter.swift in Sources */, F2C1514221F7D8E30014F1B3 /* PersistentRepository.swift in Sources */, - F282076021CEABBC0017367F /* BacktraceApiProtocol.swift in Sources */, F2D8BE3821BD7894007CFEFA /* BacktraceError.swift in Sources */, F282075821CEA31F0017367F /* BacktraceReport.swift in Sources */, F28F165821E2A0DA008E4B96 /* URLSession+Sync.swift in Sources */, 28614F9E220B6D7C00D35EFB /* DefaultAttributes.swift in Sources */, F2D8BE3A21BD78A9007CFEFA /* BacktraceApi.swift in Sources */, + F2AB636D22442B5100939BC9 /* DebuggerChecker.swift in Sources */, F2D7122121F10C45002D2A26 /* BacktraceClientConfiguration.swift in Sources */, F29CD78D21FC6BC700216C59 /* BacktraceFileManager.swift in Sources */, - F2D8BE3C21BD78D8007CFEFA /* CrashReporter+StringConvertible.swift in Sources */, F29CD79121FCC25600216C59 /* BacktraceWatcher.swift in Sources */, + F21D302B224A18D60013B5D7 /* Store.swift in Sources */, F2AFB59A22274E5400AAA1D7 /* BacktraceClientCustomizing.swift in Sources */, F282075B21CEA37A0017367F /* Repository.swift in Sources */, ); @@ -1374,11 +1504,23 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F2AB63842246E1A000939BC9 /* ReportingPolicyTests.swift in Sources */, + F2AB637922464AD000939BC9 /* BacktraceTests.swift in Sources */, + F2AB637E22464FD500939BC9 /* DebuggerCheckerMock.swift in Sources */, + F2AB638D22470E3500939BC9 /* BacktraceFileManagerTests.swift in Sources */, + F2AB636A2244243500939BC9 /* Quick+Throws.swift in Sources */, F25C0FC621C79EF50083029C /* Backtrace_iOSTests.swift in Sources */, - F25C0FC421C797D50083029C /* BacktraceTests.swift in Sources */, - F2D7121E21F10461002D2A26 /* BacktraceNetworkClientMock.swift in Sources */, + F2AB63752246484100939BC9 /* WatcherRepositoryMock.swift in Sources */, + F229D7852239A172008EC851 /* BacktraceClientTests.swift in Sources */, + 282C85EA22419C560014FE75 /* BacktraceWatcherTests.swift in Sources */, + F229D78D223A5920008EC851 /* UrlSessionMock.swift in Sources */, F29CD78A21FC5F8500216C59 /* BacktraceDatabaseTests.swift in Sources */, - F259E4DB2229A72D00F282C7 /* AttributesTests.swift in Sources */, + F2AB638A22470CD700939BC9 /* CrashReporterTests.swift in Sources */, + F2AB637B22464AD000939BC9 /* BacktraceApiMock.swift in Sources */, + F2AB63872247075100939BC9 /* DispatcherTests.swift in Sources */, + F259E4DB2229A72D00F282C7 /* AttributesProviderTests.swift in Sources */, + F2AB63722246481100939BC9 /* AttachmentTests.swift in Sources */, + F229D78F223A5F6A008EC851 /* BacktraceApiTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2616,12 +2758,12 @@ /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ - F2C1513D21F7D8E30014F1B3 /* Model.xcdatamodeld */ = { + F2AB639A22479A3200939BC9 /* Model.xcdatamodeld */ = { isa = XCVersionGroup; children = ( - F2C1513E21F7D8E30014F1B3 /* Model.xcdatamodel */, + F2AB639B22479A3200939BC9 /* Model.xcdatamodel */, ); - currentVersion = F2C1513E21F7D8E30014F1B3 /* Model.xcdatamodel */; + currentVersion = F2AB639B22479A3200939BC9 /* Model.xcdatamodel */; path = Model.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Backtrace.xcodeproj/xcshareddata/xcschemes/Backtrace-iOS.xcscheme b/Backtrace.xcodeproj/xcshareddata/xcschemes/Backtrace-iOS.xcscheme index 80093780..b07ecb29 100644 --- a/Backtrace.xcodeproj/xcshareddata/xcschemes/Backtrace-iOS.xcscheme +++ b/Backtrace.xcodeproj/xcshareddata/xcschemes/Backtrace-iOS.xcscheme @@ -26,7 +26,18 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + diff --git a/Example-iOS-ObjC/AppDelegate.m b/Example-iOS-ObjC/AppDelegate.m index dcf66199..7360e64e 100644 --- a/Example-iOS-ObjC/AppDelegate.m +++ b/Example-iOS-ObjC/AppDelegate.m @@ -11,9 +11,19 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( BacktraceCredentials *credentials = [[BacktraceCredentials alloc] initWithEndpoint: [NSURL URLWithString: @"https://backtrace.io"] token: @"token"]; - BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc] initWithCredentials: credentials - dbSettings: [BacktraceDatabaseSettings new] - reportsPerMin: 3]; + BacktraceDatabaseSettings *backtraceDatabaseSettings = [[BacktraceDatabaseSettings alloc] init]; + backtraceDatabaseSettings.maxRecordCount = 1000; + backtraceDatabaseSettings.maxDatabaseSize = 10; + backtraceDatabaseSettings.retryInterval = 5; + backtraceDatabaseSettings.retryLimit = 3; + backtraceDatabaseSettings.retryBehaviour = RetryBehaviourInterval; + backtraceDatabaseSettings.retryOrder = RetryOderStack; + + BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc] + initWithCredentials: credentials + dbSettings: backtraceDatabaseSettings + reportsPerMin: 3 + allowsAttachingDebugger: TRUE]; BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil]; BacktraceClient.shared.delegate = self; @@ -22,7 +32,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( NSArray *array = @[]; NSObject *object = array[1]; // will throw exception } @catch (NSException *exception) { - [[BacktraceClient shared] sendWithException: exception completion:^(BacktraceResult * _Nonnull result) { + NSArray *paths = @[[[NSBundle mainBundle] pathForResource: @"test" ofType: @"txt"]]; + [[BacktraceClient shared] sendWithAttachmentPaths: paths completion: ^(BacktraceResult * _Nonnull result) { NSLog(@"%@", result); }]; } @finally { @@ -31,7 +42,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( //sending NSError NSError *error = [NSError errorWithDomain: @"backtrace.domain" code: 100 userInfo: @{}]; - [[BacktraceClient shared] sendWithCompletion:^(BacktraceResult * _Nonnull result) { + NSArray *paths = @[[[NSBundle mainBundle] pathForResource: @"test" ofType: @"txt"]]; + [[BacktraceClient shared] sendWithAttachmentPaths: paths completion: ^(BacktraceResult * _Nonnull result) { NSLog(@"%@", result); }]; @@ -40,6 +52,9 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( #pragma mark - BacktraceClientDelegate - (BacktraceReport *)willSend:(BacktraceReport *)report { + NSMutableDictionary *dict = [report.attributes mutableCopy]; + [dict setObject: @"just before send" forKey: @"added"]; + report.attributes = dict; return report; } diff --git a/Example-iOS-ObjC/ViewController.m b/Example-iOS-ObjC/ViewController.m index ca669029..e2c9371d 100644 --- a/Example-iOS-ObjC/ViewController.m +++ b/Example-iOS-ObjC/ViewController.m @@ -13,7 +13,8 @@ - (void)viewDidLoad { } - (IBAction) liveReportAction: (id) sender { - [[BacktraceClient shared] sendWithCompletion:^(BacktraceResult * _Nonnull result) { + NSArray *paths = @[@"/home/test.txt"]; + [[BacktraceClient shared] sendWithAttachmentPaths:paths completion:^(BacktraceResult * _Nonnull result) { NSLog(@"%@", result.message); }]; } diff --git a/Example-iOS-ObjC/test.txt b/Example-iOS-ObjC/test.txt new file mode 100644 index 00000000..4d378346 --- /dev/null +++ b/Example-iOS-ObjC/test.txt @@ -0,0 +1,3 @@ +test from iOS simulator +test Example-iOS-ObjC +backtrace sample text file diff --git a/Example-iOS/AppDelegate.swift b/Example-iOS/AppDelegate.swift index 556700e2..b80303ed 100644 --- a/Example-iOS/AppDelegate.swift +++ b/Example-iOS/AppDelegate.swift @@ -18,16 +18,27 @@ class AppDelegate: UIResponder, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: "https://backtrace.io")!, token: "token") - let configuration = BacktraceClientConfiguration(credentials: backtraceCredentials) + let backtraceDatabaseSettings = BacktraceDatabaseSettings() + backtraceDatabaseSettings.maxRecordCount = 1000 + backtraceDatabaseSettings.maxDatabaseSize = 10 + backtraceDatabaseSettings.retryInterval = 5 + backtraceDatabaseSettings.retryLimit = 3 + backtraceDatabaseSettings.retryBehaviour = RetryBehaviour.interval + backtraceDatabaseSettings.retryOrder = RetryOder.queue + let backtraceConfiguration = BacktraceClientConfiguration(credentials: backtraceCredentials, + dbSettings: backtraceDatabaseSettings, + reportsPerMin: 10, + allowsAttachingDebugger: true) - BacktraceClient.shared = try? BacktraceClient(configuration: configuration) + BacktraceClient.shared = try? BacktraceClient(configuration: backtraceConfiguration) BacktraceClient.shared?.delegate = self BacktraceClient.shared?.userAttributes = ["foo": "bar", "testing": true] do { try throwingFunc() } catch { - BacktraceClient.shared?.send { (result) in + let filePath = Bundle.main.path(forResource: "test", ofType: "txt")! + BacktraceClient.shared?.send(attachmentPaths: [filePath]) { (result) in print(result) } } @@ -38,6 +49,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { extension AppDelegate: BacktraceClientDelegate { func willSend(_ report: BacktraceReport) -> (BacktraceReport) { + report.attributes["added"] = "just before send" return report } diff --git a/Example-iOS/ViewController.swift b/Example-iOS/ViewController.swift index dabe4280..7dbafa5d 100644 --- a/Example-iOS/ViewController.swift +++ b/Example-iOS/ViewController.swift @@ -10,9 +10,16 @@ class ViewController: UIViewController { } @IBAction func liveReportAction(_ sender: Any) { - BacktraceClient.shared?.send { (result) in + // Sned autogenerated report +// BacktraceClient.shared?.send(attachmentPaths: [], completion: { (result) in +// print(result) +// }) + + // Send NSException + let exception = NSException(name: NSExceptionName.characterConversionException, reason: "cusotm reason", userInfo: ["testUserInfo": "tests"]) + BacktraceClient.shared?.send(exception: exception, attachmentPaths: [], completion: { (result: BacktraceResult) in print(result) - } + }) } @IBAction func crashAppAction(_ sender: Any) { diff --git a/Example-iOS/test.txt b/Example-iOS/test.txt new file mode 100644 index 00000000..3cd489b8 --- /dev/null +++ b/Example-iOS/test.txt @@ -0,0 +1,3 @@ +test from iOS simulator +test Example-iOS +backtrace sample text file diff --git a/Example-macOS-ObjC/AppDelegate.m b/Example-macOS-ObjC/AppDelegate.m index 5fa0740f..0ff33858 100644 --- a/Example-macOS-ObjC/AppDelegate.m +++ b/Example-macOS-ObjC/AppDelegate.m @@ -1,33 +1,45 @@ #import "AppDelegate.h" @import Backtrace; -@interface AppDelegate () +@interface AppDelegate () @end @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + BacktraceCredentials *credentials = [[BacktraceCredentials alloc] initWithEndpoint: [NSURL URLWithString: @"https://backtrace.io"] token: @"token"]; - BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc] initWithCredentials: credentials - dbSettings: [BacktraceDatabaseSettings new] - reportsPerMin: 3]; + BacktraceDatabaseSettings *backtraceDatabaseSettings = [[BacktraceDatabaseSettings alloc] init]; + backtraceDatabaseSettings.maxRecordCount = 1000; + backtraceDatabaseSettings.maxDatabaseSize = 10; + backtraceDatabaseSettings.retryInterval = 5; + backtraceDatabaseSettings.retryLimit = 3; + backtraceDatabaseSettings.retryBehaviour = RetryBehaviourInterval; + backtraceDatabaseSettings.retryOrder = RetryOderStack; + + BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc] + initWithCredentials: credentials + dbSettings: backtraceDatabaseSettings + reportsPerMin: 3 + allowsAttachingDebugger: TRUE]; BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil]; [BacktraceClient.shared setUserAttributes: @{@"foo": @"bar"}]; + BacktraceClient.shared.delegate = self; @try { NSArray *array = @[]; NSObject *object = array[1]; //will throw exception } @catch (NSException *exception) { - [[BacktraceClient shared] sendWithCompletion:^(BacktraceResult * _Nonnull result) { + NSArray *paths = @[[[NSBundle mainBundle] pathForResource: @"test" ofType: @"txt"]]; + [[BacktraceClient shared] sendWithAttachmentPaths: paths completion: ^(BacktraceResult * _Nonnull result) { NSLog(@"%@", result); }]; } @finally { } - } @@ -35,4 +47,28 @@ - (void)applicationWillTerminate:(NSNotification *)aNotification { // Insert code here to tear down your application } +#pragma mark - BacktraceClientDelegate +- (BacktraceReport *)willSend:(BacktraceReport *)report { + NSMutableDictionary *dict = [report.attributes mutableCopy]; + [dict setObject: @"just before send" forKey: @"added"]; + report.attributes = dict; + return report; +} + +- (void)serverDidFail:(NSError *)error { + +} + +- (void)serverDidResponse:(BacktraceResult *)result { + +} + +- (NSURLRequest *)willSendRequest:(NSURLRequest *)request { + return request; +} + +- (void)didReachLimit:(BacktraceResult *)result { + +} + @end diff --git a/Example-macOS-ObjC/Info.plist b/Example-macOS-ObjC/Info.plist index e40caef4..3092d8de 100644 --- a/Example-macOS-ObjC/Info.plist +++ b/Example-macOS-ObjC/Info.plist @@ -24,7 +24,7 @@ $(MACOSX_DEPLOYMENT_TARGET) NSMainStoryboardFile Main - NSPrincipalClass - NSApplication + NSPrincipalClass + Backtrace.BacktraceCrashExceptionApplication diff --git a/Example-macOS-ObjC/test.txt b/Example-macOS-ObjC/test.txt new file mode 100644 index 00000000..7005ed78 --- /dev/null +++ b/Example-macOS-ObjC/test.txt @@ -0,0 +1,3 @@ +test from macOS +test Example-macOS-ObjC +backtrace sample text file diff --git a/README.md b/README.md index bbed6820..6b1eda8f 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ ### Create the `BacktraceClient` using `init(credentials:)` initializer and then send error/exception just by calling method `send`: - Swift + ```swift import UIKit import Backtrace @@ -24,10 +25,10 @@ import Backtrace class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - + let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: "https://backtrace.io")!, token: "token") BacktraceClient.shared = try? BacktraceClient(credentials: backtraceCredentials) @@ -46,6 +47,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ``` - Objective-C + ```objective-c #import "AppDelegate.h" @import Backtrace; @@ -57,7 +59,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - + BacktraceCredentials *credentials = [[BacktraceCredentials alloc] initWithEndpoint: [NSURL URLWithString: @"https://backtrace.io"] token: @"token"]; @@ -127,20 +129,39 @@ BacktraceClient.shared = [[BacktraceClient alloc] initWithCredentials: Backtrace ## Backtrace client configuration For more advanced usage of BacktraceClient, you can supply BacktraceClientConfiguration as a parameter. See the following example: +- Swift ```swift -let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: "https://backtrace.io")!, - token: "token") +let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: "https://backtrace.io")!, token: "token") let configuration = BacktraceClientConfiguration(credentials: backtraceCredentials, dbSettings: BacktraceDatabaseSettings(), - reportsPerMin: 10) + reportsPerMin: 10, + allowsAttachingDebugger: false) BacktraceClient.shared = try? BacktraceClient(configuration: configuration) ``` +- Objective-C +```objective-c +BacktraceCredentials *credentials = [[BacktraceCredentials alloc] + initWithEndpoint: [NSURL URLWithString: @"https://backtrace.io"] + token: @"token"]; + +BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc] + initWithCredentials: credentials + dbSettings: [[BacktraceDatabaseSettings alloc] init] + reportsPerMin: 3 + allowsAttachingDebugger: NO]; + +BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil]; +``` + +**Note:** Backtrace library will *not* send any reports if the `allowsAttachingDebugger` flag is set to `false`. + ### Database settings BacktraceClient allows you to customize the initialization of BacktraceDatabase for local storage of error reports by supplying a BacktraceDatabaseSettings parameter, as follows: + +- Swift ```swift -let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: "https://backtrace.io")!, - token: "token") +let backtraceCredentials = BacktraceCredentials(endpoint: URL(string: "https://backtrace.io")!, token: "token") let backtraceDatabaseSettings = BacktraceDatabaseSettings() backtraceDatabaseSettings.maxRecordCount = 1000 backtraceDatabaseSettings.maxDatabaseSize = 10 @@ -154,12 +175,37 @@ let backtraceConfiguration = BacktraceClientConfiguration(credentials: backtrace BacktraceClient.shared = try? BacktraceClient(configuration: backtraceConfiguration) ``` +- Objective-C +```objective-c +BacktraceCredentials *credentials = [[BacktraceCredentials alloc] + initWithEndpoint: [NSURL URLWithString: @"https://backtrace.io"] + token: @"token"]; + +BacktraceDatabaseSettings *backtraceDatabaseSettings = [[BacktraceDatabaseSettings alloc] init]; +backtraceDatabaseSettings.maxRecordCount = 1000; +backtraceDatabaseSettings.maxDatabaseSize = 10; +backtraceDatabaseSettings.retryInterval = 5; +backtraceDatabaseSettings.retryLimit = 3; +backtraceDatabaseSettings.retryBehaviour = RetryBehaviourInterval; +backtraceDatabaseSettings.retryOrder = RetryOderStack; + +BacktraceClientConfiguration *configuration = [[BacktraceClientConfiguration alloc] + initWithCredentials: credentials + dbSettings: backtraceDatabaseSettings + reportsPerMin: 3 + allowsAttachingDebugger: NO]; + +BacktraceClient.shared = [[BacktraceClient alloc] initWithConfiguration: configuration error: nil]; +``` + ### Events handling -BacktraceClient allows you to subscribe for events produced before and after sending error report: +`BacktraceClient` allows you to subscribe for events produced before and after sending each report. - Swift ```swift +// assign `self` or any other object as a `BacktraceClientDelegate` BacktraceClient.shared?.delegate = self +// handle events func willSend(_ report: BacktraceCrashReport) -> (BacktraceCrashReport) func willSendRequest(_ request: URLRequest) -> URLRequest func serverDidFail(_ error: Error) @@ -167,6 +213,19 @@ func serverDidResponse(_ result: BacktraceResult) func didReachLimit(_ result: BacktraceResult) ``` +- Objective-C +```objective-c +// assign `self` or any other object as a `BacktraceClientDelegate` +BacktraceClient.shared.delegate = self; + +//handle events +- (BacktraceReport *)willSend:(BacktraceReport *)report; +- (void)serverDidFail:(NSError *)error; +- (void)serverDidResponse:(BacktraceResult *)result; +- (NSURLRequest *)willSendRequest:(NSURLRequest *)request; +- (void)didReachLimit:(BacktraceResult *)result; +``` + ### User attributes You can add custom user attributes that should be send alongside crash and erros/exceptions: - Swift @@ -174,6 +233,28 @@ You can add custom user attributes that should be send alongside crash and erros BacktraceClient.shared?.userAttributes = ["foo": "bar", "testing": true] ``` +- Objective-C +```objective-c +BacktraceClient.shared.userAttributes = @{@"foo": @"bar", @"testing": YES}; +``` + +### Attachments +For each report you can attach files by supplying an array of file paths. +- Swift +```swift +let filePath = Bundle.main.path(forResource: "test", ofType: "txt")! +BacktraceClient.shared?.send(attachmentPaths: [filePath]) { (result) in + print(result) +} +``` +- Objectice-C +```objective-c +NSArray *paths = @[[[NSBundle mainBundle] pathForResource: @"test" ofType: @"txt"]]; +[[BacktraceClient shared] sendWithAttachmentPaths:paths completion:^(BacktraceResult * _Nonnull result) { + NSLog(@"%@", result); +}]; +``` + ## Sending an error report Registered `BacktraceClient` will be able to send an crash reports. Error report is automatically generated based. @@ -197,6 +278,20 @@ Registered `BacktraceClient` will be able to send an crash reports. Error report - (void) sendWithException: NSException completion: (void (^)(BacktraceResult * _Nonnull)) completion; ``` +### macOS note +If you want to catch additional exceptions on macOS which are not forwarded by macOS runtime, set `NSPrincipalClass` to `Backtrace.BacktraceCrashExceptionApplication` in your `Info.plist`. + +Alternatively, you can set: +- Swift +```swift +UserDefaults.standard.register(defaults: ["NSApplicationCrashOnExceptions": true]) +``` +-Objective-C +```objective-c +[[NSUserDefaults standardUserDefaults] registerDefaults:@{ @"NSApplicationCrashOnExceptions": @YES }]; +``` +but it crashes your app if you don't use `@try ... @catch`. + # FAQ ## Missing dSYM files Make sure your project is configured to generate the debug symbols: diff --git a/Sources/Features/Attributes/AttributesProvider.swift b/Sources/Features/Attributes/AttributesProvider.swift index 357fcaf2..510cc711 100644 --- a/Sources/Features/Attributes/AttributesProvider.swift +++ b/Sources/Features/Attributes/AttributesProvider.swift @@ -1,17 +1,29 @@ import Foundation final class AttributesProvider { + // attributes can be modified on runtime var userAttributes: Attributes = [:] - var defaultAttributes: Attributes { - return DefaultAttributes.current() - } + + private let bluetoothStatusListener = BluetoothStatusListener() + private var faultMessage: String? } extension AttributesProvider: SignalContext { + func set(faultMessage: String?) { + self.faultMessage = faultMessage + } + var attributes: Attributes { return userAttributes + defaultAttributes } + + var defaultAttributes: Attributes { + var defaultAttributes = DefaultAttributes.current() + defaultAttributes["bluetooth.state"] = bluetoothStatusListener.currentState + defaultAttributes["error.message"] = faultMessage + return defaultAttributes + } } extension AttributesProvider: CustomStringConvertible { diff --git a/Sources/Features/Attributes/BluetoothStatusListener.swift b/Sources/Features/Attributes/BluetoothStatusListener.swift new file mode 100644 index 00000000..47a1d060 --- /dev/null +++ b/Sources/Features/Attributes/BluetoothStatusListener.swift @@ -0,0 +1,31 @@ +import Foundation +import CoreBluetooth + +class BluetoothStatusListener: NSObject { + + private var bluetoothCentralManager: CBCentralManager + var currentState: String = "unknown" + + override init() { + let options = [CBCentralManagerOptionShowPowerAlertKey: 0] // magic bit! + let queue = DispatchQueue(label: "backtrace.bluetooth.listener", qos: DispatchQoS.background) + bluetoothCentralManager = CBCentralManager(delegate: nil, queue: queue, options: options) + super.init() + bluetoothCentralManager.delegate = self + } +} + +// MARK: - CBCentralManagerDelegate +extension BluetoothStatusListener: CBCentralManagerDelegate { + + func centralManagerDidUpdateState(_ central: CBCentralManager) { + switch central.state { + case .poweredOn: currentState = "poweredOn" + case .poweredOff: currentState = "poweredOff" + case .resetting: currentState = "resetting" + case .unauthorized: currentState = "unauthorized" + case .unsupported: currentState = "unsupported" + case .unknown: currentState = "unknown" + } + } +} diff --git a/Sources/Features/Attributes/DefaultAttributes.swift b/Sources/Features/Attributes/DefaultAttributes.swift index 850c41a7..3ce6a11f 100644 --- a/Sources/Features/Attributes/DefaultAttributes.swift +++ b/Sources/Features/Attributes/DefaultAttributes.swift @@ -1,16 +1,20 @@ // swiftlint:disable type_name import Foundation +import CoreLocation #if os(iOS) +import CoreNFC import CoreTelephony #endif struct DefaultAttributes { - + static func current() -> Attributes { - return ["backtrace.version": BacktraceVersionNumber] - + DeviceInfo.current() + return DeviceInfo.current() + ScreenInfo.current() + LocaleInfo.current() + + NetworkInfo.current() + + LocationInfo.current() + + LibInfo.current() } } @@ -20,7 +24,7 @@ protocol AttributesSourceType { struct DeviceInfo: AttributesSourceType { - enum Key: String { + private enum Key: String { // String enum values can be omitted when they are equal to the enumcase name. #if os(iOS) case deviceName = "device.name" @@ -28,10 +32,12 @@ struct DeviceInfo: AttributesSourceType { case deviceOrientation = "device.orientation" case batteryState = "battery.state" case batteryLevel = "battery.level" + case nfcReadingAvailable = "device.nfc.readingAvailable" #elseif os(macOS) case systemUptime = "system.uptime" case physicalMemory = "memory.physical" case processorCount = "processor.count" + case hostname = "hostname" #endif } @@ -46,11 +52,17 @@ struct DeviceInfo: AttributesSourceType { deviceAttributes[Key.batteryState.rawValue] = currentDevice.batteryState.name deviceAttributes[Key.batteryLevel.rawValue] = currentDevice.batteryLevel } + if #available(iOS 11.0, *) { + deviceAttributes[Key.nfcReadingAvailable.rawValue] = NFCNDEFReaderSession.readingAvailable + } else { + deviceAttributes[Key.nfcReadingAvailable.rawValue] = false + } #elseif os(macOS) let processinfo = ProcessInfo.processInfo deviceAttributes[Key.systemUptime.rawValue] = processinfo.systemUptime deviceAttributes[Key.physicalMemory.rawValue] = processinfo.physicalMemory deviceAttributes[Key.processorCount.rawValue] = processinfo.processorCount + deviceAttributes[Key.hostname.rawValue] = Host.current().name #endif return deviceAttributes } @@ -58,7 +70,7 @@ struct DeviceInfo: AttributesSourceType { struct ScreenInfo: AttributesSourceType { - enum Key: String { + private enum Key: String { #if os(iOS) case scale = "screen.scale" case width = "screen.width" @@ -101,7 +113,7 @@ struct ScreenInfo: AttributesSourceType { struct LocaleInfo: AttributesSourceType { - enum Key: String { + private enum Key: String { case languageCode = "device.lang.code" case language = "device.lang" case regionCode = "device.region.code" @@ -126,10 +138,77 @@ struct LocaleInfo: AttributesSourceType { } } +struct NetworkInfo: AttributesSourceType { + + private enum Key: String { + case status = "network.status" + } + + static func current() -> Attributes { + var networkAttributes: Attributes = [:] + networkAttributes[Key.status.rawValue] = NetworkReachability().statusName + return networkAttributes + } +} + +struct LocationInfo: AttributesSourceType { + + private enum Key: String { + case locationServicesEnabled = "location.servicesEnabled" + case locationAuthorizationStatus = "location.authorizationStatus" + } + static func current() -> [String: Any] { + var locationAttributes: [String: Any] = [:] + locationAttributes[Key.locationServicesEnabled.rawValue] = CLLocationManager.locationServicesEnabled() + locationAttributes[Key.locationAuthorizationStatus.rawValue] = CLLocationManager.authorizationStatus().name + return locationAttributes + } +} + +struct LibInfo: AttributesSourceType { + + private static let applicationGuidKey = "backtrace.unique.user.identifier" + private static let applicationLangName = "backtrace-cocoa" + + private enum Key: String { + case guid = "guid" + case langName = "lang.name" + case langVersion = "lang.version" + } + + static func current() -> Attributes { + return [Key.guid.rawValue: guid(store: UserDefaultsStore.self).uuidString, + Key.langName.rawValue: applicationLangName, + Key.langVersion.rawValue: BacktraceVersionNumber] + } + + static private func guid(store: UserDefaultsStore.Type) -> UUID { + if let uuidString: String = store.value(forKey: applicationGuidKey), let uuid = UUID(uuidString: uuidString) { + return uuid + } else { + let uuid = UUID() + store.store(uuid.uuidString, forKey: applicationGuidKey) + return uuid + } + } +} // swiftlint:enable type_name +private extension CLAuthorizationStatus { + + var name: String { + switch self { + case .authorizedAlways: return "Always" + case .authorizedWhenInUse: return "WhenInUse" + case .denied: return "Denied" + case .notDetermined: return "notDetermined" + case .restricted: return "restricted" + } + } +} + #if os(iOS) -extension UIDeviceOrientation { +private extension UIDeviceOrientation { var name: String { switch self { @@ -146,7 +225,7 @@ extension UIDeviceOrientation { #endif #if os(iOS) -extension UIDevice.BatteryState { +private extension UIDevice.BatteryState { var name: String { switch self { @@ -160,7 +239,7 @@ extension UIDevice.BatteryState { #endif #if os(iOS) -extension UIApplication.State { +private extension UIApplication.State { var name: String { switch self { diff --git a/Sources/Features/Attributes/NetworkReachability.swift b/Sources/Features/Attributes/NetworkReachability.swift new file mode 100644 index 00000000..c9e6fa55 --- /dev/null +++ b/Sources/Features/Attributes/NetworkReachability.swift @@ -0,0 +1,55 @@ +import Foundation +import SystemConfiguration + +// based on: https://github.com/Alamofire/Alamofire/blob/master/Source/NetworkReachabilityManager.swift + +final class NetworkReachability { + + var flags: SCNetworkReachabilityFlags? { + guard let reachability = reachability else { return nil } + var flags = SCNetworkReachabilityFlags() + + if SCNetworkReachabilityGetFlags(reachability, &flags) { + return flags + } + + return nil + } + + private let reachability: SCNetworkReachability? + + init() { + var address = sockaddr_in() + address.sin_len = UInt8(MemoryLayout.size) + address.sin_family = sa_family_t(AF_INET) + + self.reachability = withUnsafePointer(to: &address, { pointer in + return pointer.withMemoryRebound(to: sockaddr.self, capacity: MemoryLayout.size) { + return SCNetworkReachabilityCreateWithAddress(nil, $0) + } + }) + } +} + +extension NetworkReachability { + + var statusName: String { + guard let flags = flags else { return "unknown" } + guard isNetworkReachable(with: flags) else { return "notReachable" } + + #if os(iOS) + if flags.contains(.isWWAN) { return "reachableViaWWAN" } + #endif + + return "reachableViaEthernetOrWiFi" + } + + func isNetworkReachable(with flags: SCNetworkReachabilityFlags) -> Bool { + let isReachable = flags.contains(.reachable) + let needsConnection = flags.contains(.connectionRequired) + let canConnectAutomatically = flags.contains(.connectionOnDemand) || flags.contains(.connectionOnTraffic) + let canConnectWithoutUserInteraction = canConnectAutomatically && !flags.contains(.interventionRequired) + + return isReachable && (!needsConnection || canConnectWithoutUserInteraction) + } +} diff --git a/Sources/Features/Client/BacktraceReporter.swift b/Sources/Features/Client/BacktraceReporter.swift index fe45410b..be083cba 100644 --- a/Sources/Features/Client/BacktraceReporter.swift +++ b/Sources/Features/Client/BacktraceReporter.swift @@ -15,7 +15,7 @@ final class BacktraceReporter { self.repository = try PersistentRepository(settings: dbSettings) self.watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: reportsPerMin, - networkClient: api, + api: api, repository: repository) self.attributesProvider = AttributesProvider() self.reporter.signalContext(&attributesProvider) @@ -75,8 +75,12 @@ extension BacktraceReporter { } } - func send(exception: NSException? = nil) throws -> BacktraceResult { - let resource = try reporter.generateLiveReport(exception: exception, attributes: attributesProvider.attributes) + func send(exception: NSException? = nil, attachmentPaths: [String] = [], + faultMessage: String? = nil) throws -> BacktraceResult { + attributesProvider.set(faultMessage: faultMessage) + let resource = try reporter.generateLiveReport(exception: exception, + attributes: attributesProvider.attributes, + attachmentPaths: attachmentPaths) return try send(resource: resource) } } diff --git a/Sources/Features/Client/BacktraceResponse.swift b/Sources/Features/Client/BacktraceResponse.swift index ea97d923..fedf55d4 100644 --- a/Sources/Features/Client/BacktraceResponse.swift +++ b/Sources/Features/Client/BacktraceResponse.swift @@ -26,8 +26,8 @@ struct BacktraceResponse: Codable { } extension BacktraceResponse { - func result(backtraceReport: BacktraceReport) -> BacktraceResult { - return BacktraceResult(.ok, message: "Ok.", backtraceReport: backtraceReport) + func result(report: BacktraceReport) -> BacktraceResult { + return BacktraceResult(.ok, report: report) } } @@ -41,8 +41,8 @@ struct BacktraceErrorResponse: Codable, BacktraceError { } extension BacktraceErrorResponse { - func result(backtraceReport: BacktraceReport) -> BacktraceResult { - return BacktraceResult(.serverError, message: error.message, backtraceReport: backtraceReport) + func result(report: BacktraceReport) -> BacktraceResult { + return BacktraceResult(.serverError, report: report, message: error.message) } } diff --git a/Sources/Features/Crash Reporting/CrashReporter+StringConvertible.swift b/Sources/Features/Crash Reporting/CrashReporter+StringConvertible.swift deleted file mode 100644 index a6efa2cd..00000000 --- a/Sources/Features/Crash Reporting/CrashReporter+StringConvertible.swift +++ /dev/null @@ -1,232 +0,0 @@ -import Foundation -import Backtrace_PLCrashReporter - -protocol Composite { - var children: [Composite] { get } - var customDebugAttributes: Attributes { get } - var customDebugName: String { get } -} - -extension Composite { - var children: [Composite] { - return [] - } -} - -extension PLCrashReport: Composite { - var customDebugName: String { - return "Report" - } - - var children: [Composite] { - return [self.systemInfo, - self.machineInfo, - self.applicationInfo, - self.processInfo, - self.signalInfo, - self.machExceptionInfo, - self.exceptionInfo - ] - .compactMap { $0 } - } - - var customDebugAttributes: Attributes { - return [ - "has machine info": hasMachineInfo, - "has process info": hasProcessInfo, - "has exception info": hasExceptionInfo, - "uuid": uuidRef?.hashValue ?? -1 - ] - } -} - -extension Composite { - fileprivate func format(indentionLevel: Int) -> String { - let newline = "\n" - let tab = "\t" - let separator = ": " - let indention: (Int) -> String = { String(repeating: tab, count: $0) } - - let name = indention(indentionLevel) + customDebugName + separator - let attributes = customDebugAttributes - .mapValues { "\($0)" } - .map { indention(indentionLevel + 1) + $0.key + separator + $0.value } - let children = self.children - .map { $0.format(indentionLevel: indentionLevel + 1)} - - return [[name], attributes, children] - .joined() - .joined(separator: newline) - } -} - -extension PLCrashReport { - var info: String { - return format(indentionLevel: 0) - } -} - -extension PLCrashReportProcessInfo: Composite { - var customDebugName: String { - return "Process" - } - - var customDebugAttributes: Attributes { - return [ - "name": processName.orEmpty(), - "id": processID, - "path": processPath.orEmpty(), - "parent name": parentProcessName.orEmpty(), - "parent id": parentProcessID, - "native": native, - "start time": processStartTime?.timeIntervalSince1970 ?? 0 - ] - } -} - -extension PLCrashReportSignalInfo: Composite { - - var customDebugAttributes: Attributes { - return [ - "name": name.orEmpty(), - "code": code.orEmpty(), - "address": address - ] - } - - var customDebugName: String { - return "Signal" - } -} - -extension PLCrashReportApplicationInfo: Composite { - var customDebugName: String { - return "Application" - } - var customDebugAttributes: Attributes { - return [ - "identifier": applicationIdentifier.orEmpty(), - "version": applicationVersion.orEmpty() - ] - } -} - -extension PLCrashReportSystemInfo: Composite { - var customDebugName: String { - return "System" - } - - var customDebugAttributes: Attributes { - - return [ - "name": operatingSystem.rawValue, - "version": operatingSystemVersion.orEmpty(), - "build": operatingSystemBuild.orEmpty(), - "timestamp": timestamp?.timeIntervalSince1970 ?? 0 - ] - } -} - -extension PLCrashReportMachineInfo: Composite { - var customDebugName: String { - return "Machine" - } - - var customDebugAttributes: Attributes { - - return [ - "model name": modelName.orEmpty(), - "processor count": processorCount, - "logical processor count": logicalProcessorCount - ] - } -} - -extension PLCrashReportProcessorInfo: Composite { - var customDebugName: String { - return "Processor" - } - - var customDebugAttributes: Attributes { - - return [ - "type": type, - "subtype": subtype, - "encoding": typeEncoding.rawValue - ] - } -} - -extension PLCrashReportMachExceptionInfo: Composite { - var customDebugName: String { - return "Mach exception" - } - - var customDebugAttributes: Attributes { - let codes = self.codes as? [UInt] ?? [] - return [ - "type": type, - "codes": codes - ] - } -} - -extension PLCrashReportExceptionInfo: Composite { - var customDebugName: String { - return "Exception info" - } - - var children: [Composite] { - return [self.stackFrames as? Composite] - .compactMap { $0 } - } - - var customDebugAttributes: Attributes { - return [ - "name": exceptionName.orEmpty(), - "reason": exceptionReason.orEmpty() - ] - } -} - -extension PLCrashReportSymbolInfo: Composite { - var customDebugName: String { - return "Symbol" - } - - var customDebugAttributes: Attributes { - return [ - "symbol name": symbolName.orEmpty(), - "start address": startAddress, - "end address": endAddress - ] - } -} - -extension PLCrashReportStackFrameInfo: Composite { - var customDebugName: String { - return "Stack frame" - } - - var children: [Composite] { - return [self.symbolInfo] - .compactMap { $0 } - } - - var customDebugAttributes: Attributes { - return [ - "instruction pointer": instructionPointer - ] - } -} - -private extension Optional where Wrapped == String { - func orEmpty() -> String { - switch self { - case .some(let wrapped): - return wrapped - case .none: - return "---" - } - } -} diff --git a/Sources/Features/Crash Reporting/CrashReporter.swift b/Sources/Features/Crash Reporting/CrashReporter.swift index 64c6554b..747ecc88 100644 --- a/Sources/Features/Crash Reporting/CrashReporter.swift +++ b/Sources/Features/Crash Reporting/CrashReporter.swift @@ -1,7 +1,7 @@ import Foundation import Backtrace_PLCrashReporter -class CrashReporter: NSObject { +final class CrashReporter { private let reporter: PLCrashReporter static private let crashName = "live_report" public init(config: PLCrashReporterConfig = PLCrashReporterConfig.defaultConfiguration()) { @@ -14,20 +14,25 @@ extension CrashReporter: CrashReporting { let rawMutablePointer = UnsafeMutableRawPointer(&mutableContext) let handler: @convention(c) (_ signalInfo: UnsafeMutablePointer?, _ uContext: UnsafeMutablePointer?, - _ context: UnsafeMutableRawPointer?) -> Void = { _, _, context in - guard let attributesProvider = context?.assumingMemoryBound(to: SignalContext.self).pointee else { + _ context: UnsafeMutableRawPointer?) -> Void = { signalInfoPointer, _, context in + guard let attributesProvider = context?.assumingMemoryBound(to: SignalContext.self).pointee, + let signalInfo = signalInfoPointer?.pointee else { return } BacktraceLogger.debug("Saving custom attributes:\n\(attributesProvider.description)") + attributesProvider.set(faultMessage: "siginfo_t.si_signo: \(signalInfo.si_signo)") try? AttributesStorage.store(attributesProvider.attributes, fileName: CrashReporter.crashName) } var callbacks = PLCrashReporterCallbacks(version: 0, context: rawMutablePointer, handleSignal: handler) reporter.setCrash(&callbacks) } - func generateLiveReport(exception: NSException? = nil, attributes: Attributes) throws -> BacktraceReport { + func generateLiveReport(exception: NSException? = nil, + attributes: Attributes, + attachmentPaths: [String] = []) throws -> BacktraceReport { + let reportData = try reporter.generateLiveReport(with: exception) - return try BacktraceReport(report: reportData, attributes: attributes) + return try BacktraceReport(report: reportData, attributes: attributes, attachmentPaths: attachmentPaths) } func enableCrashReporting() throws { @@ -37,7 +42,8 @@ extension CrashReporter: CrashReporting { func pendingCrashReport() throws -> BacktraceReport { let reportData = try reporter.loadPendingCrashReportDataAndReturnError() let attributes = (try? AttributesStorage.retrieve(fileName: CrashReporter.crashName)) ?? [:] - return try BacktraceReport(report: reportData, attributes: attributes) + // NOTE: - no attachments in crash reports + return try BacktraceReport(report: reportData, attributes: attributes, attachmentPaths: []) } func hasPendingCrashes() -> Bool { diff --git a/Sources/Features/Extensions/Foundation+Extensions.swift b/Sources/Features/Extensions/Foundation+Extensions.swift index e4d2e255..0dc7963c 100644 --- a/Sources/Features/Extensions/Foundation+Extensions.swift +++ b/Sources/Features/Extensions/Foundation+Extensions.swift @@ -2,10 +2,6 @@ import Foundation extension Dictionary { - static func += (lhs: inout Dictionary, rhs: Dictionary) { - lhs.merge(rhs) { (_, new) in new } - } - static func + (lhs: Dictionary, rhs: Dictionary) -> Dictionary { return lhs.merging(rhs, uniquingKeysWith: {_, new in new}) } diff --git a/Sources/Features/Network/BacktraceApi.swift b/Sources/Features/Network/BacktraceApi.swift index 3f8ba8e6..98484c2e 100644 --- a/Sources/Features/Network/BacktraceApi.swift +++ b/Sources/Features/Network/BacktraceApi.swift @@ -21,13 +21,15 @@ extension BacktraceApi: BacktraceApiProtocol { let currentTimestamp = Date().timeIntervalSince1970 let numberOfSendsInLastOneMinute = successfulSendTimestamps.filter { currentTimestamp - $0 < 60.0 }.count guard numberOfSendsInLastOneMinute < reportsPerMin else { - return BacktraceResult.limitReached(report) + return BacktraceResult(.limitReached, report: report) } // modify before sending let modifiedBeforeSendingReport = self.delegate?.willSend?(report) ?? report + let attachments = modifiedBeforeSendingReport.attachmentPaths.compactMap { Attachment(filePath: $0) } // create request let urlRequest = try self.request.multipartUrlRequest(data: modifiedBeforeSendingReport.reportData, - attributes: modifiedBeforeSendingReport.attributes) + attributes: modifiedBeforeSendingReport.attributes, + attachments: attachments) BacktraceLogger.debug("Sending crash report:\n\(urlRequest.debugDescription)") // send report let response = session.sync(urlRequest) @@ -39,20 +41,19 @@ extension BacktraceApi: BacktraceApiProtocol { guard let httpResponse = response.urlResponse, let responseData = response.responseData else { throw HttpError.unknownError } - BacktraceLogger.debug("Sent crash: \(modifiedBeforeSendingReport.plCrashReport.info)") BacktraceLogger.debug("Response: \n\(httpResponse.debugDescription)") // check result let result = try BacktraceHttpResponseDeserializer(httpResponse: httpResponse, responseData: responseData) .result switch result { case .error(let error): - self.delegate?.serverDidResponse?(error.result(backtraceReport: modifiedBeforeSendingReport)) - return error.result(backtraceReport: modifiedBeforeSendingReport) + self.delegate?.serverDidResponse?(error.result(report: modifiedBeforeSendingReport)) + return error.result(report: modifiedBeforeSendingReport) case .success(let response): // did send successfully self.successfulSendTimestamps.append(Date().timeIntervalSince1970) - self.delegate?.serverDidResponse?(response.result(backtraceReport: modifiedBeforeSendingReport)) - return response.result(backtraceReport: modifiedBeforeSendingReport) + self.delegate?.serverDidResponse?(response.result(report: modifiedBeforeSendingReport)) + return response.result(report: modifiedBeforeSendingReport) } } } diff --git a/Sources/Features/Network/MultipartRequestType.swift b/Sources/Features/Network/MultipartRequestType.swift index 0dfed872..65d59663 100644 --- a/Sources/Features/Network/MultipartRequestType.swift +++ b/Sources/Features/Network/MultipartRequestType.swift @@ -4,7 +4,7 @@ protocol MultipartRequestType: RequestType {} extension MultipartRequestType { - func multipartUrlRequest(data: Data, attributes: Attributes) throws -> URLRequest { + func multipartUrlRequest(data: Data, attributes: Attributes, attachments: [Attachment]) throws -> URLRequest { var multipartRequest = try urlRequest() let boundary = generateBoundaryString() multipartRequest.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") @@ -23,6 +23,16 @@ extension MultipartRequestType { body.appendString("Content-Type: application/octet-stream\r\n\r\n") body.append(data) body.appendString("\r\n") + // attachments + for attachment in attachments { + body.appendString(boundaryPrefix) + // swiftlint:disable line_length + body.appendString("Content-Disposition: form-data; name=\"\(attachment.name)\"; filename=\"\(attachment.name)\"\r\n") + // swiftlint:enable line_length + body.appendString("Content-Type: \(attachment.mimeType)\r\n\r\n") + body.append(attachment.data) + body.appendString("\r\n") + } body.appendString("\(boundaryPrefix)--") multipartRequest.httpBody = body as Data diff --git a/Sources/Features/Repository/Model/Attachment.swift b/Sources/Features/Repository/Model/Attachment.swift new file mode 100644 index 00000000..7ccbdf17 --- /dev/null +++ b/Sources/Features/Repository/Model/Attachment.swift @@ -0,0 +1,29 @@ +import Foundation +#if os(iOS) +import MobileCoreServices +#endif + +struct Attachment { + let data: Data + let name: String + let mimeType: String + + init?(filePath: String) { + let fileURL = URL(fileURLWithPath: filePath) + guard let fileData = try? Data(contentsOf: fileURL, + options: Data.ReadingOptions.mappedIfSafe) else { return nil } + + mimeType = Attachment.mimeTypeForPath(fileUrl: fileURL) + name = "attachment_" + (fileURL.lastPathComponent as NSString).deletingPathExtension + "_\(arc4random())" + data = fileData + } + + static private func mimeTypeForPath(fileUrl: URL) -> String { + guard let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, + fileUrl.pathExtension as NSString, nil)? + .takeRetainedValue(), + let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)? + .takeRetainedValue() as String? else { return "application/octet-stream" } + return mimetype + } +} diff --git a/Sources/Features/Repository/Model/BacktraceReport.swift b/Sources/Features/Repository/Model/BacktraceReport.swift index 37c39b92..db575ace 100644 --- a/Sources/Features/Repository/Model/BacktraceReport.swift +++ b/Sources/Features/Repository/Model/BacktraceReport.swift @@ -3,15 +3,17 @@ import Backtrace_PLCrashReporter @objc final public class BacktraceReport: NSObject { - let reportData: Data + @objc public let reportData: Data let plCrashReport: PLCrashReport let identifier: UUID - var attributes: Attributes + @objc public var attachmentPaths: [String] + @objc public var attributes: Attributes - init(report: Data, attributes: Attributes) throws { + init(report: Data, attributes: Attributes, attachmentPaths: [String]) throws { self.plCrashReport = try PLCrashReport(data: report) reportData = report identifier = UUID() + self.attachmentPaths = attachmentPaths self.attributes = attributes super.init() } @@ -19,12 +21,14 @@ import Backtrace_PLCrashReporter init(managedObject: Crash) throws { guard let reportData = managedObject.reportData, let identifierString = managedObject.hashProperty, + let attachmentPaths = managedObject.attachmentPaths, let identifier = UUID(uuidString: identifierString) else { throw RepositoryError.canNotCreateEntityDescription } self.reportData = reportData self.plCrashReport = try PLCrashReport(data: reportData) self.identifier = identifier + self.attachmentPaths = attachmentPaths self.attributes = (try? AttributesStorage.retrieve(fileName: identifier.uuidString)) ?? [:] super.init() } diff --git a/Sources/Features/Repository/PersistentRepository.swift b/Sources/Features/Repository/PersistentRepository.swift index 9e6be168..7565e871 100644 --- a/Sources/Features/Repository/PersistentRepository.swift +++ b/Sources/Features/Repository/PersistentRepository.swift @@ -7,6 +7,7 @@ protocol PersistentStorable { static var entityName: String { get } var identifier: UUID { get } var reportData: Data { get } + var attachmentPaths: [String] { get set } var attributes: Attributes { get } init(managedObject: ManagedObjectType) throws @@ -103,6 +104,7 @@ extension PersistentRepository: Repository { newManagedObject.setValue(resource.reportData, forKey: "reportData") newManagedObject.setValue(Date(), forKey: "dateAdded") newManagedObject.setValue(0, forKey: "retryCount") + newManagedObject.setValue(resource.attachmentPaths, forKey: "attachmentPaths") try backgroundContext.save() try AttributesStorage.store(resource.attributes, fileName: resource.identifier.uuidString) } diff --git a/Sources/Features/Repository/Model.xcdatamodeld/Model.xcdatamodel/contents b/Sources/Features/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents similarity index 70% rename from Sources/Features/Repository/Model.xcdatamodeld/Model.xcdatamodel/contents rename to Sources/Features/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents index 85f34328..88bfcbe5 100644 --- a/Sources/Features/Repository/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/Sources/Features/Resources/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,12 +1,13 @@ - + + - + \ No newline at end of file diff --git a/Sources/Features/Watcher/BacktraceWatcher.swift b/Sources/Features/Watcher/BacktraceWatcher.swift index e52928a0..2a1365a8 100644 --- a/Sources/Features/Watcher/BacktraceWatcher.swift +++ b/Sources/Features/Watcher/BacktraceWatcher.swift @@ -5,66 +5,33 @@ where BacktraceRepository.Resource == BacktraceReport { let settings: BacktraceDatabaseSettings let reportsPerMin: Int - let networkClient: BacktraceApiProtocol + let api: BacktraceApiProtocol let repository: BacktraceRepository var timer: DispatchSourceTimer? let queue: DispatchQueue let batchSize: Int - init(settings: BacktraceDatabaseSettings, reportsPerMin: Int, networkClient: BacktraceApiProtocol, + init(settings: BacktraceDatabaseSettings, reportsPerMin: Int, api: BacktraceApiProtocol, repository: BacktraceRepository, dispatchQueue: DispatchQueue = DispatchQueue(label: "backtrace.timer", qos: .background), batchSize: Int = 3) throws { self.settings = settings self.reportsPerMin = reportsPerMin self.repository = repository - self.networkClient = networkClient + self.api = api self.queue = dispatchQueue self.batchSize = batchSize guard settings.retryBehaviour == .interval else { return } - configureTimer() - } - - private func configureTimer() { - let timer = DispatchSource.makeTimerSource(queue: queue) - self.timer = timer - let repeating: DispatchTimeInterval = .seconds(settings.retryInterval) - timer.schedule(deadline: DispatchTime.now() + repeating, repeating: repeating) - timer.setEventHandler { [weak self] in - guard let self = self else { return } - self.timer?.suspend() - defer { self.timer?.resume() } - do { - BacktraceLogger.debug("Retrying to send") - try self.batchRetry() - } catch { - BacktraceLogger.error(error) - } - } - timer.resume() - } - - // Takes from `repository` reports to send - private func crashReportsToSend(limit: Int) throws -> [BacktraceRepository.Resource] { - switch settings.retryOrder { - case .queue: return try repository.getOldest(count: limit) - case .stack: return try repository.getLatest(count: limit) - } + configureTimer(with: DispatchWorkItem(block: timerEventHandler)) } - private func batchRetry() throws { - // prepare set of reports to send, considering limits - let reportsToSend = try crashReportsToSend(limit: batchSize) - let currentTimestamp = Date().timeIntervalSince1970 - let numberOfSendsInLastOneMinute = networkClient.successfulSendTimestamps - .filter { currentTimestamp - $0 < 60.0 }.count - let maxReportsToSend = max(0, abs(reportsPerMin - numberOfSendsInLastOneMinute)) - let limitedReportsToSend = reportsToSend.prefix(maxReportsToSend) - BacktraceLogger.debug("Number of limited reports to send: \(limitedReportsToSend.count)") + internal func batchRetry() throws { + let reportsToSend = try limitedReportsToSend() + BacktraceLogger.debug("Number of limited reports to send: \(reportsToSend.count)") - for reportToSend in limitedReportsToSend { + for reportToSend in reportsToSend { do { - let result = try networkClient.send(reportToSend) + let result = try api.send(reportToSend) if let reportData = result.report { if result.backtraceStatus == .ok { try repository.delete(reportData) @@ -86,7 +53,57 @@ where BacktraceRepository.Resource == BacktraceReport { } deinit { + resetTimer() + } +} + +// MARK: - Timer +extension BacktraceWatcher { + + internal func configureTimer(with handler: DispatchWorkItem) { + let timer = DispatchSource.makeTimerSource(queue: queue) + self.timer = timer + let repeating: DispatchTimeInterval = .seconds(settings.retryInterval) + timer.schedule(deadline: DispatchTime.now() + repeating, repeating: repeating) + timer.setEventHandler(handler: handler) + timer.resume() + } + + internal func timerEventHandler() { + self.timer?.suspend() + defer { self.timer?.resume() } + do { + BacktraceLogger.debug("Retrying to send") + try self.batchRetry() + } catch { + BacktraceLogger.error(error) + } + } + + internal func resetTimer() { timer?.setEventHandler {} timer?.cancel() } } + +// MARK: - Reports retrieving +extension BacktraceWatcher { + + // Takes from `repository` reports to send + internal func crashReportsFromRepository(limit: Int) throws -> [BacktraceRepository.Resource] { + switch settings.retryOrder { + case .queue: return try repository.getOldest(count: limit) + case .stack: return try repository.getLatest(count: limit) + } + } + + internal func limitedReportsToSend() throws -> [BacktraceRepository.Resource] { + // prepare set of reports to send, considering limits + let reportsFromRepository = try crashReportsFromRepository(limit: batchSize) + let currentTimestamp = Date().timeIntervalSince1970 + let numberOfSendsInLastOneMinute = api.successfulSendTimestamps + .filter { currentTimestamp - $0 < 60.0 }.count + let maxReportsToSend = max(0, abs(reportsPerMin - numberOfSendsInLastOneMinute)) + return Array(reportsFromRepository.prefix(maxReportsToSend)) + } +} diff --git a/Sources/Public/BacktraceClient.swift b/Sources/Public/BacktraceClient.swift index 5eeec36c..27391f47 100644 --- a/Sources/Public/BacktraceClient.swift +++ b/Sources/Public/BacktraceClient.swift @@ -1,26 +1,51 @@ import Foundation -/// Provides the default implementation of BacktraceClientProviding protocol. +/// Provides the default implementation of `BacktraceClientProtocol` protocol. @objc open class BacktraceClient: NSObject { - /// Shared instance of BacktraceClient class. + /// Shared instance of BacktraceClient class. Should be created before send any report. @objc public static var shared: BacktraceClientProtocol? - private var reporter: BacktraceReporter - private let dispatcher: Dispatcher + /// `BacktraceClient`'s configuration. Allows to configure `BacktraceClient` in custom way. + @objc public let configuration: BacktraceClientConfiguration + private let reporter: BacktraceReporter + private let dispatcher: Dispatching + private let reportingPolicy: ReportingPolicy + + /// Initialize `BacktraceClient` with credentials. To learn more about credentials, see + /// https://help.backtrace.io/troubleshooting/what-is-a-submission-url + /// and https://help.backtrace.io/troubleshooting/what-is-a-submission-token . + /// + /// - Parameter credentials: Credentials to register in Backtrace services + /// - Throws: throws an error in cases of failure. @objc public convenience init(credentials: BacktraceCredentials) throws { try self.init(configuration: BacktraceClientConfiguration(credentials: credentials)) } - @objc public init(configuration: BacktraceClientConfiguration) throws { - dispatcher = Dispatcher() + /// Initialize `BacktraceClient` with `BacktraceClientConfiguration` instance. Allows to configure `BacktraceClient` + /// in custom way. + /// + /// - Parameter configuration: `BacktraceClient`s configuration + /// - Throws: throws an error in cases of failure. + @objc public convenience init(configuration: BacktraceClientConfiguration) throws { let api = BacktraceApi(endpoint: configuration.credentials.endpoint, token: configuration.credentials.token, reportsPerMin: configuration.reportsPerMin) - reporter = try BacktraceReporter(reporter: CrashReporter(), api: api, - dbSettings: configuration.dbSettings, - reportsPerMin: configuration.reportsPerMin) + let reporter = try BacktraceReporter(reporter: CrashReporter(), api: api, dbSettings: configuration.dbSettings, + reportsPerMin: configuration.reportsPerMin) + try self.init(configuration: configuration, debugger: DebuggerChecker.self, reporter: reporter, + dispatcher: Dispatcher(), api: api) + } + + init(configuration: BacktraceClientConfiguration, debugger: DebuggerChecking.Type = DebuggerChecker.self, + reporter: BacktraceReporter, dispatcher: Dispatching = Dispatcher(), api: BacktraceApiProtocol) throws { + + self.dispatcher = dispatcher + self.reporter = reporter + self.configuration = configuration + self.reportingPolicy = ReportingPolicy(configuration: configuration, debuggerChecker: debugger) + super.init() try startCrashReporter() } @@ -28,7 +53,8 @@ import Foundation // MARK: - BacktraceClientProviding extension BacktraceClient: BacktraceClientCustomizing { - /// BacktraceClientDelegate. Subscribe to receive all the events. + + /// The object that acts as the delegate of the `BacktraceClient`. Provide delegate to receive all the events. @objc public weak var delegate: BacktraceClientDelegate? { set { reporter.delegate = newValue @@ -37,6 +63,7 @@ extension BacktraceClient: BacktraceClientCustomizing { } } + /// Additional user attributes which are automatically added to each report. @objc public var userAttributes: Attributes { get { return reporter.userAttributes @@ -49,25 +76,57 @@ extension BacktraceClient: BacktraceClientCustomizing { // MARK: - BacktraceReporting extension BacktraceClient: BacktraceReporting { - @objc public func send(exception: NSException?, completion: @escaping ((_ result: BacktraceResult) -> Void)) { + + @objc public func send(error: Error, + attachmentPaths: [String], + completion: @escaping ((BacktraceResult) -> Void)) { + reportCrash(faultMessage: error.localizedDescription, attachmentPaths: attachmentPaths, completion: completion) + } + + @objc public func send(message: String, + attachmentPaths: [String], + completion: @escaping ((BacktraceResult) -> Void)) { + reportCrash(faultMessage: message, attachmentPaths: attachmentPaths, completion: completion) + } + + @objc public func send(exception: NSException?, + attachmentPaths: [String] = [], + completion: @escaping ((_ result: BacktraceResult) -> Void)) { + reportCrash(faultMessage: exception?.name.rawValue ?? "Unknown exception", exception: exception, + attachmentPaths: attachmentPaths, completion: completion) + } + + @objc public func send(attachmentPaths: [String] = [], + completion: @escaping ((_ result: BacktraceResult) -> Void)) { + reportCrash(attachmentPaths: attachmentPaths, completion: completion) + } + + private func reportCrash(faultMessage: String? = nil, exception: NSException? = nil, attachmentPaths: [String] = [], + completion: @escaping ((_ result: BacktraceResult) -> Void)) { + guard reportingPolicy.allowsReporting else { + completion(BacktraceResult(.debuggerAttached)) + return + } + dispatcher.dispatch({ [weak self] in guard let self = self else { return } do { - completion(try self.reporter.send(exception: exception)) + completion(try self.reporter.send(exception: exception, attachmentPaths: attachmentPaths, + faultMessage: faultMessage)) } catch { BacktraceLogger.error(error) - completion(BacktraceResult.unknownError()) + completion(BacktraceResult(.unknownError)) } }, completion: { BacktraceLogger.debug("Finished") }) } - @objc public func send(completion: @escaping ((_ result: BacktraceResult) -> Void)) { - send(exception: nil, completion: completion) - } - func startCrashReporter() throws { + guard reportingPolicy.allowsReporting else { + return + } + try reporter.enableCrashReporter() dispatcher.dispatch({ [weak self] in guard let self = self else { return } @@ -84,6 +143,8 @@ extension BacktraceClient: BacktraceReporting { // MARK: - BacktraceLogging extension BacktraceClient: BacktraceLogging { + + /// Set of logging destinations public var destinations: Set { get { return BacktraceLogger.destinations diff --git a/Sources/Public/BacktraceClientConfiguration.swift b/Sources/Public/BacktraceClientConfiguration.swift index 6c6c2fae..499e57a2 100644 --- a/Sources/Public/BacktraceClientConfiguration.swift +++ b/Sources/Public/BacktraceClientConfiguration.swift @@ -7,21 +7,35 @@ import Foundation @objc public let credentials: BacktraceCredentials /// Database settings - @objc public let dbSettings: BacktraceDatabaseSettings + @objc public var dbSettings: BacktraceDatabaseSettings = BacktraceDatabaseSettings() /// Number of records sent in 1 minute. Default: 3. - @objc public let reportsPerMin: Int + @objc public var reportsPerMin: Int = 3 + + /// Flag indicating if the Backtrace client should raport reports when the debugger is attached. + @objc public var allowsAttachingDebugger: Bool = false + + /// Produces Backtrace client configuration settings. + /// - Parameters: + /// - credentials: Backtrace server API credentials. + @objc public init(credentials: BacktraceCredentials) { + self.credentials = credentials + } /// Produces Backtrace client configuration settings. /// - Parameters: /// - credentials: Backtrace server API credentials. /// - dbSettings: Backtrace database settings - /// - reportsPerMin: Maximum number of records sent to Backtrace services in 1 minute. Default: 3. + /// - reportsPerMin: Maximum number of records sent to Backtrace services in 1 minute. Default: 3 + /// - allowsAttachingDebugger: if set to `true` BacktraceClient will report reports even when the debugger + /// is attached. Default: `false` @objc public init(credentials: BacktraceCredentials, dbSettings: BacktraceDatabaseSettings = BacktraceDatabaseSettings(), - reportsPerMin: Int = 3) { + reportsPerMin: Int = 3, + allowsAttachingDebugger: Bool = false) { self.credentials = credentials self.dbSettings = dbSettings self.reportsPerMin = reportsPerMin + self.allowsAttachingDebugger = allowsAttachingDebugger } } diff --git a/Sources/Public/BacktraceClientCustomizing.swift b/Sources/Public/BacktraceClientCustomizing.swift index 8fb112fd..0585780e 100644 --- a/Sources/Public/BacktraceClientCustomizing.swift +++ b/Sources/Public/BacktraceClientCustomizing.swift @@ -1,36 +1,69 @@ import Foundation +/// Typealias of `BacktraceClient` type. Custom Backtrace client have to implement all of these protocols. public typealias BacktraceClientProtocol = BacktraceReporting & BacktraceClientCustomizing & BacktraceLogging +/// Typealias of passing atributes to library. public typealias Attributes = [String: Any] -/// Public BacktraceClient protocol. +/// Protocol describes report customizing functionality of `BacktraceClient`. @objc public protocol BacktraceClientCustomizing { /// Additional user attributes which are automatically added to each report. @objc var userAttributes: Attributes { get set } - /// Delegates methods. + /// The object that acts as the delegate of the `BacktraceClient`. @objc weak var delegate: BacktraceClientDelegate? { get set } } +/// Protocol describes sending functionality of `BacktraceClient`. @objc public protocol BacktraceReporting { /// Automatically generates and sends a crash report to Backtrace services. /// The services response is returned in a completion block. /// /// - Parameters: + /// - error: Error which occurred + /// - attachmentPaths: Array of paths to files that should be send alongside with crash report /// - completion: Backtrace services response. - @objc func send(completion: @escaping ((_ result: BacktraceResult) -> Void)) + @objc func send(error: Error, + attachmentPaths: [String], + completion: @escaping ((_ result: BacktraceResult) -> Void)) /// Automatically generates and sends a crash report to Backtrace services. /// The services response is returned in a completion block. /// /// - Parameters: - /// - exception: instance of NSException, + /// - message: Custom message which will be sent alongsite report + /// - attachmentPaths: Array of paths to files that should be send alongside with crash report /// - completion: Backtrace services response. - @objc func send(exception: NSException?, completion: @escaping ((_ result: BacktraceResult) -> Void)) + @objc func send(message: String, + attachmentPaths: [String], + completion: @escaping ((_ result: BacktraceResult) -> Void)) + + /// Automatically generates and sends a crash report to Backtrace services. + /// The services response is returned in a completion block. + /// + /// - Parameters: + /// - attachmentPaths: Array of paths to files that should be send alongside with crash report + /// - completion: Backtrace services response. + @objc func send(attachmentPaths: [String], + completion: @escaping ((_ result: BacktraceResult) -> Void)) + + /// Automatically generates and sends a crash report to Backtrace services. + /// The services response is returned in a completion block. + /// + /// - Parameters: + /// - exception: instance of NSException + /// - attachmentPaths: Array of paths to files that should be send alongside with crash report + /// - completion: Backtrace services response. + @objc func send(exception: NSException?, + attachmentPaths: [String], + completion: @escaping ((_ result: BacktraceResult) -> Void)) } +/// Protocol describes logging functionality of `BacktraceClient`. @objc public protocol BacktraceLogging { + + /// Set of logging destinations. @objc var destinations: Set { get set } } diff --git a/Sources/Public/BacktraceClientDelegate.swift b/Sources/Public/BacktraceClientDelegate.swift index 6ddd5aca..aff3abc5 100644 --- a/Sources/Public/BacktraceClientDelegate.swift +++ b/Sources/Public/BacktraceClientDelegate.swift @@ -1,26 +1,30 @@ import Foundation -/// Events produced by BacktraceClient class. +/// Events produced by BacktraceClient class. Delegate of `BacktraceClient` can be notified about sending report status. @objc public protocol BacktraceClientDelegate: class { - /// Event execute before sending report data to Backtrace services. + /// Event execute before sending report data to Backtrace services. Allows the delegate to modify report right + /// before send. /// /// - Parameter report: Backtrace report to send /// - Returns: Modified Backtrace report. @objc optional func willSend(_ report: BacktraceReport) -> BacktraceReport - /// Event executed before HTTP request to Backtrace services. + /// Event executed before HTTP request to Backtrace services. Allows the delegate to modify request right before + /// send. /// /// - Parameter request: HTTP request to send /// - Returns: Modified HTTP request. @objc optional func willSendRequest(_ request: URLRequest) -> URLRequest - /// Event executed after receiving HTTP response from Backtrace services. + /// Event executed after receiving HTTP response from Backtrace services. Allows the delegate to react on sending + /// report result. /// /// - Parameter result: Backtrace result. @objc optional func serverDidResponse(_ result: BacktraceResult) - /// Event executed when connection to Backtrace services failed. + /// Event executed when connection to Backtrace services failed. Allows the delegate to react on sending report + /// failure. /// /// - Parameter error: Error containing information about the failure cause. @objc optional func connectionDidFail(_ error: Error) diff --git a/Sources/Public/BacktraceLogger.swift b/Sources/Public/BacktraceLogger.swift index 97249cef..7b569f38 100644 --- a/Sources/Public/BacktraceLogger.swift +++ b/Sources/Public/BacktraceLogger.swift @@ -2,10 +2,15 @@ import Foundation /// Logging levels. @objc public enum BacktraceLogLevel: Int { + /// All logs logged to the desination. case debug + /// Warnings, info and errors logged to the desination. case warning + /// Info and errors logged to the desination. case info + /// Only errors logged to the desination. case error + /// No logs logged to the desination. case none fileprivate func desc() -> String { @@ -26,9 +31,12 @@ import Foundation /// Logs Backtrace events. @objc public class BacktraceLogger: NSObject { + + /// Set of logging destinations. Defaultly, only Xcode console. Use `setDestinations(destinations:)` to replace + /// destiantions. static var destinations: Set = [BacktraceFencyConsoleDestination(level: .debug)] - /// Replaces the logging destinations + /// Replaces the logging destinations. /// /// - Parameter destinations: Logging destinations. @objc public class func setDestinations(destinations: Set) { @@ -64,7 +72,11 @@ import Foundation @objc open class BacktraceBaseDestination: NSObject { private let level: BacktraceLogLevel - + + /// Initialize `BacktraceBaseDestination` with given level. + /// + /// - Parameters: + /// - level: logging level @objc public init(level: BacktraceLogLevel) { self.level = level } @@ -101,6 +113,14 @@ import Foundation } //swiftlint:disable line_length + /// Logs the event to console destination. Formats log in custom, fency way. + /// + /// - Parameters: + /// - level: logging level + /// - msg: message to log + /// - file: the name of the file in which it appears + /// - function: the name of the declaration in which it appears + /// - line: the line number on which it appears override public func log(level: BacktraceLogLevel, msg: String, file: String = #file, function: String = #function, line: Int = #line) { print("\(BacktraceFencyConsoleDestination.dateFormatter.string(from: Date())) [\(level.desc()) Backtrace] [\(URL(fileURLWithPath: file).lastPathComponent)]:\(line) \(function) -> \(msg)") } @@ -111,6 +131,14 @@ import Foundation @objc final public class BacktraceConsoleDestination: BacktraceBaseDestination { //swiftlint:disable line_length + /// Logs the event to console destination. + /// + /// - Parameters: + /// - level: logging level + /// - msg: message to log + /// - file: the name of the file in which it appears + /// - function: the name of the declaration in which it appears + /// - line: the line number on which it appears override public func log(level: BacktraceLogLevel, msg: String, file: String = #file, function: String = #function, line: Int = #line) { print("\(Date()) [Backtrace]: \(msg)") } diff --git a/Sources/Public/BacktraceResult.swift b/Sources/Public/BacktraceResult.swift index b1035096..fc5adfb8 100644 --- a/Sources/Public/BacktraceResult.swift +++ b/Sources/Public/BacktraceResult.swift @@ -12,28 +12,17 @@ import Foundation /// Result status. @objc public var backtraceStatus: BacktraceReportStatus - init(_ status: BacktraceReportStatus, message: String, backtraceReport: BacktraceReport? = nil) { - self.message = message + init(_ status: BacktraceReportStatus, report: BacktraceReport? = nil, message: String? = nil) { + self.message = message ?? status.description self.backtraceStatus = status - self.report = backtraceReport + self.report = report super.init() } - - class func serverError(_ message: String, backtraceReport: BacktraceReport? = nil) -> BacktraceResult { - return BacktraceResult(.serverError, message: message, backtraceReport: backtraceReport) - } - - class func unknownError(_ backtraceResult: BacktraceResult? = nil) -> BacktraceResult { - return BacktraceResult(.unknownError, message: "Unknown error", - backtraceReport: backtraceResult?.report) - } - - class func limitReached(_ backtraceReport: BacktraceReport? = nil) -> BacktraceResult { - return BacktraceResult(.limitReached, message: "Limit reached.", backtraceReport: backtraceReport) - } } extension BacktraceResult { + + /// Description of `BacktraceResult` override open var description: String { return """ diff --git a/Sources/Public/Internal/BacktraceReportStatus.swift b/Sources/Public/Internal/BacktraceReportStatus.swift index c985207e..9a57a672 100644 --- a/Sources/Public/Internal/BacktraceReportStatus.swift +++ b/Sources/Public/Internal/BacktraceReportStatus.swift @@ -6,8 +6,8 @@ import Foundation case serverError /// Successfully sent data to server case ok - /// Client is not registered. - case notRegistered + /// Debugger is attached. + case debuggerAttached /// Unknown error occurred. case unknownError /// Client limit reached. @@ -21,8 +21,8 @@ extension BacktraceReportStatus: CustomStringConvertible { return "serverError" case .ok: return "ok" - case .notRegistered: - return "notRegistered" + case .debuggerAttached: + return "Application does not allow for attaching the debugger" case .unknownError: return "unknownError" case .limitReached: diff --git a/Sources/Public/Internal/CrashReporting.swift b/Sources/Public/Internal/CrashReporting.swift index fd3365df..05deeacd 100644 --- a/Sources/Public/Internal/CrashReporting.swift +++ b/Sources/Public/Internal/CrashReporting.swift @@ -1,7 +1,8 @@ import Foundation protocol CrashReporting { - func generateLiveReport(exception: NSException?, attributes: Attributes) throws -> BacktraceReport + func generateLiveReport(exception: NSException?, attributes: Attributes, + attachmentPaths: [String]) throws -> BacktraceReport func pendingCrashReport() throws -> BacktraceReport func purgePendingCrashReport() throws func hasPendingCrashes() -> Bool diff --git a/Sources/Public/Internal/DebuggerChecker.swift b/Sources/Public/Internal/DebuggerChecker.swift new file mode 100644 index 00000000..8f2e3e69 --- /dev/null +++ b/Sources/Public/Internal/DebuggerChecker.swift @@ -0,0 +1,23 @@ +import Foundation + +protocol DebuggerChecking { + static func isAttached() -> Bool +} + +struct DebuggerChecker: DebuggerChecking { + + /// Check if the debugger is attachedto the current process. + /// - see more: https://stackoverflow.com/a/4746378/6651241 + /// + /// - Returns: `true` if the debugger is attached, `false` otherwise + static func isAttached() -> Bool { + + var kinfo = kinfo_proc() + var size = MemoryLayout.stride + var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] + + sysctl(&mib, u_int(mib.count), &kinfo, &size, nil, 0) + + return (kinfo.kp_proc.p_flag & P_TRACED) != 0 + } +} diff --git a/Sources/Public/Internal/ReportingPolicy.swift b/Sources/Public/Internal/ReportingPolicy.swift new file mode 100644 index 00000000..6be16584 --- /dev/null +++ b/Sources/Public/Internal/ReportingPolicy.swift @@ -0,0 +1,19 @@ +import Foundation + +struct ReportingPolicy { + let configuration: BacktraceClientConfiguration + let debuggerChecker: DebuggerChecking.Type + + init(configuration: BacktraceClientConfiguration, debuggerChecker: DebuggerChecking.Type = DebuggerChecker.self) { + self.configuration = configuration + self.debuggerChecker = debuggerChecker + } + + var allowsReporting: Bool { + // iSDebugger / allowsDebugger | 0 | 1 + // 0 | 1 | 1 + // 1 | 0 | 1 + // + return !debuggerChecker.isAttached() || configuration.allowsAttachingDebugger + } +} diff --git a/Sources/Public/Internal/RequestType.swift b/Sources/Public/Internal/RequestType.swift index f8ec52b5..218f4606 100644 --- a/Sources/Public/Internal/RequestType.swift +++ b/Sources/Public/Internal/RequestType.swift @@ -13,7 +13,7 @@ extension RequestType { urlComponents?.queryItems = queryItems.map(URLQueryItem.init) guard let finalUrl = urlComponents?.url else { - BacktraceLogger.error("Malformed error") + BacktraceLogger.error("Malformed url") throw HttpError.malformedUrl } var request = URLRequest(url: finalUrl) diff --git a/Sources/Public/Internal/SignalContext.swift b/Sources/Public/Internal/SignalContext.swift index 3c537e79..3c9e26d7 100644 --- a/Sources/Public/Internal/SignalContext.swift +++ b/Sources/Public/Internal/SignalContext.swift @@ -4,4 +4,5 @@ protocol SignalContext: CustomStringConvertible { var attributes: Attributes { get } var userAttributes: Attributes { get set } var defaultAttributes: Attributes { get } + func set(faultMessage: String?) } diff --git a/Sources/Public/Internal/Store.swift b/Sources/Public/Internal/Store.swift new file mode 100644 index 00000000..cb8c1006 --- /dev/null +++ b/Sources/Public/Internal/Store.swift @@ -0,0 +1,23 @@ +import Foundation + +protocol Store { + static func store(_ value: T, forKey key: String) throws + static func value(forKey key: String) throws -> T? + static func removeValue(forKey key: String) throws +} + +struct UserDefaultsStore: Store { + private static let userDefaults = UserDefaults.standard + + static func store(_ value: T, forKey: String) { + userDefaults.set(value, forKey: forKey) + } + + static func value(forKey key: String) -> T? { + return userDefaults.value(forKey: key) as? T + } + + static func removeValue(forKey key: String) { + userDefaults.removeObject(forKey: key) + } +} diff --git a/Tests/AttachmentTests.swift b/Tests/AttachmentTests.swift new file mode 100644 index 00000000..a463bd54 --- /dev/null +++ b/Tests/AttachmentTests.swift @@ -0,0 +1,40 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class AttachmentTests: QuickSpec { + + override func spec() { + describe("Attachments") { + it("cannot be created from non-existing file", closure: { + expect(Attachment(filePath: "")).to(beNil()) + }) + + it("can be created from exisiting file", closure: { + let bundle = Bundle(for: type(of: self)) + let path = bundle.path(forResource: "test", ofType: "txt") + if let path = path { + expect(Attachment(filePath: path)).toNot(beNil()) + } else { + fail() + } + }) + + context("Attachment exists", closure: { + let bundle = Bundle(for: type(of: self)) + let path = bundle.path(forResource: "test", ofType: "txt") + if let path = path, let attachment = Attachment(filePath: path) { + it("has mime type: text/plain", closure: { + expect(attachment.mimeType).to(equal("text/plain")) + expect(attachment.data).toNot(beNil()) + expect(attachment.name).to(contain(["attachment_test_"])) + }) + } else { + fail() + } + }) + } + } +} diff --git a/Tests/AttributesTests.swift b/Tests/AttributesProviderTests.swift similarity index 80% rename from Tests/AttributesTests.swift rename to Tests/AttributesProviderTests.swift index b178a8da..cd77c99c 100644 --- a/Tests/AttributesTests.swift +++ b/Tests/AttributesProviderTests.swift @@ -4,11 +4,11 @@ import Nimble import Quick @testable import Backtrace -final class AttributesTests: QuickSpec { +final class AttributesProviderTests: QuickSpec { override func spec() { - describe("Collecting attributes") { - it("Collects default attributes", closure: { + describe("Attributes provider") { + it("has default values", closure: { let attributesProvider = AttributesProvider() expect(attributesProvider.defaultAttributes).toNot(beEmpty()) @@ -16,7 +16,7 @@ final class AttributesTests: QuickSpec { expect(attributesProvider.userAttributes).to(beEmpty()) }) - it("Appends client attribute", closure: { + it("allows the user add new attributes", closure: { let attributesProvider = AttributesProvider() attributesProvider.userAttributes["foo"] = "bar" diff --git a/Tests/BacktraceApiTests.swift b/Tests/BacktraceApiTests.swift new file mode 100644 index 00000000..e1d30be2 --- /dev/null +++ b/Tests/BacktraceApiTests.swift @@ -0,0 +1,93 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class BatraceApiTests: QuickSpec { + //swiftlint:disable function_body_length + override func spec() { + describe("Api") { + context("has valid endpoint and token", closure: { + let endpoint = URL(string: "https://www.backtrace.io")! + let token = "token" + let urlSession = URLSessionMock() + let api = BacktraceApi(endpoint: endpoint, token: token, session: urlSession, reportsPerMin: 3) + + it("has no delegate attached", closure: { + expect(api.delegate).to(beNil()) + }) + + it("has delegate attached", closure: { + let delegate = BacktraceClientDelegateMock() + api.delegate = delegate + expect(api.delegate).toNot(beNil()) + }) + + it("has empty timestamps list", closure: { + expect(api.successfulSendTimestamps).to(beEmpty()) + }) + + context("has well formed backtrace report", closure: { + let crashReporter = CrashReporter() + + it("returns 200 response", closure: { + expect { () -> BacktraceReportStatus in + let backtraceReport = try crashReporter.generateLiveReport(attributes: [:]) + urlSession.response = MockOkResponse(url: endpoint) + return try api.send(backtraceReport).backtraceStatus + }.to(equal(BacktraceReportStatus.ok)) + }) + + it("can modify the report before sending", closure: { + let delegate = BacktraceClientDelegateMock() + let attachmentPaths = ["path1", "path2"] + delegate.willSendClosure = { + $0.attachmentPaths = attachmentPaths + return $0 + } + api.delegate = delegate + expect { () -> [String] in + let backtraceReport = try crashReporter.generateLiveReport(attributes: [:]) + urlSession.response = MockOkResponse(url: endpoint) + return try api.send(backtraceReport).report?.attachmentPaths ?? [] + }.to(equal(attachmentPaths)) + }) + }) + + context("has invalid token", { + let crashReporter = CrashReporter() + + it("returns 403 response", closure: { + expect { () -> BacktraceReportStatus in + let backtraceReport = try crashReporter.generateLiveReport(attributes: [:]) + urlSession.response = Mock403Response(url: endpoint) + return try api.send(backtraceReport).backtraceStatus + }.to(equal(BacktraceReportStatus.serverError)) + }) + }) + + context("has no Internet connection", { + let crashReporter = CrashReporter() + + it("returns error", closure: { + expect { () -> BacktraceReportStatus in + let backtraceReport = try crashReporter.generateLiveReport(attributes: [:]) + urlSession.response = MockConnectionErrorResponse(url: endpoint) + return try api.send(backtraceReport).backtraceStatus + }.to(throwError()) + }) + + it("returns no response", closure: { + expect { () -> BacktraceReportStatus in + let backtraceReport = try crashReporter.generateLiveReport(attributes: [:]) + urlSession.response = MockNoResponse() + return try api.send(backtraceReport).backtraceStatus + }.to(throwError(HttpError.unknownError)) + }) + }) + }) + } + } + //swiftlint:enable function_body_length +} diff --git a/Tests/BacktraceClientTests.swift b/Tests/BacktraceClientTests.swift new file mode 100644 index 00000000..c6540978 --- /dev/null +++ b/Tests/BacktraceClientTests.swift @@ -0,0 +1,77 @@ +import XCTest + +import Nimble +import Quick +import Backtrace_PLCrashReporter +@testable import Backtrace + +final class BacktraceClientTests: QuickSpec { + + //swiftlint:disable function_body_length + override func spec() { + + describe("Backtrace client") { + throwingContext("Initalized with default values", closure: { + guard let endpoint = URL(string: "https://wwww.backtrace.io") else { fail(); return } + let token = "token" + let credentials = BacktraceCredentials(endpoint: endpoint, token: token) + + it("has all valid credentials", closure: { + expect(credentials.endpoint).to(be(endpoint)) + expect(credentials.token).to(be(token)) + }) + + it("has default database settings", closure: { + let defaultDbSettings = BacktraceDatabaseSettings() + expect(defaultDbSettings.maxDatabaseSize).to(be(0)) + expect(defaultDbSettings.maxRecordCount).to(be(0)) + expect(defaultDbSettings.retryInterval).to(be(5)) + expect(defaultDbSettings.retryLimit).to(be(3)) + expect(defaultDbSettings.retryBehaviour.rawValue).to(be(RetryBehaviour.interval.rawValue)) + expect(defaultDbSettings.retryOrder.rawValue).to(be(RetryOder.queue.rawValue)) + expect(defaultDbSettings.maxDatabaseSizeInBytes).to(be(0)) + }) + + it("has default configuration", closure: { + let dbSettings = BacktraceDatabaseSettings() + let reportsPerMin = 3 + let configuration = BacktraceClientConfiguration(credentials: credentials, dbSettings: dbSettings, + reportsPerMin: reportsPerMin) + expect(configuration.credentials).to(be(credentials)) + expect(configuration.reportsPerMin).to(be(reportsPerMin)) + expect(configuration.dbSettings).to(be(dbSettings)) + }) + + it("can create instance of BacktraceClient", closure: { + expect { try BacktraceClient(credentials: credentials) }.notTo(throwError()) + }) + + it("modifies the default values", closure: { + let customDbSettings = BacktraceDatabaseSettings() + let maxRecordCount = 10 + let maxDatabaseSize = 10 + let retryInterval = 10 + let retryBehaviour = RetryBehaviour.interval + let retryOrder = RetryOder.stack + let retryLimit = 10 + + customDbSettings.maxRecordCount = maxRecordCount + customDbSettings.maxDatabaseSize = maxDatabaseSize + customDbSettings.retryInterval = retryInterval + customDbSettings.retryBehaviour = retryBehaviour + customDbSettings.retryOrder = retryOrder + customDbSettings.retryLimit = retryLimit + + expect(customDbSettings.maxDatabaseSize).to(be(maxDatabaseSize)) + expect(customDbSettings.maxRecordCount).to(be(maxRecordCount)) + expect(customDbSettings.retryInterval).to(be(retryInterval)) + expect(customDbSettings.retryLimit).to(be(retryLimit)) + expect(customDbSettings.retryBehaviour.rawValue).to(be(retryBehaviour.rawValue)) + expect(customDbSettings.retryOrder.rawValue).to(be(retryOrder.rawValue)) + expect(customDbSettings.maxDatabaseSizeInBytes).to(be(1024 * 1024 * maxDatabaseSize)) + }) + }) + } + } + //swiftlint:enable function_body_length +} diff --git a/Tests/BacktraceDatabaseTests.swift b/Tests/BacktraceDatabaseTests.swift index 2ad2763e..675552a3 100644 --- a/Tests/BacktraceDatabaseTests.swift +++ b/Tests/BacktraceDatabaseTests.swift @@ -6,29 +6,43 @@ final class BacktraceDatabaseTests: QuickSpec { override func spec() { describe("Crash reporter") { - let crashReporter = CrashReporter() - do { + throwingContext("has all dependencies and empty database", closure: { + let crashReporter = CrashReporter() let repository = try PersistentRepository(settings: BacktraceDatabaseSettings()) - try repository.clear() - for _ in 0...100 { + + throwingIt("can clear database", closure: { + try repository.clear() + }) + + throwingIt("can save reports which matches to the latest saved one", closure: { + let report = try crashReporter.generateLiveReport(attributes: [:]) + try repository.save(report) + if let fetchedReport = try repository.getLatest().first { + expect(fetchedReport.reportData).to(equal(report.reportData)) + } + }) + + throwingIt("can add new report and remove it", closure: { + try repository.clear() let report = try crashReporter.generateLiveReport(attributes: [:]) try repository.save(report) - } + expect { try repository.countResources() }.to(equal(1)) + if let fetchedReport = try repository.getLatest().first { + expect(fetchedReport.reportData).to(equal(report.reportData)) + try repository.delete(fetchedReport) + expect { try repository.countResources() }.to(equal(0)) + } else { + fail() + } + }) - it("Last report", closure: { - do { + throwingIt("can add 100 new reports", closure: { + for _ in 0...100 { let report = try crashReporter.generateLiveReport(attributes: [:]) try repository.save(report) - if let fetchedReport = try repository.getLatest().first { - expect(fetchedReport.reportData).to(equal(report.reportData)) - } - } catch { - } }) - } catch { - fail(error.localizedDescription) - } + }) } } } diff --git a/Tests/BacktraceFileManagerTests.swift b/Tests/BacktraceFileManagerTests.swift new file mode 100644 index 00000000..2c3bfaec --- /dev/null +++ b/Tests/BacktraceFileManagerTests.swift @@ -0,0 +1,60 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class BacktraceFileManagerTests: QuickSpec { + + override func spec() { + describe("File manager") { + throwingContext("Exclude from context", closure: { + it("non-existing file", closure: { + let nonExistingFile = URL(fileURLWithPath: "nonExisitingFile") + expect { + try BacktraceFileManager.excludeFromBackup(nonExistingFile) + }.to(throwError(FileError.fileNotExists)) + }) + + it("http url", closure: { + guard let httpUrl = URL(string: "http://backtrace.io") else { fail(); return } + expect { + try BacktraceFileManager.excludeFromBackup(httpUrl) + }.to(throwError(FileError.unsupportedScheme)) + }) + it("existing file", closure: { + let bundle = Bundle(for: type(of: self)) + guard let path = bundle.path(forResource: "test", ofType: "txt") else { fail(); return } + let url = URL(fileURLWithPath: path) + expect { + try BacktraceFileManager.excludeFromBackup(url) + }.toNot(throwError()) + }) + }) + + throwingContext("Size of file", closure: { + it("non-existing file", closure: { + let nonExistingFile = URL(fileURLWithPath: "nonExisitingFile") + expect { + try BacktraceFileManager.sizeOfFile(at: nonExistingFile) + }.to(throwError(FileError.fileNotExists)) + }) + + it("http url", closure: { + guard let httpUrl = URL(string: "http://backtrace.io") else { fail(); return } + expect { + try BacktraceFileManager.sizeOfFile(at: httpUrl) + }.to(throwError(FileError.unsupportedScheme)) + }) + it("existing file", closure: { + let bundle = Bundle(for: type(of: self)) + guard let path = bundle.path(forResource: "test", ofType: "txt") else { fail(); return } + let url = URL(fileURLWithPath: path) + expect { + try BacktraceFileManager.sizeOfFile(at: url) + }.toNot(throwError()) + }) + }) + } + } +} diff --git a/Tests/BacktraceTests.swift b/Tests/BacktraceTests.swift index 9e7839ce..51aa5254 100644 --- a/Tests/BacktraceTests.swift +++ b/Tests/BacktraceTests.swift @@ -13,47 +13,50 @@ final class BacktraceTests: QuickSpec { }) it("generate live report 10 times", closure: { for _ in 0...10 { - expect{ try crashReporter.generateLiveReport(attributes: [:]) } + expect { try crashReporter.generateLiveReport(attributes: [:]) } .toNot(throwError()) } }) describe("Backtrace API") { - describe("Valid credentials", closure: { + context("has valid credentials", closure: { var networkClientWithValidCredentials: BacktraceApiProtocol { - return BacktraceNetworkClientMock(config: .validCredentials) + return BacktraceApiMock(config: .validCredentials) } it("sends crash report", closure: { - expect { try networkClientWithValidCredentials.send(try crashReporter.generateLiveReport(attributes: [:]))} - .toNotEventually(throwError(), timeout: 10, pollInterval: 0.5, description: "Crash report should be successfully sent.") + expect { try networkClientWithValidCredentials + .send(try crashReporter.generateLiveReport(attributes: [:]))} + .toNotEventually(throwError(), timeout: 10, pollInterval: 0.5) }) }) - describe("Invalid endpoint", closure: { + context("has invalid endpoint", closure: { var networkClientWithInvalidEndpoint: BacktraceApiProtocol { - return BacktraceNetworkClientMock(config: .invalidEndpoint) + return BacktraceApiMock(config: .invalidEndpoint) } it("fails to send crash report with invalid endpoint", closure: { - expect { try networkClientWithInvalidEndpoint.send(try crashReporter.generateLiveReport(attributes: [:]))} + expect { try networkClientWithInvalidEndpoint + .send(try crashReporter.generateLiveReport(attributes: [:]))} .toEventually(throwError()) }) it("throws error while trying to send crash report", closure: { let error = HttpError.unknownError - expect { try BacktraceReporter(reporter: crashReporter, api: networkClientWithInvalidEndpoint, dbSettings: BacktraceDatabaseSettings(), reportsPerMin: 3).send() } - .toEventually(throwError(), timeout: 10, pollInterval: 0.5, description: "Should fail to send a crash report") + expect { try BacktraceReporter(reporter: crashReporter, api: networkClientWithInvalidEndpoint, + dbSettings: BacktraceDatabaseSettings(), + reportsPerMin: 3).send() } + .toEventually(throwError(), timeout: 10, pollInterval: 0.5) }) }) - describe("Invalid token", closure: { + context("has invalid token", closure: { var networkClientWithInvalidToken: BacktraceApiProtocol { - return BacktraceNetworkClientMock(config: .invalidToken) + return BacktraceApiMock(config: .invalidToken) } it("fails to send crash report with invalid token", closure: { - do { + expect { let report = try crashReporter.generateLiveReport(attributes: [:]) - expect { try networkClientWithInvalidToken.send(report).backtraceStatus} - .toEventually(equal(BacktraceReportStatus.serverError), timeout: 10, pollInterval: 0.5, description: "Status code should be 403 - Forbidden.") - } catch { - fail(error.localizedDescription) - } + return try networkClientWithInvalidToken.send(report).backtraceStatus + } + .toEventually(equal(BacktraceReportStatus.serverError), timeout: 10, pollInterval: 0.5, + description: "Status code should be 403 - Forbidden.") }) }) } diff --git a/Tests/BacktraceWatcherTests.swift b/Tests/BacktraceWatcherTests.swift new file mode 100644 index 00000000..4484380b --- /dev/null +++ b/Tests/BacktraceWatcherTests.swift @@ -0,0 +1,199 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class BacktraceWatcherTests: QuickSpec { + //swiftlint:disable function_body_length + override func spec() { + describe("Watcher") { + let dbSettings = BacktraceDatabaseSettings() + let networkClientMockConfig = BacktraceApiMock.Configuration.validCredentials + let api = BacktraceApiMock(config: networkClientMockConfig) + let repository = WatcherRepositoryMock() + + context("when passed correct parameters") { + it("then initialize without throwing error") { + expect { + try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + }.notTo(throwError()) + } + + throwingIt("then pass prameters properly") { + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + expect(watcher.settings).to(be(dbSettings)) + expect(watcher.reportsPerMin).to(equal(3)) + expect(watcher.api).to(be(api)) + expect(watcher.repository).to(be(repository)) + expect(watcher.batchSize).to(equal(3)) + expect(watcher.timer).toNot(beNil()) + } + + context("with RetryBehaviour.none") { + throwingIt("then timer should be nil") { + dbSettings.retryBehaviour = .none + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + expect(watcher.timer).to(beNil()) + } + } + } + + context("when configure timer with handler") { + throwingIt("then timer fires handler") { + dbSettings.retryInterval = 1 + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + watcher.resetTimer() + + waitUntil(timeout: TimeInterval(dbSettings.retryInterval + 1)) { done in + watcher.configureTimer(with: DispatchWorkItem(block: { + done() + })) + } + } + } + + describe("retrive crashes from repository") { + throwingBeforeEach { + try repository.clear() + } + + context("when retrive") { + throwingIt("then not throw error") { + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + try repository.save(BacktraceWatcherTests.backtraceReport(for: ["testOrder": 1])) + + expect { try watcher.crashReportsFromRepository(limit: 1) }.toNot(throwError()) + } + } + + context("when retrive in queue order") { + throwingIt("then get oldest") { + dbSettings.retryOrder = .queue + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + let firstReport = try BacktraceWatcherTests.backtraceReport(for: ["testOrder": 1]) + try repository.save(firstReport) + let secondReport = try BacktraceWatcherTests.backtraceReport(for: ["testOrder": 2]) + try repository.save(secondReport) + let thirdReport = try BacktraceWatcherTests.backtraceReport(for: ["testOrder": 3]) + try repository.save(thirdReport) + + let reports = try watcher.crashReportsFromRepository(limit: 2) + expect(reports.count).to(equal(2)) + expect(reports).toNot(contain(firstReport)) + expect(reports).to(contain(secondReport, thirdReport)) + } + } + + context("when retrive in stack order ") { + throwingIt("then get latest") { + dbSettings.retryOrder = .stack + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + let firstReport = try BacktraceWatcherTests.backtraceReport(for: ["testOrder": 1]) + try repository.save(firstReport) + let secondReport = try BacktraceWatcherTests.backtraceReport(for: ["testOrder": 2]) + try repository.save(secondReport) + let thirdReport = try BacktraceWatcherTests.backtraceReport(for: ["testOrder": 3]) + try repository.save(thirdReport) + + let reports = try watcher.crashReportsFromRepository(limit: 2) + + expect(reports.count).to(equal(2)) + expect(reports).toNot(contain(thirdReport)) + expect(reports).to(contain(firstReport, secondReport)) + } + } + } + + describe("batch retry") { + throwingBeforeEach { + try repository.clear() + } + + context("when send one-element batch successfully") { + throwingIt("then watcher not throwing error") { + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + try repository.save(BacktraceWatcherTests.backtraceReport(for: ["testOrder": 1])) + + expect { try watcher.batchRetry() }.toNot(throwError()) + } + + throwingIt("then report is removed from repository") { + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + try repository.save(BacktraceWatcherTests.backtraceReport(for: ["testOrder": 1])) + try watcher.batchRetry() + + expect(try watcher.repository.countResources()).to(equal(0)) + } + } + + context("when send two-element batch successfully") { + throwingIt("then all sent reports are removed from repository") { + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, api: api, + repository: repository, batchSize: 3) + try repository.save(BacktraceWatcherTests.backtraceReport(for: ["testOrder": 1])) + try repository.save(BacktraceWatcherTests.backtraceReport(for: ["testOrder": 2])) + try watcher.batchRetry() + + expect(try watcher.repository.countResources()).to(equal(0)) + } + } + + context("when connection error") { + throwingIt("then do nothing") { + let networkClientMockConfig = BacktraceApiMock.Configuration.invalidEndpoint + let failureNetworkClient = BacktraceApiMock(config: networkClientMockConfig) + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, + api: failureNetworkClient, repository: repository, + batchSize: 3) + try repository.save(BacktraceWatcherTests.backtraceReport(for: ["testOrder": 1])) + + try watcher.batchRetry() + expect(try watcher.repository.countResources()).to(equal(1)) + } + } + + context("when limit reached error") { + let networkClientMockConfig = BacktraceApiMock.Configuration.limitReached + let failureNetworkClient = BacktraceApiMock(config: networkClientMockConfig) + throwingIt("then report is not removed from repository") { + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, + api: failureNetworkClient, repository: repository, + batchSize: 3) + try repository.save(BacktraceWatcherTests.backtraceReport(for: ["testOrder": 1])) + + try watcher.batchRetry() + expect(try watcher.repository.countResources()).to(equal(1)) + } + + throwingIt("then increment counter") { + let watcher = try BacktraceWatcher(settings: dbSettings, reportsPerMin: 3, + api: failureNetworkClient, repository: repository, + batchSize: 3) + let report = try BacktraceWatcherTests.backtraceReport(for: ["testOrder": 1]) + try repository.save(report) + + expect(watcher.repository.retryCount(for: report)).to(equal(0)) + try watcher.batchRetry() + expect(watcher.repository.retryCount(for: report)).to(equal(1)) + } + } + } + } + //swiftlint:enable function_body_length + } + + private static func backtraceReport(for attributes: Attributes) throws -> BacktraceReport { + let crashReporter = CrashReporter() + return try crashReporter.generateLiveReport(attributes: attributes) + } +} diff --git a/Tests/CrashReporterTests.swift b/Tests/CrashReporterTests.swift new file mode 100644 index 00000000..b89d58b8 --- /dev/null +++ b/Tests/CrashReporterTests.swift @@ -0,0 +1,19 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class CrashReporterTests: QuickSpec { + + override func spec() { + describe("Crash reporter") { + let crashReporter = CrashReporter() + it("has no pending crashes", closure: { + expect(crashReporter.hasPendingCrashes()).to(beFalse()) + expect { try crashReporter.pendingCrashReport() }.to(throwError()) + expect { try crashReporter.purgePendingCrashReport() }.to(throwError()) + }) + } + } +} diff --git a/Tests/DispatcherTests.swift b/Tests/DispatcherTests.swift new file mode 100644 index 00000000..e0996dca --- /dev/null +++ b/Tests/DispatcherTests.swift @@ -0,0 +1,25 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class DispatcherTests: QuickSpec { + + override func spec() { + describe("Dispatcher") { + it("calls the completion closure", closure: { + let dispatcher = Dispatcher() + var closureCalled = false + var finished = false + dispatcher.dispatch({ + closureCalled = true + }, completion: { + finished = true + }) + expect(finished).toEventually(beTrue(), timeout: 5, pollInterval: 0.1) + expect(closureCalled).toEventually(beTrue(), timeout: 5, pollInterval: 0.1) + }) + } + } +} diff --git a/Tests/Helpers/Quick+Throws.swift b/Tests/Helpers/Quick+Throws.swift new file mode 100644 index 00000000..3c06e12f --- /dev/null +++ b/Tests/Helpers/Quick+Throws.swift @@ -0,0 +1,157 @@ +import XCTest + +import Nimble +import Quick + +// MARK: Throwing extension for most common Quick methods. + +/** + A closure executed before an example is run. + */ +public typealias ThrowingBeforeExampleClosure = () throws -> Void + +/** + A closure executed before an example is run. The closure is given example metadata, + which contains information about the example that is about to be run. + */ +public typealias ThrowingBeforeExampleWithMetadataClosure = (_ exampleMetadata: ExampleMetadata) throws -> Void + +/** + A closure executed after an example is run. + */ +public typealias ThrowingAfterExampleClosure = ThrowingBeforeExampleClosure + +/** + A closure executed after an example is run. The closure is given example metadata, + which contains information about the example that has just finished running. + */ +public typealias ThrowingAfterExampleWithMetadataClosure = ThrowingBeforeExampleWithMetadataClosure + +// MARK: Suite Hooks + +/** + A closure executed before any examples are run. + */ +public typealias ThrowingBeforeSuiteClosure = ThrowingBeforeExampleClosure + +/** + A closure executed after all examples have finished running. + */ +public typealias ThrowingAfterSuiteClosure = ThrowingBeforeSuiteClosure + +public func throwingContext(_ description: String, flags: FilterFlags = [:], closure: () throws -> Void) { + context(description, flags: flags, closure: { + do { + try closure() + } catch { + fail(error.localizedDescription) + } + }) +} + +public func throwingIt(_ description: String, flags: FilterFlags = [:], file: String = #file, line: UInt = #line, + closure: @escaping () throws -> Void) { + it(description, flags: flags, file: file, line: line) { + do { + try closure() + } catch { + fail(error.localizedDescription, line: line) + } + } +} + +public func throwingBeforeEach(_ closure: @escaping ThrowingBeforeExampleClosure) { + beforeEach { + do { + try closure() + } catch { + fail(error.localizedDescription) + } + } +} + +/** + Identical to Quick.DSL.beforeEach, except the closure is provided with + metadata on the example that the closure is being run prior to. + */ +public func throwingBeforeEach(_ closure: @escaping ThrowingBeforeExampleWithMetadataClosure) { + beforeEach { (exampleMetadata) in + do { + try closure(exampleMetadata) + } catch { + fail(error.localizedDescription) + } + } +} + +/** + Defines a closure to be run after each example in the current example + group. This closure is not run for pending or otherwise disabled examples. + An example group may contain an unlimited number of afterEach. They'll be + run in the order they're defined, but you shouldn't rely on that behavior. + + - parameter closure: The closure to be run after each example. + */ +public func throwingAfterEach(_ closure: @escaping ThrowingAfterExampleClosure) { + afterEach { + do { + try closure() + } catch { + fail(error.localizedDescription) + } + } +} + +/** + Identical to Quick.DSL.afterEach, except the closure is provided with + metadata on the example that the closure is being run after. + */ +public func throwignAfterEach(_ closure: @escaping ThrowingAfterExampleWithMetadataClosure) { + afterEach { (exampleMetadata) in + do { + try closure(exampleMetadata) + } catch { + fail(error.localizedDescription) + } + } +} + +/** + Defines a closure to be run prior to any examples in the test suite. + You may define an unlimited number of these closures, but there is no + guarantee as to the order in which they're run. + + If the test suite crashes before the first example is run, this closure + will not be executed. + + - parameter closure: The closure to be run prior to any examples in the test suite. + */ +public func throwingBeforeSuite(_ closure: @escaping ThrowingBeforeSuiteClosure) { + beforeSuite { + do { + try closure() + } catch { + fail(error.localizedDescription) + } + } +} + +/** + Defines a closure to be run after all of the examples in the test suite. + You may define an unlimited number of these closures, but there is no + guarantee as to the order in which they're run. + + If the test suite crashes before all examples are run, this closure + will not be executed. + + - parameter closure: The closure to be run after all of the examples in the test suite. + */ +public func throwingAfterSuite(_ closure: @escaping ThrowingAfterSuiteClosure) { + afterSuite { + do { + try closure() + } catch { + fail(error.localizedDescription) + } + } +} diff --git a/Tests/BacktraceNetworkClientMock.swift b/Tests/Mocks/BacktraceApiMock.swift similarity index 60% rename from Tests/BacktraceNetworkClientMock.swift rename to Tests/Mocks/BacktraceApiMock.swift index c7252d81..398a2096 100644 --- a/Tests/BacktraceNetworkClientMock.swift +++ b/Tests/Mocks/BacktraceApiMock.swift @@ -2,8 +2,8 @@ import Foundation @testable import Backtrace -final class BacktraceNetworkClientMock: BacktraceApiProtocol { - var delegate: BacktraceClientDelegate? +final class BacktraceApiMock: BacktraceApiProtocol { + weak var delegate: BacktraceClientDelegate? var successfulSendTimestamps: [TimeInterval] = [] @@ -11,25 +11,33 @@ final class BacktraceNetworkClientMock: BacktraceApiProtocol { case invalidToken case invalidEndpoint case validCredentials + case limitReached } static func invalidTokenResponse(_ report: BacktraceReport) -> BacktraceResult { return BacktraceErrorResponse(error: BacktraceErrorResponse.ResponseError(code: 1897, message: "Forbidden")) - .result(backtraceReport: report) + .result(report: report) } static func invalidCredentials(_ report: BacktraceReport) -> BacktraceResult { - return BacktraceResponse(response: "Ok.", rxid: "xx-xx", fingerprint: "xx-xx", unique: true).result(backtraceReport: report) + return BacktraceResponse(response: "Ok.", rxid: "xx-xx", fingerprint: "xx-xx", unique: true) + .result(report: report) + } + + static func limitReachedResponse(_ report: BacktraceReport) -> BacktraceResult { + return BacktraceResult(.limitReached, report: report) } func send(_ report: BacktraceReport) throws -> BacktraceResult { switch config { case .invalidToken: - return BacktraceNetworkClientMock.invalidTokenResponse(report) + return BacktraceApiMock.invalidTokenResponse(report) case .invalidEndpoint: throw HttpError.unknownError case .validCredentials: - return BacktraceNetworkClientMock.invalidCredentials(report) + return BacktraceApiMock.invalidCredentials(report) + case .limitReached: + return BacktraceApiMock.limitReachedResponse(report) } } diff --git a/Tests/Mocks/DebuggerCheckerMock.swift b/Tests/Mocks/DebuggerCheckerMock.swift new file mode 100644 index 00000000..a43be6b3 --- /dev/null +++ b/Tests/Mocks/DebuggerCheckerMock.swift @@ -0,0 +1,14 @@ +import XCTest +@testable import Backtrace + +struct AttachedDebuggerCheckerMock: DebuggerChecking { + static func isAttached() -> Bool { + return true + } +} + +struct DetachedDebuggerCheckerMock: DebuggerChecking { + static func isAttached() -> Bool { + return false + } +} diff --git a/Tests/Mocks/UrlSessionMock.swift b/Tests/Mocks/UrlSessionMock.swift new file mode 100644 index 00000000..33893cbb --- /dev/null +++ b/Tests/Mocks/UrlSessionMock.swift @@ -0,0 +1,122 @@ +import Foundation +import XCTest +import Backtrace + +typealias VoidClosure = () -> Void + +//based on: https://medium.com/@johnsundell/mocking-in-swift-56a913ee7484 +final class URLSessionMock: URLSession { + typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void + // Properties that enable us to set exactly what data or error + // we want our mocked URLSession to return for any request. + var response: MockResponse? + + override func dataTask(with request: URLRequest, + completionHandler: @escaping CompletionHandler) -> URLSessionDataTask { + return URLSessionDataTaskMock { [weak self] in + guard let self = self else { return } + completionHandler(self.response?.data, self.response?.urlResponse, self.response?.error) + } + } +} + +// We create a partial mock by subclassing the original class +final class URLSessionDataTaskMock: URLSessionDataTask { + private let closure: () -> Void + init(closure: @escaping () -> Void) { + self.closure = closure + } + // We override the 'resume' method and simply call our closure + // instead of actually resuming any task. + override func resume() { + closure() + } +} + +final class BacktraceClientDelegateMock: BacktraceClientDelegate { + + var willSendClosure: ((BacktraceReport) -> BacktraceReport)? + var willSendRequestClosure: ((URLRequest) -> URLRequest)? + var serverDidResponseClosure: ((BacktraceResult) -> Void)? + var connectionDidFailClosure: ((Error) -> Void)? + var didReachLimitClosure: ((BacktraceResult) -> Void)? + + func willSend(_ report: BacktraceReport) -> BacktraceReport { + return willSendClosure?(report) ?? report + } + + func willSendRequest(_ request: URLRequest) -> URLRequest { + return willSendRequestClosure?(request) ?? request + } + + func serverDidResponse(_ result: BacktraceResult) { + serverDidResponseClosure?(result) + } + + func connectionDidFail(_ error: Error) { + connectionDidFailClosure?(error) + } + + func didReachLimit(_ result: BacktraceResult) { + didReachLimitClosure?(result) + } +} + +protocol MockResponse { + var data: Data? { get } + var error: Error? { get } + var urlResponse: URLResponse? { get } +} + +struct MockOkResponse: MockResponse { + let data: Data? + let error: Error? + let urlResponse: URLResponse? + + init(url: URL) { + urlResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: "1.1", headerFields: nil) + let body: [String: Any] = ["response": "ok", + "_rxid": "04000000-4ca4-4002-0000-000000000000", + "fingerprint": "7edeff2cd1c15068c918dcefe7db4301ee6314cee654ef44d8da941a4a75924e", + "unique": false] + data = try? JSONSerialization.data(withJSONObject: body) + error = nil + } +} + +struct Mock403Response: MockResponse { + let data: Data? + let error: Error? + let urlResponse: URLResponse? + + init(url: URL) { + urlResponse = HTTPURLResponse(url: url, statusCode: 403, httpVersion: "1.1", headerFields: nil) + let body = ["error": ["code": 6, "message": "invalid token"]] + data = try? JSONSerialization.data(withJSONObject: body) + error = nil + } +} + +struct MockConnectionErrorResponse: MockResponse { + let data: Data? + let error: Error? + let urlResponse: URLResponse? + + init(url: URL) { + urlResponse = nil + data = nil + error = NSError(domain: "backtrace.connection.error", code: 100, userInfo: nil) + } +} + +struct MockNoResponse: MockResponse { + let data: Data? + let error: Error? + let urlResponse: URLResponse? + + init() { + urlResponse = nil + data = nil + error = nil + } +} diff --git a/Tests/Mocks/WatcherRepositoryMock.swift b/Tests/Mocks/WatcherRepositoryMock.swift new file mode 100644 index 00000000..5dcbb3fb --- /dev/null +++ b/Tests/Mocks/WatcherRepositoryMock.swift @@ -0,0 +1,64 @@ +import Foundation +@testable import Backtrace + +final class WatcherRepositoryMock { + class StoredResource { + let resource: Resource + var retryCount: Int = 0 + + init(_ resource: Resource) { + self.resource = resource + } + } + var storage: [StoredResource] = [] + + func retryCount(for resource: Resource) -> Int { + if let idx = storage.firstIndex(where: { $0.resource == resource }) { + return storage[idx].retryCount + } + return 0 + } +} + +extension WatcherRepositoryMock: Repository { + + func save(_ resource: Resource) throws { + storage.append(StoredResource(resource)) + } + + func delete(_ resource: Resource) throws { + if let idx = storage.firstIndex(where: { $0.resource == resource }) { + storage.remove(at: idx) + } + } + + func getAll() throws -> [Resource] { + return storage.map { $0.resource } + } + + func get(sortDescriptors: [NSSortDescriptor]?, predicate: NSPredicate?, fetchLimit: Int?) throws -> [Resource] { + return [] + } + + func incrementRetryCount(_ resource: Resource, limit: Int) throws { + if let idx = storage.firstIndex(where: { $0.resource == resource }) { + storage[idx].retryCount += 1 + } + } + + func getLatest(count: Int) throws -> [Resource] { + return Array(storage.map { $0.resource }.prefix(count)) + } + + func getOldest(count: Int) throws -> [Resource] { + return Array(storage.map { $0.resource }.suffix(count)) + } + + func countResources() throws -> Int { + return storage.count + } + + func clear() throws { + storage.removeAll() + } +} diff --git a/Tests/ReportingPolicyTests.swift b/Tests/ReportingPolicyTests.swift new file mode 100644 index 00000000..5a63ca84 --- /dev/null +++ b/Tests/ReportingPolicyTests.swift @@ -0,0 +1,46 @@ +import XCTest + +import Nimble +import Quick +@testable import Backtrace + +final class ReportingPolicyTests: QuickSpec { + override func spec() { + describe("ReportingPolicy") { + throwingContext("Valid credentials", closure: { + guard let endpoint = URL(string: "https://wwww.backtrace.io") else { fail(); return } + let token = "token" + let credentials = BacktraceCredentials(endpoint: endpoint, token: token) + context("Allows reporting when debugger is attached", closure: { + let configuration = BacktraceClientConfiguration(credentials: credentials, + allowsAttachingDebugger: true) + it("has attached debugger", closure: { + expect(ReportingPolicy(configuration: configuration, + debuggerChecker: AttachedDebuggerCheckerMock.self).allowsReporting) + .to(beTrue()) + }) + it("has no debugger attached", closure: { + expect(ReportingPolicy(configuration: configuration, + debuggerChecker: DetachedDebuggerCheckerMock.self).allowsReporting) + .to(beTrue()) + }) + }) + + context("Disallows reporting when debugger is attached", closure: { + let configuration = BacktraceClientConfiguration(credentials: credentials, + allowsAttachingDebugger: false) + it("has attached debugger", closure: { + expect(ReportingPolicy(configuration: configuration, + debuggerChecker: AttachedDebuggerCheckerMock.self).allowsReporting) + .to(beFalse()) + }) + it("has no debugger attached", closure: { + expect(ReportingPolicy(configuration: configuration, + debuggerChecker: DetachedDebuggerCheckerMock.self).allowsReporting) + .to(beTrue()) + }) + }) + }) + } + } +} diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 4383c360..a7113622 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -39,3 +39,8 @@ lane :common_tests do |options| open_report: true ) end + +desc "Lint pod" +lane :lint_pod do + pod_lib_lint(allow_warnings: true, verbose: true) +end diff --git a/fastlane/README.md b/fastlane/README.md index f7301d49..321b44f7 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -20,6 +20,11 @@ or alternatively using `brew cask install fastlane` fastlane common_tests ``` Run tests +### lint_pod +``` +fastlane lint_pod +``` +Lint pod ----