Skip to content

Commit 764eb71

Browse files
committed
[tests] Move concurrency tests to new file
1 parent 156e3a4 commit 764eb71

File tree

3 files changed

+285
-231
lines changed

3 files changed

+285
-231
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
3CC063E62B6D7F96002BB07F /* OneSignalUserMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */; };
146146
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */; };
147147
3CC063EF2B6D7FE8002BB07F /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; };
148+
3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */; };
148149
3CC9A6342AFA1FDE008F68FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */; };
149150
3CC9A6362AFA26E7008F68FD /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */; };
150151
3CCF44BE299B17290021964D /* OneSignalWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CCF44BC299B17290021964D /* OneSignalWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -1272,6 +1273,7 @@
12721273
3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalUserMocks.swift; sourceTree = "<group>"; };
12731274
3CC063EB2B6D7FE8002BB07F /* OneSignalUserTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OneSignalUserTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
12741275
3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalUserTests.swift; sourceTree = "<group>"; };
1276+
3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConcurrencyTests.swift; sourceTree = "<group>"; };
12751277
3CC9A6332AFA1FDD008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
12761278
3CC9A6352AFA26E7008F68FD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
12771279
3CCF44BC299B17290021964D /* OneSignalWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalWrapper.h; sourceTree = "<group>"; };
@@ -2147,6 +2149,7 @@
21472149
children = (
21482150
3CDE664A2BFC2A55006DA114 /* OneSignalUserTests-Bridging-Header.h */,
21492151
3CC063ED2B6D7FE8002BB07F /* OneSignalUserTests.swift */,
2152+
3CC890342C5BF9A7002CB4CC /* UserConcurrencyTests.swift */,
21502153
3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */,
21512154
3CDE664B2BFC2A56006DA114 /* OneSignalUserObjcTests.m */,
21522155
);
@@ -4087,6 +4090,7 @@
40874090
files = (
40884091
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */,
40894092
3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */,
4093+
3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */,
40904094
3CDE664C2BFC2A56006DA114 /* OneSignalUserObjcTests.m in Sources */,
40914095
);
40924096
runOnlyForDeploymentPostprocessing = 0;

iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift

Lines changed: 0 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)