@react-native-firebase/crashlytics 26.4.0 → 27.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/RNFBCrashlytics.podspec +7 -1
  3. package/android/build.gradle +8 -0
  4. package/android/proguard-rules.pro +20 -0
  5. package/android/src/main/java/io/invertase/firebase/crashlytics/Constants.kt +42 -0
  6. package/android/src/main/java/io/invertase/firebase/crashlytics/{JavaScriptError.java → JavaScriptError.kt} +4 -6
  7. package/android/src/main/java/io/invertase/firebase/crashlytics/NativeRNFBTurboCrashlytics.java +11 -4
  8. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExit.java +117 -0
  9. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsInternal.java +80 -0
  10. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsNativeHelper.kt +45 -0
  11. package/android/src/main/java/io/invertase/firebase/crashlytics/{UnhandledPromiseRejection.java → UnhandledPromiseRejection.kt} +4 -6
  12. package/android/src/test/java/io/invertase/firebase/crashlytics/ConstantsAndJsErrorTypesTest.java +84 -0
  13. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExitBarrierTest.java +122 -0
  14. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsFatalExitTest.java +189 -0
  15. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsInternalTest.java +199 -0
  16. package/android/src/test/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsNativeHelperTest.java +108 -0
  17. package/dist/module/version.js +1 -1
  18. package/dist/typescript/lib/index.d.ts +1 -1
  19. package/dist/typescript/lib/version.d.ts +1 -1
  20. package/ios/RNFBCrashlytics/RNFBCrashlyticsDebuggerProbe.swift +121 -0
  21. package/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.h +98 -0
  22. package/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.m +182 -0
  23. package/ios/RNFBCrashlytics/RNFBCrashlyticsModule.mm +57 -75
  24. package/ios/RNFBCrashlytics.xcodeproj/project.pbxproj +14 -0
  25. package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/FIRCrashlyticsHostStub.m +273 -0
  26. package/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/Firebase.h +56 -0
  27. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsDebuggerProbeTests.swift +112 -0
  28. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsFatalExitTests.m +268 -0
  29. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsHelperHostStubTests.m +119 -0
  30. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsUnitTests.xcodeproj/project.pbxproj +265 -0
  31. package/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsUnitTests.xcodeproj/xcshareddata/xcschemes/RNFBCrashlyticsUnitTests.xcscheme +69 -0
  32. package/ios_config.sh +38 -5
  33. package/lib/version.ts +1 -1
  34. package/package.json +4 -4
  35. package/android/src/main/java/io/invertase/firebase/crashlytics/Constants.java +0 -29
  36. package/android/src/main/java/io/invertase/firebase/crashlytics/ReactNativeFirebaseCrashlyticsNativeHelper.java +0 -18
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Host-only stub for macOS XCTest. Not shipped in the production pod.
3
+ */
4
+ #import <Firebase/Firebase.h>
5
+ #import <stdatomic.h>
6
+
7
+ @implementation FIRStackFrame {
8
+ NSString *_symbol;
9
+ NSString *_file;
10
+ uint32_t _line;
11
+ }
12
+
13
+ + (instancetype)stackFrameWithSymbol:(NSString *)symbol file:(NSString *)file line:(uint32_t)line {
14
+ FIRStackFrame *frame = [[FIRStackFrame alloc] init];
15
+ frame->_symbol = [symbol copy];
16
+ frame->_file = [file copy];
17
+ frame->_line = line;
18
+ return frame;
19
+ }
20
+
21
+ - (NSString *)symbol {
22
+ return _symbol;
23
+ }
24
+
25
+ - (NSString *)file {
26
+ return _file;
27
+ }
28
+
29
+ - (uint32_t)line {
30
+ return _line;
31
+ }
32
+
33
+ @end
34
+
35
+ @implementation FIRExceptionModel {
36
+ NSString *_name;
37
+ NSString *_reason;
38
+ }
39
+
40
+ + (instancetype)exceptionModelWithName:(NSString *)name reason:(NSString *)reason {
41
+ FIRExceptionModel *model = [[FIRExceptionModel alloc] init];
42
+ model->_name = [name copy];
43
+ model->_reason = [reason copy];
44
+ return model;
45
+ }
46
+
47
+ - (NSString *)name {
48
+ return _name;
49
+ }
50
+
51
+ - (NSString *)reason {
52
+ return _reason;
53
+ }
54
+
55
+ @end
56
+
57
+ @implementation FIRCrashlytics
58
+
59
+ static BOOL sDidCrashDuringPreviousExecutionValue;
60
+ static BOOL sCheckForUnsentReportsValue;
61
+ static NSMutableArray<NSString *> *sLoggedMessages;
62
+ static NSMutableDictionary *sCustomValues;
63
+ static NSString *sUserIDValue;
64
+ static NSUInteger sDeleteUnsentReportsCallCount;
65
+ static NSUInteger sSendUnsentReportsCallCount;
66
+ static FIRExceptionModel *sLastRecordedExceptionModel;
67
+ static NSMutableArray<FIRExceptionModel *> *sSavedExceptionModels;
68
+ static void (^sOnExceptionModelSaved)(FIRExceptionModel *);
69
+ static BOOL sContextInitPending;
70
+ static NSMutableArray<dispatch_block_t> *sPendingSaves;
71
+ // Bumped by resetTestState so saves enqueued by an earlier test are dropped.
72
+ static atomic_uint_fast64_t sSaveGeneration;
73
+
74
+ + (void)initialize {
75
+ if (self == [FIRCrashlytics class]) {
76
+ sLoggedMessages = [NSMutableArray new];
77
+ sCustomValues = [NSMutableDictionary new];
78
+ sSavedExceptionModels = [NSMutableArray new];
79
+ sPendingSaves = [NSMutableArray new];
80
+ }
81
+ }
82
+
83
+ + (instancetype)crashlytics {
84
+ static FIRCrashlytics *shared;
85
+ static dispatch_once_t once;
86
+ dispatch_once(&once, ^{
87
+ shared = [[FIRCrashlytics alloc] init];
88
+ });
89
+ return shared;
90
+ }
91
+
92
+ + (BOOL)didCrashDuringPreviousExecutionValue {
93
+ return sDidCrashDuringPreviousExecutionValue;
94
+ }
95
+
96
+ + (void)setDidCrashDuringPreviousExecutionValue:(BOOL)didCrashDuringPreviousExecutionValue {
97
+ sDidCrashDuringPreviousExecutionValue = didCrashDuringPreviousExecutionValue;
98
+ }
99
+
100
+ + (BOOL)checkForUnsentReportsValue {
101
+ return sCheckForUnsentReportsValue;
102
+ }
103
+
104
+ + (void)setCheckForUnsentReportsValue:(BOOL)checkForUnsentReportsValue {
105
+ sCheckForUnsentReportsValue = checkForUnsentReportsValue;
106
+ }
107
+
108
+ + (NSArray<NSString *> *)loggedMessages {
109
+ return [sLoggedMessages copy];
110
+ }
111
+
112
+ + (void)setLoggedMessages:(NSArray<NSString *> *)loggedMessages {
113
+ [sLoggedMessages removeAllObjects];
114
+ if (loggedMessages != nil) {
115
+ [sLoggedMessages addObjectsFromArray:loggedMessages];
116
+ }
117
+ }
118
+
119
+ + (NSDictionary *)customValues {
120
+ return [sCustomValues copy];
121
+ }
122
+
123
+ + (void)setCustomValues:(NSDictionary *)customValues {
124
+ [sCustomValues removeAllObjects];
125
+ if (customValues != nil) {
126
+ [sCustomValues addEntriesFromDictionary:customValues];
127
+ }
128
+ }
129
+
130
+ + (NSString *)userIDValue {
131
+ return sUserIDValue;
132
+ }
133
+
134
+ + (void)setUserIDValue:(NSString *)userIDValue {
135
+ sUserIDValue = [userIDValue copy];
136
+ }
137
+
138
+ + (NSUInteger)deleteUnsentReportsCallCount {
139
+ return sDeleteUnsentReportsCallCount;
140
+ }
141
+
142
+ + (void)setDeleteUnsentReportsCallCount:(NSUInteger)deleteUnsentReportsCallCount {
143
+ sDeleteUnsentReportsCallCount = deleteUnsentReportsCallCount;
144
+ }
145
+
146
+ + (NSUInteger)sendUnsentReportsCallCount {
147
+ return sSendUnsentReportsCallCount;
148
+ }
149
+
150
+ + (void)setSendUnsentReportsCallCount:(NSUInteger)sendUnsentReportsCallCount {
151
+ sSendUnsentReportsCallCount = sendUnsentReportsCallCount;
152
+ }
153
+
154
+ + (FIRExceptionModel *)lastRecordedExceptionModel {
155
+ return sLastRecordedExceptionModel;
156
+ }
157
+
158
+ + (void)setLastRecordedExceptionModel:(FIRExceptionModel *)lastRecordedExceptionModel {
159
+ sLastRecordedExceptionModel = lastRecordedExceptionModel;
160
+ }
161
+
162
+ + (NSArray<FIRExceptionModel *> *)savedExceptionModels {
163
+ return [sSavedExceptionModels copy];
164
+ }
165
+
166
+ + (void (^)(FIRExceptionModel *))onExceptionModelSaved {
167
+ return sOnExceptionModelSaved;
168
+ }
169
+
170
+ + (void)setOnExceptionModelSaved:(void (^)(FIRExceptionModel *))onExceptionModelSaved {
171
+ sOnExceptionModelSaved = [onExceptionModelSaved copy];
172
+ }
173
+
174
+ + (void)holdContextInit {
175
+ @synchronized(sPendingSaves) {
176
+ sContextInitPending = YES;
177
+ }
178
+ }
179
+
180
+ + (void)fulfillContextInit {
181
+ NSArray<dispatch_block_t> *saves;
182
+ @synchronized(sPendingSaves) {
183
+ sContextInitPending = NO;
184
+ saves = [sPendingSaves copy];
185
+ [sPendingSaves removeAllObjects];
186
+ }
187
+ for (dispatch_block_t save in saves) {
188
+ dispatch_async(dispatch_get_main_queue(), save);
189
+ }
190
+ }
191
+
192
+ + (void)resetTestState {
193
+ sDidCrashDuringPreviousExecutionValue = NO;
194
+ sCheckForUnsentReportsValue = NO;
195
+ [sLoggedMessages removeAllObjects];
196
+ [sCustomValues removeAllObjects];
197
+ sUserIDValue = nil;
198
+ sDeleteUnsentReportsCallCount = 0;
199
+ sSendUnsentReportsCallCount = 0;
200
+ sLastRecordedExceptionModel = nil;
201
+ [sSavedExceptionModels removeAllObjects];
202
+ sOnExceptionModelSaved = nil;
203
+ @synchronized(sPendingSaves) {
204
+ sContextInitPending = NO;
205
+ [sPendingSaves removeAllObjects];
206
+ }
207
+ atomic_fetch_add(&sSaveGeneration, 1);
208
+ }
209
+
210
+ - (void)checkForUnsentReportsWithCompletion:(void (^)(BOOL unsentReports))completion {
211
+ if (completion != nil) {
212
+ completion(sCheckForUnsentReportsValue);
213
+ }
214
+ }
215
+
216
+ - (void)deleteUnsentReports {
217
+ sDeleteUnsentReportsCallCount += 1;
218
+ }
219
+
220
+ - (BOOL)didCrashDuringPreviousExecution {
221
+ return sDidCrashDuringPreviousExecutionValue;
222
+ }
223
+
224
+ - (void)log:(NSString *)message {
225
+ if (message != nil) {
226
+ [sLoggedMessages addObject:message];
227
+ }
228
+ }
229
+
230
+ - (void)sendUnsentReports {
231
+ sSendUnsentReportsCallCount += 1;
232
+ }
233
+
234
+ - (void)setCustomValue:(id)value forKey:(NSString *)key {
235
+ if (key == nil) {
236
+ return;
237
+ }
238
+ if (value == nil) {
239
+ [sCustomValues removeObjectForKey:key];
240
+ } else {
241
+ sCustomValues[key] = value;
242
+ }
243
+ }
244
+
245
+ - (void)setUserID:(NSString *)userID {
246
+ sUserIDValue = [userID copy];
247
+ }
248
+
249
+ - (void)recordExceptionModel:(FIRExceptionModel *)exceptionModel {
250
+ sLastRecordedExceptionModel = exceptionModel;
251
+ // Mirrors FirebaseCrashlytics: once context init is fulfilled, the save block is dispatched to
252
+ // the main queue and writes synchronously when it runs. While init is pending, FBLPromise holds
253
+ // the block as an observer and dispatches it to the main queue only when the promise fulfills.
254
+ uint_fast64_t generation = atomic_load(&sSaveGeneration);
255
+ dispatch_block_t save = ^{
256
+ if (generation != atomic_load(&sSaveGeneration)) {
257
+ return;
258
+ }
259
+ [sSavedExceptionModels addObject:exceptionModel];
260
+ if (sOnExceptionModelSaved != nil) {
261
+ sOnExceptionModelSaved(exceptionModel);
262
+ }
263
+ };
264
+ @synchronized(sPendingSaves) {
265
+ if (sContextInitPending) {
266
+ [sPendingSaves addObject:save];
267
+ return;
268
+ }
269
+ }
270
+ dispatch_async(dispatch_get_main_queue(), save);
271
+ }
272
+
273
+ @end
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Host-only stub for macOS XCTest. Not shipped in the production pod.
3
+ * Enough of Firebase Crashlytics for compiling RNFBCrashlyticsHelper.m
4
+ * and asserting SDK call wiring without linking the real SDK.
5
+ */
6
+ #import <Foundation/Foundation.h>
7
+
8
+ @interface FIRStackFrame : NSObject
9
+ @property(nonatomic, readonly, copy) NSString *symbol;
10
+ @property(nonatomic, readonly, copy) NSString *file;
11
+ @property(nonatomic, readonly, assign) uint32_t line;
12
+ + (instancetype)stackFrameWithSymbol:(NSString *)symbol file:(NSString *)file line:(uint32_t)line;
13
+ @end
14
+
15
+ @interface FIRExceptionModel : NSObject
16
+ @property(nonatomic, readonly, copy) NSString *name;
17
+ @property(nonatomic, readonly, copy) NSString *reason;
18
+ @property(nonatomic, copy, nullable) NSArray<FIRStackFrame *> *stackTrace;
19
+ + (instancetype)exceptionModelWithName:(NSString *)name reason:(NSString *)reason;
20
+ @end
21
+
22
+ @interface FIRCrashlytics : NSObject
23
+
24
+ + (instancetype)crashlytics;
25
+
26
+ - (void)checkForUnsentReportsWithCompletion:(void (^)(BOOL unsentReports))completion;
27
+ - (void)deleteUnsentReports;
28
+ - (BOOL)didCrashDuringPreviousExecution;
29
+ - (void)log:(NSString *)message;
30
+ - (void)sendUnsentReports;
31
+ - (void)setCustomValue:(nullable id)value forKey:(NSString *)key;
32
+ - (void)setUserID:(NSString *)userID;
33
+ - (void)recordExceptionModel:(FIRExceptionModel *)exceptionModel;
34
+
35
+ /** Test seams */
36
+ @property(class, nonatomic, assign) BOOL didCrashDuringPreviousExecutionValue;
37
+ @property(class, nonatomic, assign) BOOL checkForUnsentReportsValue;
38
+ @property(class, nonatomic, copy, nullable) NSArray<NSString *> *loggedMessages;
39
+ @property(class, nonatomic, copy, nullable) NSDictionary *customValues;
40
+ @property(class, nonatomic, copy, nullable) NSString *userIDValue;
41
+ @property(class, nonatomic, assign) NSUInteger deleteUnsentReportsCallCount;
42
+ @property(class, nonatomic, assign) NSUInteger sendUnsentReportsCallCount;
43
+ @property(class, nonatomic, copy, nullable) FIRExceptionModel *lastRecordedExceptionModel;
44
+ /** Models whose main-queue save block has run (see recordExceptionModel:). */
45
+ @property(class, nonatomic, readonly, copy) NSArray<FIRExceptionModel *> *savedExceptionModels;
46
+ /** Called on the main queue right after a save block runs. */
47
+ @property(class, nonatomic, copy, nullable) void (^onExceptionModelSaved)(FIRExceptionModel *model);
48
+ /** Holds later saves like observers of a pending context-init promise, until fulfillContextInit. */
49
+ + (void)holdContextInit;
50
+
51
+ /** Dispatches the held saves to the main queue and stops holding new ones. */
52
+ + (void)fulfillContextInit;
53
+
54
+ + (void)resetTestState;
55
+
56
+ @end
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import Darwin
18
+ import XCTest
19
+
20
+ final class RNFBCrashlyticsDebuggerProbeTests: XCTestCase {
21
+
22
+ override func setUp() {
23
+ super.setUp()
24
+ RNFBCrashlyticsDebuggerProbe.resetForTesting()
25
+ }
26
+
27
+ override func tearDown() {
28
+ RNFBCrashlyticsDebuggerProbe.resetForTesting()
29
+ super.tearDown()
30
+ }
31
+
32
+ /// Writes a `kinfo_proc` with the given `p_flag` and returns 0 (success).
33
+ private func successProbe(pFlag: Int32) -> RNFBCrashlyticsDebuggerProbe.SysctlProbe {
34
+ { name, namelen, oldp, oldlenp, _, _ in
35
+ XCTAssertEqual(namelen, 4)
36
+ XCTAssertEqual(name[0], CTL_KERN)
37
+ XCTAssertEqual(name[1], KERN_PROC)
38
+ XCTAssertEqual(name[2], KERN_PROC_PID)
39
+ XCTAssertEqual(name[3], getpid())
40
+ guard let oldp else {
41
+ return -1
42
+ }
43
+ var info = kinfo_proc()
44
+ info.kp_proc.p_flag = pFlag
45
+ oldp.assumingMemoryBound(to: kinfo_proc.self).pointee = info
46
+ oldlenp.pointee = MemoryLayout<kinfo_proc>.size
47
+ return 0
48
+ }
49
+ }
50
+
51
+ /// Returns -1 after setting `errno` (sysctl failure path).
52
+ private func failureProbe(errnoValue: Int32, pFlag: Int32 = 0) -> RNFBCrashlyticsDebuggerProbe
53
+ .SysctlProbe
54
+ {
55
+ { _, _, oldp, oldlenp, _, _ in
56
+ if let oldp {
57
+ var info = kinfo_proc()
58
+ info.kp_proc.p_flag = pFlag
59
+ oldp.assumingMemoryBound(to: kinfo_proc.self).pointee = info
60
+ oldlenp.pointee = MemoryLayout<kinfo_proc>.size
61
+ }
62
+ errno = errnoValue
63
+ return -1
64
+ }
65
+ }
66
+
67
+ func testPublicPath_liveSysctl_returnsBoolWithoutCrash() {
68
+ // Exercises default Darwin.sysctl + once-cache. Under LLDB this may be true;
69
+ // on a non-debugged CI host it is typically false — do not assert the value.
70
+ _ = RNFBCrashlyticsDebuggerProbe.isDebuggerAttached()
71
+ // Second call must hit the once-cache return path.
72
+ _ = RNFBCrashlyticsDebuggerProbe.isDebuggerAttached()
73
+ }
74
+
75
+ func testEvaluate_notTraced_returnsFalse() {
76
+ let result = RNFBCrashlyticsDebuggerProbe.evaluateUncachedForTesting(
77
+ using: successProbe(pFlag: 0))
78
+ XCTAssertFalse(result)
79
+ }
80
+
81
+ func testEvaluate_pTraced_returnsTrue() {
82
+ let result = RNFBCrashlyticsDebuggerProbe.evaluateUncachedForTesting(
83
+ using: successProbe(pFlag: P_TRACED))
84
+ XCTAssertTrue(result)
85
+ }
86
+
87
+ func testEvaluate_sysctlFailure_logsAndReturnsFalse() {
88
+ let result = RNFBCrashlyticsDebuggerProbe.evaluateUncachedForTesting(
89
+ using: failureProbe(errnoValue: EINVAL, pFlag: 0))
90
+ XCTAssertFalse(result)
91
+ }
92
+
93
+ func testEvaluate_sysctlFailureWithTracedBit_stillTrueMatchingPrePortFallthrough() {
94
+ // Pre-port control flow still inspects P_TRACED after a failed sysctl.
95
+ let result = RNFBCrashlyticsDebuggerProbe.evaluateUncachedForTesting(
96
+ using: failureProbe(errnoValue: EFAULT, pFlag: P_TRACED))
97
+ XCTAssertTrue(result)
98
+ }
99
+
100
+ func testOnceCache_secondCallSkipsProbe() {
101
+ var probeCount = 0
102
+ let counting: RNFBCrashlyticsDebuggerProbe.SysctlProbe = { name, namelen, oldp, oldlenp, newp,
103
+ newlen in
104
+ probeCount += 1
105
+ return self.successProbe(pFlag: 0)(name, namelen, oldp, oldlenp, newp, newlen)
106
+ }
107
+ RNFBCrashlyticsDebuggerProbe.sysctlImpl = counting
108
+ XCTAssertFalse(RNFBCrashlyticsDebuggerProbe.isDebuggerAttached())
109
+ XCTAssertFalse(RNFBCrashlyticsDebuggerProbe.isDebuggerAttached())
110
+ XCTAssertEqual(probeCount, 1)
111
+ }
112
+ }
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Copyright (c) 2016-present Invertase Limited & Contributors
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this library except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ #import <XCTest/XCTest.h>
18
+
19
+ #import <Firebase/Firebase.h>
20
+ #import "RNFBCrashlyticsHelper.h"
21
+
22
+ @interface RNFBCrashlyticsFatalExitTests : XCTestCase
23
+ @end
24
+
25
+ @implementation RNFBCrashlyticsFatalExitTests {
26
+ NSMutableArray<NSString *> *_events;
27
+ }
28
+
29
+ - (void)setUp {
30
+ [super setUp];
31
+ [FIRCrashlytics resetTestState];
32
+ _events = [NSMutableArray new];
33
+ }
34
+
35
+ - (void)tearDown {
36
+ [FIRCrashlytics resetTestState];
37
+ [super tearDown];
38
+ }
39
+
40
+ - (void)appendEvent:(NSString *)event {
41
+ @synchronized(_events) {
42
+ [_events addObject:event];
43
+ }
44
+ }
45
+
46
+ - (NSArray<NSString *> *)eventsSnapshot {
47
+ @synchronized(_events) {
48
+ return [_events copy];
49
+ }
50
+ }
51
+
52
+ - (void)recordFatal {
53
+ [RNFBCrashlyticsHelper recordJavaScriptErrorWithMessage:@"fatal"
54
+ frames:@[]
55
+ isUnhandledPromiseRejection:NO];
56
+ }
57
+
58
+ /** Lets late watchdog / queue blocks run so "at most once" assertions are meaningful. */
59
+ - (void)drainFor:(NSTimeInterval)seconds {
60
+ XCTestExpectation *drained = [self expectationWithDescription:@"drained"];
61
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(seconds * NSEC_PER_SEC)),
62
+ dispatch_get_main_queue(), ^{
63
+ [drained fulfill];
64
+ });
65
+ [self waitForExpectations:@[ drained ] timeout:seconds + 5];
66
+ }
67
+
68
+ /** Returns once every block already enqueued on serial `queue` has run (a happens-before edge). */
69
+ - (void)drainQueue:(dispatch_queue_t)queue {
70
+ dispatch_block_t noop = ^{
71
+ };
72
+ dispatch_sync(queue, noop);
73
+ }
74
+
75
+ - (void)testShared_isProductionSingleton {
76
+ XCTAssertNotNil([RNFBCrashlyticsFatalExit shared]);
77
+ XCTAssertEqual([RNFBCrashlyticsFatalExit shared], [RNFBCrashlyticsFatalExit shared]);
78
+ XCTAssertEqualWithAccuracy(RNFBCrashlyticsFatalExitWatchdogTimeout, 2.0, 0.0001);
79
+ }
80
+
81
+ - (void)testRecordThenTerminate_savesOnMainBeforeTerminatorRuns {
82
+ FIRCrashlytics.onExceptionModelSaved = ^(FIRExceptionModel *model) {
83
+ [self appendEvent:[@"save:" stringByAppendingString:model.reason]];
84
+ };
85
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
86
+ __block NSUInteger terminations = 0;
87
+ // Held suspended until the main-queue terminate has run, so the watchdog cannot win under load.
88
+ dispatch_queue_t watchdogQueue =
89
+ dispatch_queue_create("rnfb.crashlytics.test.watchdog", DISPATCH_QUEUE_SERIAL);
90
+ dispatch_suspend(watchdogQueue);
91
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
92
+ initWithTerminator:^{
93
+ terminations += 1;
94
+ [self appendEvent:@"exit"];
95
+ XCTAssertTrue([NSThread isMainThread]);
96
+ [terminated fulfill];
97
+ }
98
+ terminateQueue:dispatch_get_main_queue()
99
+ watchdogQueue:watchdogQueue
100
+ watchdogTimeout:0.2];
101
+
102
+ // Production calls in on the module's own (non-main) method queue.
103
+ __block BOOL started = NO;
104
+ dispatch_queue_t moduleQueue =
105
+ dispatch_queue_create("rnfb.crashlytics.test.module", DISPATCH_QUEUE_SERIAL);
106
+ dispatch_async(moduleQueue, ^{
107
+ started = [fatalExit recordThenTerminate:^{
108
+ [self recordFatal];
109
+ }];
110
+ });
111
+
112
+ [self waitForExpectations:@[ terminated ] timeout:5];
113
+ dispatch_resume(watchdogQueue);
114
+ [self drainFor:0.4]; // past the watchdog, which must not terminate again
115
+ [self drainQueue:watchdogQueue];
116
+ [self drainQueue:moduleQueue]; // `started` is written on moduleQueue
117
+
118
+ XCTAssertTrue(started);
119
+ XCTAssertEqual(terminations, 1u);
120
+ NSArray *expected = @[ @"save:fatal", @"exit" ];
121
+ XCTAssertEqualObjects([self eventsSnapshot], expected);
122
+ XCTAssertEqual(FIRCrashlytics.savedExceptionModels.count, 1u);
123
+ XCTAssertEqualObjects(FIRCrashlytics.savedExceptionModels.firstObject.name, @"JavaScriptError");
124
+ }
125
+
126
+ - (void)testRecordThenTerminate_contextInitPending_terminatesBeforeSave {
127
+ [FIRCrashlytics holdContextInit];
128
+ FIRCrashlytics.onExceptionModelSaved = ^(FIRExceptionModel *model) {
129
+ [self appendEvent:[@"save:" stringByAppendingString:model.reason]];
130
+ };
131
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
132
+ __block NSUInteger terminations = 0;
133
+ __block NSUInteger savedAtExit = NSUIntegerMax;
134
+ __block BOOL recordedAtExit = NO;
135
+ dispatch_queue_t watchdogQueue =
136
+ dispatch_queue_create("rnfb.crashlytics.test.watchdog", DISPATCH_QUEUE_SERIAL);
137
+ dispatch_suspend(watchdogQueue);
138
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
139
+ initWithTerminator:^{
140
+ terminations += 1;
141
+ savedAtExit = FIRCrashlytics.savedExceptionModels.count;
142
+ recordedAtExit = FIRCrashlytics.lastRecordedExceptionModel != nil;
143
+ [self appendEvent:@"exit"];
144
+ [FIRCrashlytics fulfillContextInit];
145
+ [terminated fulfill];
146
+ }
147
+ terminateQueue:dispatch_get_main_queue()
148
+ watchdogQueue:watchdogQueue
149
+ watchdogTimeout:0.2];
150
+
151
+ __block BOOL started = NO;
152
+ dispatch_queue_t moduleQueue =
153
+ dispatch_queue_create("rnfb.crashlytics.test.module", DISPATCH_QUEUE_SERIAL);
154
+ dispatch_async(moduleQueue, ^{
155
+ started = [fatalExit recordThenTerminate:^{
156
+ [self recordFatal];
157
+ }];
158
+ });
159
+
160
+ [self waitForExpectations:@[ terminated ] timeout:5];
161
+ dispatch_resume(watchdogQueue);
162
+ [self drainFor:0.4];
163
+ [self drainQueue:watchdogQueue];
164
+ [self drainQueue:moduleQueue];
165
+
166
+ XCTAssertTrue(started);
167
+ XCTAssertEqual(terminations, 1u);
168
+ XCTAssertTrue(recordedAtExit);
169
+ XCTAssertEqual(savedAtExit, 0u);
170
+ NSArray *expected = @[ @"exit", @"save:fatal" ];
171
+ XCTAssertEqualObjects([self eventsSnapshot], expected);
172
+ }
173
+
174
+ - (void)testWatchdog_terminatesWhenTerminateQueueIsHung {
175
+ dispatch_queue_t hungQueue = dispatch_queue_create("rnfb.crashlytics.test.hung", 0);
176
+ dispatch_semaphore_t release = dispatch_semaphore_create(0);
177
+ dispatch_async(hungQueue, ^{
178
+ dispatch_semaphore_wait(release, DISPATCH_TIME_FOREVER);
179
+ });
180
+
181
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
182
+ __block NSUInteger terminations = 0;
183
+ __block BOOL terminatedWhileHung = NO;
184
+ __block BOOL hung = YES;
185
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
186
+ initWithTerminator:^{
187
+ terminations += 1;
188
+ terminatedWhileHung = hung;
189
+ [terminated fulfill];
190
+ }
191
+ terminateQueue:hungQueue
192
+ watchdogQueue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
193
+ watchdogTimeout:0.1];
194
+
195
+ XCTAssertTrue([fatalExit recordThenTerminate:^{
196
+ [self recordFatal];
197
+ }]);
198
+ [self waitForExpectations:@[ terminated ] timeout:5];
199
+
200
+ hung = NO;
201
+ dispatch_semaphore_signal(release);
202
+ [self drainQueue:hungQueue]; // the queued terminate block has now run
203
+
204
+ XCTAssertTrue(terminatedWhileHung);
205
+ XCTAssertEqual(terminations, 1u);
206
+ }
207
+
208
+ - (void)testRecordRaises_stillTerminatesAndIgnoresLaterFatals {
209
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
210
+ __block NSUInteger terminations = 0;
211
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
212
+ initWithTerminator:^{
213
+ terminations += 1;
214
+ [terminated fulfill];
215
+ }
216
+ terminateQueue:dispatch_get_main_queue()
217
+ watchdogQueue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
218
+ watchdogTimeout:0.1];
219
+
220
+ BOOL first = [fatalExit recordThenTerminate:^{
221
+ [NSException raise:NSInternalInconsistencyException format:@"record failed"];
222
+ }];
223
+ BOOL second = [fatalExit recordThenTerminate:^{
224
+ [self recordFatal];
225
+ }];
226
+
227
+ [self waitForExpectations:@[ terminated ] timeout:5];
228
+ [self drainFor:0.3];
229
+
230
+ XCTAssertTrue(first);
231
+ XCTAssertFalse(second);
232
+ XCTAssertEqual(terminations, 1u);
233
+ XCTAssertEqual(FIRCrashlytics.savedExceptionModels.count, 0u);
234
+ }
235
+
236
+ - (void)testSecondFatalWhileTerminating_isIgnored {
237
+ XCTestExpectation *terminated = [self expectationWithDescription:@"terminated"];
238
+ __block NSUInteger terminations = 0;
239
+ __block NSUInteger records = 0;
240
+ RNFBCrashlyticsFatalExit *fatalExit = [[RNFBCrashlyticsFatalExit alloc]
241
+ initWithTerminator:^{
242
+ terminations += 1;
243
+ [terminated fulfill];
244
+ }
245
+ terminateQueue:dispatch_get_main_queue()
246
+ watchdogQueue:dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)
247
+ watchdogTimeout:0.1];
248
+
249
+ BOOL first = [fatalExit recordThenTerminate:^{
250
+ records += 1;
251
+ [self recordFatal];
252
+ }];
253
+ BOOL second = [fatalExit recordThenTerminate:^{
254
+ records += 1;
255
+ [self recordFatal];
256
+ }];
257
+
258
+ [self waitForExpectations:@[ terminated ] timeout:5];
259
+ [self drainFor:0.3];
260
+
261
+ XCTAssertTrue(first);
262
+ XCTAssertFalse(second);
263
+ XCTAssertEqual(records, 1u);
264
+ XCTAssertEqual(terminations, 1u);
265
+ XCTAssertEqual(FIRCrashlytics.savedExceptionModels.count, 1u);
266
+ }
267
+
268
+ @end