[#126] TCA component for user logs (#526)

- OSLogger for the defined categories
- TCA logger for the TCA logs
- WalletLogger for the secant logs
- SDKLogger passed to the SDK
- unit tests for the loggers
- export category OS logs
- share txt files (sdk, tca, wallet logs) via native share dialog
- timestamp extension so we see even milliseconds
- txt files up to some X size
- simple button enable/disable logic and wrapping the export work in the Task
- TODO for empty catches
- OSLogger refactored to OSLogger_, just temporary change
- export and share divided into business logic and view logic parts
- unit tests for the TCA part
- async let syntax for the export logs
- simple activity indicator so testers know export is in progress
- static date formatters so we don't instantiate it over and over
This commit is contained in:
Lukas Korba 2023-02-01 09:08:22 +01:00 committed by GitHub
parent 0d2d898f4e
commit 1aca887800
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 892 additions and 19 deletions

View File

@ -99,6 +99,11 @@
9E01F8282833CDA0000EFC57 /* ScanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E01F8272833CDA0000EFC57 /* ScanTests.swift */; }; 9E01F8282833CDA0000EFC57 /* ScanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E01F8272833CDA0000EFC57 /* ScanTests.swift */; };
9E02B56A27FED43E005B809B /* FileManagerInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B56927FED43E005B809B /* FileManagerInterface.swift */; }; 9E02B56A27FED43E005B809B /* FileManagerInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B56927FED43E005B809B /* FileManagerInterface.swift */; };
9E02B56C27FED475005B809B /* DatabaseFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */; }; 9E02B56C27FED475005B809B /* DatabaseFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */; };
9E0F5741297E7F1D005304FA /* TCALogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F5740297E7F1C005304FA /* TCALogger.swift */; };
9E0F5743297EB96C005304FA /* TCALoggerReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F5742297EB96C005304FA /* TCALoggerReducer.swift */; };
9E0F5745297EBA1B005304FA /* LogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F5744297EBA1B005304FA /* LogStore.swift */; };
9E0F5747297EE5F3005304FA /* OSLogger_.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F5746297EE5F3005304FA /* OSLogger_.swift */; };
9E0F574B2980260D005304FA /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F574A2980260D005304FA /* LoggerTests.swift */; };
9E153A5F2920CE2700112F41 /* MnemonicInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A5E2920CD5100112F41 /* MnemonicInterface.swift */; }; 9E153A5F2920CE2700112F41 /* MnemonicInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A5E2920CD5100112F41 /* MnemonicInterface.swift */; };
9E153A602920CE2700112F41 /* MnemonicLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A5C2920CD5100112F41 /* MnemonicLiveKey.swift */; }; 9E153A602920CE2700112F41 /* MnemonicLiveKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A5C2920CD5100112F41 /* MnemonicLiveKey.swift */; };
9E153A612920CE2700112F41 /* MnemonicMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A5B2920CD5100112F41 /* MnemonicMocks.swift */; }; 9E153A612920CE2700112F41 /* MnemonicMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E153A5B2920CD5100112F41 /* MnemonicMocks.swift */; };
@ -137,6 +142,10 @@
9E5BF648282277BE00BA3F17 /* NotificationCenterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF647282277BE00BA3F17 /* NotificationCenterInterface.swift */; }; 9E5BF648282277BE00BA3F17 /* NotificationCenterInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF647282277BE00BA3F17 /* NotificationCenterInterface.swift */; };
9E5BF64F2823E94900BA3F17 /* TransactionAddressTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */; }; 9E5BF64F2823E94900BA3F17 /* TransactionAddressTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */; };
9E5BF6502823E94900BA3F17 /* TransactionAddressTextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64E2823E94900BA3F17 /* TransactionAddressTextFieldStore.swift */; }; 9E5BF6502823E94900BA3F17 /* TransactionAddressTextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64E2823E94900BA3F17 /* TransactionAddressTextFieldStore.swift */; };
9E612C6F2987A9B100D09B09 /* UIShareDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E612C6E2987A9B100D09B09 /* UIShareDialog.swift */; };
9E612C7229880E9200D09B09 /* LogsHandlerInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E612C7129880E9200D09B09 /* LogsHandlerInterface.swift */; };
9E612C7429880F2200D09B09 /* LogsHandlerLive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E612C7329880F2200D09B09 /* LogsHandlerLive.swift */; };
9E612C7629880FC900D09B09 /* LogsHandlerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E612C7529880FC900D09B09 /* LogsHandlerTest.swift */; };
9E66122A287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */; }; 9E66122A287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */; };
9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */; }; 9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */; };
9E6612312878337F00C75B70 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9E6612302878337F00C75B70 /* Lottie */; }; 9E6612312878337F00C75B70 /* Lottie in Frameworks */ = {isa = PBXBuildFile; productRef = 9E6612302878337F00C75B70 /* Lottie */; };
@ -395,6 +404,11 @@
9E01F8272833CDA0000EFC57 /* ScanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanTests.swift; sourceTree = "<group>"; }; 9E01F8272833CDA0000EFC57 /* ScanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanTests.swift; sourceTree = "<group>"; };
9E02B56927FED43E005B809B /* FileManagerInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerInterface.swift; sourceTree = "<group>"; }; 9E02B56927FED43E005B809B /* FileManagerInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerInterface.swift; sourceTree = "<group>"; };
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFilesTests.swift; sourceTree = "<group>"; }; 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFilesTests.swift; sourceTree = "<group>"; };
9E0F5740297E7F1C005304FA /* TCALogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCALogger.swift; sourceTree = "<group>"; };
9E0F5742297EB96C005304FA /* TCALoggerReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TCALoggerReducer.swift; sourceTree = "<group>"; };
9E0F5744297EBA1B005304FA /* LogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogStore.swift; sourceTree = "<group>"; };
9E0F5746297EE5F3005304FA /* OSLogger_.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLogger_.swift; sourceTree = "<group>"; };
9E0F574A2980260D005304FA /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = "<group>"; };
9E153A5B2920CD5100112F41 /* MnemonicMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicMocks.swift; sourceTree = "<group>"; }; 9E153A5B2920CD5100112F41 /* MnemonicMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicMocks.swift; sourceTree = "<group>"; };
9E153A5C2920CD5100112F41 /* MnemonicLiveKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicLiveKey.swift; sourceTree = "<group>"; }; 9E153A5C2920CD5100112F41 /* MnemonicLiveKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicLiveKey.swift; sourceTree = "<group>"; };
9E153A5D2920CD5100112F41 /* MnemonicTestKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicTestKey.swift; sourceTree = "<group>"; }; 9E153A5D2920CD5100112F41 /* MnemonicTestKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MnemonicTestKey.swift; sourceTree = "<group>"; };
@ -434,6 +448,10 @@
9E5BF647282277BE00BA3F17 /* NotificationCenterInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterInterface.swift; sourceTree = "<group>"; }; 9E5BF647282277BE00BA3F17 /* NotificationCenterInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterInterface.swift; sourceTree = "<group>"; };
9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextField.swift; sourceTree = "<group>"; }; 9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextField.swift; sourceTree = "<group>"; };
9E5BF64E2823E94900BA3F17 /* TransactionAddressTextFieldStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextFieldStore.swift; sourceTree = "<group>"; }; 9E5BF64E2823E94900BA3F17 /* TransactionAddressTextFieldStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextFieldStore.swift; sourceTree = "<group>"; };
9E612C6E2987A9B100D09B09 /* UIShareDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIShareDialog.swift; sourceTree = "<group>"; };
9E612C7129880E9200D09B09 /* LogsHandlerInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsHandlerInterface.swift; sourceTree = "<group>"; };
9E612C7329880F2200D09B09 /* LogsHandlerLive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsHandlerLive.swift; sourceTree = "<group>"; };
9E612C7529880FC900D09B09 /* LogsHandlerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsHandlerTest.swift; sourceTree = "<group>"; };
9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCircularProgressSnapshotTests.swift; sourceTree = "<group>"; }; 9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCircularProgressSnapshotTests.swift; sourceTree = "<group>"; };
9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatusSnapshot.swift; sourceTree = "<group>"; }; 9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatusSnapshot.swift; sourceTree = "<group>"; };
9E6612322878338C00C75B70 /* LottieAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimation.swift; sourceTree = "<group>"; }; 9E6612322878338C00C75B70 /* LottieAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LottieAnimation.swift; sourceTree = "<group>"; };
@ -962,6 +980,17 @@
path = ScanTests; path = ScanTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9E0F573F297E7F00005304FA /* Logging */ = {
isa = PBXGroup;
children = (
9E0F5740297E7F1C005304FA /* TCALogger.swift */,
9E0F5742297EB96C005304FA /* TCALoggerReducer.swift */,
9E0F5744297EBA1B005304FA /* LogStore.swift */,
9E0F5746297EE5F3005304FA /* OSLogger_.swift */,
);
path = Logging;
sourceTree = "<group>";
};
9E153A5A2920CCE700112F41 /* Mnemonic */ = { 9E153A5A2920CCE700112F41 /* Mnemonic */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1109,6 +1138,24 @@
path = TransactionAddress; path = TransactionAddress;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9E612C6D2987A96500D09B09 /* UIKitBridge */ = {
isa = PBXGroup;
children = (
9E612C6E2987A9B100D09B09 /* UIShareDialog.swift */,
);
path = UIKitBridge;
sourceTree = "<group>";
};
9E612C7029880E6700D09B09 /* LogsHandler */ = {
isa = PBXGroup;
children = (
9E612C7129880E9200D09B09 /* LogsHandlerInterface.swift */,
9E612C7329880F2200D09B09 /* LogsHandlerLive.swift */,
9E612C7529880FC900D09B09 /* LogsHandlerTest.swift */,
);
path = LogsHandler;
sourceTree = "<group>";
};
9E6612342878341F00C75B70 /* Lotties */ = { 9E6612342878341F00C75B70 /* Lotties */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1226,6 +1273,7 @@
9E7FE0BB282D1DC200C374E8 /* Utils */ = { 9E7FE0BB282D1DC200C374E8 /* Utils */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9E0F573F297E7F00005304FA /* Logging */,
9E7FE0D4282D281800C374E8 /* Array+Chunked.swift */, 9E7FE0D4282D281800C374E8 /* Array+Chunked.swift */,
F9C165B3274031F600592F76 /* Bindings.swift */, F9C165B3274031F600592F76 /* Bindings.swift */,
0DACFA7E27208CE00039EEA5 /* Clamped.swift */, 0DACFA7E27208CE00039EEA5 /* Clamped.swift */,
@ -1260,6 +1308,7 @@
9EB863882922CC0E003D0F8B /* FeedbackGenerator */, 9EB863882922CC0E003D0F8B /* FeedbackGenerator */,
9EB863B52923C4ED003D0F8B /* FileManager */, 9EB863B52923C4ED003D0F8B /* FileManager */,
9EBDF981291F91B1000A1A05 /* LocalAuthentication */, 9EBDF981291F91B1000A1A05 /* LocalAuthentication */,
9E612C7029880E6700D09B09 /* LogsHandler */,
9E153A5A2920CCE700112F41 /* Mnemonic */, 9E153A5A2920CCE700112F41 /* Mnemonic */,
9EB863B42923C490003D0F8B /* NotificationCenter */, 9EB863B42923C490003D0F8B /* NotificationCenter */,
9EB8638F2922D000003D0F8B /* NumberFormatter */, 9EB8638F2922D000003D0F8B /* NumberFormatter */,
@ -1650,6 +1699,7 @@
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */, 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */,
9E39112D283F91600073DD9A /* ZatoshiTests.swift */, 9E39112D283F91600073DD9A /* ZatoshiTests.swift */,
0DB4E0B02881F2DB00947B78 /* WalletBalance+testing.swift */, 0DB4E0B02881F2DB00947B78 /* WalletBalance+testing.swift */,
9E0F574A2980260D005304FA /* LoggerTests.swift */,
); );
path = UtilTests; path = UtilTests;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1726,6 +1776,7 @@
children = ( children = (
F9971A6227680DFE00A2DB75 /* SettingsStore.swift */, F9971A6227680DFE00A2DB75 /* SettingsStore.swift */,
F9971A6427680DFE00A2DB75 /* SettingsView.swift */, F9971A6427680DFE00A2DB75 /* SettingsView.swift */,
9E612C6D2987A96500D09B09 /* UIKitBridge */,
); );
path = Settings; path = Settings;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1996,6 +2047,7 @@
2EB7758727FC67FD00269373 /* TransactionAmountTextFieldStore.swift in Sources */, 2EB7758727FC67FD00269373 /* TransactionAmountTextFieldStore.swift in Sources */,
669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */, 669FDAE9272C23B3007B9422 /* CircularFrame.swift in Sources */,
9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */, 9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */,
9E612C7229880E9200D09B09 /* LogsHandlerInterface.swift in Sources */,
9EBDF960291E657B000A1A05 /* DeeplinkTestKey.swift in Sources */, 9EBDF960291E657B000A1A05 /* DeeplinkTestKey.swift in Sources */,
34E0AF1128DEE5220034CF37 /* Wedge.swift in Sources */, 34E0AF1128DEE5220034CF37 /* Wedge.swift in Sources */,
F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */, F96B41E8273B501F0021B49A /* TransactionDetailView.swift in Sources */,
@ -2013,9 +2065,11 @@
0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */, 0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */,
9EAB467A2861EA6A002904A0 /* TransactionRowView.swift in Sources */, 9EAB467A2861EA6A002904A0 /* TransactionRowView.swift in Sources */,
9EB8638C2922CD4A003D0F8B /* FeedbackGeneratorTestKey.swift in Sources */, 9EB8638C2922CD4A003D0F8B /* FeedbackGeneratorTestKey.swift in Sources */,
9E0F5741297E7F1D005304FA /* TCALogger.swift in Sources */,
0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidationFlowStore.swift in Sources */, 0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidationFlowStore.swift in Sources */,
9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */, 9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */,
9E9ADA7D2938F4C00071767B /* RootInitialization.swift in Sources */, 9E9ADA7D2938F4C00071767B /* RootInitialization.swift in Sources */,
9E612C7429880F2200D09B09 /* LogsHandlerLive.swift in Sources */,
9EBDF967291ECDA2000A1A05 /* AudioServicesTestKey.swift in Sources */, 9EBDF967291ECDA2000A1A05 /* AudioServicesTestKey.swift in Sources */,
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */, 0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */,
9EBDF97E291F7EB0000A1A05 /* AppVersionInterface.swift in Sources */, 9EBDF97E291F7EB0000A1A05 /* AppVersionInterface.swift in Sources */,
@ -2035,6 +2089,7 @@
9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */, 9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */,
F9971A5327680DD000A2DB75 /* ProfileStore.swift in Sources */, F9971A5327680DD000A2DB75 /* ProfileStore.swift in Sources */,
346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */, 346D41E428DF0B8600963F36 /* CheckCircle.swift in Sources */,
9E0F5745297EBA1B005304FA /* LogStore.swift in Sources */,
9EB863AA29239EB2003D0F8B /* RecoveryPhraseRandomizer.swift in Sources */, 9EB863AA29239EB2003D0F8B /* RecoveryPhraseRandomizer.swift in Sources */,
9EB863C52923C8AF003D0F8B /* FileManagerTest.swift in Sources */, 9EB863C52923C8AF003D0F8B /* FileManagerTest.swift in Sources */,
9EB863BF2923C72C003D0F8B /* SecItemLive.swift in Sources */, 9EB863BF2923C72C003D0F8B /* SecItemLive.swift in Sources */,
@ -2053,6 +2108,7 @@
0D18581B272728D60046B928 /* PhraseChip.swift in Sources */, 0D18581B272728D60046B928 /* PhraseChip.swift in Sources */,
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */, 9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */,
9E153A6F292167FF00112F41 /* ZcashSDKEnvironmentTestKey.swift in Sources */, 9E153A6F292167FF00112F41 /* ZcashSDKEnvironmentTestKey.swift in Sources */,
9E0F5743297EB96C005304FA /* TCALoggerReducer.swift in Sources */,
0DF482BA2787ADA800EB37D6 /* ConditionalModifier.swift in Sources */, 0DF482BA2787ADA800EB37D6 /* ConditionalModifier.swift in Sources */,
9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */, 9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */,
3448CB3228E47666006ADEDB /* NotEnoughFreeSpaceView.swift in Sources */, 3448CB3228E47666006ADEDB /* NotEnoughFreeSpaceView.swift in Sources */,
@ -2110,6 +2166,7 @@
66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */, 66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */,
663FABA0271D876200E495F8 /* PrimaryButton.swift in Sources */, 663FABA0271D876200E495F8 /* PrimaryButton.swift in Sources */,
663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */, 663FAB9C271D874D00E495F8 /* ActiveButton.swift in Sources */,
9E612C6F2987A9B100D09B09 /* UIShareDialog.swift in Sources */,
9EBDF956291E5E86000A1A05 /* DatabaseFilesLiveKey.swift in Sources */, 9EBDF956291E5E86000A1A05 /* DatabaseFilesLiveKey.swift in Sources */,
9E2F1C842809B606004E65FE /* DebugMenu.swift in Sources */, 9E2F1C842809B606004E65FE /* DebugMenu.swift in Sources */,
9E153A5F2920CE2700112F41 /* MnemonicInterface.swift in Sources */, 9E153A5F2920CE2700112F41 /* MnemonicInterface.swift in Sources */,
@ -2148,6 +2205,7 @@
F9971A6627680DFE00A2DB75 /* SettingsView.swift in Sources */, F9971A6627680DFE00A2DB75 /* SettingsView.swift in Sources */,
F96B41EB273B50520021B49A /* Strings.swift in Sources */, F96B41EB273B50520021B49A /* Strings.swift in Sources */,
9EB863D02923D3FC003D0F8B /* SDKSynchronizerMocks.swift in Sources */, 9EB863D02923D3FC003D0F8B /* SDKSynchronizerMocks.swift in Sources */,
9E612C7629880FC900D09B09 /* LogsHandlerTest.swift in Sources */,
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */, 2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */,
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */, F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
F9971A6027680DF600A2DB75 /* ScanStore.swift in Sources */, F9971A6027680DF600A2DB75 /* ScanStore.swift in Sources */,
@ -2174,6 +2232,7 @@
34E5F2F328E46DB700C17E5F /* DiskSpaceChecker.swift in Sources */, 34E5F2F328E46DB700C17E5F /* DiskSpaceChecker.swift in Sources */,
F9C165C42740403600592F76 /* TransactionSentView.swift in Sources */, F9C165C42740403600592F76 /* TransactionSentView.swift in Sources */,
9E153A6E292167FF00112F41 /* ZcashSDKEnvironmentLiveKey.swift in Sources */, 9E153A6E292167FF00112F41 /* ZcashSDKEnvironmentLiveKey.swift in Sources */,
9E0F5747297EE5F3005304FA /* OSLogger_.swift in Sources */,
F9971A5927680DDE00A2DB75 /* RequestStore.swift in Sources */, F9971A5927680DDE00A2DB75 /* RequestStore.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -2205,6 +2264,7 @@
9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */, 9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */,
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */, 0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
9E9ECC9A28589E150099D5A2 /* RecoveryPhraseValidationFlowSnapshotTests.swift in Sources */, 9E9ECC9A28589E150099D5A2 /* RecoveryPhraseValidationFlowSnapshotTests.swift in Sources */,
9E0F574B2980260D005304FA /* LoggerTests.swift in Sources */,
9EAFEB822805793200199FC9 /* RootTests.swift in Sources */, 9EAFEB822805793200199FC9 /* RootTests.swift in Sources */,
9E391132284644580073DD9A /* AppInitializationTests.swift in Sources */, 9E391132284644580073DD9A /* AppInitializationTests.swift in Sources */,
9E9ECC9928589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift in Sources */, 9E9ECC9928589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift in Sources */,

View File

@ -0,0 +1,19 @@
//
// LogsHandlerInterface.swift
// secant-testnet
//
// Created by Lukáš Korba on 30.01.2023.
//
import Foundation
import ComposableArchitecture
extension DependencyValues {
var logsHandler: LogsHandlerClient {
get { self[LogsHandlerClient.self] }
set { self[LogsHandlerClient.self] = newValue }
}
}
struct LogsHandlerClient {
let exportAndStoreLogs: (URL, URL, URL) async throws -> Void
}

View File

@ -0,0 +1,39 @@
//
// LogsHandlerLive.swift
// secant-testnet
//
// Created by Lukáš Korba on 30.01.2023.
//
import Foundation
import ComposableArchitecture
extension LogsHandlerClient: DependencyKey {
static let liveValue = LogsHandlerClient(
exportAndStoreLogs: { tempSDKDir, tempTCADir, tempWalletDir in
async let sdkLogs = LogsHandlerClient.exportAndStoreLogsFor(key: LoggerConstants.sdkLogs, atURL: tempSDKDir)
async let tcaLogs = LogsHandlerClient.exportAndStoreLogsFor(key: LoggerConstants.tcaLogs, atURL: tempTCADir)
async let walletLogs = LogsHandlerClient.exportAndStoreLogsFor(key: LoggerConstants.walletLogs, atURL: tempWalletDir)
let logs = try await [sdkLogs, tcaLogs, walletLogs]
try logs.forEach { logsHandler in
try logsHandler.result.write(to: logsHandler.dir, atomically: true, encoding: String.Encoding.utf8)
}
}
)
}
private extension LogsHandlerClient {
static func exportAndStoreLogsFor(key: String, atURL: URL) async throws -> (result: String, dir: URL) {
let logsStr = try await LogStore.exportCategory(key)
var result = ""
logsStr?.forEach({ line in
result.append(line)
result.append("\n\n")
})
return (result: result, dir: atURL)
}
}

View File

@ -0,0 +1,15 @@
//
// LogsHandlerTest.swift
// secant-testnet
//
// Created by Lukáš Korba on 30.01.2023.
//
import ComposableArchitecture
import XCTestDynamicOverlay
extension LogsHandlerClient: TestDependencyKey {
static let testValue = Self(
exportAndStoreLogs: XCTUnimplemented("\(Self.self).exportAndStoreLogs")
)
}

View File

@ -25,6 +25,8 @@ extension RootReducer {
Reduce { state, action in Reduce { state, action in
switch action { switch action {
case .initialization(.appDelegate(.didFinishLaunching)): case .initialization(.appDelegate(.didFinishLaunching)):
// TODO: [#524] finish all the wallet events according to definition, https://github.com/zcash/secant-ios-wallet/issues/524
LoggerProxy.event(".appDelegate(.didFinishLaunching)")
/// We need to fetch data from keychain, in order to be 100% sure the kecyhain can be read we delay the check a bit /// We need to fetch data from keychain, in order to be 100% sure the kecyhain can be read we delay the check a bit
return Effect(value: .initialization(.checkWalletInitialization)) return Effect(value: .initialization(.checkWalletInitialization))
.delay(for: 0.02, scheduler: mainQueue) .delay(for: 0.02, scheduler: mainQueue)

View File

@ -139,7 +139,8 @@ extension RootReducer {
spendParamsURL: try databaseFiles.spendParamsURLFor(network), spendParamsURL: try databaseFiles.spendParamsURLFor(network),
outputParamsURL: try databaseFiles.outputParamsURLFor(network), outputParamsURL: try databaseFiles.outputParamsURLFor(network),
viewingKeys: [viewingKey], viewingKeys: [viewingKey],
walletBirthday: birthday walletBirthday: birthday,
loggerProxy: OSLogger_(logLevel: .debug, category: LoggerConstants.sdkLogs)
) )
return initializer return initializer
@ -173,7 +174,7 @@ extension RootStore {
static var placeholder: RootStore { static var placeholder: RootStore {
RootStore( RootStore(
initialState: .placeholder, initialState: .placeholder,
reducer: RootReducer()._printChanges() reducer: RootReducer().logging()._printChanges()
) )
} }
} }

View File

@ -10,25 +10,49 @@ struct SettingsReducer: ReducerProtocol {
case backupPhrase case backupPhrase
} }
var destination: Destination?
var exportLogsDisabled = false
var isSharingLogs = false
var phraseDisplayState: RecoveryPhraseDisplayReducer.State var phraseDisplayState: RecoveryPhraseDisplayReducer.State
var rescanDialog: ConfirmationDialogState<SettingsReducer.Action>? var rescanDialog: ConfirmationDialogState<SettingsReducer.Action>?
var destination: Destination?
var tempSDKDir: URL {
let tempDir = FileManager.default.temporaryDirectory
let sdkFileName = "sdkLogs.txt"
return tempDir.appendingPathComponent(sdkFileName)
}
var tempTCADir: URL {
let tempDir = FileManager.default.temporaryDirectory
let sdkFileName = "tcaLogs.txt"
return tempDir.appendingPathComponent(sdkFileName)
}
var tempWalletDir: URL {
let tempDir = FileManager.default.temporaryDirectory
let sdkFileName = "walletLogs.txt"
return tempDir.appendingPathComponent(sdkFileName)
}
} }
enum Action: Equatable { enum Action: Equatable {
case backupWallet case backupWallet
case backupWalletAccessRequest case backupWalletAccessRequest
case cancelRescan case cancelRescan
case exportLogs
case fullRescan case fullRescan
case logsExported
case logsShareFinished
case phraseDisplay(RecoveryPhraseDisplayReducer.Action) case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case quickRescan case quickRescan
case rescanBlockchain case rescanBlockchain
case updateDestination(SettingsReducer.State.Destination?) case updateDestination(SettingsReducer.State.Destination?)
} }
@Dependency(\.localAuthentication) var localAuthentication @Dependency(\.localAuthentication) var localAuthentication
@Dependency(\.mnemonic) var mnemonic @Dependency(\.mnemonic) var mnemonic
@Dependency(\.sdkSynchronizer) var sdkSynchronizer @Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.logsHandler) var logsHandler
@Dependency(\.walletStorage) var walletStorage @Dependency(\.walletStorage) var walletStorage
var body: some ReducerProtocol<State, Action> { var body: some ReducerProtocol<State, Action> {
@ -57,6 +81,26 @@ struct SettingsReducer: ReducerProtocol {
state.rescanDialog = nil state.rescanDialog = nil
return .none return .none
case .exportLogs:
state.exportLogsDisabled = true
return .run { [state] send in
do {
try await logsHandler.exportAndStoreLogs(state.tempSDKDir, state.tempTCADir, state.tempWalletDir)
await send(.logsExported)
} catch {
// TODO: [#527] address the error here https://github.com/zcash/secant-ios-wallet/issues/527
}
}
case .logsExported:
state.exportLogsDisabled = false
state.isSharingLogs = true
return .none
case .logsShareFinished:
state.isSharingLogs = false
return .none
case .rescanBlockchain: case .rescanBlockchain:
state.rescanDialog = .init( state.rescanDialog = .init(
title: TextState("Rescan"), title: TextState("Rescan"),
@ -94,7 +138,7 @@ extension SettingsViewStore {
send: SettingsReducer.Action.updateDestination send: SettingsReducer.Action.updateDestination
) )
} }
var bindingForBackupPhrase: Binding<Bool> { var bindingForBackupPhrase: Binding<Bool> {
self.destinationBinding.map( self.destinationBinding.map(
extract: { $0 == .backupPhrase }, extract: { $0 == .backupPhrase },

View File

@ -3,7 +3,7 @@ import ComposableArchitecture
struct SettingsView: View { struct SettingsView: View {
let store: SettingsStore let store: SettingsStore
var body: some View { var body: some View {
WithViewStore(store) { viewStore in WithViewStore(store) { viewStore in
VStack { VStack {
@ -22,7 +22,26 @@ struct SettingsView: View {
.primaryButtonStyle .primaryButtonStyle
.frame(height: 50) .frame(height: 50)
.padding(.horizontal, 30) .padding(.horizontal, 30)
Button(
action: { viewStore.send(.exportLogs) },
label: {
if viewStore.exportLogsDisabled {
HStack {
ProgressView()
Text("Exporting...")
}
} else {
Text("Export & share logs")
}
}
)
.primaryButtonStyle
.frame(height: 50)
.padding(.horizontal, 30)
.padding(.top, 30)
.disabled(viewStore.exportLogsDisabled)
Spacer() Spacer()
} }
.navigationTitle("Settings") .navigationTitle("Settings")
@ -37,6 +56,17 @@ struct SettingsView: View {
RecoveryPhraseDisplayView(store: store.backupPhraseStore()) RecoveryPhraseDisplayView(store: store.backupPhraseStore())
} }
) )
if viewStore.isSharingLogs {
UIShareDialogView(
activityItems: [viewStore.tempSDKDir, viewStore.tempWalletDir, viewStore.tempTCADir]
) {
viewStore.send(.logsShareFinished)
}
// UIShareDialogView only wraps UIActivityViewController presentation
// so frame is set to 0 to not break SwiftUIs layout
.frame(width: 0, height: 0)
}
} }
} }
} }

View File

@ -0,0 +1,54 @@
//
// UIShareDialog.swift
// secant-testnet
//
// Created by Lukáš Korba on 30.01.2023.
//
import Foundation
import UIKit
import SwiftUI
public class UIShareDialog: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}
extension UIShareDialog {
public func doInitialSetup(activityItems: [Any], completion: @escaping () -> Void) {
DispatchQueue.main.async {
let activityVC = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
UIApplication.shared.connectedScenes.map({ $0 as? UIWindowScene })
.compactMap({ $0 })
.first?.windows.first?.rootViewController?.present(
activityVC,
animated: true,
completion: completion
)
}
}
}
struct UIShareDialogView: UIViewRepresentable {
let activityItems: [Any]
let completion: () -> Void
public func makeUIView(context: UIViewRepresentableContext<UIShareDialogView>) -> UIShareDialog {
let view = UIShareDialog()
view.doInitialSetup(activityItems: activityItems, completion: completion)
return view
}
public func updateUIView(_ uiView: UIShareDialog, context: UIViewRepresentableContext<UIShareDialogView>) {
// We can leave it empty here because the view is just handler how to bridge UIKit's UIActivityViewController
// presentation into SwiftUI. The view itself is not visible, only instantiated, therefore no updates needed.
}
public typealias UIViewType = UIShareDialog
}

View File

@ -20,6 +20,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool { ) -> Bool {
walletLogger = OSLogger_(logLevel: .debug, category: LoggerConstants.walletLogs)
// set the default behavior for the NSDecimalNumber // set the default behavior for the NSDecimalNumber
NSDecimalNumber.defaultBehavior = Zatoshi.decimalHandler NSDecimalNumber.defaultBehavior = Zatoshi.decimalHandler
rootViewStore.send(.initialization(.appDelegate(.didFinishLaunching))) rootViewStore.send(.initialization(.appDelegate(.didFinishLaunching)))

View File

@ -8,12 +8,25 @@
import Foundation import Foundation
extension Date { extension Date {
static let timestampFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy/MM/dd HH:mm:ss.SSSS"
return formatter
}()
static let humanReadableFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}()
func timestamp() -> String {
return String(format: "%@", Date.timestampFormatter.string(from: self))
}
func asHumanReadable() -> String { func asHumanReadable() -> String {
let dateFormatter = DateFormatter() return Date.humanReadableFormatter.string(from: self)
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
return dateFormatter.string(from: self)
} }
} }

View File

@ -0,0 +1,38 @@
//
// LogStore.swift
// secant-testnet
//
// Created by Lukáš Korba on 23.01.2023.
//
import Foundation
import OSLog
enum LogStore {
static func exportCategory(
_ category: String,
hoursToThePast: TimeInterval = 168,
fileSize: Int = 1_000_000
) async throws -> [String]? {
guard let bundle = Bundle.main.bundleIdentifier else { return nil }
let store = try OSLogStore(scope: .currentProcessIdentifier)
let date = Date.now.addingTimeInterval(-hoursToThePast * 3600)
let position = store.position(date: date)
var res: [String] = []
var size = 0
let entries = try store.getEntries(at: position).reversed()
for entry in entries {
guard let logEntry = entry as? OSLogEntryLog else { continue }
guard logEntry.subsystem == bundle && logEntry.category == category else { continue }
guard size < fileSize else { break }
size += logEntry.composedMessage.utf8.count
res.append("[\(logEntry.date.timestamp())] \(logEntry.composedMessage)")
}
return res
}
}

View File

@ -0,0 +1,135 @@
//
// OSLogger_.swift
// secant-testnet
//
// Created by Lukáš Korba on 23.01.2023.
//
import Foundation
import ZcashLightClientKit
import os
enum LoggerConstants {
static let sdkLogs = "sdkLogs"
static let tcaLogs = "tcaLogs"
static let walletLogs = "walletLogs"
}
var walletLogger: ZcashLightClientKit.Logger?
enum LoggerProxy {
static func debug(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.debug(message, file: file, function: function, line: line)
}
static func info(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.info(message, file: file, function: function, line: line)
}
static func event(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.event(message, file: file, function: function, line: line)
}
static func warn(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.warn(message, file: file, function: function, line: line)
}
static func error(_ message: String, file: StaticString = #file, function: StaticString = #function, line: Int = #line) {
walletLogger?.error(message, file: file, function: function, line: line)
}
}
// TODO: [#529] the swiftlint rule as well as OSLogger_ will be removed once secant adopts latest SDK changes https://github.com/zcash/secant-ios-wallet/issues/529
// swiftlint:disable:next type_name
class OSLogger_: ZcashLightClientKit.Logger {
enum LogLevel: Int {
case debug
case error
case warning
case event
case info
}
private(set) var oslog: OSLog?
var level: LogLevel
init(logLevel: LogLevel, category: String) {
self.level = logLevel
if let bundleName = Bundle.main.bundleIdentifier {
self.oslog = OSLog(subsystem: bundleName, category: category)
}
}
func debug(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue == LogLevel.debug.rawValue else { return }
log(level: "DEBUG 🐞", message: message, file: file, function: function, line: line)
}
func error(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue <= LogLevel.error.rawValue else { return }
log(level: "ERROR 💥", message: message, file: file, function: function, line: line)
}
func warn(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue <= LogLevel.warning.rawValue else { return }
log(level: "WARNING ⚠️", message: message, file: file, function: function, line: line)
}
func event(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue <= LogLevel.event.rawValue else { return }
log(level: "EVENT ⏱", message: message, file: file, function: function, line: line)
}
func info(
_ message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard level.rawValue <= LogLevel.info.rawValue else { return }
log(level: "INFO ", message: message, file: file, function: function, line: line)
}
private func log(
level: String,
message: String,
file: StaticString = #file,
function: StaticString = #function,
line: Int = #line
) {
guard let oslog else { return }
let fileName = (String(describing: file) as NSString).lastPathComponent
os_log(
"[%{public}@] %{public}@ - %{public}@ - Line: %{public}d -> %{public}@",
log: oslog,
level,
fileName,
String(describing: function),
line,
message
)
}
}

View File

@ -0,0 +1,25 @@
//
// TCALogger.swift
// secant-testnet
//
// Created by Lukáš Korba on 23.01.2023.
//
import Foundation
import os
class TCALogger: OSLogger_ { }
extension TCALogger {
static let live = TCALogger(logLevel: .debug, category: LoggerConstants.tcaLogs)
func tcaDebug(_ message: String) {
guard let oslog else { return }
os_log(
"%{public}@",
log: oslog,
message
)
}
}

View File

@ -0,0 +1,73 @@
//
// TCALoggerReducer.swift
// secant-testnet
//
// Created by Lukáš Korba on 23.01.2023.
//
import ComposableArchitecture
extension ReducerProtocol {
@inlinable
public func logging(
_ logger: ReducerLogger<State, Action>? = .tcaLogger
) -> LogChangesReducer<Self> {
LogChangesReducer<Self>(base: self, logger: logger)
}
}
public struct ReducerLogger<State, Action> {
private let _logChange: (_ receivedAction: Action, _ oldState: State, _ newState: State) -> Void
public init(
logChange: @escaping (_ receivedAction: Action, _ oldState: State, _ newState: State) -> Void
) {
self._logChange = logChange
}
public func logChange(receivedAction: Action, oldState: State, newState: State) {
self._logChange(receivedAction, oldState, newState)
}
}
extension ReducerLogger {
public static var tcaLogger: Self {
Self { receivedAction, oldState, newState in
var target = ""
target.write("received action:\n")
CustomDump.customDump(receivedAction, to: &target, indent: 2)
target.write("\n")
target.write(diff(oldState, newState).map { "\($0)\n" } ?? " (No state changes)\n")
TCALogger.live.tcaDebug("\(target)")
}
}
}
public struct LogChangesReducer<Base: ReducerProtocol>: ReducerProtocol {
@usableFromInline let base: Base
@usableFromInline let logger: ReducerLogger<Base.State, Base.Action>?
@usableFromInline
init(base: Base, logger: ReducerLogger<Base.State, Base.Action>?) {
self.base = base
self.logger = logger
}
@inlinable
public func reduce(
into state: inout Base.State, action: Base.Action
) -> EffectTask<Base.Action> {
guard let logger else {
return self.base.reduce(into: &state, action: action)
}
let oldState = state
let effects = self.base.reduce(into: &state, action: action)
return effects.merge(
with: .fireAndForget { [newState = state] in
logger.logChange(receivedAction: action, oldState: oldState, newState: newState)
}
)
}
}

View File

@ -99,6 +99,7 @@ class SettingsTests: XCTestCase {
func testRescanBlockchain_Cancelling() async throws { func testRescanBlockchain_Cancelling() async throws {
let store = TestStore( let store = TestStore(
initialState: SettingsReducer.State( initialState: SettingsReducer.State(
destination: nil,
phraseDisplayState: .init(), phraseDisplayState: .init(),
rescanDialog: .init( rescanDialog: .init(
title: TextState("Rescan"), title: TextState("Rescan"),
@ -108,8 +109,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)), .default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel")) .cancel(TextState("Cancel"))
] ]
), )
destination: nil
), ),
reducer: SettingsReducer() reducer: SettingsReducer()
) )
@ -122,6 +122,7 @@ class SettingsTests: XCTestCase {
func testRescanBlockchain_QuickRescanClearance() async throws { func testRescanBlockchain_QuickRescanClearance() async throws {
let store = TestStore( let store = TestStore(
initialState: SettingsReducer.State( initialState: SettingsReducer.State(
destination: nil,
phraseDisplayState: .init(), phraseDisplayState: .init(),
rescanDialog: .init( rescanDialog: .init(
title: TextState("Rescan"), title: TextState("Rescan"),
@ -131,8 +132,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)), .default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel")) .cancel(TextState("Cancel"))
] ]
), )
destination: nil
), ),
reducer: SettingsReducer() reducer: SettingsReducer()
) )
@ -145,6 +145,7 @@ class SettingsTests: XCTestCase {
func testRescanBlockchain_FullRescanClearance() async throws { func testRescanBlockchain_FullRescanClearance() async throws {
let store = TestStore( let store = TestStore(
initialState: SettingsReducer.State( initialState: SettingsReducer.State(
destination: nil,
phraseDisplayState: .init(), phraseDisplayState: .init(),
rescanDialog: .init( rescanDialog: .init(
title: TextState("Rescan"), title: TextState("Rescan"),
@ -154,8 +155,7 @@ class SettingsTests: XCTestCase {
.default(TextState("Full rescan"), action: .send(.fullRescan)), .default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel")) .cancel(TextState("Cancel"))
] ]
), )
destination: nil
), ),
reducer: SettingsReducer() reducer: SettingsReducer()
) )
@ -164,4 +164,58 @@ class SettingsTests: XCTestCase {
state.rescanDialog = nil state.rescanDialog = nil
} }
} }
func testExportLogs_ButtonDisableShareEnable() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
),
reducer: SettingsReducer()
)
store.dependencies.logsHandler = LogsHandlerClient(exportAndStoreLogs: { _, _, _ in })
_ = await store.send(.exportLogs) { state in
state.exportLogsDisabled = true
}
await store.receive(.logsExported) { state in
state.exportLogsDisabled = false
state.isSharingLogs = true
}
}
func testLogShareFinished() async throws {
let store = TestStore(
initialState: SettingsReducer.State(
destination: nil,
isSharingLogs: true,
phraseDisplayState: .init(),
rescanDialog: .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
buttons: [
.default(TextState("Quick rescan"), action: .send(.quickRescan)),
.default(TextState("Full rescan"), action: .send(.fullRescan)),
.cancel(TextState("Cancel"))
]
)
),
reducer: SettingsReducer()
)
_ = await store.send(.logsShareFinished) { state in
state.isSharingLogs = false
}
}
} }

View File

@ -0,0 +1,270 @@
//
// LoggerTests.swift
// secantTests
//
// Created by Lukáš Korba on 24.01.2023.
//
import XCTest
@testable import secant_testnet
import OSLog
class LoggerTests: XCTestCase {
func testOSLogger_loggingAndExport() throws {
let category = "testOSLogger_loggingAndExport"
let osLogger = OSLogger_(logLevel: .debug, category: category)
let testMessage = "test message"
osLogger.debug(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_DebugLevel_DebugLog() throws {
let category = "testOSLogger_DebugLevel_DebugLog"
let osLogger = OSLogger_(logLevel: .debug, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_ErrorLevel_ErrorLog() throws {
let category = "testOSLogger_ErrorLevel_ErrorLog"
let osLogger = OSLogger_(logLevel: .debug, category: category)
let testMessage = "error message"
osLogger.error(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_WarningLevel_WarningLog() throws {
let category = "testOSLogger_WarningLevel_WarningLog"
let osLogger = OSLogger_(logLevel: .warning, category: category)
let testMessage = "warning message"
osLogger.warn(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_EventLevel_EventLog() throws {
let category = "testOSLogger_EventLevel_EventLog"
let osLogger = OSLogger_(logLevel: .event, category: category)
let testMessage = "event message"
osLogger.event(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_InfoLevel_InfoLog() throws {
let category = "testOSLogger_InfoLevel_InfoLog"
let osLogger = OSLogger_(logLevel: .info, category: category)
let testMessage = "info message"
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
func testOSLogger_DebugLevel_OtherLogs() throws {
let category = "testOSLogger_DebugLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .debug, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 5)
}
func testOSLogger_ErrorLevel_OtherLogs() throws {
let category = "testOSLogger_ErrorLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .error, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 4)
}
func testOSLogger_WarningLevel_OtherLogs() throws {
let category = "testOSLogger_WarningLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .warning, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 3)
}
func testOSLogger_EventLevel_OtherLogs() throws {
let category = "testOSLogger_EventLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .event, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 2)
}
func testOSLogger_InfoLevel_OtherLogs() throws {
let category = "testOSLogger_InfoLevel_OtherLogs"
let osLogger = OSLogger_(logLevel: .info, category: category)
let testMessage = "debug message"
osLogger.debug(testMessage)
osLogger.error(testMessage)
osLogger.warn(testMessage)
osLogger.event(testMessage)
osLogger.info(testMessage)
let logs = TestLogStore.exportCategory(category)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
}
func testWalletLogger() throws {
let category = "testWalletLogger"
walletLogger = OSLogger_(logLevel: .info, category: category)
let testMessage = "wallet test message"
LoggerProxy.info(testMessage)
let logs = TestLogStore.exportCategory(category)
XCTAssertNotNil(logs)
guard let logs else { return }
XCTAssertEqual(logs.count, 1)
let loggedMessage = logs[0].osLoggedMessage()
XCTAssertEqual(testMessage, loggedMessage)
}
}
extension String {
func osLoggedMessage() -> String? {
let split = components(separatedBy: "-> ")
if split.count == 2 {
return split[1]
}
return nil
}
}
enum TestLogStore {
static func exportCategory(_ category: String, hoursToThePast: TimeInterval = 24) -> [String]? {
guard let bundle = Bundle.main.bundleIdentifier else { return nil }
do {
let store = try OSLogStore(scope: .currentProcessIdentifier)
let date = Date.now.addingTimeInterval(-hoursToThePast * 3600)
let position = store.position(date: date)
var entries: [String] = []
entries = try store
.getEntries(at: position)
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == bundle && $0.category == category }
.map { "[\($0.date.formatted())] \($0.composedMessage)" }
return entries
} catch {
return nil
}
}
}