@@ -64,237 +64,6 @@ final class OneSignalUserTests: XCTestCase {
6464 XCTAssertEqual ( userInstanceExternalId, " my-external-id " )
6565 }
6666
67- /**
68- This test reproduces a crash in the Operation Repo's flushing delta queue.
69- It is possible for two threads to flush concurrently.
70- However, this test does not crash 100% of the time.
71- */
72- func testOperationRepoFlushingConcurrency( ) throws {
73- /* Setup */
74- OneSignalCoreImpl . setSharedClient ( MockOneSignalClient ( ) )
75-
76- /* When */
77-
78- // 1. Enqueue 10 Deltas to the Operation Repo
79- for num in 0 ... 9 {
80- OneSignalUserManagerImpl . sharedInstance. addTag ( key: " tag \( num) " , value: " value " )
81- }
82-
83- // 2. Flush the delta queue from 4 multiple threads
84- for _ in 1 ... 4 {
85- DispatchQueue . global ( ) . async {
86- print ( " 🧪 flushDeltaQueue on thread \( Thread . current) " )
87- OSOperationRepo . sharedInstance. addFlushDeltaQueueToDispatchQueue ( )
88- }
89- }
90-
91- /* Then */
92- // There are two places that can crash, as multiple threads are manipulating arrays:
93- // 1. OpRepo: `deltaQueue.remove(at: index)` index out of bounds
94- // 2. OSPropertyOperationExecutor: `deltaQueue.append(delta)` EXC_BAD_ACCESS
95- }
96-
97- /**
98- This test reproduces a crash in the Subscription Executor.
99- It is possible for two threads to modify and cache queues concurrently.
100- */
101- func testSubscriptionExecutorConcurrency( ) throws {
102- /* Setup */
103-
104- let client = MockOneSignalClient ( )
105- client. setMockResponseForRequest (
106- request: " <OSRequestDeleteSubscription with subscriptionModel: nil> " ,
107- response: [ : ]
108- )
109- OneSignalCoreImpl . setSharedClient ( client)
110-
111- let executor = OSSubscriptionOperationExecutor ( )
112- OSOperationRepo . sharedInstance. addExecutor ( executor)
113-
114- /* When */
115-
116- DispatchQueue . concurrentPerform ( iterations: 50 ) { _ in
117- // 1. Enqueue Remove Subscription Deltas to the Operation Repo
118- OSOperationRepo . sharedInstance. enqueueDelta ( OSDelta ( name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID ( ) . uuidString, model: OSSubscriptionModel ( type: . email, address: nil , subscriptionId: UUID ( ) . uuidString, reachable: true , isDisabled: false , changeNotifier: OSEventProducer ( ) ) , property: " email " , value: " email " ) )
119- OSOperationRepo . sharedInstance. enqueueDelta ( OSDelta ( name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID ( ) . uuidString, model: OSSubscriptionModel ( type: . email, address: nil , subscriptionId: UUID ( ) . uuidString, reachable: true , isDisabled: false , changeNotifier: OSEventProducer ( ) ) , property: " email " , value: " email " ) )
120-
121- // 2. Flush Operation Repo
122- OSOperationRepo . sharedInstance. addFlushDeltaQueueToDispatchQueue ( )
123-
124- // 3. Simulate updating the executor's request queue from a network response
125- executor. executeDeleteSubscriptionRequest ( OSRequestDeleteSubscription ( subscriptionModel: OSSubscriptionModel ( type: . email, address: nil , subscriptionId: UUID ( ) . uuidString, reachable: true , isDisabled: false , changeNotifier: OSEventProducer ( ) ) ) , inBackground: false )
126- executor. executeDeleteSubscriptionRequest ( OSRequestDeleteSubscription ( subscriptionModel: OSSubscriptionModel ( type: . email, address: nil , subscriptionId: UUID ( ) . uuidString, reachable: true , isDisabled: false , changeNotifier: OSEventProducer ( ) ) ) , inBackground: false )
127- }
128-
129- // 4. Run background threads
130- OneSignalCoreMocks . waitForBackgroundThreads ( seconds: 0.5 )
131-
132- /* Then */
133- // Previously caused crash: signal SIGABRT - malloc: double free for ptr
134- // Assert that every request SDK makes has a response set, and is handled
135- XCTAssertTrue ( client. allRequestsHandled)
136- }
137-
138- /**
139- This test reproduces a crash in the Identity Executor.
140- It is possible for two threads to modify and cache queues concurrently.
141- */
142- func testIdentityExecutorConcurrency( ) throws {
143- /* Setup */
144- let client = MockOneSignalClient ( )
145- let aliases = [ UUID ( ) . uuidString: " id " ]
146-
147- OneSignalCoreImpl . setSharedClient ( client)
148- MockUserRequests . setAddAliasesResponse ( with: client, aliases: aliases)
149-
150- let executor = OSIdentityOperationExecutor ( )
151- OSOperationRepo . sharedInstance. addExecutor ( executor)
152-
153- /* When */
154-
155- DispatchQueue . concurrentPerform ( iterations: 50 ) { _ in
156- // 1. Enqueue Add Alias Deltas to the Operation Repo
157- OSOperationRepo . sharedInstance. enqueueDelta ( OSDelta ( name: OS_ADD_ALIAS_DELTA, identityModelId: UUID ( ) . uuidString, model: OSIdentityModel ( aliases: [ OS_ONESIGNAL_ID: UUID ( ) . uuidString] , changeNotifier: OSEventProducer ( ) ) , property: " aliases " , value: aliases) )
158- OSOperationRepo . sharedInstance. enqueueDelta ( OSDelta ( name: OS_ADD_ALIAS_DELTA, identityModelId: UUID ( ) . uuidString, model: OSIdentityModel ( aliases: [ OS_ONESIGNAL_ID: UUID ( ) . uuidString] , changeNotifier: OSEventProducer ( ) ) , property: " aliases " , value: aliases) )
159-
160- // 2. Flush Operation Repo
161- OSOperationRepo . sharedInstance. addFlushDeltaQueueToDispatchQueue ( )
162-
163- // 3. Simulate updating the executor's request queue from a network response
164- executor. executeAddAliasesRequest ( OSRequestAddAliases ( aliases: aliases, identityModel: OSIdentityModel ( aliases: [ OS_ONESIGNAL_ID: UUID ( ) . uuidString] , changeNotifier: OSEventProducer ( ) ) ) , inBackground: false )
165- executor. executeAddAliasesRequest ( OSRequestAddAliases ( aliases: aliases, identityModel: OSIdentityModel ( aliases: [ OS_ONESIGNAL_ID: UUID ( ) . uuidString] , changeNotifier: OSEventProducer ( ) ) ) , inBackground: false )
166- }
167-
168- // 4. Run background threads
169- OneSignalCoreMocks . waitForBackgroundThreads ( seconds: 0.5 )
170-
171- /* Then */
172- // Previously caused crash: signal SIGABRT - malloc: double free for ptr
173- // Assert that every request SDK makes has a response set, and is handled
174- XCTAssertTrue ( client. allRequestsHandled)
175- }
176-
177- /**
178- This test aims to ensure concurrency safety in the Property Executor.
179- It is possible for two threads to modify and cache queues concurrently.
180- */
181- func testPropertyExecutorConcurrency( ) throws {
182- /* Setup */
183- let client = MockOneSignalClient ( )
184- // Ensure all requests fire the executor's callback so it will modify queues and cache it
185- client. fireSuccessForAllRequests = true
186- OneSignalCoreImpl . setSharedClient ( client)
187-
188- let identityModel = OSIdentityModel ( aliases: [ OS_ONESIGNAL_ID: UUID ( ) . uuidString] , changeNotifier: OSEventProducer ( ) )
189- OneSignalUserManagerImpl . sharedInstance. addIdentityModelToRepo ( identityModel)
190-
191- let executor = OSPropertyOperationExecutor ( )
192- OSOperationRepo . sharedInstance. addExecutor ( executor)
193-
194- /* When */
195-
196- DispatchQueue . concurrentPerform ( iterations: 50 ) { _ in
197- // 1. Enqueue Deltas to the Operation Repo
198- OSOperationRepo . sharedInstance. enqueueDelta ( OSDelta ( name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel. modelId, model: OSPropertiesModel ( changeNotifier: OSEventProducer ( ) ) , property: " language " , value: UUID ( ) . uuidString) )
199- OSOperationRepo . sharedInstance. enqueueDelta ( OSDelta ( name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel. modelId, model: OSPropertiesModel ( changeNotifier: OSEventProducer ( ) ) , property: " language " , value: UUID ( ) . uuidString) )
200-
201- // 2. Flush Operation Repo
202- OSOperationRepo . sharedInstance. addFlushDeltaQueueToDispatchQueue ( )
203-
204- // 3. Simulate updating the executor's request queue from a network response
205- executor. executeUpdatePropertiesRequest ( OSRequestUpdateProperties ( params: [ " properties " : [ " language " : UUID ( ) . uuidString] , " refresh_device_metadata " : false ] , identityModel: identityModel) , inBackground: false )
206- }
207-
208- // 4. Run background threads
209- OneSignalCoreMocks . waitForBackgroundThreads ( seconds: 0.5 )
210-
211- /* Then */
212- // No crash
213- }
214-
215- /**
216- This test aims to ensure concurrency safety in the User Executor.
217- It is possible for two threads to modify and cache queues concurrently.
218- Currently, this executor only allows one request to send at a time, which should prevent concurrent access.
219- But out of caution and future-proofing, this test is added.
220- */
221- func testUserExecutorConcurrency( ) throws {
222- /* Setup */
223-
224- let client = MockOneSignalClient ( )
225- // Ensure all requests fire the executor's callback so it will modify queues and cache it
226- client. fireSuccessForAllRequests = true
227- OneSignalCoreImpl . setSharedClient ( client)
228-
229- let identityModel1 = OSIdentityModel ( aliases: [ OS_ONESIGNAL_ID: UUID ( ) . uuidString] , changeNotifier: OSEventProducer ( ) )
230- let identityModel2 = OSIdentityModel ( aliases: [ OS_ONESIGNAL_ID: UUID ( ) . uuidString] , changeNotifier: OSEventProducer ( ) )
231-
232- let userExecutor = OSUserExecutor ( )
233-
234- /* When */
235-
236- DispatchQueue . concurrentPerform ( iterations: 50 ) { _ in
237- let identifyRequest = OSRequestIdentifyUser ( aliasLabel: OS_EXTERNAL_ID, aliasId: UUID ( ) . uuidString, identityModelToIdentify: identityModel1, identityModelToUpdate: identityModel2)
238- let fetchRequest = OSRequestFetchUser ( identityModel: identityModel1, aliasLabel: OS_ONESIGNAL_ID, aliasId: UUID ( ) . uuidString, onNewSession: false )
239-
240- // Append and execute requests simultaneously
241- userExecutor. appendToQueue ( identifyRequest)
242- userExecutor. appendToQueue ( fetchRequest)
243- userExecutor. executeIdentifyUserRequest ( identifyRequest)
244- userExecutor. executeFetchUserRequest ( fetchRequest)
245- }
246-
247- // Run background threads
248- OneSignalCoreMocks . waitForBackgroundThreads ( seconds: 0.5 )
249-
250- /* Then */
251- // No crash
252- }
253-
254- /**
255- This test reproduced a crash when the property model is being encoded.
256- */
257- func testEncodingPropertiesModel_withConcurrency_doesNotCrash( ) throws {
258- /* Setup */
259- let propertiesModel = OSPropertiesModel ( changeNotifier: OSEventProducer ( ) )
260-
261- /* When */
262- DispatchQueue . concurrentPerform ( iterations: 5_000 ) { i in
263- // 1. Add tags
264- for num in 0 ... 9 {
265- propertiesModel. addTags ( [ " \( i) tag \( num) " : " value " ] )
266- }
267-
268- // 2. Encode the model
269- OneSignalUserDefaults . initShared ( ) . saveCodeableData ( forKey: " PropertyModel " , withValue: propertiesModel)
270-
271- // 3. Clear the tags
272- propertiesModel. clearData ( )
273- }
274- }
275-
276- /**
277- This test reproduced a crash when the identity model is being encoded.
278- */
279- func testEncodingIdentityModel_withConcurrency_doesNotCrash( ) throws {
280- /* Setup */
281- let identityModel = OSIdentityModel ( aliases: nil , changeNotifier: OSEventProducer ( ) )
282-
283- /* When */
284- DispatchQueue . concurrentPerform ( iterations: 5_000 ) { i in
285- // 1. Add aliases
286- for num in 0 ... 9 {
287- identityModel. addAliases ( [ " \( i) alias \( num) " : " value " ] )
288- }
289-
290- // 2. Encode the model
291- OneSignalUserDefaults . initShared ( ) . saveCodeableData ( forKey: " IdentityModel " , withValue: identityModel)
292-
293- // 2. Clear the aliases
294- identityModel. clearData ( )
295- }
296- }
297-
29867 /**
29968 Tests multiple user updates should be combined and sent together.
30069 Multiple session times should be added.
0 commit comments