diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift index 6096ebed4..969b83236 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift @@ -137,6 +137,7 @@ public class MockOneSignalClient: NSObject, IOneSignalClient { } public func setMockResponseForRequest(request: String, response: [String: Any]) { + print("๐Ÿ’› setMockResponseForRequest \(request) \(response)") mockResponses[request] = response } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift index d6297fbd3..540dcca40 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift @@ -47,7 +47,7 @@ extension OSModelStoreListener { store.changeSubscription.subscribe(self) } - func close() { + public func close() { store.changeSubscription.unsubscribe(self) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 2a605bc26..f2d971be4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -120,7 +120,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { var identityModelRepo = OSIdentityModelRepo() - private var hasCalledStart = false + var hasCalledStart = false private var jwtExpiredHandler: OSJwtExpiredHandler? @@ -139,7 +139,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { return user } - private var _user: OSUserInternal? + var _user: OSUserInternal? // This is a user instance to operate on when there is no app_id and/or privacy consent yet, effectively no-op. // The models are not added to any model stores. diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift index e0eb6facd..c1dc1c147 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift @@ -51,31 +51,35 @@ public class OneSignalUserMocks: NSObject { */ public static func resetUserManager() { OneSignalUserManagerImpl.sharedInstance.identityModelRepo.reset() - + + // Model store listeners unsubscribe to their models + // User Manager start() will subscribe + OneSignalUserManagerImpl.sharedInstance.identityModelStoreListener.close() + OneSignalUserManagerImpl.sharedInstance.propertiesModelStoreListener.close() + OneSignalUserManagerImpl.sharedInstance.subscriptionModelStoreListener.close() + OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModelStoreListener.close() + OneSignalUserManagerImpl.sharedInstance.identityModelStore.clearModelsFromStore() OneSignalUserManagerImpl.sharedInstance.propertiesModelStore.clearModelsFromStore() OneSignalUserManagerImpl.sharedInstance.subscriptionModelStore.clearModelsFromStore() OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModelStore.clearModelsFromStore() - let propertyExecutor = OSPropertyOperationExecutor() - let identityExecutor = OSIdentityOperationExecutor() - let subscriptionExecutor = OSSubscriptionOperationExecutor() - OneSignalUserManagerImpl.sharedInstance.propertyExecutor = propertyExecutor - OneSignalUserManagerImpl.sharedInstance.identityExecutor = identityExecutor - OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor = subscriptionExecutor + + // TODO: Reset Operation Repo first // OSCoreMocks.resetOperationRepo() - OSOperationRepo.sharedInstance.addExecutor(identityExecutor) - OSOperationRepo.sharedInstance.addExecutor(propertyExecutor) - OSOperationRepo.sharedInstance.addExecutor(subscriptionExecutor) + + OneSignalUserManagerImpl.sharedInstance._user = nil + OneSignalUserManagerImpl.sharedInstance.hasCalledStart = false } } extension OSIdentityModelRepo { func reset() { + print("๐Ÿ’› reset") self.models = [:] } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index 622df026a..f7a4334fa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -69,78 +69,79 @@ final class OneSignalUserTests: XCTestCase { It is possible for two threads to flush concurrently. However, this test does not crash 100% of the time. */ -// func testOperationRepoFlushingConcurrency() throws { -// /* Setup */ -// OneSignalCoreImpl.setSharedClient(MockOneSignalClient()) -// -// /* When */ -// -// // 1. Enqueue 10 Deltas to the Operation Repo -// for num in 0...9 { -// OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag\(num)", value: "value") -// } -// -// // 2. Flush the delta queue from 4 multiple threads -// for _ in 1...4 { -// DispatchQueue.global().async { -// print("๐Ÿงช flushDeltaQueue on thread \(Thread.current)") -// OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() -// } -// } -// -// /* Then */ -// // There are two places that can crash, as multiple threads are manipulating arrays: -// // 1. OpRepo: `deltaQueue.remove(at: index)` index out of bounds -// // 2. OSPropertyOperationExecutor: `deltaQueue.append(delta)` EXC_BAD_ACCESS -// } + func testOperationRepoFlushingConcurrency() throws { + /* Setup */ + OneSignalCoreImpl.setSharedClient(MockOneSignalClient()) + + /* When */ + + // 1. Enqueue 10 Deltas to the Operation Repo + for num in 0...9 { + OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag\(num)", value: "value") + } + + // 2. Flush the delta queue from 4 multiple threads + for _ in 1...4 { + DispatchQueue.global().async { + print("๐Ÿงช flushDeltaQueue on thread \(Thread.current)") + OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() + } + } + + /* Then */ + // There are two places that can crash, as multiple threads are manipulating arrays: + // 1. OpRepo: `deltaQueue.remove(at: index)` index out of bounds + // 2. OSPropertyOperationExecutor: `deltaQueue.append(delta)` EXC_BAD_ACCESS + } /** This test reproduced a crash when the property model is being encoded. */ -// func testEncodingPropertiesModel_withConcurrency_doesNotCrash() throws { -// /* Setup */ -// let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer()) -// -// /* When */ -// DispatchQueue.concurrentPerform(iterations: 5_000) { i in -// // 1. Add tags -// for num in 0...9 { -// propertiesModel.addTags(["\(i)tag\(num)": "value"]) -// } -// -// // 2. Encode the model -// OneSignalUserDefaults.initShared().saveCodeableData(forKey: "PropertyModel", withValue: propertiesModel) -// -// // 3. Clear the tags -// propertiesModel.clearData() -// } -// } + func testEncodingPropertiesModel_withConcurrency_doesNotCrash() throws { + /* Setup */ + let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer()) + + /* When */ + DispatchQueue.concurrentPerform(iterations: 5_000) { i in + // 1. Add tags + for num in 0...9 { + propertiesModel.addTags(["\(i)tag\(num)": "value"]) + } + + // 2. Encode the model + OneSignalUserDefaults.initShared().saveCodeableData(forKey: "PropertyModel", withValue: propertiesModel) + + // 3. Clear the tags + propertiesModel.clearData() + } + } /** This test reproduced a crash when the identity model is being encoded. */ -// func testEncodingIdentityModel_withConcurrency_doesNotCrash() throws { -// /* Setup */ -// let identityModel = OSIdentityModel(aliases: nil, changeNotifier: OSEventProducer()) -// -// /* When */ -// DispatchQueue.concurrentPerform(iterations: 5_000) { i in -// // 1. Add aliases -// for num in 0...9 { -// identityModel.addAliases(["\(i)alias\(num)": "value"]) -// } -// -// // 2. Encode the model -// OneSignalUserDefaults.initShared().saveCodeableData(forKey: "IdentityModel", withValue: identityModel) -// -// // 2. Clear the aliases -// identityModel.clearData() -// } -// } + func testEncodingIdentityModel_withConcurrency_doesNotCrash() throws { + /* Setup */ + let identityModel = OSIdentityModel(aliases: nil, changeNotifier: OSEventProducer()) + + /* When */ + DispatchQueue.concurrentPerform(iterations: 5_000) { i in + // 1. Add aliases + for num in 0...9 { + identityModel.addAliases(["\(i)alias\(num)": "value"]) + } + + // 2. Encode the model + OneSignalUserDefaults.initShared().saveCodeableData(forKey: "IdentityModel", withValue: identityModel) + + // 2. Clear the aliases + identityModel.clearData() + } + } func testSwitchUser_sendsCorrectTags() throws { /* Setup */ + print("๐Ÿ’› starting tests") let client = MockOneSignalClient() // 1. Set up mock responses for the anonymous user @@ -203,7 +204,7 @@ final class OneSignalUserTests: XCTestCase { /* Then */ // Assert that every request SDK makes has a response set, and is handled - XCTAssertTrue(client.allRequestsHandled) + XCTAssertTrue(client.allRequestsHandled) // Assert there is only one request containing these tags and they are sent to userA XCTAssertTrue(client.onlyOneRequest(