Skip to content

Commit 67f9838

Browse files
Mastersam07mikehardy
authored andcommitted
test(crashlytics, ios): cover JS fatal exit before context init and correct header limits
1 parent c3f15e3 commit 67f9838

4 files changed

Lines changed: 93 additions & 6 deletions

File tree

‎packages/crashlytics/ios/RNFBCrashlytics/RNFBCrashlyticsHelper.h‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,10 @@ FOUNDATION_EXPORT const NSTimeInterval RNFBCrashlyticsFatalExitWatchdogTimeout;
7171
* is hung. The terminator runs at most once.
7272
*
7373
* Known limit: a JS fatal recorded before Crashlytics finished initialising queues its save as a
74-
* promise observer instead of a main-queue block, so it can still be lost (unchanged behaviour).
75-
* The FIFO ordering also relies on FBLPromise's default dispatch queue being the main queue; if
76-
* that changes, only the watchdog bounds the wait.
74+
* promise observer instead of a main-queue block, so it can still be lost.
75+
* The FIFO ordering also relies on FBLPromise's default dispatch queue being the main queue; if an
76+
* app changes it, the save runs off the main queue and the terminator can run first, losing the
77+
* report. The watchdog only covers a hung `terminateQueue`.
7778
*/
7879
@interface RNFBCrashlyticsFatalExit : NSObject
7980

‎packages/crashlytics/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/FIRCrashlyticsHostStub.m‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ @implementation FIRCrashlytics
6666
static FIRExceptionModel *sLastRecordedExceptionModel;
6767
static NSMutableArray<FIRExceptionModel *> *sSavedExceptionModels;
6868
static void (^sOnExceptionModelSaved)(FIRExceptionModel *);
69+
static BOOL sContextInitPending;
70+
static NSMutableArray<dispatch_block_t> *sPendingSaves;
6971
// Bumped by resetTestState so saves enqueued by an earlier test are dropped.
7072
static atomic_uint_fast64_t sSaveGeneration;
7173

@@ -74,6 +76,7 @@ + (void)initialize {
7476
sLoggedMessages = [NSMutableArray new];
7577
sCustomValues = [NSMutableDictionary new];
7678
sSavedExceptionModels = [NSMutableArray new];
79+
sPendingSaves = [NSMutableArray new];
7780
}
7881
}
7982

@@ -168,6 +171,24 @@ + (void)setOnExceptionModelSaved:(void (^)(FIRExceptionModel *))onExceptionModel
168171
sOnExceptionModelSaved = [onExceptionModelSaved copy];
169172
}
170173

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+
171192
+ (void)resetTestState {
172193
sDidCrashDuringPreviousExecutionValue = NO;
173194
sCheckForUnsentReportsValue = NO;
@@ -179,6 +200,10 @@ + (void)resetTestState {
179200
sLastRecordedExceptionModel = nil;
180201
[sSavedExceptionModels removeAllObjects];
181202
sOnExceptionModelSaved = nil;
203+
@synchronized(sPendingSaves) {
204+
sContextInitPending = NO;
205+
[sPendingSaves removeAllObjects];
206+
}
182207
atomic_fetch_add(&sSaveGeneration, 1);
183208
}
184209

@@ -224,17 +249,25 @@ - (void)setUserID:(NSString *)userID {
224249
- (void)recordExceptionModel:(FIRExceptionModel *)exceptionModel {
225250
sLastRecordedExceptionModel = exceptionModel;
226251
// Mirrors FirebaseCrashlytics: once context init is fulfilled, the save block is dispatched to
227-
// the main queue and writes synchronously when it runs.
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.
228254
uint_fast64_t generation = atomic_load(&sSaveGeneration);
229-
dispatch_async(dispatch_get_main_queue(), ^{
255+
dispatch_block_t save = ^{
230256
if (generation != atomic_load(&sSaveGeneration)) {
231257
return;
232258
}
233259
[sSavedExceptionModels addObject:exceptionModel];
234260
if (sOnExceptionModelSaved != nil) {
235261
sOnExceptionModelSaved(exceptionModel);
236262
}
237-
});
263+
};
264+
@synchronized(sPendingSaves) {
265+
if (sContextInitPending) {
266+
[sPendingSaves addObject:save];
267+
return;
268+
}
269+
}
270+
dispatch_async(dispatch_get_main_queue(), save);
238271
}
239272

240273
@end

‎packages/crashlytics/ios/RNFBCrashlyticsUnitTests/HostStubs/Firebase/Firebase.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
@property(class, nonatomic, readonly, copy) NSArray<FIRExceptionModel *> *savedExceptionModels;
4646
/** Called on the main queue right after a save block runs. */
4747
@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;
4853

4954
+ (void)resetTestState;
5055

‎packages/crashlytics/ios/RNFBCrashlyticsUnitTests/RNFBCrashlyticsFatalExitTests.m‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,54 @@ - (void)testRecordThenTerminate_savesOnMainBeforeTerminatorRuns {
123123
XCTAssertEqualObjects(FIRCrashlytics.savedExceptionModels.firstObject.name, @"JavaScriptError");
124124
}
125125

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+
126174
- (void)testWatchdog_terminatesWhenTerminateQueueIsHung {
127175
dispatch_queue_t hungQueue = dispatch_queue_create("rnfb.crashlytics.test.hung", 0);
128176
dispatch_semaphore_t release = dispatch_semaphore_create(0);

0 commit comments

Comments
 (0)