@@ -61,74 +61,213 @@ - (void)testBackgroundTracking {
61
61
}];
62
62
}
63
63
64
- /* * Tests that synchronous observer registration works correctly and observers are immediately available. */
64
+ /* * Tests that synchronous observer registration works correctly and observers are immediately
65
+ * available. */
65
66
- (void )testObservers_synchronousRegistrationAddsObserver {
66
67
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter ];
67
68
FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc ] init ];
68
69
XCTAssertNotNil (tracker);
69
-
70
+
70
71
[notificationCenter postNotificationName: UIApplicationDidBecomeActiveNotification
71
- object: [UIApplication sharedApplication ]];
72
+ object: [UIApplication sharedApplication ]];
72
73
XCTAssertEqual (tracker.traceBackgroundState , FPRTraceStateForegroundOnly);
73
-
74
+
74
75
tracker = nil ;
75
76
XCTAssertNil (tracker);
76
77
XCTAssertNoThrow ([notificationCenter postNotificationName: UIApplicationDidBecomeActiveNotification
77
78
object: [UIApplication sharedApplication ]]);
78
- XCTAssertNoThrow ([notificationCenter postNotificationName: UIApplicationDidEnterBackgroundNotification
79
- object: [UIApplication sharedApplication ]]);
79
+ XCTAssertNoThrow ([notificationCenter
80
+ postNotificationName: UIApplicationDidEnterBackgroundNotification
81
+ object: [UIApplication sharedApplication ]]);
80
82
}
81
83
82
84
/* * Tests rapid creation and deallocation to verify race condition fix. */
83
85
- (void )testRapidCreationAndDeallocation_noRaceCondition {
84
- for (int i = 0 ; i < 100 ; i++) {
86
+ // This test simulates the real crash scenario by forcing async dispatch timing
87
+ XCTestExpectation *expectation =
88
+ [self expectationWithDescription: @" All async operations complete" ];
89
+
90
+ __block int completedOperations = 0 ;
91
+ const int totalOperations = 50 ;
92
+
93
+ for (int i = 0 ; i < totalOperations; i++) {
85
94
@autoreleasepool {
86
95
FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc ] init ];
87
96
XCTAssertNotNil (tracker);
88
-
89
- [[NSNotificationCenter defaultCenter ] postNotificationName: UIApplicationDidBecomeActiveNotification
90
- object: [UIApplication sharedApplication ]];
97
+
98
+ // Force multiple runloop cycles to increase chance of race condition
99
+ dispatch_async (dispatch_get_main_queue (), ^{
100
+ // This would crash with old async registration if tracker is deallocated
101
+ [[NSNotificationCenter defaultCenter ]
102
+ postNotificationName: UIApplicationDidBecomeActiveNotification
103
+ object: [UIApplication sharedApplication ]];
104
+
105
+ // Increment counter and fulfill expectation when done
106
+ completedOperations++;
107
+ if (completedOperations == totalOperations) {
108
+ [expectation fulfill ];
109
+ }
110
+ });
111
+
112
+ // Tracker deallocates here immediately due to @autoreleasepool
91
113
}
92
114
}
93
-
94
- XCTAssertNoThrow ([[NSNotificationCenter defaultCenter ] postNotificationName: UIApplicationDidBecomeActiveNotification
95
- object: [UIApplication sharedApplication ]]);
96
- XCTAssertNoThrow ([[NSNotificationCenter defaultCenter ] postNotificationName: UIApplicationDidEnterBackgroundNotification
97
- object: [UIApplication sharedApplication ]]);
115
+
116
+ // Wait for all async operations to complete
117
+ [self waitForExpectationsWithTimeout: 10.0
118
+ handler: ^(NSError *error) {
119
+ XCTAssertNil (
120
+ error, @" Operations timed out - potential deadlock or crash" );
121
+ }];
122
+
123
+ // Additional safety check - post more notifications after everything is done
124
+ XCTAssertNoThrow ([[NSNotificationCenter defaultCenter ]
125
+ postNotificationName: UIApplicationDidBecomeActiveNotification
126
+ object: [UIApplication sharedApplication ]]);
127
+ XCTAssertNoThrow ([[NSNotificationCenter defaultCenter ]
128
+ postNotificationName: UIApplicationDidEnterBackgroundNotification
129
+ object: [UIApplication sharedApplication ]]);
98
130
}
99
131
100
132
/* * Tests that observers are registered immediately after init on main thread. */
101
133
- (void )testObservers_immediateRegistrationOnMainThread {
102
134
XCTAssertTrue ([NSThread isMainThread ]);
103
-
135
+
104
136
FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc ] init ];
105
-
106
- [[NSNotificationCenter defaultCenter ] postNotificationName: UIApplicationDidBecomeActiveNotification
107
- object: [UIApplication sharedApplication ]];
108
-
137
+
138
+ [[NSNotificationCenter defaultCenter ]
139
+ postNotificationName: UIApplicationDidBecomeActiveNotification
140
+ object: [UIApplication sharedApplication ]];
141
+
109
142
XCTAssertEqual (tracker.traceBackgroundState , FPRTraceStateForegroundOnly);
110
143
}
111
144
112
145
/* * Tests observer registration when created from background thread. */
113
146
- (void )testObservers_registrationFromBackgroundThread {
114
147
XCTestExpectation *expectation = [self expectationWithDescription: @" Background thread creation" ];
115
-
148
+
116
149
dispatch_async (dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{
117
150
FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc ] init ];
118
151
XCTAssertNotNil (tracker);
119
-
152
+
120
153
dispatch_async (dispatch_get_main_queue (), ^{
121
- [[NSNotificationCenter defaultCenter ] postNotificationName: UIApplicationDidBecomeActiveNotification
122
- object: [UIApplication sharedApplication ]];
123
-
154
+ [[NSNotificationCenter defaultCenter ]
155
+ postNotificationName: UIApplicationDidBecomeActiveNotification
156
+ object: [UIApplication sharedApplication ]];
157
+
124
158
XCTAssertEqual (tracker.traceBackgroundState , FPRTraceStateForegroundOnly);
125
159
[expectation fulfill ];
126
160
});
127
161
});
128
-
129
- [self waitForExpectationsWithTimeout: 5.0 handler: ^(NSError *error) {
130
- XCTAssertNil (error, @" Test timed out" );
131
- }];
162
+
163
+ [self waitForExpectationsWithTimeout: 5.0
164
+ handler: ^(NSError *error) {
165
+ XCTAssertNil (error, @" Test timed out" );
166
+ }];
167
+ }
168
+
169
+ /* * Tests the exact crash scenario with async dispatch timing that would crash with old
170
+ * implementation. */
171
+ - (void )testAsyncDispatch_wouldCrashWithOldImplementation {
172
+ // This test simulates what the OLD code would do and should crash with async registration
173
+ // With the NEW synchronous code, this should pass safely
174
+
175
+ XCTestExpectation *expectation = [self expectationWithDescription: @" Async crash test complete" ];
176
+
177
+ __block int remainingOperations = 200 ;
178
+
179
+ for (int i = 0 ; i < 200 ; i++) {
180
+ @autoreleasepool {
181
+ FPRTraceBackgroundActivityTracker *tracker = [[FPRTraceBackgroundActivityTracker alloc ] init ];
182
+ XCTAssertNotNil (tracker);
183
+
184
+ // Simulate the old problematic pattern
185
+ __weak typeof (tracker) weakTracker = tracker;
186
+
187
+ // This mimics what the OLD async registration would do
188
+ dispatch_async (dispatch_get_main_queue (), ^{
189
+ // In old code: tracker might be deallocated here → CRASH
190
+ // In new code: observers already registered synchronously → SAFE
191
+
192
+ if (weakTracker) {
193
+ [[NSNotificationCenter defaultCenter ]
194
+ postNotificationName: UIApplicationDidBecomeActiveNotification
195
+ object: [UIApplication sharedApplication ]];
196
+ }
197
+
198
+ remainingOperations--;
199
+ if (remainingOperations == 0 ) {
200
+ [expectation fulfill ];
201
+ }
202
+ });
203
+
204
+ // Immediately deallocate tracker - this creates the race condition window
205
+ }
206
+
207
+ // Force runloop processing to increase race condition likelihood
208
+ [[NSRunLoop currentRunLoop ] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.001 ]];
209
+ }
210
+
211
+ [self waitForExpectationsWithTimeout: 5.0
212
+ handler: ^(NSError *error) {
213
+ XCTAssertNil (error, @" Async crash test failed" );
214
+ }];
215
+ }
216
+
217
+ @end
218
+
219
+ /* *
220
+ * CRASH REPRODUCTION TEST - Only use this to verify the original bug exists
221
+ * This simulates the original async registration pattern that would cause crashes
222
+ */
223
+
224
+ @interface CrashReproductionTracker : NSObject
225
+ @property (nonatomic , readwrite ) int traceBackgroundState;
226
+ @end
227
+
228
+ @implementation CrashReproductionTracker
229
+
230
+ - (instancetype )init {
231
+ self = [super init ];
232
+ if (self) {
233
+ _traceBackgroundState = 0 ;
234
+
235
+ // This is the ORIGINAL problematic code that would crash
236
+ dispatch_async (dispatch_get_main_queue (), ^{
237
+ [[NSNotificationCenter defaultCenter ] addObserver: self
238
+ selector: @selector (handleNotification: )
239
+ name: UIApplicationDidBecomeActiveNotification
240
+ object: [UIApplication sharedApplication ]];
241
+ });
242
+ }
243
+ return self;
244
+ }
245
+
246
+ - (void )handleNotification : (NSNotification *)notification {
247
+ _traceBackgroundState = 1 ;
248
+ }
249
+
250
+ - (void )dealloc {
251
+ [[NSNotificationCenter defaultCenter ] removeObserver: self ];
252
+ }
253
+
254
+ /* *
255
+ * CRASH REPRODUCTION TEST - Only use this to verify the original bug exists
256
+ * This simulates the original async registration pattern that would cause crashes
257
+ * WARNING: This test is commented out because it WILL crash with the original async pattern
258
+ */
259
+
260
+ - (void )testCrashReproduction_originalAsyncBug {
261
+ // This test WILL crash with the original async pattern
262
+ for (int i = 0 ; i < 100 ; i++) {
263
+ @autoreleasepool {
264
+ CrashReproductionTracker *tracker = [[CrashReproductionTracker alloc ] init ];
265
+ // tracker deallocates here, but async block is still queued → CRASH
266
+ }
267
+
268
+ // Process run loop to execute queued async blocks
269
+ [[NSRunLoop currentRunLoop ] runUntilDate: [NSDate dateWithTimeIntervalSinceNow: 0.01 ]];
270
+ }
132
271
}
133
272
134
273
@end
0 commit comments