From e07ee4a882ad8deb4403387248e78836cc6acd4d Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 22 Apr 2021 11:02:25 -0700 Subject: [PATCH 01/26] add multi-client tests for datafile handler --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 18 +- OptimizelyTests/Info.plist | 22 -- OptimizelyTests/OptimizelyTests.swift | 41 --- .../DefaultDatafileHandler.swift | 154 ++++++------ .../Datastore/DataStoreFile.swift | 1 - Sources/Utils/AtomicProperty.swift | 2 +- .../DatafileHandlerTests.swift | 4 +- .../MultiClientsTests_DatafileHandler.swift | 238 ++++++++++++++++++ Tests/TestUtils/MockUrlSession.swift | 31 ++- Tests/TestUtils/OTUtils.swift | 56 +++++ 10 files changed, 400 insertions(+), 167 deletions(-) delete mode 100644 OptimizelyTests/Info.plist delete mode 100644 OptimizelyTests/OptimizelyTests.swift create mode 100644 Tests/OptimizelyTests-Common/MultiClientsTests_DatafileHandler.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index c0411cfd..73e4330a 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1337,6 +1337,8 @@ 6EA425A52218E6AE00B074B5 /* (null) in Sources */ = {isa = PBXBuildFile; }; 6EA426602219242100B074B5 /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */; }; 6EA4266F2219243D00B074B5 /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E614DCD21E3F389005982A1 /* Optimizely.framework */; }; + 6EA641F3262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA641F2262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift */; }; + 6EA641F4262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA641F2262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift */; }; 6EC6DD3124ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD3024ABF6990017D296 /* OptimizelyClient+Decide.swift */; }; 6EC6DD3224ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD3024ABF6990017D296 /* OptimizelyClient+Decide.swift */; }; 6EC6DD3324ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD3024ABF6990017D296 /* OptimizelyClient+Decide.swift */; }; @@ -1859,8 +1861,6 @@ 6E86CEA124FDC836005DAFED /* OptimizelyUserContext+ObjC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OptimizelyUserContext+ObjC.swift"; sourceTree = ""; }; 6E86CEB224FF20DE005DAFED /* OptimizelyUserContextTests_Objc.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OptimizelyUserContextTests_Objc.m; sourceTree = ""; }; 6E981FC1232C363300FADDD6 /* DecisionListenerTests_Datafile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecisionListenerTests_Datafile.swift; sourceTree = ""; }; - 6E98E96C25F9784D00986EF5 /* OptimizelyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyTests.swift; sourceTree = ""; }; - 6E98E96E25F9784D00986EF5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 6E994B3325A3E6EA00999262 /* DecisionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionResponse.swift; sourceTree = ""; }; 6EA0FB1E251A5AEC00EC002D /* bucketer_test3.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = bucketer_test3.json; sourceTree = ""; }; 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyConfig.swift; sourceTree = ""; }; @@ -1870,6 +1870,7 @@ 6EA425792218E61E00B074B5 /* OptimizelyTests-DataModel-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-DataModel-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA4265B2219242100B074B5 /* OptimizelyTests-Others-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-Others-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA4266A2219243D00B074B5 /* OptimizelyTests-Others-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-Others-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6EA641F2262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiClientsTests_DatafileHandler.swift; sourceTree = ""; }; 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_Legacy.swift; sourceTree = ""; }; 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6EBAEB7421E3FEF900D13AA9 /* OptimizelyTests-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2019,7 +2020,6 @@ children = ( 6E75165D22C520D400B2B157 /* Sources */, 6E75196022C5211100B2B157 /* Tests */, - 6E98E96B25F9784D00986EF5 /* OptimizelyTests */, 0B7CB0C321AC5FE2007B77E5 /* Products */, 5793E12CFD024A5E8594F535 /* Pods */, 87DE4DE091B80D1F13BBD781 /* Frameworks */, @@ -2356,6 +2356,7 @@ 6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */, 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */, 6E86CEB224FF20DE005DAFED /* OptimizelyUserContextTests_Objc.m */, + 6EA641F2262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift */, ); path = "OptimizelyTests-Common"; sourceTree = ""; @@ -2450,15 +2451,6 @@ path = "OptimizelyTests-Others"; sourceTree = ""; }; - 6E98E96B25F9784D00986EF5 /* OptimizelyTests */ = { - isa = PBXGroup; - children = ( - 6E98E96C25F9784D00986EF5 /* OptimizelyTests.swift */, - 6E98E96E25F9784D00986EF5 /* Info.plist */, - ); - path = OptimizelyTests; - sourceTree = ""; - }; 6EC6DD3F24ABF8180017D296 /* Optimizely+Decide */ = { isa = PBXGroup; children = ( @@ -3846,6 +3838,7 @@ 6E9B116022C5487100C22D81 /* DecisionServiceTests_Experiments.swift in Sources */, 6E9B116322C5487100C22D81 /* BucketTests_GroupToExp.swift in Sources */, 6E7516AF22C520D400B2B157 /* DefaultLogger.swift in Sources */, + 6EA641F4262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift in Sources */, 6EF8DE2524BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75194522C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185522C520D400B2B157 /* ProjectConfig.swift in Sources */, @@ -4061,6 +4054,7 @@ 6E9B114622C5486E00C22D81 /* DecisionServiceTests_Experiments.swift in Sources */, 6E9B114922C5486E00C22D81 /* BucketTests_GroupToExp.swift in Sources */, 6E75182B22C520D400B2B157 /* BatchEvent.swift in Sources */, + 6EA641F3262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift in Sources */, 6EF8DE1E24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75190322C520D500B2B157 /* Attribute.swift in Sources */, 6E75192722C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, diff --git a/OptimizelyTests/Info.plist b/OptimizelyTests/Info.plist deleted file mode 100644 index 64d65ca4..00000000 --- a/OptimizelyTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/OptimizelyTests/OptimizelyTests.swift b/OptimizelyTests/OptimizelyTests.swift deleted file mode 100644 index bece3b62..00000000 --- a/OptimizelyTests/OptimizelyTests.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright 2020-2021, Optimizely, Inc. and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -class OptimizelyTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Sources/Customization/DefaultDatafileHandler.swift b/Sources/Customization/DefaultDatafileHandler.swift index 1477b657..dd494833 100644 --- a/Sources/Customization/DefaultDatafileHandler.swift +++ b/Sources/Customization/DefaultDatafileHandler.swift @@ -23,7 +23,7 @@ open class DefaultDatafileHandler: OPTDatafileHandler { // lazy load the logger from the logger factory. lazy var logger = OPTLoggerFactory.getLogger() // the timers for all sdk keys are atomic to allow for thread access. - var timers: AtomicProperty<[String:(timer: Timer?, interval: Int)]> = AtomicProperty(property: [String: (Timer?, Int)]()) + var timers = AtomicProperty(property: [String: (timer: Timer?, interval: Int)]()) // we will use a simple user defaults datastore let dataStore = DataStoreUserDefaults() // datastore for Datafile downloads @@ -36,7 +36,7 @@ open class DefaultDatafileHandler: OPTDatafileHandler { } public func setPeriodicInterval(sdkKey: String, interval: Int) { - timers.performAtomic { (timers) in + timers.performAtomic { timers in if timers[sdkKey] == nil { timers[sdkKey] = (nil, interval) return @@ -46,17 +46,14 @@ open class DefaultDatafileHandler: OPTDatafileHandler { public func hasPeriodicInterval(sdkKey: String) -> Bool { var result = true - self.timers.performAtomic(atomicOperation: { (timers) in - if !timers.contains(where: { $0.key == sdkKey}) { - result = false - } - }) + timers.performAtomic { timers in + result = timers[sdkKey] != nil + } return result } - + public func downloadDatafile(sdkKey: String) -> Data? { - var datafile: Data? let group = DispatchGroup() @@ -77,41 +74,6 @@ open class DefaultDatafileHandler: OPTDatafileHandler { return datafile } - open func getSession(resourceTimeoutInterval: Double?) -> URLSession { - let config = URLSessionConfiguration.ephemeral - if let resourceTimeoutInterval = resourceTimeoutInterval, - resourceTimeoutInterval > 0 { - config.timeoutIntervalForResource = TimeInterval(resourceTimeoutInterval) - } - return URLSession(configuration: config) - } - - open func getRequest(sdkKey: String) -> URLRequest? { - let str = String(format: endPointStringFormat, sdkKey) - guard let url = URL(string: str) else { return nil } - - var request = URLRequest(url: url) - - if let lastModified = dataStore.getLastModified(sdkKey: sdkKey), isDatafileSaved(sdkKey: sdkKey) { - request.setLastModified(lastModified: lastModified) - } - - return request - } - - open func getResponseData(sdkKey: String, response: HTTPURLResponse, url: URL?) -> Data? { - if let url = url, let data = try? Data(contentsOf: url) { - self.logger.d { String(data: data, encoding: .utf8) ?? "" } - self.saveDatafile(sdkKey: sdkKey, dataFile: data) - if let lastModified = response.getLastModified() { - self.dataStore.setLastModified(sdkKey: sdkKey, lastModified: lastModified) } - - return data - } - - return nil - } - open func downloadDatafile(sdkKey: String, returnCacheIfNoChange: Bool, resourceTimeoutInterval: Double?, @@ -164,10 +126,81 @@ open class DefaultDatafileHandler: OPTDatafileHandler { } } + open func getSession(resourceTimeoutInterval: Double?) -> URLSession { + let config = URLSessionConfiguration.ephemeral + if let resourceTimeoutInterval = resourceTimeoutInterval, + resourceTimeoutInterval > 0 { + config.timeoutIntervalForResource = TimeInterval(resourceTimeoutInterval) + } + return URLSession(configuration: config) + } + + open func getRequest(sdkKey: String) -> URLRequest? { + let str = String(format: endPointStringFormat, sdkKey) + guard let url = URL(string: str) else { return nil } + + var request = URLRequest(url: url) + + if let lastModified = dataStore.getLastModified(sdkKey: sdkKey), isDatafileSaved(sdkKey: sdkKey) { + request.setLastModified(lastModified: lastModified) + } + + return request + } + + open func getResponseData(sdkKey: String, response: HTTPURLResponse, url: URL?) -> Data? { + if let url = url, let data = try? Data(contentsOf: url) { + self.logger.d { String(data: data, encoding: .utf8) ?? "" } + self.saveDatafile(sdkKey: sdkKey, dataFile: data) + if let lastModified = response.getLastModified() { + self.dataStore.setLastModified(sdkKey: sdkKey, lastModified: lastModified) } + + return data + } + + return nil + } + + public func startUpdates(sdkKey: String, datafileChangeNotification: ((Data) -> Void)?) { + if let value = timers.property?[sdkKey], !(value.timer?.isValid ?? false) { + startPeriodicUpdates(sdkKey: sdkKey, updateInterval: value.interval, datafileChangeNotification: datafileChangeNotification) + } + } + + public func stopUpdates(sdkKey: String) { + stopPeriodicUpdates(sdkKey: sdkKey) + } + + public func stopAllUpdates() { + stopPeriodicUpdates() + } + open func createDataStore(sdkKey: String) -> OPTDataStore { return DataStoreFile(storeName: sdkKey) } + + public func saveDatafile(sdkKey: String, dataFile: Data) { + getDatafileCache(sdkKey: sdkKey).saveItem(forKey: sdkKey, value: dataFile) + } + + public func loadSavedDatafile(sdkKey: String) -> Data? { + return getDatafileCache(sdkKey: sdkKey).getItem(forKey: sdkKey) as? Data + } + + public func isDatafileSaved(sdkKey: String) -> Bool { + return getDatafileCache(sdkKey: sdkKey).getItem(forKey: sdkKey) as? Data != nil + } + + public func removeSavedDatafile(sdkKey: String) { + getDatafileCache(sdkKey: sdkKey).removeItem(forKey: sdkKey) + } + +} + +// MARK: - internals +extension DefaultDatafileHandler { + func startPeriodicUpdates(sdkKey: String, updateInterval: Int, datafileChangeNotification: ((Data) -> Void)?) { let now = Date() @@ -244,21 +277,6 @@ open class DefaultDatafileHandler: OPTDatafileHandler { logger.i("Stopping timer for all datafile updates") stopPeriodicUpdates(sdkKey: key) } - - } - - public func startUpdates(sdkKey: String, datafileChangeNotification: ((Data) -> Void)?) { - if let value = timers.property?[sdkKey], !(value.timer?.isValid ?? false) { - startPeriodicUpdates(sdkKey: sdkKey, updateInterval: value.interval, datafileChangeNotification: datafileChangeNotification) - } - } - - public func stopUpdates(sdkKey: String) { - stopPeriodicUpdates(sdkKey: sdkKey) - } - - public func stopAllUpdates() { - stopPeriodicUpdates() } func getDatafileCache(sdkKey: String) -> OPTDataStore { @@ -271,24 +289,10 @@ open class DefaultDatafileHandler: OPTDatafileHandler { } } - public func saveDatafile(sdkKey: String, dataFile: Data) { - getDatafileCache(sdkKey: sdkKey).saveItem(forKey: sdkKey, value: dataFile) - } - - public func loadSavedDatafile(sdkKey: String) -> Data? { - return getDatafileCache(sdkKey: sdkKey).getItem(forKey: sdkKey) as? Data - } - - public func isDatafileSaved(sdkKey: String) -> Bool { - return getDatafileCache(sdkKey: sdkKey).getItem(forKey: sdkKey) as? Data != nil - } - - public func removeSavedDatafile(sdkKey: String) { - getDatafileCache(sdkKey: sdkKey).removeItem(forKey: sdkKey) - } - } +// MARK: - others + extension DataStoreUserDefaults { func getLastModified(sdkKey: String) -> String? { return getItem(forKey: "OPTLastModified-" + sdkKey) as? String diff --git a/Sources/Implementation/Datastore/DataStoreFile.swift b/Sources/Implementation/Datastore/DataStoreFile.swift index ca4a4c9b..6f658fc3 100644 --- a/Sources/Implementation/Datastore/DataStoreFile.swift +++ b/Sources/Implementation/Datastore/DataStoreFile.swift @@ -39,7 +39,6 @@ open class DataStoreFile: OPTDataStore where T: Codable { } else { self.url = URL(fileURLWithPath: storeName) } - } func isArray() -> Bool { diff --git a/Sources/Utils/AtomicProperty.swift b/Sources/Utils/AtomicProperty.swift index 3cdc93c0..acd29469 100644 --- a/Sources/Utils/AtomicProperty.swift +++ b/Sources/Utils/AtomicProperty.swift @@ -49,7 +49,7 @@ class AtomicProperty { // perform an atomic operation on the atomic property // the operation will not run if the property is nil. - public func performAtomic(atomicOperation:((_ prop:inout T) -> Void)) { + public func performAtomic(atomicOperation: ((_ prop:inout T) -> Void)) { lock.sync(flags: DispatchWorkItemFlags.barrier) { if var prop = _property { atomicOperation(&prop) diff --git a/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift b/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift index 756682e2..a72bc861 100644 --- a/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift @@ -81,14 +81,14 @@ class DatafileHandlerTests: XCTestCase { func testDatafileDownload500() { - var localUrl:URL? + var localUrl: URL? // create a dummy file at a url to use as or datafile cdn location localUrl = OTUtils.saveAFile(name: sdkKey, data: "{}".data(using: .utf8)!) // default datafile handler class InnerDatafileHandler : DefaultDatafileHandler { - var localFileUrl:URL? + var localFileUrl: URL? // override getSession to return our own session. override func getSession(resourceTimeoutInterval: Double?) -> URLSession { diff --git a/Tests/OptimizelyTests-Common/MultiClientsTests_DatafileHandler.swift b/Tests/OptimizelyTests-Common/MultiClientsTests_DatafileHandler.swift new file mode 100644 index 00000000..7fcd0a73 --- /dev/null +++ b/Tests/OptimizelyTests-Common/MultiClientsTests_DatafileHandler.swift @@ -0,0 +1,238 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +import XCTest + +class MultiClientsTests_DatafileHandler: XCTestCase { + + let testSdkKeyBasename = "testSdkKey" + var sdkKeys = [String]() + var handler = DefaultDatafileHandler() + + override func setUp() { + OTUtils.bindLoggerForTest(.info) + OTUtils.createDocumentDirectoryIfNotAvailable() + + makeSdkKeys(10) + } + + override func tearDown() { + OTUtils.removeAllBinders() + OTUtils.removeAllFiles(including: testSdkKeyBasename) + } + + func testConcurrentAccessPeriodicInterval() { + makeSdkKeys(100) + + let result = runConcurrent(for: sdkKeys) { _, _ in + let maxCnt = 100 + for _ in 0..(property: 0) + let recvSuccess = AtomicProperty(property: 0) + + let result = runConcurrent(for: sdkKeys, timeoutInSecs: 10) { sdkKey, idx in + var statusCode: Int = 0 + var passError: Bool = false + + switch idx { + case 0.. Void) -> Bool { + let group = DispatchGroup() + + for (idx, item) in items.enumerated() { + group.enter() + DispatchQueue.global().async { + //print("[MultiClientsTest] starting for \(sdkKey)") + task(item, idx) + //print("[MultiClientsTest] ending for \(sdkKey)") + group.leave() + } + } + + let timeout = DispatchTime.now() + .seconds(timeoutInSecs) + let result = group.wait(timeout: timeout) + return result == .success + } + + func makeSdkKeys(_ num: Int) { + sdkKeys = [] + for i in 0.. URLSession { + return MockUrlSession(failureCode: failureCode, withError: passError, localUrl: localUrl) + } + } + +} + diff --git a/Tests/TestUtils/MockUrlSession.swift b/Tests/TestUtils/MockUrlSession.swift index 7cf6be17..9b892766 100644 --- a/Tests/TestUtils/MockUrlSession.swift +++ b/Tests/TestUtils/MockUrlSession.swift @@ -22,15 +22,15 @@ import Foundation // and returns that. // the response also includes the url for the data download. // the cdn url is used to get the datafile if the datafile is not in cache -class MockUrlSession : URLSession { - var downloadCacheUrl:URL? - let failureCode:Int - let passError:Bool - class MockDownloadTask : URLSessionDownloadTask { - - var task:()->Void +class MockUrlSession: URLSession { + var downloadCacheUrl: URL? + let failureCode: Int + let passError: Bool + + class MockDownloadTask: URLSessionDownloadTask { + var task: () -> Void - init(_ task:@escaping ()->Void) { + init(_ task: @escaping () -> Void) { self.task = task } @@ -39,13 +39,19 @@ class MockUrlSession : URLSession { } } - init (failureCode:Int, withError:Bool) { + init(failureCode: Int, withError: Bool) { self.failureCode = failureCode self.passError = withError } + init(failureCode: Int, withError: Bool, localUrl: URL?) { + self.failureCode = failureCode + self.passError = withError + self.downloadCacheUrl = localUrl + } + convenience override init() { - self.init(failureCode:0, withError:false) + self.init(failureCode: 0, withError: false) } override func downloadTask(with request: URLRequest, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask { @@ -55,12 +61,11 @@ class MockUrlSession : URLSession { if (self.passError) { let error = OptimizelyError.datafileDownloadFailed("failure") - completionHandler(self.downloadCacheUrl, nil, error ) + completionHandler(self.downloadCacheUrl, nil, error) } else { let response = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: nil) - - completionHandler(self.downloadCacheUrl, response, nil ) + completionHandler(self.downloadCacheUrl, response, nil) } } diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index 0ac029fe..5836e82d 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -138,6 +138,23 @@ class OTUtils { } } + // MARK: - HandlerRegistryService + + static func bindLoggerForTest(_ level: OptimizelyLogLevel? = nil) { + if let level = level { + DefaultLogger.logLevel = level + } + let logger = DefaultLogger() + + let binder: Binder = Binder(service: OPTLogger.self, factory: type(of: logger).init) + HandlerRegistryService.shared.registerBinding(binder: binder) + } + + static func removeAllBinders() { + HandlerRegistryService.shared.binders.property?.removeAll() + } + + // MARK: - UPS static func getVariationFromUPS(ups: OPTUserProfileService, userId: String, experimentId: String) -> String? { @@ -181,6 +198,8 @@ class OTUtils { return negativeMaxValueAllowed * 2.0 } + // MARK: - files + static func saveAFile(name:String, data:Data) -> URL? { let ds = DataStoreFile(storeName: name, async: false) ds.saveItem(forKey: name, value: data) @@ -194,6 +213,43 @@ class OTUtils { return ds.url } + + static func removeAllFiles(including: String) { + removeAllFiles(including: including, in: .documentDirectory) + removeAllFiles(including: including, in: .cachesDirectory) + } + + static func removeAllFiles(including: String, in directory: FileManager.SearchPathDirectory) { + if let docUrl = FileManager.default.urls(for: directory, in: .userDomainMask).first { + if let names = try? FileManager.default.contentsOfDirectory(atPath: docUrl.path) { + names.forEach{ name in + if name.contains(including) { + let fileUrl = docUrl.appendingPathComponent(name) + do { + try FileManager.default.removeItem(at: fileUrl) + //print("[OTUtils] removed file: '\(name)' from '\(directory)' directory") + } catch { + //print("[OTUtils] removing file failed for '\(name)' from '\(directory)' directory: \(error)") + } + } + } + } + } + } + + static func createDocumentDirectoryIfNotAvailable() { + // documentDirectory may not exist for simulator unit test (iOS11+). create it if not found. + + if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + if (!FileManager.default.fileExists(atPath: url.path)) { + do { + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) + } catch { + print(error) + } + } + } + } // MARK: - others From b4153a096b98387bb9cd5b04b25dd0796808ee9f Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 23 Apr 2021 17:39:07 -0700 Subject: [PATCH 02/26] fix datafile handler sync with atomic dictionary fix datafile handler sync with atomic dictionary cleanup clean up --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 346 +++++++++++++++++- .../xcschemes/OptimizelySwiftSDK-iOS.xcscheme | 24 ++ .../DefaultDatafileHandler.swift | 4 +- .../Datastore/DataStoreFile.swift | 1 + Sources/Utils/AtomicArray.swift | 83 +++++ Sources/Utils/AtomicDictionary.swift | 45 +++ .../AtomicArrayTests.swift | 49 +++ .../AtomicDictionaryTests.swift | 42 +++ .../DatafileHandlerTests_MultiClients.swift} | 34 +- 9 files changed, 601 insertions(+), 27 deletions(-) create mode 100644 Sources/Utils/AtomicArray.swift create mode 100644 Sources/Utils/AtomicDictionary.swift create mode 100644 Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift create mode 100644 Tests/OptimizelyTests-MultiClients/AtomicDictionaryTests.swift rename Tests/{OptimizelyTests-Common/MultiClientsTests_DatafileHandler.swift => OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift} (85%) diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 73e4330a..48a1f214 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -389,6 +389,7 @@ 6E14CDC82423FA0800010234 /* bot_filtering_enabled.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E75197B22C5211100B2B157 /* bot_filtering_enabled.json */; }; 6E14CDC92423FA0800010234 /* simple_datafile.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E75197C22C5211100B2B157 /* simple_datafile.json */; }; 6E14CDCA2423FA0800010234 /* unsupported_version.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E75197D22C5211100B2B157 /* unsupported_version.json */; }; + 6E2D5DAE26338CA00002077F /* AtomicDictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2D5DAD26338CA00002077F /* AtomicDictionaryTests.swift */; }; 6E34A6172319EBB800BAE302 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E34A6162319EBB700BAE302 /* Notifications.swift */; }; 6E34A6182319EBB800BAE302 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E34A6162319EBB700BAE302 /* Notifications.swift */; }; 6E34A6192319EBB800BAE302 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E34A6162319EBB700BAE302 /* Notifications.swift */; }; @@ -431,6 +432,114 @@ 6E34A647231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E34A63D231ED28600BAE302 /* empty_datafile_new_account_id.json */; }; 6E34A648231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E34A63D231ED28600BAE302 /* empty_datafile_new_account_id.json */; }; 6E34A649231ED28600BAE302 /* empty_datafile_new_account_id.json in Resources */ = {isa = PBXBuildFile; fileRef = 6E34A63D231ED28600BAE302 /* empty_datafile_new_account_id.json */; }; + 6E424BDD263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BDE263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BDF263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE0263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE1263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE2263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE3263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE4263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE5263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE6263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE7263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE8263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BE9263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BEA263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BEB263228E90081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424BFC263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424BFD263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424BFE263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424BFF263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C00263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C01263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C02263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C03263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C04263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C05263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C06263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C07263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C08263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C09263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C0A263228FD0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424C41263249620081004A /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */; }; + 6E424C88263249B80081004A /* DatafileHandlerTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA641F2262F4E6900E29532 /* DatafileHandlerTests_MultiClients.swift */; }; + 6E424CB926324B1D0081004A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166D22C520D400B2B157 /* Constants.swift */; }; + 6E424CBA26324B1D0081004A /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E34A6162319EBB700BAE302 /* Notifications.swift */; }; + 6E424CBB26324B1D0081004A /* MurmurHash3.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166E22C520D400B2B157 /* MurmurHash3.swift */; }; + 6E424CBC26324B1D0081004A /* HandlerRegistryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166F22C520D400B2B157 /* HandlerRegistryService.swift */; }; + 6E424CBD26324B1D0081004A /* LogMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167022C520D400B2B157 /* LogMessage.swift */; }; + 6E424CBE26324B1D0081004A /* AtomicProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167122C520D400B2B157 /* AtomicProperty.swift */; }; + 6E424CBF26324B1D0081004A /* AtomicArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BDC263228E90081004A /* AtomicArray.swift */; }; + 6E424CC026324B1D0081004A /* AtomicDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424BFB263228FD0081004A /* AtomicDictionary.swift */; }; + 6E424CC126324B1D0081004A /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167222C520D400B2B157 /* Utils.swift */; }; + 6E424CC226324B1D0081004A /* SDKVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167322C520D400B2B157 /* SDKVersion.swift */; }; + 6E424CD326324B270081004A /* OptimizelyError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166722C520D400B2B157 /* OptimizelyError.swift */; }; + 6E424CD426324B270081004A /* OptimizelyLogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166822C520D400B2B157 /* OptimizelyLogLevel.swift */; }; + 6E424CD526324B270081004A /* OptimizelyClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166922C520D400B2B157 /* OptimizelyClient.swift */; }; + 6E424CD626324B270081004A /* OptimizelyClient+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166A22C520D400B2B157 /* OptimizelyClient+ObjC.swift */; }; + 6E424CD726324B270081004A /* OptimizelyResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166B22C520D400B2B157 /* OptimizelyResult.swift */; }; + 6E424CD826324B270081004A /* OptimizelyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */; }; + 6E424CD926324B270081004A /* OptimizelyConfig+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */; }; + 6E424CDA26324B270081004A /* OptimizelyJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78CAF572445AD8D009FE876 /* OptimizelyJSON.swift */; }; + 6E424CDB26324B270081004A /* OptimizelyJSON+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78CAFA324486E0A009FE876 /* OptimizelyJSON+ObjC.swift */; }; + 6E424CEC26324B620081004A /* DefaultLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75165F22C520D400B2B157 /* DefaultLogger.swift */; }; + 6E424CED26324B620081004A /* DefaultUserProfileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166022C520D400B2B157 /* DefaultUserProfileService.swift */; }; + 6E424CEE26324B620081004A /* DefaultEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166122C520D400B2B157 /* DefaultEventDispatcher.swift */; }; + 6E424CEF26324B620081004A /* DefaultDatafileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167D22C520D400B2B157 /* DefaultDatafileHandler.swift */; }; + 6E424CF026324B620081004A /* OPTLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166322C520D400B2B157 /* OPTLogger.swift */; }; + 6E424CF126324B620081004A /* OPTUserProfileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166422C520D400B2B157 /* OPTUserProfileService.swift */; }; + 6E424CF226324B620081004A /* OPTEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75166522C520D400B2B157 /* OPTEventDispatcher.swift */; }; + 6E424CF326324B620081004A /* OPTDatafileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7516A422C520D400B2B157 /* OPTDatafileHandler.swift */; }; + 6E424CF426324B620081004A /* DecisionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E623F01253F9045000617D0 /* DecisionInfo.swift */; }; + 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167E22C520D400B2B157 /* DefaultBucketer.swift */; }; + 6E424CF626324B620081004A /* DefaultNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167F22C520D400B2B157 /* DefaultNotificationCenter.swift */; }; + 6E424CF726324B620081004A /* DefaultDecisionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168022C520D400B2B157 /* DefaultDecisionService.swift */; }; + 6E424CF826324B620081004A /* DecisionReasons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF8DE3024BF7D69008B9488 /* DecisionReasons.swift */; }; + 6E424CF926324B620081004A /* DecisionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E994B3325A3E6EA00999262 /* DecisionResponse.swift */; }; + 6E424CFA26324B620081004A /* DataStoreMemory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168222C520D400B2B157 /* DataStoreMemory.swift */; }; + 6E424CFB26324B620081004A /* DataStoreUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168322C520D400B2B157 /* DataStoreUserDefaults.swift */; }; + 6E424CFC26324B620081004A /* DataStoreFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168422C520D400B2B157 /* DataStoreFile.swift */; }; + 6E424CFD26324B620081004A /* DataStoreQueueStackImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168522C520D400B2B157 /* DataStoreQueueStackImpl.swift */; }; + 6E424CFE26324B620081004A /* BatchEventBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168722C520D400B2B157 /* BatchEventBuilder.swift */; }; + 6E424CFF26324B620081004A /* BatchEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168A22C520D400B2B157 /* BatchEvent.swift */; }; + 6E424D0026324B620081004A /* EventForDispatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168B22C520D400B2B157 /* EventForDispatch.swift */; }; + 6E424D0126324B620081004A /* SemanticVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B97DD93249D327F003DE606 /* SemanticVersion.swift */; }; + 6E424D0226324B620081004A /* Audience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169822C520D400B2B157 /* Audience.swift */; }; + 6E424D0326324B620081004A /* AttributeValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169922C520D400B2B157 /* AttributeValue.swift */; }; + 6E424D0426324B620081004A /* ConditionLeaf.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169A22C520D400B2B157 /* ConditionLeaf.swift */; }; + 6E424D0526324B620081004A /* ConditionHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169B22C520D400B2B157 /* ConditionHolder.swift */; }; + 6E424D0626324B620081004A /* UserAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169C22C520D400B2B157 /* UserAttribute.swift */; }; + 6E424D0726324B620081004A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168C22C520D400B2B157 /* Event.swift */; }; + 6E424D0826324B620081004A /* ProjectConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168D22C520D400B2B157 /* ProjectConfig.swift */; }; + 6E424D0926324B620081004A /* FeatureVariable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168E22C520D400B2B157 /* FeatureVariable.swift */; }; + 6E424D0A26324B620081004A /* Rollout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75168F22C520D400B2B157 /* Rollout.swift */; }; + 6E424D0B26324B620081004A /* Variation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169022C520D400B2B157 /* Variation.swift */; }; + 6E424D0C26324B620081004A /* TrafficAllocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169122C520D400B2B157 /* TrafficAllocation.swift */; }; + 6E424D0D26324B620081004A /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169222C520D400B2B157 /* Project.swift */; }; + 6E424D0E26324B620081004A /* Experiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169322C520D400B2B157 /* Experiment.swift */; }; + 6E424D0F26324B620081004A /* FeatureFlag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169422C520D400B2B157 /* FeatureFlag.swift */; }; + 6E424D1026324B620081004A /* Group.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169522C520D400B2B157 /* Group.swift */; }; + 6E424D1126324B620081004A /* Variable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169622C520D400B2B157 /* Variable.swift */; }; + 6E424D1226324B620081004A /* Attribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169D22C520D400B2B157 /* Attribute.swift */; }; + 6E424D1326324B620081004A /* BackgroundingCallbacks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75169F22C520D400B2B157 /* BackgroundingCallbacks.swift */; }; + 6E424D1426324B620081004A /* OPTNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7516A022C520D400B2B157 /* OPTNotificationCenter.swift */; }; + 6E424D1526324B620081004A /* DataStoreQueueStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7516A122C520D400B2B157 /* DataStoreQueueStack.swift */; }; + 6E424D1626324B620081004A /* OPTDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7516A222C520D400B2B157 /* OPTDataStore.swift */; }; + 6E424D1726324B620081004A /* OPTDecisionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7516A322C520D400B2B157 /* OPTDecisionService.swift */; }; + 6E424D1826324B620081004A /* OPTBucketer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7516A522C520D400B2B157 /* OPTBucketer.swift */; }; + 6E424D1926324B620081004A /* ArrayEventForDispatch+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167522C520D400B2B157 /* ArrayEventForDispatch+Extension.swift */; }; + 6E424D1A26324B620081004A /* OptimizelyClient+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167622C520D400B2B157 /* OptimizelyClient+Extension.swift */; }; + 6E424D1B26324B620081004A /* DataStoreQueueStackImpl+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167722C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift */; }; + 6E424D1C26324B620081004A /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75167822C520D400B2B157 /* Array+Extension.swift */; }; + 6E424D2E26324BBA0081004A /* MockUrlSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7519B722C5211100B2B157 /* MockUrlSession.swift */; }; + 6E424D2F26324BBA0081004A /* OTUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E7519B822C5211100B2B157 /* OTUtils.swift */; }; + 6E424D5026324C4D0081004A /* OptimizelyDecideOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */; }; + 6E424D5126324C4D0081004A /* OptimizelyDecision.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF8DE0A24BD1BB1008B9488 /* OptimizelyDecision.swift */; }; + 6E424D5226324C4D0081004A /* OptimizelyClient+Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD3024ABF6990017D296 /* OptimizelyClient+Decide.swift */; }; + 6E424D5326324C4D0081004A /* OptimizelyUserContext+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E86CEA124FDC836005DAFED /* OptimizelyUserContext+ObjC.swift */; }; + 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD4024ABF89B0017D296 /* OptimizelyUserContext.swift */; }; + 6E424D7626324DBD0081004A /* AtomicArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */; }; 6E593FB625BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */; }; 6E5AB69323F6130D007A82B1 /* OptimizelyClientTests_Init_Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */; }; 6E5AB69423F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */; }; @@ -1337,8 +1446,6 @@ 6EA425A52218E6AE00B074B5 /* (null) in Sources */ = {isa = PBXBuildFile; }; 6EA426602219242100B074B5 /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */; }; 6EA4266F2219243D00B074B5 /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E614DCD21E3F389005982A1 /* Optimizely.framework */; }; - 6EA641F3262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA641F2262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift */; }; - 6EA641F4262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA641F2262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift */; }; 6EC6DD3124ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD3024ABF6990017D296 /* OptimizelyClient+Decide.swift */; }; 6EC6DD3224ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD3024ABF6990017D296 /* OptimizelyClient+Decide.swift */; }; 6EC6DD3324ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD3024ABF6990017D296 /* OptimizelyClient+Decide.swift */; }; @@ -1607,6 +1714,13 @@ remoteGlobalIDString = 6EBAEB6B21E3FEF800D13AA9; remoteInfo = "OptimizelySwiftSDK-iOS"; }; + 6E424C42263249620081004A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0B7CB0B921AC5FE2007B77E5 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 6EBAEB6B21E3FEF800D13AA9; + remoteInfo = "OptimizelySwiftSDK-iOS"; + }; 6E614DD721E3F38A005982A1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 0B7CB0B921AC5FE2007B77E5 /* Project object */; @@ -1690,10 +1804,15 @@ 5F38B9FBC88543893307E7F4 /* Pods-OptimizelyTests-Common-tvOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OptimizelyTests-Common-tvOS.debug.xcconfig"; path = "Target Support Files/Pods-OptimizelyTests-Common-tvOS/Pods-OptimizelyTests-Common-tvOS.debug.xcconfig"; sourceTree = ""; }; 6E14CD632423F80B00010234 /* OptimizelyTests-Batch-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-Batch-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide.swift; sourceTree = ""; }; + 6E2D5DAD26338CA00002077F /* AtomicDictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicDictionaryTests.swift; sourceTree = ""; }; 6E34A6162319EBB700BAE302 /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; 6E34A623231ED04900BAE302 /* empty_datafile_new_project_id.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = empty_datafile_new_project_id.json; sourceTree = ""; }; 6E34A624231ED04900BAE302 /* empty_datafile_new_revision.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = empty_datafile_new_revision.json; sourceTree = ""; }; 6E34A63D231ED28600BAE302 /* empty_datafile_new_account_id.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = empty_datafile_new_account_id.json; sourceTree = ""; }; + 6E424BDC263228E90081004A /* AtomicArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicArray.swift; sourceTree = ""; }; + 6E424BFB263228FD0081004A /* AtomicDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicDictionary.swift; sourceTree = ""; }; + 6E424C3C263249620081004A /* OptimizelyTests-MultiClients-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-MultiClients-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicArrayTests.swift; sourceTree = ""; }; 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Decide.swift; sourceTree = ""; }; 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Sync.swift; sourceTree = ""; }; 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Async.swift; sourceTree = ""; }; @@ -1870,7 +1989,7 @@ 6EA425792218E61E00B074B5 /* OptimizelyTests-DataModel-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-DataModel-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA4265B2219242100B074B5 /* OptimizelyTests-Others-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-Others-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA4266A2219243D00B074B5 /* OptimizelyTests-Others-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-Others-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; - 6EA641F2262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiClientsTests_DatafileHandler.swift; sourceTree = ""; }; + 6EA641F2262F4E6900E29532 /* DatafileHandlerTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatafileHandlerTests_MultiClients.swift; sourceTree = ""; }; 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyUserContextTests_Decide_Legacy.swift; sourceTree = ""; }; 6EBAEB6C21E3FEF800D13AA9 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6EBAEB7421E3FEF900D13AA9 /* OptimizelyTests-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1910,6 +2029,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E424C39263249620081004A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E424C41263249620081004A /* Optimizely.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E614DCA21E3F389005982A1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2044,6 +2171,7 @@ 6E14CD632423F80B00010234 /* OptimizelyTests-Batch-iOS.xctest */, BD6485812491474500F30986 /* Optimizely.framework */, 75C719BB25E4519B0084187E /* Optimizely.framework */, + 6E424C3C263249620081004A /* OptimizelyTests-MultiClients-iOS.xctest */, ); name = Products; sourceTree = ""; @@ -2067,6 +2195,16 @@ path = "OptimizelyTests-Batch-iOS"; sourceTree = ""; }; + 6E424C672632498D0081004A /* OptimizelyTests-MultiClients */ = { + isa = PBXGroup; + children = ( + 6EA641F2262F4E6900E29532 /* DatafileHandlerTests_MultiClients.swift */, + 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */, + 6E2D5DAD26338CA00002077F /* AtomicDictionaryTests.swift */, + ); + path = "OptimizelyTests-MultiClients"; + sourceTree = ""; + }; 6E6BE008237F547200FE8274 /* optmizelyConfig */ = { isa = PBXGroup; children = ( @@ -2141,6 +2279,8 @@ 6E75166F22C520D400B2B157 /* HandlerRegistryService.swift */, 6E75167022C520D400B2B157 /* LogMessage.swift */, 6E75167122C520D400B2B157 /* AtomicProperty.swift */, + 6E424BDC263228E90081004A /* AtomicArray.swift */, + 6E424BFB263228FD0081004A /* AtomicDictionary.swift */, 6E75167222C520D400B2B157 /* Utils.swift */, 6E75167322C520D400B2B157 /* SDKVersion.swift */, ); @@ -2261,6 +2401,7 @@ isa = PBXGroup; children = ( 6E75197E22C5211100B2B157 /* OptimizelyTests-Common */, + 6E424C672632498D0081004A /* OptimizelyTests-MultiClients */, 6E14CD642423F80B00010234 /* OptimizelyTests-Batch-iOS */, 6E75199B22C5211100B2B157 /* OptimizelyTests-DataModel */, 6E7519B922C5211100B2B157 /* OptimizelyTests-APIs */, @@ -2356,7 +2497,6 @@ 6E7E9B362523F8BF009E4426 /* OptimizelyUserContextTests_Decide_Reasons.swift */, 6EB97BCC24C89DFB00068883 /* OptimizelyUserContextTests_Decide_Legacy.swift */, 6E86CEB224FF20DE005DAFED /* OptimizelyUserContextTests_Objc.m */, - 6EA641F2262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift */, ); path = "OptimizelyTests-Common"; sourceTree = ""; @@ -2544,6 +2684,24 @@ productReference = 6E14CD632423F80B00010234 /* OptimizelyTests-Batch-iOS.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 6E424C3B263249620081004A /* OptimizelyTests-MultiClients-iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6E424C44263249620081004A /* Build configuration list for PBXNativeTarget "OptimizelyTests-MultiClients-iOS" */; + buildPhases = ( + 6E424C38263249620081004A /* Sources */, + 6E424C39263249620081004A /* Frameworks */, + 6E424C3A263249620081004A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 6E424C43263249620081004A /* PBXTargetDependency */, + ); + name = "OptimizelyTests-MultiClients-iOS"; + productName = "OptimizelyTests-MultiClients-iOS"; + productReference = 6E424C3C263249620081004A /* OptimizelyTests-MultiClients-iOS.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 6E614DCC21E3F389005982A1 /* OptimizelySwiftSDK-tvOS */ = { isa = PBXNativeTarget; buildConfigurationList = 6E614DE221E3F38A005982A1 /* Build configuration list for PBXNativeTarget "OptimizelySwiftSDK-tvOS" */; @@ -2811,6 +2969,9 @@ 6E14CD622423F80B00010234 = { CreatedOnToolsVersion = 11.3.1; }; + 6E424C3B263249620081004A = { + CreatedOnToolsVersion = 12.3; + }; 6E614DCC21E3F389005982A1 = { CreatedOnToolsVersion = 10.1; }; @@ -2872,6 +3033,7 @@ 75C719BA25E4519B0084187E /* OptimizelySwiftSDK-watchOS */, 6EBAEB7321E3FEF900D13AA9 /* OptimizelyTests-iOS */, 6EA425692218E60A00B074B5 /* OptimizelyTests-Common-iOS */, + 6E424C3B263249620081004A /* OptimizelyTests-MultiClients-iOS */, 6E14CD622423F80B00010234 /* OptimizelyTests-Batch-iOS */, 6E636B8B2236C91F00AF3CEF /* OptimizelyTests-APIs-iOS */, 6EA425782218E61E00B074B5 /* OptimizelyTests-DataModel-iOS */, @@ -2924,6 +3086,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E424C3A263249620081004A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E614DCB21E3F389005982A1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -3410,6 +3579,7 @@ 6E14CD962423F9A700010234 /* Variable.swift in Sources */, 6E14CD942423F9A700010234 /* FeatureFlag.swift in Sources */, 6E14CD9D2423F9C300010234 /* OPTDatafileHandler.swift in Sources */, + 6E424BE3263228E90081004A /* AtomicArray.swift in Sources */, 6EF8DE3624BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E14CD7C2423F98D00010234 /* DefaultDatafileHandler.swift in Sources */, 6E14CD9B2423F9C300010234 /* OPTDataStore.swift in Sources */, @@ -3418,6 +3588,7 @@ 6E14CD762423F97900010234 /* DefaultLogger.swift in Sources */, 6E14CD922423F9A700010234 /* Project.swift in Sources */, 6E14CDA72423F9C300010234 /* LogMessage.swift in Sources */, + 6E424C02263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E14CD8C2423F9A700010234 /* Event.swift in Sources */, 6E14CDAC2423F9EB00010234 /* OTUtils.swift in Sources */, 6E14CD972423F9A700010234 /* Attribute.swift in Sources */, @@ -3468,6 +3639,91 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 6E424C38263249620081004A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E424CEC26324B620081004A /* DefaultLogger.swift in Sources */, + 6E424CED26324B620081004A /* DefaultUserProfileService.swift in Sources */, + 6E424CEE26324B620081004A /* DefaultEventDispatcher.swift in Sources */, + 6E424CEF26324B620081004A /* DefaultDatafileHandler.swift in Sources */, + 6E424CF026324B620081004A /* OPTLogger.swift in Sources */, + 6E424CF126324B620081004A /* OPTUserProfileService.swift in Sources */, + 6E424CF226324B620081004A /* OPTEventDispatcher.swift in Sources */, + 6E424D2E26324BBA0081004A /* MockUrlSession.swift in Sources */, + 6E424CF326324B620081004A /* OPTDatafileHandler.swift in Sources */, + 6E424CF426324B620081004A /* DecisionInfo.swift in Sources */, + 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */, + 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */, + 6E424CF626324B620081004A /* DefaultNotificationCenter.swift in Sources */, + 6E424CF726324B620081004A /* DefaultDecisionService.swift in Sources */, + 6E424CF826324B620081004A /* DecisionReasons.swift in Sources */, + 6E424CF926324B620081004A /* DecisionResponse.swift in Sources */, + 6E424CFA26324B620081004A /* DataStoreMemory.swift in Sources */, + 6E424CFB26324B620081004A /* DataStoreUserDefaults.swift in Sources */, + 6E424CFC26324B620081004A /* DataStoreFile.swift in Sources */, + 6E424CFD26324B620081004A /* DataStoreQueueStackImpl.swift in Sources */, + 6E424CFE26324B620081004A /* BatchEventBuilder.swift in Sources */, + 6E424CFF26324B620081004A /* BatchEvent.swift in Sources */, + 6E424D0026324B620081004A /* EventForDispatch.swift in Sources */, + 6E424D0126324B620081004A /* SemanticVersion.swift in Sources */, + 6E424D0226324B620081004A /* Audience.swift in Sources */, + 6E424D0326324B620081004A /* AttributeValue.swift in Sources */, + 6E424D0426324B620081004A /* ConditionLeaf.swift in Sources */, + 6E424D0526324B620081004A /* ConditionHolder.swift in Sources */, + 6E424D5226324C4D0081004A /* OptimizelyClient+Decide.swift in Sources */, + 6E424D0626324B620081004A /* UserAttribute.swift in Sources */, + 6E424D0726324B620081004A /* Event.swift in Sources */, + 6E424D0826324B620081004A /* ProjectConfig.swift in Sources */, + 6E424D0926324B620081004A /* FeatureVariable.swift in Sources */, + 6E424D0A26324B620081004A /* Rollout.swift in Sources */, + 6E2D5DAE26338CA00002077F /* AtomicDictionaryTests.swift in Sources */, + 6E424D0B26324B620081004A /* Variation.swift in Sources */, + 6E424D0C26324B620081004A /* TrafficAllocation.swift in Sources */, + 6E424D0D26324B620081004A /* Project.swift in Sources */, + 6E424D0E26324B620081004A /* Experiment.swift in Sources */, + 6E424D0F26324B620081004A /* FeatureFlag.swift in Sources */, + 6E424D1026324B620081004A /* Group.swift in Sources */, + 6E424D1126324B620081004A /* Variable.swift in Sources */, + 6E424D1226324B620081004A /* Attribute.swift in Sources */, + 6E424D1326324B620081004A /* BackgroundingCallbacks.swift in Sources */, + 6E424D1426324B620081004A /* OPTNotificationCenter.swift in Sources */, + 6E424D5026324C4D0081004A /* OptimizelyDecideOption.swift in Sources */, + 6E424D5126324C4D0081004A /* OptimizelyDecision.swift in Sources */, + 6E424D1526324B620081004A /* DataStoreQueueStack.swift in Sources */, + 6E424D1626324B620081004A /* OPTDataStore.swift in Sources */, + 6E424D2F26324BBA0081004A /* OTUtils.swift in Sources */, + 6E424D1726324B620081004A /* OPTDecisionService.swift in Sources */, + 6E424D1826324B620081004A /* OPTBucketer.swift in Sources */, + 6E424D1926324B620081004A /* ArrayEventForDispatch+Extension.swift in Sources */, + 6E424D1A26324B620081004A /* OptimizelyClient+Extension.swift in Sources */, + 6E424D1B26324B620081004A /* DataStoreQueueStackImpl+Extension.swift in Sources */, + 6E424D1C26324B620081004A /* Array+Extension.swift in Sources */, + 6E424D7626324DBD0081004A /* AtomicArrayTests.swift in Sources */, + 6E424CD326324B270081004A /* OptimizelyError.swift in Sources */, + 6E424D5326324C4D0081004A /* OptimizelyUserContext+ObjC.swift in Sources */, + 6E424CD426324B270081004A /* OptimizelyLogLevel.swift in Sources */, + 6E424CD526324B270081004A /* OptimizelyClient.swift in Sources */, + 6E424CD626324B270081004A /* OptimizelyClient+ObjC.swift in Sources */, + 6E424CD726324B270081004A /* OptimizelyResult.swift in Sources */, + 6E424CD826324B270081004A /* OptimizelyConfig.swift in Sources */, + 6E424CD926324B270081004A /* OptimizelyConfig+ObjC.swift in Sources */, + 6E424CDA26324B270081004A /* OptimizelyJSON.swift in Sources */, + 6E424CDB26324B270081004A /* OptimizelyJSON+ObjC.swift in Sources */, + 6E424CB926324B1D0081004A /* Constants.swift in Sources */, + 6E424CBA26324B1D0081004A /* Notifications.swift in Sources */, + 6E424CBB26324B1D0081004A /* MurmurHash3.swift in Sources */, + 6E424CBC26324B1D0081004A /* HandlerRegistryService.swift in Sources */, + 6E424CBD26324B1D0081004A /* LogMessage.swift in Sources */, + 6E424CBE26324B1D0081004A /* AtomicProperty.swift in Sources */, + 6E424CBF26324B1D0081004A /* AtomicArray.swift in Sources */, + 6E424CC026324B1D0081004A /* AtomicDictionary.swift in Sources */, + 6E424CC126324B1D0081004A /* Utils.swift in Sources */, + 6E424CC226324B1D0081004A /* SDKVersion.swift in Sources */, + 6E424C88263249B80081004A /* DatafileHandlerTests_MultiClients.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 6E614DC921E3F389005982A1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -3512,6 +3768,7 @@ 6E7517ED22C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E75172B22C520D400B2B157 /* Constants.swift in Sources */, 6E7516BF22C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, + 6E424BDE263228E90081004A /* AtomicArray.swift in Sources */, 6E7517E122C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75178B22C520D400B2B157 /* OptimizelyClient+Extension.swift in Sources */, 6E75177F22C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -3541,6 +3798,7 @@ 6E75174322C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E75188922C520D400B2B157 /* Project.swift in Sources */, 0B97DD95249D327F003DE606 /* SemanticVersion.swift in Sources */, + 6E424BFD263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E7518B922C520D400B2B157 /* Variable.swift in Sources */, 6E34A6182319EBB800BAE302 /* Notifications.swift in Sources */, ); @@ -3569,6 +3827,7 @@ 6E75179E22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7518C022C520D400B2B157 /* Variable.swift in Sources */, 6E75181822C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, + 6E424BE8263228E90081004A /* AtomicArray.swift in Sources */, 6EF8DE3B24BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75185422C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E9B11B422C5489500C22D81 /* OTUtils.swift in Sources */, @@ -3577,6 +3836,7 @@ 6E7516EA22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7518A822C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75187822C520D400B2B157 /* Variation.swift in Sources */, + 6E424C07263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E7517F422C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E7518FC22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E34A61F2319EBB800BAE302 /* Notifications.swift in Sources */, @@ -3643,6 +3903,7 @@ 6E9B11E022C548A200C22D81 /* OptimizelyClientTests_Group.swift in Sources */, 6E75187422C520D400B2B157 /* Variation.swift in Sources */, C78CAFA924486E0A009FE876 /* OptimizelyJSON+ObjC.swift in Sources */, + 6E424BE4263228E90081004A /* AtomicArray.swift in Sources */, 6E9B11D622C548A200C22D81 /* OptimizelyClientTests_DatafileHandler.swift in Sources */, 6E7518C822C520D400B2B157 /* Audience.swift in Sources */, 6E75174622C520D400B2B157 /* HandlerRegistryService.swift in Sources */, @@ -3658,6 +3919,7 @@ 6E7516B622C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6ECB60D7234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m in Sources */, 6E75195822C520D500B2B157 /* OPTBucketer.swift in Sources */, + 6E424C03263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E994B3A25A3E6EA00999262 /* DecisionResponse.swift in Sources */, 6E75170A22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6E9B11AC22C5489300C22D81 /* OTUtils.swift in Sources */, @@ -3771,6 +4033,7 @@ 6E7516D122C520D400B2B157 /* OPTLogger.swift in Sources */, 6E75185F22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E7517E722C520D400B2B157 /* DefaultDecisionService.swift in Sources */, + 6E424BE7263228E90081004A /* AtomicArray.swift in Sources */, 6E7516DD22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E75188F22C520D400B2B157 /* Project.swift in Sources */, 6E75195B22C520D500B2B157 /* OPTBucketer.swift in Sources */, @@ -3788,6 +4051,7 @@ 6EA2CC2B2345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E75182322C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6EF8DE1524BD1BB2008B9488 /* OptimizelyDecision.swift in Sources */, + 6E424C06263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75174922C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E7516F522C520D400B2B157 /* OptimizelyError.swift in Sources */, 6E75188322C520D400B2B157 /* TrafficAllocation.swift in Sources */, @@ -3822,6 +4086,7 @@ 6E9B117522C5487100C22D81 /* DecisionServiceTests.swift in Sources */, 6E75179F22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7516BB22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, + 6E424C08263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E86CEAD24FDC84A005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E75184922C520D400B2B157 /* Event.swift in Sources */, 0B97DDA4249D4A27003DE606 /* SemanticVersion.swift in Sources */, @@ -3838,7 +4103,6 @@ 6E9B116022C5487100C22D81 /* DecisionServiceTests_Experiments.swift in Sources */, 6E9B116322C5487100C22D81 /* BucketTests_GroupToExp.swift in Sources */, 6E7516AF22C520D400B2B157 /* DefaultLogger.swift in Sources */, - 6EA641F4262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift in Sources */, 6EF8DE2524BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75194522C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185522C520D400B2B157 /* ProjectConfig.swift in Sources */, @@ -3869,6 +4133,7 @@ 6E7518FD22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E7518E522C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E623F0D253F9045000617D0 /* DecisionInfo.swift in Sources */, + 6E424BE9263228E90081004A /* AtomicArray.swift in Sources */, 6E9B117222C5487100C22D81 /* EventDispatcherTests.swift in Sources */, 6E9B116922C5487100C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E75192122C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, @@ -4017,8 +4282,10 @@ 6E7518CE22C520D400B2B157 /* Audience.swift in Sources */, 6E75189222C520D400B2B157 /* Project.swift in Sources */, 6E7516F822C520D400B2B157 /* OptimizelyError.swift in Sources */, + 6E424C09263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75189E22C520D400B2B157 /* Experiment.swift in Sources */, 6E75178822C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, + 6E424BEA263228E90081004A /* AtomicArray.swift in Sources */, 6E7518AA22C520D400B2B157 /* FeatureFlag.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4038,6 +4305,7 @@ 6E9B115B22C5486E00C22D81 /* DecisionServiceTests.swift in Sources */, 6E75172122C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E75186722C520D400B2B157 /* Rollout.swift in Sources */, + 6E424C01263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E86CEA524FDC836005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E7518A322C520D400B2B157 /* FeatureFlag.swift in Sources */, 0B97DD9E249D4A22003DE606 /* SemanticVersion.swift in Sources */, @@ -4054,7 +4322,6 @@ 6E9B114622C5486E00C22D81 /* DecisionServiceTests_Experiments.swift in Sources */, 6E9B114922C5486E00C22D81 /* BucketTests_GroupToExp.swift in Sources */, 6E75182B22C520D400B2B157 /* BatchEvent.swift in Sources */, - 6EA641F3262F4E6900E29532 /* MultiClientsTests_DatafileHandler.swift in Sources */, 6EF8DE1E24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, 6E75190322C520D500B2B157 /* Attribute.swift in Sources */, 6E75192722C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, @@ -4085,6 +4352,7 @@ 6E7517D722C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75181F22C520D400B2B157 /* BatchEventBuilder.swift in Sources */, 6E623F06253F9045000617D0 /* DecisionInfo.swift in Sources */, + 6E424BE2263228E90081004A /* AtomicArray.swift in Sources */, 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */, 6E9B114F22C5486E00C22D81 /* DefaultUserProfileServiceTests.swift in Sources */, 6E7518F722C520D500B2B157 /* UserAttribute.swift in Sources */, @@ -4233,8 +4501,10 @@ 6E7518C922C520D400B2B157 /* Audience.swift in Sources */, 6E75188D22C520D400B2B157 /* Project.swift in Sources */, 6E7516F322C520D400B2B157 /* OptimizelyError.swift in Sources */, + 6E424C04263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75189922C520D400B2B157 /* Experiment.swift in Sources */, 6E75178322C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, + 6E424BE5263228E90081004A /* AtomicArray.swift in Sources */, 6E7518A522C520D400B2B157 /* FeatureFlag.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4259,6 +4529,8 @@ 6E7516D022C520D400B2B157 /* OPTLogger.swift in Sources */, 6E7518B222C520D400B2B157 /* Group.swift in Sources */, 6EF8DE3924BF7D69008B9488 /* DecisionReasons.swift in Sources */, + 6E424BE6263228E90081004A /* AtomicArray.swift in Sources */, + 6E424C05263228FD0081004A /* AtomicDictionary.swift in Sources */, 6EC6DD4924ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75188222C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6ECB60D0234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, @@ -4341,6 +4613,8 @@ 6E7516D522C520D400B2B157 /* OPTLogger.swift in Sources */, 6E7518B722C520D400B2B157 /* Group.swift in Sources */, 6EF8DE3E24BF7D69008B9488 /* DecisionReasons.swift in Sources */, + 6E424BEB263228E90081004A /* AtomicArray.swift in Sources */, + 6E424C0A263228FD0081004A /* AtomicDictionary.swift in Sources */, 6EC6DD4E24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75188722C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6ECB60D5234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, @@ -4447,6 +4721,7 @@ 6E7517A222C520D400B2B157 /* Array+Extension.swift in Sources */, 6E75194822C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7518E822C520D400B2B157 /* ConditionHolder.swift in Sources */, + 6E424BDD263228E90081004A /* AtomicArray.swift in Sources */, 6E75191822C520D500B2B157 /* OPTNotificationCenter.swift in Sources */, 6E7518B822C520D400B2B157 /* Variable.swift in Sources */, 6E75173622C520D400B2B157 /* MurmurHash3.swift in Sources */, @@ -4476,6 +4751,7 @@ 6E7517EC22C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E75174E22C520D400B2B157 /* LogMessage.swift in Sources */, 0B97DD94249D327F003DE606 /* SemanticVersion.swift in Sources */, + 6E424BFC263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75175A22C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E34A6172319EBB800BAE302 /* Notifications.swift in Sources */, ); @@ -4504,6 +4780,7 @@ 6E75179822C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7518BA22C520D400B2B157 /* Variable.swift in Sources */, 6E75181222C520D400B2B157 /* DataStoreQueueStackImpl.swift in Sources */, + 6E424BE1263228E90081004A /* AtomicArray.swift in Sources */, 6EF8DE3424BF7D69008B9488 /* DecisionReasons.swift in Sources */, 6E75184E22C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E9B11A822C5489200C22D81 /* OTUtils.swift in Sources */, @@ -4512,6 +4789,7 @@ 6E7516E422C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, 6E7518A222C520D400B2B157 /* FeatureFlag.swift in Sources */, 6E75187222C520D400B2B157 /* Variation.swift in Sources */, + 6E424C00263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E7517EE22C520D400B2B157 /* DataStoreMemory.swift in Sources */, 6E9B11A622C5488900C22D81 /* iOSOnlyTests.swift in Sources */, 6E34A6192319EBB800BAE302 /* Notifications.swift in Sources */, @@ -4588,6 +4866,7 @@ 75C71A1225E454460084187E /* OPTUserProfileService.swift in Sources */, 75C71A1325E454460084187E /* OPTEventDispatcher.swift in Sources */, 75C71A1425E454460084187E /* DefaultDatafileHandler.swift in Sources */, + 6E424BE0263228E90081004A /* AtomicArray.swift in Sources */, 75C71A1525E454460084187E /* DecisionInfo.swift in Sources */, 75C71A1625E454460084187E /* DefaultBucketer.swift in Sources */, 75C71A1725E454460084187E /* DefaultNotificationCenter.swift in Sources */, @@ -4611,6 +4890,7 @@ 75C71A2925E454460084187E /* ProjectConfig.swift in Sources */, 75C71A2A25E454460084187E /* FeatureVariable.swift in Sources */, 75C71A2B25E454460084187E /* Rollout.swift in Sources */, + 6E424BFF263228FD0081004A /* AtomicDictionary.swift in Sources */, 75C71A2C25E454460084187E /* Variation.swift in Sources */, 75C71A2D25E454460084187E /* TrafficAllocation.swift in Sources */, 75C71A2E25E454460084187E /* Project.swift in Sources */, @@ -4685,6 +4965,7 @@ BD64855D2491474500F30986 /* Array+Extension.swift in Sources */, BD64855E2491474500F30986 /* OPTDatafileHandler.swift in Sources */, BD64855F2491474500F30986 /* ConditionHolder.swift in Sources */, + 6E424BDF263228E90081004A /* AtomicArray.swift in Sources */, BD6485602491474500F30986 /* OPTNotificationCenter.swift in Sources */, BD6485612491474500F30986 /* Variable.swift in Sources */, BD6485622491474500F30986 /* MurmurHash3.swift in Sources */, @@ -4714,6 +4995,7 @@ BD6485782491474500F30986 /* DataStoreMemory.swift in Sources */, BD6485792491474500F30986 /* LogMessage.swift in Sources */, BD1C3E8524E4399C0084B4DA /* SemanticVersion.swift in Sources */, + 6E424BFE263228FD0081004A /* AtomicDictionary.swift in Sources */, BD64857A2491474500F30986 /* AtomicProperty.swift in Sources */, BD64857B2491474500F30986 /* Notifications.swift in Sources */, ); @@ -4727,6 +5009,11 @@ target = 6EBAEB6B21E3FEF800D13AA9 /* OptimizelySwiftSDK-iOS */; targetProxy = 6E14CD692423F80B00010234 /* PBXContainerItemProxy */; }; + 6E424C43263249620081004A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 6EBAEB6B21E3FEF800D13AA9 /* OptimizelySwiftSDK-iOS */; + targetProxy = 6E424C42263249620081004A /* PBXContainerItemProxy */; + }; 6E614DD821E3F38A005982A1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6E614DCC21E3F389005982A1 /* OptimizelySwiftSDK-tvOS */; @@ -4949,6 +5236,44 @@ }; name = Release; }; + 6E424C45263249620081004A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = D3Q4WJTK93; + INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.optimizely.abcExtended.OptimizelyTests-MultiClients-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6E424C46263249620081004A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = D3Q4WJTK93; + INFOPLIST_FILE = Tests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.optimizely.abcExtended.OptimizelyTests-MultiClients-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 6E614DDE21E3F38A005982A1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -5585,6 +5910,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 6E424C44263249620081004A /* Build configuration list for PBXNativeTarget "OptimizelyTests-MultiClients-iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6E424C45263249620081004A /* Debug */, + 6E424C46263249620081004A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 6E614DE221E3F38A005982A1 /* Build configuration list for PBXNativeTarget "OptimizelySwiftSDK-tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/OptimizelySwiftSDK.xcodeproj/xcshareddata/xcschemes/OptimizelySwiftSDK-iOS.xcscheme b/OptimizelySwiftSDK.xcodeproj/xcshareddata/xcschemes/OptimizelySwiftSDK-iOS.xcscheme index 9ef2d3da..a98d8e07 100644 --- a/OptimizelySwiftSDK.xcodeproj/xcshareddata/xcschemes/OptimizelySwiftSDK-iOS.xcscheme +++ b/OptimizelySwiftSDK.xcodeproj/xcshareddata/xcschemes/OptimizelySwiftSDK-iOS.xcscheme @@ -20,6 +20,20 @@ ReferencedContainer = "container:OptimizelySwiftSDK.xcodeproj"> + + + + + + + + () + // and our download queue to speed things up. let downloadQueue = DispatchQueue(label: "DefaultDatafileHandlerQueue") diff --git a/Sources/Implementation/Datastore/DataStoreFile.swift b/Sources/Implementation/Datastore/DataStoreFile.swift index 6f658fc3..ca4a4c9b 100644 --- a/Sources/Implementation/Datastore/DataStoreFile.swift +++ b/Sources/Implementation/Datastore/DataStoreFile.swift @@ -39,6 +39,7 @@ open class DataStoreFile: OPTDataStore where T: Codable { } else { self.url = URL(fileURLWithPath: storeName) } + } func isArray() -> Bool { diff --git a/Sources/Utils/AtomicArray.swift b/Sources/Utils/AtomicArray.swift new file mode 100644 index 00000000..db0bdc20 --- /dev/null +++ b/Sources/Utils/AtomicArray.swift @@ -0,0 +1,83 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +import Foundation + +class AtomicArray: AtomicCollection { + private var _property: [T] + + init(_ property: [T] = []) { + self._property = property + } + + subscript(index: Int) -> T? { + get { + returnWithLock { + _property[index] + } + } + set { + performWithLock { + if let value = newValue { + self._property[index] = value + } + } + } + } + + var count: Int { + returnWithLock { + _property.count + }! + } + + func append(_ item: T) { + performWithLock { + self._property.append(item) + } + } + + func append(contentsOf items: [T]) { + performWithLock { + self._property.append(contentsOf: items) + } + } +} + +// MARK: - AtomicCollection + +class AtomicCollection { + var lock: DispatchQueue = { + let name = "AtomicCollection" + String(Int.random(in: 0..<100000)) + return DispatchQueue(label: name, attributes: .concurrent) + }() + + func returnWithLock(_ action: () throws -> E?) -> E? { + var result: E? + lock.sync { + result = try? action() + } + return result + } + + func performWithLock(_ action: @escaping () throws -> Void) { + lock.async(flags: .barrier) { + try? action() + } + } +} + diff --git a/Sources/Utils/AtomicDictionary.swift b/Sources/Utils/AtomicDictionary.swift new file mode 100644 index 00000000..b81c006c --- /dev/null +++ b/Sources/Utils/AtomicDictionary.swift @@ -0,0 +1,45 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +import Foundation + +class AtomicDictionary: AtomicCollection where K: Hashable { + private var _property: [K: V] + + init(_ property: [K: V] = [:]) { + self._property = property + } + + subscript(key: K) -> V? { + get { + returnWithLock { + _property[key] + } + } + set { + performWithLock { + self._property[key] = newValue + } + } + } + + var count: Int { + returnWithLock { + _property.count + }! + } +} diff --git a/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift b/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift new file mode 100644 index 00000000..676b924d --- /dev/null +++ b/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift @@ -0,0 +1,49 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +import XCTest + +class AtomicArrayTests: XCTestCase { + + func testAtomicArray() { + let a = AtomicArray([0, 1, 2, 3, 4]) + XCTAssert(a.count == 5) + XCTAssert(a[0] == 0) + XCTAssert(a[4] == 4) + a[0] = 100 + a[4] = 400 + XCTAssert(a[0] == 100) + XCTAssert(a[4] == 400) + + let b = AtomicArray([0, 1, 2, 3, 4]) + b.append(5) + XCTAssert(b.count == 6) + XCTAssert(b[0] == 0) + XCTAssert(b[4] == 4) + XCTAssert(b[5] == 5) + + b.append(contentsOf: [10, 20, 30]) + XCTAssert(b.count == 9) + XCTAssert(b[0] == 0) + XCTAssert(b[4] == 4) + XCTAssert(b[5] == 5) + XCTAssert(b[6] == 10) + XCTAssert(b[7] == 20) + XCTAssert(b[8] == 30) + } + +} diff --git a/Tests/OptimizelyTests-MultiClients/AtomicDictionaryTests.swift b/Tests/OptimizelyTests-MultiClients/AtomicDictionaryTests.swift new file mode 100644 index 00000000..e3814bf2 --- /dev/null +++ b/Tests/OptimizelyTests-MultiClients/AtomicDictionaryTests.swift @@ -0,0 +1,42 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +import XCTest + +class AtomicDictionaryTests: XCTestCase { + + func testAtomicDictionary() { + let a = AtomicDictionary() + XCTAssert(a.count == 0) + XCTAssert(a["k1"] == nil) + + a["k1"] = 100 + a["k2"] = 2 + a["k1"] = 1 + + XCTAssert(a.count == 2) + XCTAssert(a["k1"] == 1) + XCTAssert(a["k2"] == 2) + + a["k1"] = nil + + XCTAssert(a.count == 1) + XCTAssert(a["k1"] == nil) + XCTAssert(a["k2"] == 2) + } + +} diff --git a/Tests/OptimizelyTests-Common/MultiClientsTests_DatafileHandler.swift b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift similarity index 85% rename from Tests/OptimizelyTests-Common/MultiClientsTests_DatafileHandler.swift rename to Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift index 7fcd0a73..a4b9a487 100644 --- a/Tests/OptimizelyTests-Common/MultiClientsTests_DatafileHandler.swift +++ b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift @@ -17,7 +17,7 @@ import XCTest -class MultiClientsTests_DatafileHandler: XCTestCase { +class DatafileHandlerTests_MultiClients: XCTestCase { let testSdkKeyBasename = "testSdkKey" var sdkKeys = [String]() @@ -55,17 +55,16 @@ class MultiClientsTests_DatafileHandler: XCTestCase { func testConcurrentAccessDatafileCaches() { makeSdkKeys(100) - let result = runConcurrent(for: sdkKeys) { _, _ in - let maxCnt = 100 + let result = runConcurrent(for: sdkKeys) { sdkKey, idx in + let maxCnt = 10 for _ in 0.. Void) -> Bool { let group = DispatchGroup() for (idx, item) in items.enumerated() { group.enter() - DispatchQueue.global().async { - //print("[MultiClientsTest] starting for \(sdkKey)") + + // NOTE: do not use DispatchQueue.global(), which looks like a deadlock because of too many threads + DispatchQueue(label: item).async { task(item, idx) - //print("[MultiClientsTest] ending for \(sdkKey)") group.leave() } } From bfabdae64ffa8a0447a937a62c75d92d3d26f5f5 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 26 Apr 2021 11:09:24 -0700 Subject: [PATCH 03/26] change name for AtomicWrapper --- Sources/Utils/AtomicArray.swift | 22 ++++++++++------------ Sources/Utils/AtomicDictionary.swift | 9 ++++----- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/Sources/Utils/AtomicArray.swift b/Sources/Utils/AtomicArray.swift index db0bdc20..02ec7ed7 100644 --- a/Sources/Utils/AtomicArray.swift +++ b/Sources/Utils/AtomicArray.swift @@ -13,11 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. // - import Foundation -class AtomicArray: AtomicCollection { +class AtomicArray: AtomicWrapper { private var _property: [T] init(_ property: [T] = []) { @@ -26,12 +25,12 @@ class AtomicArray: AtomicCollection { subscript(index: Int) -> T? { get { - returnWithLock { + returnAtomic { _property[index] } } set { - performWithLock { + performAtomic { if let value = newValue { self._property[index] = value } @@ -40,33 +39,33 @@ class AtomicArray: AtomicCollection { } var count: Int { - returnWithLock { + returnAtomic { _property.count }! } func append(_ item: T) { - performWithLock { + performAtomic { self._property.append(item) } } func append(contentsOf items: [T]) { - performWithLock { + performAtomic { self._property.append(contentsOf: items) } } } -// MARK: - AtomicCollection +// MARK: - AtomicWrapper -class AtomicCollection { +class AtomicWrapper { var lock: DispatchQueue = { let name = "AtomicCollection" + String(Int.random(in: 0..<100000)) return DispatchQueue(label: name, attributes: .concurrent) }() - func returnWithLock(_ action: () throws -> E?) -> E? { + func returnAtomic(_ action: () throws -> E?) -> E? { var result: E? lock.sync { result = try? action() @@ -74,10 +73,9 @@ class AtomicCollection { return result } - func performWithLock(_ action: @escaping () throws -> Void) { + func performAtomic(_ action: @escaping () throws -> Void) { lock.async(flags: .barrier) { try? action() } } } - diff --git a/Sources/Utils/AtomicDictionary.swift b/Sources/Utils/AtomicDictionary.swift index b81c006c..4ab687b6 100644 --- a/Sources/Utils/AtomicDictionary.swift +++ b/Sources/Utils/AtomicDictionary.swift @@ -14,10 +14,9 @@ // limitations under the License. // - import Foundation -class AtomicDictionary: AtomicCollection where K: Hashable { +class AtomicDictionary: AtomicWrapper where K: Hashable { private var _property: [K: V] init(_ property: [K: V] = [:]) { @@ -26,19 +25,19 @@ class AtomicDictionary: AtomicCollection where K: Hashable { subscript(key: K) -> V? { get { - returnWithLock { + returnAtomic { _property[key] } } set { - performWithLock { + performAtomic { self._property[key] = newValue } } } var count: Int { - returnWithLock { + returnAtomic { _property.count }! } From e7091de213e84f17484a3e2736028c2705d580d8 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 26 Apr 2021 11:14:50 -0700 Subject: [PATCH 04/26] remove extra line from custom xcode header --- .../xcshareddata/IDETemplateMacros.plist | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OptimizelySwiftSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist b/OptimizelySwiftSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist index fb12816b..24affa1b 100644 --- a/OptimizelySwiftSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist +++ b/OptimizelySwiftSDK.xcworkspace/xcshareddata/IDETemplateMacros.plist @@ -17,8 +17,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// - +// From 6282d4d1b1f1f6c3a88f92b7981c3666832b0578 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 26 Apr 2021 17:10:27 -0700 Subject: [PATCH 05/26] add more concurrent tests for datafile handler --- .../DemoObjcApp.xcodeproj/project.pbxproj | 17 +- .../xcschemes/DemoObjciOS.xcscheme | 2 +- .../xcschemes/DemoObjctvOS.xcscheme | 2 +- .../DemoSwiftApp.xcodeproj/project.pbxproj | 17 +- .../xcschemes/DemoSwiftiOS.xcscheme | 2 +- .../xcschemes/DemoSwifttvOS.xcscheme | 2 +- .../xcschemes/DemoSwiftwatchOS.xcscheme | 2 +- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 30 ++- .../xcschemes/OptimizelySwiftSDK-iOS.xcscheme | 2 +- .../OptimizelySwiftSDK-macOS.xcscheme | 2 +- .../OptimizelySwiftSDK-tvOS.xcscheme | 2 +- .../OptimizelySwiftSDK-watchOS.xcscheme | 2 +- .../DefaultDatafileHandler.swift | 97 +++++---- ...ptimizelyClientTests_DatafileHandler.swift | 2 +- .../DatafileHandlerTests.swift | 2 +- .../AtomicArrayTests.swift | 1 - .../AtomicDictionaryTests.swift | 1 - .../DatafileHandlerTests_MultiClients.swift | 199 ++++++++++++------ Tests/TestUtils/MockDatafileHandler.swift | 41 ++++ Tests/TestUtils/MockUrlSession.swift | 16 +- Tests/TestUtils/OTUtils.swift | 52 +++-- 21 files changed, 346 insertions(+), 147 deletions(-) create mode 100644 Tests/TestUtils/MockDatafileHandler.swift diff --git a/DemoObjCApp/DemoObjcApp.xcodeproj/project.pbxproj b/DemoObjCApp/DemoObjcApp.xcodeproj/project.pbxproj index 1cf4c980..6b7b0e8a 100644 --- a/DemoObjCApp/DemoObjcApp.xcodeproj/project.pbxproj +++ b/DemoObjCApp/DemoObjcApp.xcodeproj/project.pbxproj @@ -134,6 +134,13 @@ remoteGlobalIDString = 6E614DCC21E3F389005982A1; remoteInfo = "OptimizelySwiftSDK-tvOS"; }; + 6E8A3D2A26373B3F00DAEA13 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6E4D2FE722C5457F00062EB3 /* OptimizelySwiftSDK.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 6E424C3C263249620081004A; + remoteInfo = "OptimizelyTests-MultiClients-iOS"; + }; 6EAAB6D42602892A00294B8A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6E4D2FE722C5457F00062EB3 /* OptimizelySwiftSDK.xcodeproj */; @@ -243,6 +250,7 @@ 6EAAB6D52602892A00294B8A /* Optimizely.framework */, 6E4D2FFB22C5457F00062EB3 /* OptimizelyTests-iOS.xctest */, 6E4D2FFD22C5457F00062EB3 /* OptimizelyTests-Common-iOS.xctest */, + 6E8A3D2B26373B3F00DAEA13 /* OptimizelyTests-MultiClients-iOS.xctest */, 6EF41A9C2523D23E00EAADF1 /* OptimizelyTests-Batch-iOS.xctest */, 6E4D2FFF22C5457F00062EB3 /* OptimizelyTests-APIs-iOS.xctest */, 6E4D300122C5457F00062EB3 /* OptimizelyTests-DataModel-iOS.xctest */, @@ -427,7 +435,7 @@ 6EF7496721E40467008B22A0 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1230; + LastUpgradeCheck = 1240; ORGANIZATIONNAME = Optimizely; TargetAttributes = { 6EF7498D21E404BB008B22A0 = { @@ -553,6 +561,13 @@ remoteRef = 6E4D300C22C5457F00062EB3 /* PBXContainerItemProxy */; sourceTree = BUILT_PRODUCTS_DIR; }; + 6E8A3D2B26373B3F00DAEA13 /* OptimizelyTests-MultiClients-iOS.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "OptimizelyTests-MultiClients-iOS.xctest"; + remoteRef = 6E8A3D2A26373B3F00DAEA13 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 6EAAB6D52602892A00294B8A /* Optimizely.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; diff --git a/DemoObjCApp/DemoObjcApp.xcodeproj/xcshareddata/xcschemes/DemoObjciOS.xcscheme b/DemoObjCApp/DemoObjcApp.xcodeproj/xcshareddata/xcschemes/DemoObjciOS.xcscheme index ad67caf8..d6437bbb 100644 --- a/DemoObjCApp/DemoObjcApp.xcodeproj/xcshareddata/xcschemes/DemoObjciOS.xcscheme +++ b/DemoObjCApp/DemoObjcApp.xcodeproj/xcshareddata/xcschemes/DemoObjciOS.xcscheme @@ -1,6 +1,6 @@ () // and our download queue to speed things up. let downloadQueue = DispatchQueue(label: "DefaultDatafileHandlerQueue") - public required init() { - - } - - public func setPeriodicInterval(sdkKey: String, interval: Int) { - timers.performAtomic { timers in - if timers[sdkKey] == nil { - timers[sdkKey] = (nil, interval) - return - } - } - } + public required init() {} - public func hasPeriodicInterval(sdkKey: String) -> Bool { - var result = true - timers.performAtomic { timers in - result = timers[sdkKey] != nil - } - - return result - } - - public func downloadDatafile(sdkKey: String) -> Data? { - var datafile: Data? - let group = DispatchGroup() - - group.enter() - - downloadDatafile(sdkKey: sdkKey) { (result) in - switch result { - case .success(let data): - datafile = data - case .failure(let error): - self.logger.e(error.reason) - } - group.leave() - } - - group.wait() - - return datafile - } + // MARK: - download datafile open func downloadDatafile(sdkKey: String, returnCacheIfNoChange: Bool, @@ -128,6 +89,27 @@ open class DefaultDatafileHandler: OPTDatafileHandler { } } + public func downloadDatafile(sdkKey: String) -> Data? { + var datafile: Data? + let group = DispatchGroup() + + group.enter() + + downloadDatafile(sdkKey: sdkKey) { (result) in + switch result { + case .success(let data): + datafile = data + case .failure(let error): + self.logger.e(error.reason) + } + group.leave() + } + + group.wait() + + return datafile + } + open func getSession(resourceTimeoutInterval: Double?) -> URLSession { let config = URLSessionConfiguration.ephemeral if let resourceTimeoutInterval = resourceTimeoutInterval, @@ -143,7 +125,7 @@ open class DefaultDatafileHandler: OPTDatafileHandler { var request = URLRequest(url: url) - if let lastModified = dataStore.getLastModified(sdkKey: sdkKey), isDatafileSaved(sdkKey: sdkKey) { + if let lastModified = sharedDataStore.getLastModified(sdkKey: sdkKey), isDatafileSaved(sdkKey: sdkKey) { request.setLastModified(lastModified: lastModified) } @@ -155,7 +137,8 @@ open class DefaultDatafileHandler: OPTDatafileHandler { self.logger.d { String(data: data, encoding: .utf8) ?? "" } self.saveDatafile(sdkKey: sdkKey, dataFile: data) if let lastModified = response.getLastModified() { - self.dataStore.setLastModified(sdkKey: sdkKey, lastModified: lastModified) } + self.sharedDataStore.setLastModified(sdkKey: sdkKey, lastModified: lastModified) + } return data } @@ -163,6 +146,26 @@ open class DefaultDatafileHandler: OPTDatafileHandler { return nil } + // MARK: - periodic updates + + public func setPeriodicInterval(sdkKey: String, interval: Int) { + timers.performAtomic { timers in + if timers[sdkKey] == nil { + timers[sdkKey] = (nil, interval) + return + } + } + } + + public func hasPeriodicInterval(sdkKey: String) -> Bool { + var result = true + timers.performAtomic { timers in + result = timers[sdkKey] != nil + } + + return result + } + public func startUpdates(sdkKey: String, datafileChangeNotification: ((Data) -> Void)?) { if let value = timers.property?[sdkKey], !(value.timer?.isValid ?? false) { startPeriodicUpdates(sdkKey: sdkKey, updateInterval: value.interval, datafileChangeNotification: datafileChangeNotification) @@ -177,6 +180,8 @@ open class DefaultDatafileHandler: OPTDatafileHandler { stopPeriodicUpdates() } + // MARK: - datafile store + open func createDataStore(sdkKey: String) -> OPTDataStore { return DataStoreFile(storeName: sdkKey) } @@ -212,7 +217,6 @@ extension DefaultDatafileHandler { } let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(updateInterval), repeats: false) { (timer) in - self.performPerodicDownload(sdkKey: sdkKey, startTime: now, updateInterval: updateInterval, @@ -220,6 +224,7 @@ extension DefaultDatafileHandler { timer.invalidate() } + self.timers.performAtomic(atomicOperation: { (timers) in if let interval = timers[sdkKey]?.interval { timers[sdkKey] = (timer, interval) diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_DatafileHandler.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_DatafileHandler.swift index ea88f796..2c8d8c0d 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_DatafileHandler.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_DatafileHandler.swift @@ -58,7 +58,7 @@ class OptimizelyClientTests_DatafileHandler: XCTestCase { let data = OTUtils.loadJSONDatafile("api_datafile") handler.saveDatafile(sdkKey: sdkKey, dataFile: data!) - handler.dataStore.setLastModified(sdkKey: sdkKey, lastModified: "1234") + handler.sharedDataStore.setLastModified(sdkKey: sdkKey, lastModified: "1234") // set the url to use as our datafile download url handler.localFileUrl = fileUrl diff --git a/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift b/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift index a72bc861..2991b745 100644 --- a/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift @@ -271,7 +271,7 @@ class DatafileHandlerTests: XCTestCase { let handler = InnerDatafileHandler() //save the cached datafile.. handler.saveDatafile(sdkKey: sdkKey, dataFile: "{}".data(using: .utf8)!) - handler.dataStore.setLastModified(sdkKey: sdkKey, lastModified: "1234") + handler.sharedDataStore.setLastModified(sdkKey: sdkKey, lastModified: "1234") // set the url to use as our datafile download url handler.localFileUrl = fileUrl diff --git a/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift b/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift index 676b924d..986a03af 100644 --- a/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift +++ b/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. // - import XCTest diff --git a/Tests/OptimizelyTests-MultiClients/AtomicDictionaryTests.swift b/Tests/OptimizelyTests-MultiClients/AtomicDictionaryTests.swift index e3814bf2..68526637 100644 --- a/Tests/OptimizelyTests-MultiClients/AtomicDictionaryTests.swift +++ b/Tests/OptimizelyTests-MultiClients/AtomicDictionaryTests.swift @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. // - import XCTest diff --git a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift index a4b9a487..0ef2bcaa 100644 --- a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift @@ -13,7 +13,6 @@ // See the License for the specific language governing permissions and // limitations under the License. // - import XCTest @@ -31,52 +30,20 @@ class DatafileHandlerTests_MultiClients: XCTestCase { } override func tearDown() { - OTUtils.removeAllBinders() - OTUtils.removeAllFiles(including: testSdkKeyBasename) + OTUtils.clearAllBinders() + OTUtils.clearAllTestStorage(including: testSdkKeyBasename) } - func testConcurrentAccessPeriodicInterval() { - makeSdkKeys(100) - - let result = runConcurrent(for: sdkKeys) { _, _ in - let maxCnt = 100 - for _ in 0.. URLSession { - return MockUrlSession(failureCode: failureCode, withError: passError, localUrl: localUrl) - } - } - } diff --git a/Tests/TestUtils/MockDatafileHandler.swift b/Tests/TestUtils/MockDatafileHandler.swift new file mode 100644 index 00000000..ca1c43ac --- /dev/null +++ b/Tests/TestUtils/MockDatafileHandler.swift @@ -0,0 +1,41 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class MockDatafileHandler: DefaultDatafileHandler { + let failureCode: Int + let passError: Bool + let sdkKey: String + let localUrl: URL? + let lastModified: String? + + init(failureCode: Int = 0, passError: Bool = false, sdkKey: String, strData: String = "{}", lastModified: String? = nil) { + self.failureCode = failureCode + self.passError = passError + self.sdkKey = sdkKey + self.localUrl = OTUtils.saveAFile(name: sdkKey, data: strData.data(using: .utf8)!) + self.lastModified = lastModified + } + + public required init() { + fatalError("init() has not been implemented") + } + + override func getSession(resourceTimeoutInterval: Double?) -> URLSession { + return MockUrlSession(failureCode: failureCode, withError: passError, localUrl: localUrl, lastModified: lastModified) + } +} diff --git a/Tests/TestUtils/MockUrlSession.swift b/Tests/TestUtils/MockUrlSession.swift index 9b892766..6f19b2ff 100644 --- a/Tests/TestUtils/MockUrlSession.swift +++ b/Tests/TestUtils/MockUrlSession.swift @@ -23,9 +23,10 @@ import Foundation // the response also includes the url for the data download. // the cdn url is used to get the datafile if the datafile is not in cache class MockUrlSession: URLSession { - var downloadCacheUrl: URL? let failureCode: Int let passError: Bool + var downloadCacheUrl: URL? + var lastModified: String? class MockDownloadTask: URLSessionDownloadTask { var task: () -> Void @@ -44,10 +45,11 @@ class MockUrlSession: URLSession { self.passError = withError } - init(failureCode: Int, withError: Bool, localUrl: URL?) { + init(failureCode: Int, withError: Bool, localUrl: URL?, lastModified: String?) { self.failureCode = failureCode self.passError = withError self.downloadCacheUrl = localUrl + self.lastModified = lastModified } convenience override init() { @@ -64,7 +66,15 @@ class MockUrlSession: URLSession { completionHandler(self.downloadCacheUrl, nil, error) } else { - let response = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: nil) + var headers = [String: String]() + if let lastModified = self.lastModified { + headers["Last-Modified"] = lastModified + } + + let response = HTTPURLResponse(url: request.url!, + statusCode: statusCode, + httpVersion: nil, + headerFields: headers) completionHandler(self.downloadCacheUrl, response, nil) } } diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index 5836e82d..80494231 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -150,7 +150,7 @@ class OTUtils { HandlerRegistryService.shared.registerBinding(binder: binder) } - static func removeAllBinders() { + static func clearAllBinders() { HandlerRegistryService.shared.binders.property?.removeAll() } @@ -180,24 +180,6 @@ class OTUtils { ups.save(userProfile: profile) } - // MARK: - big numbers - - static var positiveMaxValueAllowed: Double { - return pow(2, 53) - } - - static var negativeMaxValueAllowed: Double { - return -pow(2, 53) - } - - static var positiveTooBigValue: Double { - return positiveMaxValueAllowed * 2.0 - } - - static var negativeTooBigValue: Double { - return negativeMaxValueAllowed * 2.0 - } - // MARK: - files static func saveAFile(name:String, data:Data) -> URL? { @@ -214,11 +196,24 @@ class OTUtils { return ds.url } + static func clearAllTestStorage(including: String) { + removeAllFiles(including: including) + removeAllUserDefaults(including: including) + } + static func removeAllFiles(including: String) { removeAllFiles(including: including, in: .documentDirectory) removeAllFiles(including: including, in: .cachesDirectory) } + static func removeAllUserDefaults(including: String) { + let allKeys = UserDefaults.standard.dictionaryRepresentation().keys + allKeys.filter{ $0.contains(including) }.forEach{ itemKey in + UserDefaults.standard.removeObject(forKey: itemKey) + //print("[OTUtils] removed UserDefaults: '\(itemKey)'") + } + } + static func removeAllFiles(including: String, in directory: FileManager.SearchPathDirectory) { if let docUrl = FileManager.default.urls(for: directory, in: .userDomainMask).first { if let names = try? FileManager.default.contentsOfDirectory(atPath: docUrl.path) { @@ -250,6 +245,25 @@ class OTUtils { } } } + + // MARK: - big numbers + + static var positiveMaxValueAllowed: Double { + return pow(2, 53) + } + + static var negativeMaxValueAllowed: Double { + return -pow(2, 53) + } + + static var positiveTooBigValue: Double { + return positiveMaxValueAllowed * 2.0 + } + + static var negativeTooBigValue: Double { + return negativeMaxValueAllowed * 2.0 + } + // MARK: - others From e21b11d37eefedaad9e1965810a1fd9ee5f1fe78 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 26 Apr 2021 17:37:06 -0700 Subject: [PATCH 06/26] clean up --- .../DatafileHandlerTests_MultiClients.swift | 52 ++++++++++++------- Tests/TestUtils/MockDatafileHandler.swift | 1 + 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift index 0ef2bcaa..566af7a0 100644 --- a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift @@ -40,6 +40,9 @@ class DatafileHandlerTests_MultiClients: XCTestCase { makeSdkKeys(100) let result = runConcurrent(for: sdkKeys, timeoutInSecs: 10) { sdkKey, _ in + + // NOTE: using multiple DatafileHandler instances + let mockHandler = MockDatafileHandler(failureCode: 0, passError: false, sdkKey: sdkKey, @@ -104,6 +107,8 @@ class DatafileHandlerTests_MultiClients: XCTestCase { passError = false } + // NOTE: using multiple DatafileHandler instances + let mockHandler = MockDatafileHandler(failureCode: statusCode, passError: passError, sdkKey: sdkKey, @@ -147,6 +152,8 @@ class DatafileHandlerTests_MultiClients: XCTestCase { let result = runConcurrent(for: sdkKeys, timeoutInSecs: 10) { sdkKey, _ in let expectedLastModified = "date-for-\(sdkKey)" + // NOTE: using multiple DatafileHandler instances + let mockHandler = MockDatafileHandler(failureCode: 0, passError: false, sdkKey: sdkKey, @@ -170,31 +177,15 @@ class DatafileHandlerTests_MultiClients: XCTestCase { XCTAssertTrue(result, "Concurrent tasks timed out") } - // MARK: - Periodic Interval - - func testConcurrentAccessPeriodicInterval() { - makeSdkKeys(100) - - let result = runConcurrent(for: sdkKeys) { _, _ in - let maxCnt = 100 - for _ in 0.. URLSession { return MockUrlSession(failureCode: failureCode, withError: passError, localUrl: localUrl, lastModified: lastModified) } + } From 65d11fa10e2cfdacd0d20b3500b53ac86b2478ad Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 27 Apr 2021 14:35:05 -0700 Subject: [PATCH 07/26] change tests to a shared datafile handler --- ...ptimizelyClientTests_DatafileHandler.swift | 37 +--- .../DatafileHandlerTests.swift | 208 ++---------------- .../DatafileHandlerTests_MultiClients.swift | 90 ++++---- Tests/TestUtils/MockDatafileHandler.swift | 39 ++-- Tests/TestUtils/MockUrlSession.swift | 56 +++-- Tests/TestUtils/OTUtils.swift | 14 +- 6 files changed, 132 insertions(+), 312 deletions(-) diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_DatafileHandler.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_DatafileHandler.swift index 2c8d8c0d..9b512c3a 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_DatafileHandler.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_DatafileHandler.swift @@ -21,46 +21,23 @@ class OptimizelyClientTests_DatafileHandler: XCTestCase { let sdkKey = "localcdnTestSDKKey" override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - HandlerRegistryService.shared.binders.property?.removeAll() + OTUtils.bindLoggerForTest(.info) + OTUtils.createDocumentDirectoryIfNotAvailable() } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - _ = OTUtils.removeAFile(name: sdkKey) - - HandlerRegistryService.shared.binders.property?.removeAll() - + OTUtils.clearAllBinders() + OTUtils.clearAllTestStorage(including: sdkKey) } func testOptimizelyClientWithCachedDatafile() { - var fileUrl:URL? - - // create a dummy file at a url to use as our datafile local download - fileUrl = OTUtils.saveAFile(name: "localcdn", data: OTUtils.loadJSONDatafile("api_datafile")!) - - // default datafile handler - class InnerDatafileHandler : DefaultDatafileHandler { - var localFileUrl:URL? - // override getSession to return our own session. - override func getSession(resourceTimeoutInterval: Double?) -> URLSession { - - let session = MockUrlSession() - session.downloadCacheUrl = localFileUrl - - return session - } - } - // create test datafile handler - let handler = InnerDatafileHandler() + let handler = MockDatafileHandler(statusCode: 0, passError: false, localResponseData: OTUtils.loadJSONDatafileString("api_datafile")) //save the cached datafile.. let data = OTUtils.loadJSONDatafile("api_datafile") handler.saveDatafile(sdkKey: sdkKey, dataFile: data!) handler.sharedDataStore.setLastModified(sdkKey: sdkKey, lastModified: "1234") - // set the url to use as our datafile download url - handler.localFileUrl = fileUrl HandlerRegistryService.shared.registerBinding(binder: Binder(sdkKey: sdkKey, service: OPTDatafileHandler.self, strategy: .reUse, isSingleton: true, inst: handler)) @@ -76,9 +53,7 @@ class OptimizelyClientTests_DatafileHandler: XCTestCase { } wait(for: [expectation], timeout: 3) - - try? FileManager.default.removeItem(at: fileUrl!) - + client.datafileHandler?.stopAllUpdates() HandlerRegistryService.shared.binders.property?.removeAll() diff --git a/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift b/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift index 2991b745..bfc817cb 100644 --- a/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift @@ -21,26 +21,13 @@ class DatafileHandlerTests: XCTestCase { let sdkKey = "localcdnTestSDKKey" override func setUp() { - - HandlerRegistryService.shared.binders.property?.removeAll() - // Put setup code here. This method is called before the invocation of each test method in the class. - if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - if (!FileManager.default.fileExists(atPath: url.path)) { - do { - try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) - } catch { - print(error) - } - - } - } - + OTUtils.bindLoggerForTest(.info) + OTUtils.createDocumentDirectoryIfNotAvailable() } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - _ = OTUtils.removeAFile(name: sdkKey) - HandlerRegistryService.shared.binders.property?.removeAll() + OTUtils.clearAllBinders() + OTUtils.clearAllTestStorage(including: sdkKey) } func testDatafileHandler() { @@ -80,37 +67,12 @@ class DatafileHandlerTests: XCTestCase { } func testDatafileDownload500() { - - var localUrl: URL? - - // create a dummy file at a url to use as or datafile cdn location - localUrl = OTUtils.saveAFile(name: sdkKey, data: "{}".data(using: .utf8)!) + OTUtils.createDatafileCache(sdkKey: sdkKey) - // default datafile handler - class InnerDatafileHandler : DefaultDatafileHandler { - var localFileUrl: URL? - // override getSession to return our own session. - override func getSession(resourceTimeoutInterval: Double?) -> URLSession { - - // will return 500 - let session = MockUrlSession(failureCode: 500, withError: false) - session.downloadCacheUrl = localFileUrl - - return session - } - } - - // create test datafile handler - let handler = InnerDatafileHandler() - // set the url to use as our datafile download url - handler.localFileUrl = localUrl - + let handler = MockDatafileHandler(statusCode: 500, passError: false) let expectation = XCTestExpectation(description: "wait to get no-nil data") - // initiate download task which should pass back a 304 but still return non nil - // since the datafile was not in cache. handler.downloadDatafile(sdkKey: sdkKey) { (result) in - if case let .success(data) = result { XCTAssert(data != nil) expectation.fulfill() @@ -118,42 +80,15 @@ class DatafileHandlerTests: XCTestCase { } wait(for: [expectation], timeout: 3) - // finally remove the datafile when complete. - try? FileManager.default.removeItem(at: localUrl!) } func testDatafileDownloadFailureWithCache() { - - var localUrl:URL? - - // create a dummy file at a url to use as or datafile cdn location - localUrl = OTUtils.saveAFile(name: sdkKey, data: Data()) + OTUtils.createDatafileCache(sdkKey: sdkKey) - // default datafile handler - class InnerDatafileHandler : DefaultDatafileHandler { - var localFileUrl:URL? - // override getSession to return our own session. - override func getSession(resourceTimeoutInterval: Double?) -> URLSession { - - // will return error - let session = MockUrlSession(failureCode: 0, withError: true) - session.downloadCacheUrl = localFileUrl - - return session - } - } - - // create test datafile handler - let handler = InnerDatafileHandler() - // set the url to use as our datafile download url - handler.localFileUrl = localUrl - + let handler = MockDatafileHandler(statusCode: 0, passError: true) let expectation = XCTestExpectation(description: "wait to get no-nil data") - // initiate download task which should pass back a 304 but still return non nil - // since the datafile was not in cache. handler.downloadDatafile(sdkKey: sdkKey) { (result) in - if case let .success(data) = result { XCTAssert(data != nil) expectation.fulfill() @@ -161,37 +96,15 @@ class DatafileHandlerTests: XCTestCase { } wait(for: [expectation], timeout: 3) - // finally remove the datafile when complete. - try? FileManager.default.removeItem(at: localUrl!) } - func testDatafileDownloadFailureWithNoCache() { - - - // default datafile handler - class InnerDatafileHandler : DefaultDatafileHandler { - var localFileUrl:URL? - // override getSession to return our own session. - override func getSession(resourceTimeoutInterval: Double?) -> URLSession { - - // will return error - let session = MockUrlSession(failureCode: 0, withError: true) - - return session - } - } - - // create test datafile handler - let handler = InnerDatafileHandler() - // remove the cached file just in case. + func testDatafileDownloadFailureWithNoCache() { + let handler = MockDatafileHandler(statusCode: 0, passError: true) handler.removeSavedDatafile(sdkKey: sdkKey) let expectation = XCTestExpectation(description: "wait to get nil data") - // initiate download task which should pass back a 304 but still return non nil - // since the datafile was not in cache. handler.downloadDatafile(sdkKey: sdkKey) { (result) in - if case .success(_) = result { XCTAssert(false) } @@ -204,38 +117,12 @@ class DatafileHandlerTests: XCTestCase { } func testDatafileDownload304NoCache() { - - var localUrl:URL? - - // create a dummy file at a url to use as or datafile cdn location - localUrl = OTUtils.saveAFile(name: "localcdn", data: Data()) - - // default datafile handler - class InnerDatafileHandler : DefaultDatafileHandler { - var localFileUrl:URL? - // override getSession to return our own session. - override func getSession(resourceTimeoutInterval: Double?) -> URLSession { - - let session = MockUrlSession(failureCode: 0, withError: false) - session.downloadCacheUrl = localFileUrl - - return session - } - } - - // create test datafile handler - let handler = InnerDatafileHandler() - //remove any cached datafile.. + let handler = MockDatafileHandler(statusCode: 0, passError: false) handler.removeSavedDatafile(sdkKey: sdkKey) - // set the url to use as our datafile download url - handler.localFileUrl = localUrl let expectation = XCTestExpectation(description: "wait to get no-nil data") - // initiate download task which should pass back a 304 but still return non nil - // since the datafile was not in cache. handler.downloadDatafile(sdkKey: sdkKey) { (result) in - if case let .success(data) = result { XCTAssert(data != nil) expectation.fulfill() @@ -243,42 +130,16 @@ class DatafileHandlerTests: XCTestCase { } wait(for: [expectation], timeout: 3) - // finally remove the datafile when complete. - try? FileManager.default.removeItem(at: localUrl!) } func testDatafileDownload304WithCache() { - - var fileUrl:URL? - - // create a dummy file at a url to use as our datafile local download location - fileUrl = OTUtils.saveAFile(name: "localcdn", data: Data()) - - // default datafile handler - class InnerDatafileHandler : DefaultDatafileHandler { - var localFileUrl:URL? - // override getSession to return our own session. - override func getSession(resourceTimeoutInterval: Double?) -> URLSession { + OTUtils.createDatafileCache(sdkKey: sdkKey) - let session = MockUrlSession(failureCode: 0, withError: false) - session.downloadCacheUrl = localFileUrl - - return session - } - } - - // create test datafile handler - let handler = InnerDatafileHandler() - //save the cached datafile.. - handler.saveDatafile(sdkKey: sdkKey, dataFile: "{}".data(using: .utf8)!) + let handler = MockDatafileHandler(statusCode: 0, passError: false) handler.sharedDataStore.setLastModified(sdkKey: sdkKey, lastModified: "1234") - // set the url to use as our datafile download url - handler.localFileUrl = fileUrl let expectation = XCTestExpectation(description: "wait to get no-nil data") - // initiate download task which should pass back a 304 but still return non nil - // since the datafile was not in cache. handler.downloadDatafile(sdkKey: sdkKey) { (result) in if case let .success(data) = result { // should come back as nil since got 304 and datafile in cache. @@ -295,13 +156,9 @@ class DatafileHandlerTests: XCTestCase { XCTAssert(data != nil) expectation2.fulfill() } - } - wait(for: [expectation, expectation2], timeout: 3) - // finally remove the datafile when complete. - try? FileManager.default.removeItem(at: fileUrl!) } func testSetPeriodicInterval() { @@ -321,17 +178,8 @@ class DatafileHandlerTests: XCTestCase { } func testPeriodicDownload() { - class FakeDatafileHandler: DefaultDatafileHandler { - let data = Data() - override func downloadDatafile(sdkKey: String, - returnCacheIfNoChange: Bool, - resourceTimeoutInterval: Double?, - completionHandler: @escaping DatafileDownloadCompletionHandler) { - completionHandler(.success(data)) - } - } let expection = XCTestExpectation(description: "Expect 10 periodic downloads") - let handler = FakeDatafileHandler() + let handler = MockDatafileHandler(statusCode: 200, passError: false) let now = Date() var count = 0 var seconds = 0 @@ -351,18 +199,8 @@ class DatafileHandlerTests: XCTestCase { } func testPeriodicDownload_PollingShouldNotBeAccumulatedWhileInBackground() { - class FakeDatafileHandler: DefaultDatafileHandler { - let data = Data() - override func downloadDatafile(sdkKey: String, - returnCacheIfNoChange: Bool, - resourceTimeoutInterval: Double?, - completionHandler: @escaping DatafileDownloadCompletionHandler) { - completionHandler(.success(data)) - } - } - let expectation = XCTestExpectation(description: "polling") - let handler = FakeDatafileHandler() + let handler = MockDatafileHandler(statusCode: 200, passError: false) let now = Date() let updateInterval = 1 @@ -425,19 +263,12 @@ class DatafileHandlerTests: XCTestCase { } func testPeriodicDownloadWithOptimizlyClient_SameRevision() { - class FakeDatafileHandler: DefaultDatafileHandler { - let data = OTUtils.loadJSONDatafile("typed_audience_datafile") - override func downloadDatafile(sdkKey: String, - returnCacheIfNoChange: Bool, - resourceTimeoutInterval: Double?, - completionHandler: @escaping DatafileDownloadCompletionHandler) { - completionHandler(.success(data)) - } - } let expection = XCTestExpectation(description: "Expect no notification") expection.isInverted = true - let handler = FakeDatafileHandler() + let handler = MockDatafileHandler(statusCode: 200, + passError: false, + localResponseData: OTUtils.loadJSONDatafileString("typed_audience_datafile")) let optimizely = OptimizelyClient(sdkKey: "testPeriodicDownloadWithOptimizlyClient_SameRevision", datafileHandler: handler, periodicDownloadInterval: 1) @@ -497,7 +328,6 @@ class DatafileHandlerTests: XCTestCase { // override getSession to return our own session. override func getSession(resourceTimeoutInterval: Double?) -> URLSession { var session = MockUrlSession(failureCode: 200, withError: false) - session.downloadCacheUrl = localFileUrl // will return 500 if let _ = resourceTimeoutInterval { session = MockUrlSession(failureCode: 408, withError: true) @@ -512,7 +342,6 @@ class DatafileHandlerTests: XCTestCase { let expectation = XCTestExpectation(description: "should fail before 10") handler.downloadDatafile(sdkKey: "invalidKey1212121", resourceTimeoutInterval: 3) { (result) in - if case let .failure(error) = result { print(error) XCTAssert(true) @@ -531,7 +360,6 @@ class DatafileHandlerTests: XCTestCase { let expectation = XCTestExpectation(description: "will wait for response.") handler.downloadDatafile(sdkKey: "invalidKeyXXXXX") { (result) in - if case let .success(data) = result { print(data ?? "") XCTAssert(true) diff --git a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift index 566af7a0..93538a32 100644 --- a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift @@ -37,28 +37,25 @@ class DatafileHandlerTests_MultiClients: XCTestCase { // MARK: - downloadDatafile func testConcurrentDownloadDatafiles() { + // use a shared DatafileHandler instance + let mockHandler = MockDatafileHandler(statusCode: 0, passError: false) + makeSdkKeys(100) let result = runConcurrent(for: sdkKeys, timeoutInSecs: 10) { sdkKey, _ in - - // NOTE: using multiple DatafileHandler instances - - let mockHandler = MockDatafileHandler(failureCode: 0, - passError: false, - sdkKey: sdkKey, - strData: sdkKey) - let group = DispatchGroup() group.enter() mockHandler.downloadDatafile(sdkKey: sdkKey, returnCacheIfNoChange: false, resourceTimeoutInterval: 10) { result in + let expectedDatafile = MockDatafileHandler.getDatafile(sdkKey: sdkKey) + switch result { case .success(let data): if let data = data { let str = String(data: data, encoding: .utf8) - XCTAssert(str == sdkKey) + XCTAssert(str == expectedDatafile) } else { XCTAssert(false) } @@ -79,19 +76,19 @@ class DatafileHandlerTests_MultiClients: XCTestCase { let numSdkKeys = 100 makeSdkKeys(numSdkKeys) + // set response code + error distribution + let num304 = 20 let num400 = 20 let numError = 20 let numSuccess = 40 XCTAssert(num304 + num400 + numError + numSuccess == numSdkKeys) - - let recv304 = AtomicProperty(property: 0) - let recvSuccess = AtomicProperty(property: 0) - - let result = runConcurrent(for: sdkKeys, timeoutInSecs: 10) { sdkKey, idx in + + var settingsMap = [String: (Int, Bool)]() + for (idx, sdkKey) in sdkKeys.enumerated() { var statusCode: Int = 0 var passError: Bool = false - + switch idx { case 0..(property: 0) + let recvSuccess = AtomicProperty(property: 0) + + let result = runConcurrent(for: sdkKeys, timeoutInSecs: 10) { sdkKey, idx in let group = DispatchGroup() group.enter() mockHandler.downloadDatafile(sdkKey: sdkKey, returnCacheIfNoChange: false, resourceTimeoutInterval: 10) { result in + let expectedDatafile = MockDatafileHandler.getDatafile(sdkKey: sdkKey) + switch result { case .success(let data): if let data = data { recvSuccess.performAtomic{ $0 += 1 } let str = String(data: data, encoding: .utf8) - XCTAssert(str == sdkKey) - } else if statusCode == 304 { + XCTAssert(str == expectedDatafile) + } else if settingsMap[sdkKey]!.0 == 304 { recv304.performAtomic{ $0 += 1 } } default: @@ -147,18 +151,14 @@ class DatafileHandlerTests_MultiClients: XCTestCase { } func testConcurrentAccessLastModified() { + // use a shared DatafileHandler instance + let mockHandler = MockDatafileHandler(statusCode: 0, passError: false) + makeSdkKeys(100) let result = runConcurrent(for: sdkKeys, timeoutInSecs: 10) { sdkKey, _ in - let expectedLastModified = "date-for-\(sdkKey)" + let expectedLastModified = MockDatafileHandler.getLastModified(sdkKey: sdkKey) - // NOTE: using multiple DatafileHandler instances - - let mockHandler = MockDatafileHandler(failureCode: 0, - passError: false, - sdkKey: sdkKey, - strData: sdkKey, - lastModified: expectedLastModified) let group = DispatchGroup() @@ -183,9 +183,6 @@ class DatafileHandlerTests_MultiClients: XCTestCase { makeSdkKeys(100) let result = runConcurrent(for: sdkKeys) { sdkKey, idx in - - // NOTE: using a single DatafileHandler instance - let maxCnt = 10 for _ in 0.. URLSession { - return MockUrlSession(failureCode: failureCode, withError: passError, localUrl: localUrl, lastModified: lastModified) + if let settingsMap = settingsMap { + return MockUrlSession(settingsMap: settingsMap) + } else { + return MockUrlSession(failureCode: statusCode, withError: passError, localResponseData: localResponseData) + } + } + + // MARK: - helpers + + static func getDatafile(sdkKey: String) -> String { + return "datafile-for-\(sdkKey)" + } + + static func getLastModified(sdkKey: String) -> String { + return "date-for-\(sdkKey)" } } diff --git a/Tests/TestUtils/MockUrlSession.swift b/Tests/TestUtils/MockUrlSession.swift index 6f19b2ff..dd34b773 100644 --- a/Tests/TestUtils/MockUrlSession.swift +++ b/Tests/TestUtils/MockUrlSession.swift @@ -23,10 +23,10 @@ import Foundation // the response also includes the url for the data download. // the cdn url is used to get the datafile if the datafile is not in cache class MockUrlSession: URLSession { - let failureCode: Int - let passError: Bool - var downloadCacheUrl: URL? - var lastModified: String? + var failureCode: Int + var passError: Bool + var localResponseData: String? + var settingsMap: [String: (Int, Bool)]? class MockDownloadTask: URLSessionDownloadTask { var task: () -> Void @@ -40,42 +40,50 @@ class MockUrlSession: URLSession { } } - init(failureCode: Int, withError: Bool) { + init(failureCode: Int = 0, withError: Bool = false, localResponseData: String? = nil) { self.failureCode = failureCode self.passError = withError + self.localResponseData = localResponseData } - - init(failureCode: Int, withError: Bool, localUrl: URL?, lastModified: String?) { - self.failureCode = failureCode - self.passError = withError - self.downloadCacheUrl = localUrl - self.lastModified = lastModified - } - - convenience override init() { - self.init(failureCode: 0, withError: false) + + init(settingsMap: [String: (Int, Bool)]) { + self.failureCode = 0 + self.passError = false + self.settingsMap = settingsMap } override func downloadTask(with request: URLRequest, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask { + var headers = [String: String]() + let sdkKey = request.url!.path.split(separator: "/").last!.replacingOccurrences(of: ".json", with: "") + + if let settings = settingsMap?[sdkKey] { + (failureCode, passError) = settings + } + + if localResponseData == nil { + let datafile = MockDatafileHandler.getDatafile(sdkKey: sdkKey) + let lastModifiedResponse = MockDatafileHandler.getLastModified(sdkKey: sdkKey) + + localResponseData = datafile + headers["Last-Modified"] = lastModifiedResponse + } + + // this filename should be different from sdkKey (to avoid conflict with datafile cache) + let fileName = "\(sdkKey)-for-response" + let downloadCacheUrl = OTUtils.saveAFile(name: fileName, data: localResponseData!.data(using: .utf8)!) return MockDownloadTask() { let statusCode = self.failureCode != 0 ? self.failureCode : (request.getLastModified() != nil ? 304 : 200) if (self.passError) { let error = OptimizelyError.datafileDownloadFailed("failure") - completionHandler(self.downloadCacheUrl, nil, error) - } - else { - var headers = [String: String]() - if let lastModified = self.lastModified { - headers["Last-Modified"] = lastModified - } - + completionHandler(downloadCacheUrl, nil, error) + } else { let response = HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: nil, headerFields: headers) - completionHandler(self.downloadCacheUrl, response, nil) + completionHandler(downloadCacheUrl, response, nil) } } diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index 80494231..fdde5cc4 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -106,6 +106,11 @@ class OTUtils { } } + static func loadJSONDatafileString(_ filename: String) -> String? { + guard let data = loadJSONDatafile(filename) else { return nil } + return String(bytes: data, encoding: .utf8) + } + static func loadJSONFile(_ filename: String) -> Data? { return loadJSONDatafile(filename) } @@ -182,20 +187,25 @@ class OTUtils { // MARK: - files - static func saveAFile(name:String, data:Data) -> URL? { + static func saveAFile(name: String, data: Data) -> URL? { let ds = DataStoreFile(storeName: name, async: false) ds.saveItem(forKey: name, value: data) return ds.url } - static func removeAFile(name:String) -> URL? { + static func removeAFile(name: String) -> URL? { let ds = DataStoreFile(storeName: name, async: false) ds.removeItem(forKey: name) return ds.url } + static func createDatafileCache(sdkKey: String, contents: String? = nil) { + let data = (contents ?? "datafile-for-\(sdkKey)").data(using: .utf8)! + _ = saveAFile(name: sdkKey, data: data) + } + static func clearAllTestStorage(including: String) { removeAllFiles(including: including) removeAllUserDefaults(including: including) From 826d40b08515759b8b40d8725bf166703b3700e5 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 27 Apr 2021 14:45:59 -0700 Subject: [PATCH 08/26] fix atomic return --- Sources/Utils/AtomicArray.swift | 6 +++--- Sources/Utils/AtomicDictionary.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Utils/AtomicArray.swift b/Sources/Utils/AtomicArray.swift index 02ec7ed7..5ec286bd 100644 --- a/Sources/Utils/AtomicArray.swift +++ b/Sources/Utils/AtomicArray.swift @@ -25,7 +25,7 @@ class AtomicArray: AtomicWrapper { subscript(index: Int) -> T? { get { - returnAtomic { + return getAtomic { _property[index] } } @@ -39,7 +39,7 @@ class AtomicArray: AtomicWrapper { } var count: Int { - returnAtomic { + return getAtomic { _property.count }! } @@ -65,7 +65,7 @@ class AtomicWrapper { return DispatchQueue(label: name, attributes: .concurrent) }() - func returnAtomic(_ action: () throws -> E?) -> E? { + func getAtomic(_ action: () throws -> E?) -> E? { var result: E? lock.sync { result = try? action() diff --git a/Sources/Utils/AtomicDictionary.swift b/Sources/Utils/AtomicDictionary.swift index 4ab687b6..6833507e 100644 --- a/Sources/Utils/AtomicDictionary.swift +++ b/Sources/Utils/AtomicDictionary.swift @@ -25,7 +25,7 @@ class AtomicDictionary: AtomicWrapper where K: Hashable { subscript(key: K) -> V? { get { - returnAtomic { + return getAtomic { _property[key] } } @@ -37,7 +37,7 @@ class AtomicDictionary: AtomicWrapper where K: Hashable { } var count: Int { - returnAtomic { + return getAtomic { _property.count }! } From 77fb808c3edd4415ed901ae5c2261c76764fe690 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 27 Apr 2021 14:48:21 -0700 Subject: [PATCH 09/26] clean up --- Sources/Utils/AtomicArray.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Utils/AtomicArray.swift b/Sources/Utils/AtomicArray.swift index 5ec286bd..af148ff0 100644 --- a/Sources/Utils/AtomicArray.swift +++ b/Sources/Utils/AtomicArray.swift @@ -61,7 +61,7 @@ class AtomicArray: AtomicWrapper { class AtomicWrapper { var lock: DispatchQueue = { - let name = "AtomicCollection" + String(Int.random(in: 0..<100000)) + let name = "AtomicWrapper" + String(Int.random(in: 0..<100000)) return DispatchQueue(label: name, attributes: .concurrent) }() From d6d9fac7ad854a3b251f3c522cadd6ad37035e46 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 27 Apr 2021 17:16:45 -0700 Subject: [PATCH 10/26] clean up --- Tests/OptimizelyTests-Common/DatafileHandlerTests.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift b/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift index bfc817cb..669d01eb 100644 --- a/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift +++ b/Tests/OptimizelyTests-Common/DatafileHandlerTests.swift @@ -229,7 +229,7 @@ class DatafileHandlerTests: XCTestCase { } func testPeriodicDownload_PollingPeriodAdjustedByDelay() { - class FakeDatafileHandler: DefaultDatafileHandler { + class LocalDatafileHandler: DefaultDatafileHandler { let data = Data() override func downloadDatafile(sdkKey: String, returnCacheIfNoChange: Bool, @@ -241,7 +241,7 @@ class DatafileHandlerTests: XCTestCase { } let expectation = XCTestExpectation(description: "polling") - let handler = FakeDatafileHandler() + let handler = LocalDatafileHandler() let now = Date() let updateInterval = 2 @@ -286,7 +286,7 @@ class DatafileHandlerTests: XCTestCase { } func testPeriodicDownloadWithOptimizlyClient_DifferentRevision() { - class FakeDatafileHandler: DefaultDatafileHandler { + class LocalDatafileHandler: DefaultDatafileHandler { let data1 = OTUtils.loadJSONDatafile("typed_audience_datafile") let data2 = OTUtils.loadJSONDatafile("api_datafile") var flag = false @@ -300,8 +300,9 @@ class DatafileHandlerTests: XCTestCase { flag.toggle() } } + let expection = XCTestExpectation(description: "Expect 10 periodic downloads") - let handler = FakeDatafileHandler() + let handler = LocalDatafileHandler() let optimizely = OptimizelyClient(sdkKey: "testPeriodicDownloadWithOptimizlyClient_DifferentRevision", datafileHandler: handler, periodicDownloadInterval: 1) From e8523480a47c1505e75772b90fa7aecbbeb5e220 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Wed, 28 Apr 2021 11:25:23 -0700 Subject: [PATCH 11/26] fix per review --- .../AtomicArrayTests.swift | 32 +++++++++++++++++++ .../AtomicDictionaryTests.swift | 21 ++++++++++++ .../DatafileHandlerTests_MultiClients.swift | 29 +++-------------- Tests/TestUtils/OTUtils.swift | 32 +++++++++++++++++++ 4 files changed, 90 insertions(+), 24 deletions(-) diff --git a/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift b/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift index 986a03af..b356f0a3 100644 --- a/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift +++ b/Tests/OptimizelyTests-MultiClients/AtomicArrayTests.swift @@ -45,4 +45,36 @@ class AtomicArrayTests: XCTestCase { XCTAssert(b[8] == 30) } + func testConcurrentReadWrite() { + let num = 10000 + let a = AtomicArray() + (0..() + + let numConcurrency = 100 + let numIterations = 10000 + let result = OTUtils.runConcurrent(count: numConcurrency) { idx in + (0..() + + let numConcurrency = 100 + let numIterations = 10000 + let result = OTUtils.runConcurrent(count: numConcurrency) { idx in + for i in 0..(property: 0) let recvSuccess = AtomicProperty(property: 0) - let result = runConcurrent(for: sdkKeys, timeoutInSecs: 10) { sdkKey, idx in + let result = OTUtils.runConcurrent(for: sdkKeys, timeoutInSecs: 10) { idx, sdkKey in let group = DispatchGroup() group.enter() @@ -156,10 +156,9 @@ class DatafileHandlerTests_MultiClients: XCTestCase { makeSdkKeys(100) - let result = runConcurrent(for: sdkKeys, timeoutInSecs: 10) { sdkKey, _ in + let result = OTUtils.runConcurrent(for: sdkKeys, timeoutInSecs: 10) { _, sdkKey in let expectedLastModified = MockDatafileHandler.getLastModified(sdkKey: sdkKey) - let group = DispatchGroup() group.enter() @@ -182,7 +181,7 @@ class DatafileHandlerTests_MultiClients: XCTestCase { func testConcurrentAccessDatafileCaches() { makeSdkKeys(100) - let result = runConcurrent(for: sdkKeys) { sdkKey, idx in + let result = OTUtils.runConcurrent(for: sdkKeys) { idx, sdkKey in let maxCnt = 10 for _ in 0.. Void) -> Bool { - let group = DispatchGroup() - - for (idx, item) in items.enumerated() { - group.enter() - - // NOTE: do not use DispatchQueue.global(), which looks like a deadlock because of too many threads - DispatchQueue(label: item).async { - task(item, idx) - group.leave() - } - } - - let timeout = DispatchTime.now() + .seconds(timeoutInSecs) - let result = group.wait(timeout: timeout) - return result == .success - } - func makeSdkKeys(_ num: Int) { sdkKeys = [] for i in 0.. Void) -> Bool { + let group = DispatchGroup() + + for (idx, item) in items.enumerated() { + group.enter() + + // NOTE: do not use DispatchQueue.global(), which looks like a deadlock because of too many threads + DispatchQueue(label: item).async { + task(idx, item) + group.leave() + } + } + + let timeout = DispatchTime.now() + .seconds(timeoutInSecs) + let result = group.wait(timeout: timeout) + return result == .success + } + + static func runConcurrent(count: Int, + timeoutInSecs: Int = 10, + task: @escaping (Int) -> Void) -> Bool { + let items = (0.. Date: Tue, 27 Apr 2021 17:43:24 -0700 Subject: [PATCH 12/26] clean up --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 30 ++++++++++++++ .../OptimizelyClientTests_Evaluation.swift | 4 +- .../OptimizelyClientTests_Group.swift | 2 +- .../OptimizelyClientTests_Others.swift | 2 +- .../BatchEventBuilderTests_Attributes.swift | 10 ++--- .../BatchEventBuilderTests_EventTags.swift | 6 +-- .../BatchEventBuilderTests_Events.swift | 14 +++---- .../DecisionListenerTests.swift | 2 +- .../EventDispatcherTests.swift | 6 +-- .../OptimizelyUserContextTests_Decide.swift | 33 ++++++---------- .../EventDispatcherTests_MultiClients.swift | 39 +++++++++++++++++++ Tests/TestUtils/MockEventDispatcher.swift | 33 ++++++++++++++++ Tests/TestUtils/OTUtils.swift | 16 -------- 13 files changed, 136 insertions(+), 61 deletions(-) create mode 100644 Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift create mode 100644 Tests/TestUtils/MockEventDispatcher.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 5a3dc500..64332984 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -543,6 +543,19 @@ 6E593FB625BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */; }; 6E5AB69323F6130D007A82B1 /* OptimizelyClientTests_Init_Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */; }; 6E5AB69423F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */; }; + 6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D120C2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift */; }; + 6E5D121F2638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12202638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12212638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12222638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12232638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12242638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12252638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12262638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12272638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12282638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D12292638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; + 6E5D122A2638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */; }; 6E614DD621E3F38A005982A1 /* Optimizely.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E614DCD21E3F389005982A1 /* Optimizely.framework */; }; 6E623F02253F9045000617D0 /* DecisionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E623F01253F9045000617D0 /* DecisionInfo.swift */; }; 6E623F03253F9045000617D0 /* DecisionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E623F01253F9045000617D0 /* DecisionInfo.swift */; }; @@ -1828,6 +1841,8 @@ 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Decide.swift; sourceTree = ""; }; 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Sync.swift; sourceTree = ""; }; 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Async.swift; sourceTree = ""; }; + 6E5D120C2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDispatcherTests_MultiClients.swift; sourceTree = ""; }; + 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEventDispatcher.swift; sourceTree = ""; }; 6E614DCD21E3F389005982A1 /* Optimizely.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Optimizely.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6E614DD521E3F38A005982A1 /* OptimizelyTests-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6E623F01253F9045000617D0 /* DecisionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionInfo.swift; sourceTree = ""; }; @@ -2214,6 +2229,7 @@ 6EA641F2262F4E6900E29532 /* DatafileHandlerTests_MultiClients.swift */, 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */, 6E2D5DAD26338CA00002077F /* AtomicDictionaryTests.swift */, + 6E5D120C2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift */, ); path = "OptimizelyTests-MultiClients"; sourceTree = ""; @@ -2566,6 +2582,7 @@ 6E7519B822C5211100B2B157 /* OTUtils.swift */, 6E8A3D472637408500DAEA13 /* MockDatafileHandler.swift */, 6E7519B722C5211100B2B157 /* MockUrlSession.swift */, + 6E5D121E2638DDF4000ABFC3 /* MockEventDispatcher.swift */, ); path = TestUtils; sourceTree = ""; @@ -3615,6 +3632,7 @@ C78CAF5C2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E14CDA52423F9C300010234 /* MurmurHash3.swift in Sources */, 6E86CEA824FDC847005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, + 6E5D12222638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E14CD912423F9A700010234 /* TrafficAllocation.swift in Sources */, 6E14CD6F2423F93E00010234 /* OptimizelyError.swift in Sources */, 6EC6DD3624ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, @@ -3668,6 +3686,7 @@ 6E424D2E26324BBA0081004A /* MockUrlSession.swift in Sources */, 6E424CF326324B620081004A /* OPTDatafileHandler.swift in Sources */, 6E424CF426324B620081004A /* DecisionInfo.swift in Sources */, + 6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */, 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */, 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */, 6E424CF626324B620081004A /* DefaultNotificationCenter.swift in Sources */, @@ -3734,6 +3753,7 @@ 6E424CBE26324B1D0081004A /* AtomicProperty.swift in Sources */, 6E424CBF26324B1D0081004A /* AtomicArray.swift in Sources */, 6E424CC026324B1D0081004A /* AtomicDictionary.swift in Sources */, + 6E5D12212638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E424CC126324B1D0081004A /* Utils.swift in Sources */, 6E424CC226324B1D0081004A /* SDKVersion.swift in Sources */, 6E424C88263249B80081004A /* DatafileHandlerTests_MultiClients.swift in Sources */, @@ -3865,6 +3885,7 @@ C78CAF612445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E7516D222C520D400B2B157 /* OPTLogger.swift in Sources */, 6E86CEAC24FDC849005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, + 6E5D12272638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75186022C520D400B2B157 /* FeatureVariable.swift in Sources */, 6E7517E822C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6EC6DD3B24ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, @@ -3959,6 +3980,7 @@ 6E75183822C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E75175222C520D400B2B157 /* LogMessage.swift in Sources */, 6E9B11DB22C548A200C22D81 /* OptimizelyClientTests_Variables.swift in Sources */, + 6E5D12232638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6ECB60C6234D329500016D41 /* OptimizelyClientTests_OptimizelyConfig.swift in Sources */, 6EA2CC282345618E001E7531 /* OptimizelyConfig.swift in Sources */, 6E75189822C520D400B2B157 /* Experiment.swift in Sources */, @@ -4072,6 +4094,7 @@ 6E424C06263228FD0081004A /* AtomicDictionary.swift in Sources */, 6E75174922C520D400B2B157 /* HandlerRegistryService.swift in Sources */, 6E7516F522C520D400B2B157 /* OptimizelyError.swift in Sources */, + 6E5D12262638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75188322C520D400B2B157 /* TrafficAllocation.swift in Sources */, 6E86CEAB24FDC849005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, 6E7517CF22C520D400B2B157 /* DefaultBucketer.swift in Sources */, @@ -4117,6 +4140,7 @@ 6E75170F22C520D400B2B157 /* OptimizelyClient.swift in Sources */, 6EC6DD4C24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E7517E922C520D400B2B157 /* DefaultDecisionService.swift in Sources */, + 6E5D12282638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E9B116A22C5487100C22D81 /* BucketTests_Base.swift in Sources */, 6E9B115F22C5487100C22D81 /* MurmurTests.swift in Sources */, 6E9B116022C5487100C22D81 /* DecisionServiceTests_Experiments.swift in Sources */, @@ -4254,6 +4278,7 @@ 6E7516E022C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E34A6212319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B119D22C5488300C22D81 /* UserAttributeTests.swift in Sources */, + 6E5D12292638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75183E22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B11A022C5488300C22D81 /* ExperimentTests.swift in Sources */, 6E7516EC22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, @@ -4338,6 +4363,7 @@ 6E75178122C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, 6EC6DD4524ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E7517CB22C520D400B2B157 /* DefaultBucketer.swift in Sources */, + 6E5D12202638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E9B115022C5486E00C22D81 /* BucketTests_Base.swift in Sources */, 6E9B114522C5486E00C22D81 /* MurmurTests.swift in Sources */, 6E9B114622C5486E00C22D81 /* DecisionServiceTests_Experiments.swift in Sources */, @@ -4475,6 +4501,7 @@ 6E7516DB22C520D400B2B157 /* OPTUserProfileService.swift in Sources */, 6E34A61C2319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B118722C5488100C22D81 /* UserAttributeTests.swift in Sources */, + 6E5D12242638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E75183922C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B118A22C5488100C22D81 /* ExperimentTests.swift in Sources */, 6E7516E722C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, @@ -4556,6 +4583,7 @@ 6E424C05263228FD0081004A /* AtomicDictionary.swift in Sources */, 6EC6DD4924ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75188222C520D400B2B157 /* TrafficAllocation.swift in Sources */, + 6E5D12252638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6ECB60D0234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E9B11E322C548AF00C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176022C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -4641,6 +4669,7 @@ 6E424C0A263228FD0081004A /* AtomicDictionary.swift in Sources */, 6EC6DD4E24ABF89B0017D296 /* OptimizelyUserContext.swift in Sources */, 6E75188722C520D400B2B157 /* TrafficAllocation.swift in Sources */, + 6E5D122A2638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6ECB60D5234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E9B11E522C548B100C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176522C520D400B2B157 /* AtomicProperty.swift in Sources */, @@ -4827,6 +4856,7 @@ C78CAF5A2445AD8D009FE876 /* OptimizelyJSON.swift in Sources */, 6E75194A22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E86CEA724FDC846005DAFED /* OptimizelyUserContext+ObjC.swift in Sources */, + 6E5D121F2638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, 6E7516CC22C520D400B2B157 /* OPTLogger.swift in Sources */, 6E75185A22C520D400B2B157 /* FeatureVariable.swift in Sources */, 6EC6DD3424ABF6990017D296 /* OptimizelyClient+Decide.swift in Sources */, diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift index 8a934ba6..bd1e3701 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Evaluation.swift @@ -22,7 +22,7 @@ class OptimizelyClientTests_Evaluation: XCTestCase { var datafile: Data? var optimizely: OptimizelyClient? - var eventDispatcher: FakeEventDispatcher? + var eventDispatcher: MockEventDispatcher? // MARK: - Attribute Value Range @@ -210,7 +210,7 @@ class OptimizelyClientTests_Evaluation: XCTestCase { } func testActivateDispatchWithAttributeValues() { - let eventDispatcher = FakeEventDispatcher() + let eventDispatcher = MockEventDispatcher() let optimizely = OTUtils.createOptimizely(datafileName: "audience_targeting", clearUserProfileService: true, eventDispatcher: eventDispatcher)! diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Group.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Group.swift index f0b5ce12..6c0b6af8 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Group.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Group.swift @@ -23,7 +23,7 @@ class OptimizelyClientTests_Group: XCTestCase { var datafile: Data? var optimizely: OptimizelyClient? - var eventDispatcher: FakeEventDispatcher? + var eventDispatcher: MockEventDispatcher? // MARK: - Attribute Value Range diff --git a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Others.swift b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Others.swift index 40951a93..69023128 100644 --- a/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Others.swift +++ b/Tests/OptimizelyTests-APIs/OptimizelyClientTests_Others.swift @@ -39,7 +39,7 @@ class OptimizelyClientTests_Others: XCTestCase { let kUserId = "user" var optimizely: OptimizelyClient! - let eventDispatcher = FakeEventDispatcher() + let eventDispatcher = MockEventDispatcher() override func setUp() { super.setUp() diff --git a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Attributes.swift b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Attributes.swift index b3913aee..758c7dbf 100644 --- a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Attributes.swift +++ b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Attributes.swift @@ -24,13 +24,13 @@ class BatchEventBuilderTests_Attributes: XCTestCase { let userId = "test_user_1" var optimizely: OptimizelyClient! - var eventDispatcher: FakeEventDispatcher! + var eventDispatcher: MockEventDispatcher! var project: Project! // MARK: - setup override func setUp() { - eventDispatcher = FakeEventDispatcher() + eventDispatcher = MockEventDispatcher() optimizely = OTUtils.createOptimizely(datafileName: "audience_targeting", clearUserProfileService: true, @@ -262,7 +262,7 @@ class BatchEventBuilderTests_Attributes: XCTestCase { extension BatchEventBuilderTests_Attributes { func testBotFilteringWhenTrue() { - eventDispatcher = FakeEventDispatcher() + eventDispatcher = MockEventDispatcher() optimizely = OTUtils.createOptimizely(datafileName: "bot_filtering_enabled", clearUserProfileService: true, eventDispatcher: eventDispatcher) @@ -281,7 +281,7 @@ extension BatchEventBuilderTests_Attributes { } func testBotFilteringWhenFalse() { - eventDispatcher = FakeEventDispatcher() + eventDispatcher = MockEventDispatcher() optimizely = OTUtils.createOptimizely(datafileName: "bot_filtering_enabled", clearUserProfileService: true, eventDispatcher: eventDispatcher) @@ -302,7 +302,7 @@ extension BatchEventBuilderTests_Attributes { } func testBotFilteringWhenNil() { - eventDispatcher = FakeEventDispatcher() + eventDispatcher = MockEventDispatcher() optimizely = OTUtils.createOptimizely(datafileName: "bot_filtering_enabled", clearUserProfileService: true, eventDispatcher: eventDispatcher) diff --git a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_EventTags.swift b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_EventTags.swift index 80b212a2..da168522 100644 --- a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_EventTags.swift +++ b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_EventTags.swift @@ -22,11 +22,11 @@ class BatchEventBuilderTests_EventTags: XCTestCase { let eventKey = "event_single_targeted_exp" var optimizely: OptimizelyClient! - var eventDispatcher: FakeEventDispatcher! + var eventDispatcher: MockEventDispatcher! var project: Project! override func setUp() { - eventDispatcher = FakeEventDispatcher() + eventDispatcher = MockEventDispatcher() optimizely = OTUtils.createOptimizely(datafileName: "audience_targeting", clearUserProfileService: true, @@ -322,7 +322,7 @@ extension BatchEventBuilderTests_EventTags { extension BatchEventBuilderTests_EventTags { - func getDispatchEvent(dispatcher: FakeEventDispatcher) -> [String: Any]? { + func getDispatchEvent(dispatcher: MockEventDispatcher) -> [String: Any]? { optimizely.eventLock.sync{} let eventForDispatch = dispatcher.events.first! diff --git a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Events.swift b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Events.swift index 8822144b..c1b6d036 100644 --- a/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Events.swift +++ b/Tests/OptimizelyTests-Common/BatchEventBuilderTests_Events.swift @@ -23,12 +23,12 @@ class BatchEventBuilderTests_Events: XCTestCase { let featureKey = "feature_1" var optimizely: OptimizelyClient! - var eventDispatcher: FakeEventDispatcher! + var eventDispatcher: MockEventDispatcher! var project: Project! let datafile = OTUtils.loadJSONDatafile("api_datafile")! override func setUp() { - eventDispatcher = FakeEventDispatcher() + eventDispatcher = MockEventDispatcher() optimizely = OTUtils.createOptimizely(datafileName: "audience_targeting", clearUserProfileService: true, eventDispatcher: eventDispatcher)! @@ -222,7 +222,7 @@ class BatchEventBuilderTests_Events: XCTestCase { extension BatchEventBuilderTests_Events { func testImpressionEventWithUserNotInExperimentAndRollout() { - let eventDispatcher2 = FakeEventDispatcher() + let eventDispatcher2 = MockEventDispatcher() let fakeOptimizelyManager = FakeManager(sdkKey: "12345", eventDispatcher: eventDispatcher2) try! fakeOptimizelyManager.start(datafile: datafile) @@ -252,7 +252,7 @@ extension BatchEventBuilderTests_Events { } func testImpressionEventWithWithUserInRollout() { - let eventDispatcher2 = FakeEventDispatcher() + let eventDispatcher2 = MockEventDispatcher() let fakeOptimizelyManager = FakeManager(sdkKey: "12345", eventDispatcher: eventDispatcher2) try! fakeOptimizelyManager.start(datafile: datafile) @@ -287,7 +287,7 @@ extension BatchEventBuilderTests_Events { } func testImpressionEventWithUserInExperiment() { - let eventDispatcher2 = FakeEventDispatcher() + let eventDispatcher2 = MockEventDispatcher() let fakeOptimizelyManager = FakeManager(sdkKey: "12345", eventDispatcher: eventDispatcher2) try! fakeOptimizelyManager.start(datafile: datafile) @@ -326,12 +326,12 @@ extension BatchEventBuilderTests_Events { extension BatchEventBuilderTests_Events { - func getFirstEvent(dispatcher: FakeEventDispatcher) -> EventForDispatch? { + func getFirstEvent(dispatcher: MockEventDispatcher) -> EventForDispatch? { optimizely.eventLock.sync{} return dispatcher.events.first } - func getFirstEventJSON(dispatcher: FakeEventDispatcher) -> [String: Any]? { + func getFirstEventJSON(dispatcher: MockEventDispatcher) -> [String: Any]? { guard let event = getFirstEvent(dispatcher: dispatcher) else { return nil } let json = try! JSONSerialization.jsonObject(with: event.body, options: .allowFragments) as! [String: Any] diff --git a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift index 1ba6edfc..db09dd9c 100644 --- a/Tests/OptimizelyTests-Common/DecisionListenerTests.swift +++ b/Tests/OptimizelyTests-Common/DecisionListenerTests.swift @@ -38,7 +38,7 @@ class DecisionListenerTests: XCTestCase { var datafile: Data! var optimizely: FakeManager! - let eventDispatcher = FakeEventDispatcher() + let eventDispatcher = MockEventDispatcher() var notificationCenter: OPTNotificationCenter! // MARK: - SetUp diff --git a/Tests/OptimizelyTests-Common/EventDispatcherTests.swift b/Tests/OptimizelyTests-Common/EventDispatcherTests.swift index c2f29ac0..855b64db 100644 --- a/Tests/OptimizelyTests-Common/EventDispatcherTests.swift +++ b/Tests/OptimizelyTests-Common/EventDispatcherTests.swift @@ -188,7 +188,7 @@ class EventDispatcherTests: XCTestCase { } func testDispatcherCustom() { - let dispatcher = FakeEventDispatcher() + let dispatcher = MockEventDispatcher() dispatcher.dispatchEvent(event: EventForDispatch(body: Data())) { (_) -> Void in @@ -282,7 +282,7 @@ class EventDispatcherTests: XCTestCase { } func testEventQueueFormatCompatibilty() { - class MockEventDispatcher: DefaultEventDispatcher { + class LocalEventDispatcher: DefaultEventDispatcher { override func sendEvent(event: EventForDispatch, completionHandler: @escaping DispatchCompletionHandler) { completionHandler(.success(Data())) } @@ -308,7 +308,7 @@ class EventDispatcherTests: XCTestCase { // verify that a new dataStore can read an existing queue items - let dispatcher = MockEventDispatcher() + let dispatcher = LocalEventDispatcher() XCTAssert(dispatcher.dataStore.count == 2) dispatcher.flushEvents() diff --git a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide.swift b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide.swift index 920b3e7c..411152f7 100644 --- a/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide.swift +++ b/Tests/OptimizelyTests-Common/OptimizelyUserContextTests_Decide.swift @@ -123,14 +123,14 @@ extension OptimizelyUserContextTests_Decide { XCTAssertEqual(decision.variationKey, "variation_with_traffic") XCTAssertTrue(decision.enabled) - XCTAssertNotNil(eventDispatcher.eventSent) + XCTAssertFalse(eventDispatcher.events.isEmpty) - let eventSent = eventDispatcher.eventSent! + let eventSent = eventDispatcher.events.first! let event = try! JSONDecoder().decode(BatchEvent.self, from: eventSent.body) let eventDecision: Decision = event.visitors[0].snapshots[0].decisions![0] let metadata = eventDecision.metaData - let desc = eventDispatcher.eventSent!.description + let desc = eventSent.description XCTAssert(desc.contains("campaign_activated")) XCTAssertEqual(eventDecision.experimentID, "10420810910") @@ -151,14 +151,14 @@ extension OptimizelyUserContextTests_Decide { optimizely.eventLock.sync{} - XCTAssertNotNil(eventDispatcher.eventSent) + XCTAssertFalse(eventDispatcher.events.isEmpty) - let eventSent = eventDispatcher.eventSent! + let eventSent = eventDispatcher.events.first! let event = try! JSONDecoder().decode(BatchEvent.self, from: eventSent.body) let eventDecision: Decision = event.visitors[0].snapshots[0].decisions![0] let metadata = eventDecision.metaData - let desc = eventDispatcher.eventSent!.description + let desc = eventSent.description XCTAssert(desc.contains("campaign_activated")) XCTAssertEqual(eventDecision.variationID, "") @@ -181,7 +181,7 @@ extension OptimizelyUserContextTests_Decide { XCTAssertNil(decision.variationKey) XCTAssertFalse(decision.enabled) - XCTAssertNil(eventDispatcher.eventSent) + XCTAssert(eventDispatcher.events.isEmpty) } // sendFlagDecisions = false @@ -196,7 +196,7 @@ extension OptimizelyUserContextTests_Decide { optimizely.eventLock.sync{} - XCTAssertNotNil(eventDispatcher.eventSent) + XCTAssertFalse(eventDispatcher.events.isEmpty) } func testDecide_shouldNotSendImpressionForRollout_withSendFlagDecisionsOff() { @@ -208,7 +208,7 @@ extension OptimizelyUserContextTests_Decide { optimizely.eventLock.sync{} - XCTAssertNil(eventDispatcher.eventSent) + XCTAssert(eventDispatcher.events.isEmpty) } } @@ -351,7 +351,7 @@ extension OptimizelyUserContextTests_Decide { optimizely.eventLock.sync{} XCTAssertTrue(decision.enabled) - XCTAssertNil(eventDispatcher.eventSent) + XCTAssert(eventDispatcher.events.isEmpty) } func testDecideOptions_useUPSbyDefault() { @@ -581,15 +581,4 @@ extension OptimizelyUserContextTests_Decide { decisionService.userProfileService.save(userProfile: profile) } } - -class MockEventDispatcher: OPTEventDispatcher { - var eventSent: EventForDispatch? - - func dispatchEvent(event: EventForDispatch, completionHandler: DispatchCompletionHandler?) { - eventSent = event - } - - func flushEvents() { - - } -} + diff --git a/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift new file mode 100644 index 00000000..6cae7e6a --- /dev/null +++ b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift @@ -0,0 +1,39 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class EventDispatcherTests_MultiClients: XCTestCase { + + override func setUpWithError() throws { + } + + override func tearDownWithError() throws { + } + + func testConcurrentDispatchEvents() { + + } + + func testConcurrentFlushEvents() { + + } + + func testConcurrentSendEvents() { + + } + +} diff --git a/Tests/TestUtils/MockEventDispatcher.swift b/Tests/TestUtils/MockEventDispatcher.swift new file mode 100644 index 00000000..d2756758 --- /dev/null +++ b/Tests/TestUtils/MockEventDispatcher.swift @@ -0,0 +1,33 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class MockEventDispatcher: OPTEventDispatcher { + public var events = [EventForDispatch]() + + required init() {} + + func dispatchEvent(event: EventForDispatch, completionHandler: DispatchCompletionHandler?) { + events.append(event) + } + + /// Attempts to flush the event queue if there are any events to process. + func flushEvents() { + events.removeAll() + } + +} diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index 3a0bf6c4..d67bc0ac 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -315,19 +315,3 @@ class OTUtils { } -class FakeEventDispatcher: OPTEventDispatcher { - - public var events: [EventForDispatch] = [EventForDispatch]() - required init() { - - } - - func dispatchEvent(event: EventForDispatch, completionHandler: DispatchCompletionHandler?) { - events.append(event) - } - - /// Attempts to flush the event queue if there are any events to process. - func flushEvents() { - events.removeAll() - } -} From 5425df31738411ccac142e41edc29550b64f4701 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Tue, 27 Apr 2021 17:59:49 -0700 Subject: [PATCH 13/26] clean up --- .../EventDispatcherTests.swift | 76 +++++-------------- Tests/TestUtils/OTUtils.swift | 8 +- 2 files changed, 26 insertions(+), 58 deletions(-) diff --git a/Tests/OptimizelyTests-Common/EventDispatcherTests.swift b/Tests/OptimizelyTests-Common/EventDispatcherTests.swift index 855b64db..94265798 100644 --- a/Tests/OptimizelyTests-Common/EventDispatcherTests.swift +++ b/Tests/OptimizelyTests-Common/EventDispatcherTests.swift @@ -21,35 +21,14 @@ class EventDispatcherTests: XCTestCase { var eventDispatcher: DefaultEventDispatcher? override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - #if os(tvOS) - let directory = FileManager.SearchPathDirectory.cachesDirectory - #else - let directory = FileManager.SearchPathDirectory.documentDirectory - #endif - - if let url = FileManager.default.urls(for: directory, in: .userDomainMask).first { - if (!FileManager.default.fileExists(atPath: url.path)) { - do { - try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) - } catch { - print(error) - } - - } - } + OTUtils.createDocumentDirectoryIfNotAvailable() } override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. if let dispatcher = eventDispatcher { dispatcher.flushEvents() - dispatcher.dispatcher.sync { - } + dispatcher.dispatcher.sync {} } - - eventDispatcher = nil - } func testDefaultDispatcher() { @@ -58,19 +37,16 @@ class EventDispatcherTests: XCTestCase { pEventD.flushEvents() - eventDispatcher?.dispatcher.sync { - } + eventDispatcher?.dispatcher.sync {} pEventD.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) - eventDispatcher?.dispatcher.sync { - } + eventDispatcher?.dispatcher.sync {} XCTAssert(eventDispatcher?.dataStore.count == 1) eventDispatcher?.flushEvents() - eventDispatcher?.dispatcher.sync { - } + eventDispatcher?.dispatcher.sync {} XCTAssert(eventDispatcher?.dataStore.count == 0) @@ -98,8 +74,7 @@ class EventDispatcherTests: XCTestCase { dispatcher.dataStore.save(item: EventForDispatch(body: Data())) dispatcher.flushEvents() - dispatcher.dispatcher.sync { - } + dispatcher.dispatcher.sync {} XCTAssert(dispatcher.events.count == 2) } @@ -109,16 +84,13 @@ class EventDispatcherTests: XCTestCase { let pEventD: OPTEventDispatcher = eventDispatcher! eventDispatcher?.timerInterval = 1 let wait = {() in - self.eventDispatcher?.dispatcher.sync { - } + self.eventDispatcher?.dispatcher.sync {} } pEventD.flushEvents() wait() - pEventD.dispatchEvent(event: EventForDispatch(body: Data())) { (_) -> Void in - - } + pEventD.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) wait() XCTAssert(eventDispatcher?.dataStore.count == 1) @@ -136,17 +108,14 @@ class EventDispatcherTests: XCTestCase { eventDispatcher = DefaultEventDispatcher( backingStore: .userDefaults) let pEventD: OPTEventDispatcher = eventDispatcher! eventDispatcher?.timerInterval = 1 - let wait = {() in - self.eventDispatcher?.dispatcher.sync { - } + let wait = { + self.eventDispatcher?.dispatcher.sync {} } pEventD.flushEvents() wait() - pEventD.dispatchEvent(event: EventForDispatch(body: Data())) { (_) -> Void in - - } + pEventD.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) wait() XCTAssert(eventDispatcher?.dataStore.count == 1) @@ -164,16 +133,14 @@ class EventDispatcherTests: XCTestCase { eventDispatcher = DefaultEventDispatcher( backingStore: .memory) let pEventD: OPTEventDispatcher = eventDispatcher! eventDispatcher?.timerInterval = 1 - let wait = {() in - self.eventDispatcher?.dispatcher.sync { - } + let wait = { + self.eventDispatcher?.dispatcher.sync {} } pEventD.flushEvents() wait() - pEventD.dispatchEvent(event: EventForDispatch(body: Data())) { (_) -> Void in - } + pEventD.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) wait() XCTAssert(eventDispatcher?.dataStore.count == 1) @@ -190,9 +157,7 @@ class EventDispatcherTests: XCTestCase { func testDispatcherCustom() { let dispatcher = MockEventDispatcher() - dispatcher.dispatchEvent(event: EventForDispatch(body: Data())) { (_) -> Void in - - } + dispatcher.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) XCTAssert(dispatcher.events.count == 1) @@ -205,14 +170,11 @@ class EventDispatcherTests: XCTestCase { eventDispatcher = DefaultEventDispatcher(timerInterval: 1) eventDispatcher?.flushEvents() - eventDispatcher?.dispatcher.sync { - } + eventDispatcher?.dispatcher.sync {} - eventDispatcher?.dispatchEvent(event: EventForDispatch(body: Data())) { (_) -> Void in - } + eventDispatcher?.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) - eventDispatcher?.dispatcher.sync { - } + eventDispatcher?.dispatcher.sync {} eventDispatcher?.applicationDidBecomeActive() eventDispatcher?.applicationDidEnterBackground() @@ -224,7 +186,7 @@ class EventDispatcherTests: XCTestCase { group.enter() - eventDispatcher?.sendEvent(event: EventForDispatch(body: Data())) { (_) -> Void in + eventDispatcher?.sendEvent(event: EventForDispatch(body: Data())) { _ in sent = true group.leave() } diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index d67bc0ac..a5d5ebca 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -245,7 +245,13 @@ class OTUtils { static func createDocumentDirectoryIfNotAvailable() { // documentDirectory may not exist for simulator unit test (iOS11+). create it if not found. - if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + #if os(tvOS) + let directory = FileManager.SearchPathDirectory.cachesDirectory + #else + let directory = FileManager.SearchPathDirectory.documentDirectory + #endif + + if let url = FileManager.default.urls(for: directory, in: .userDomainMask).first { if (!FileManager.default.fileExists(atPath: url.path)) { do { try FileManager.default.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) From 5888f66836624d490a1e2ca86eb53f32df34efd4 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 29 Apr 2021 11:07:42 -0700 Subject: [PATCH 14/26] clean up --- .../DefaultEventDispatcher.swift | 79 +++++++++---------- .../EventDispatcherTests_Batch.swift | 24 +++--- .../EventDispatcherTests.swift | 49 ++++++------ .../EventDispatcherTests_MultiClients.swift | 26 ++++++ Tests/TestUtils/OTUtils.swift | 6 +- 5 files changed, 104 insertions(+), 80 deletions(-) diff --git a/Sources/Customization/DefaultEventDispatcher.swift b/Sources/Customization/DefaultEventDispatcher.swift index 6b4d6419..0a6e9310 100644 --- a/Sources/Customization/DefaultEventDispatcher.swift +++ b/Sources/Customization/DefaultEventDispatcher.swift @@ -21,9 +21,7 @@ public enum DataStoreType { } open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { - - static let sharedInstance = - DefaultEventDispatcher() + static let sharedInstance = DefaultEventDispatcher() // timer-interval for batching (0 = no batching, negative = use default) var timerInterval: TimeInterval @@ -38,19 +36,15 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { static public let maxQueueSize = 10000 static let maxFailureCount = 3 } - - // the max failure count. there is no backoff timer. - + lazy var logger = OPTLoggerFactory.getLogger() - var backingStore: DataStoreType - var backingStoreName: String // for dispatching events - let dispatcher = DispatchQueue(label: "DefaultEventDispatcherQueue") + let lock = DispatchQueue(label: "DefaultEventDispatcherQueue") // using a datastore queue with a backing file - let dataStore: DataStoreQueueStackImpl + let queue: DataStoreQueueStackImpl // timer as a atomic property. - var timer: AtomicProperty = AtomicProperty() + var timer = AtomicProperty() var observerProjectId: NSObjectProtocol? var observerRevision: NSObjectProtocol? @@ -64,19 +58,16 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { self.timerInterval = timerInterval >= 0 ? timerInterval : DefaultValues.timeInterval self.maxQueueSize = maxQueueSize >= 100 ? maxQueueSize : DefaultValues.maxQueueSize - self.backingStore = backingStore - self.backingStoreName = dataStoreName - switch backingStore { case .file: - self.dataStore = DataStoreQueueStackImpl(queueStackName: "OPTEventQueue", - dataStore: DataStoreFile<[Data]>(storeName: backingStoreName)) + self.queue = DataStoreQueueStackImpl(queueStackName: "OPTEventQueue", + dataStore: DataStoreFile<[Data]>(storeName: dataStoreName)) case .memory: - self.dataStore = DataStoreQueueStackImpl(queueStackName: "OPTEventQueue", - dataStore: DataStoreMemory<[Data]>(storeName: backingStoreName)) + self.queue = DataStoreQueueStackImpl(queueStackName: "OPTEventQueue", + dataStore: DataStoreMemory<[Data]>(storeName: dataStoreName)) case .userDefaults: - self.dataStore = DataStoreQueueStackImpl(queueStackName: "OPTEventQueue", - dataStore: DataStoreUserDefaults()) + self.queue = DataStoreQueueStackImpl(queueStackName: "OPTEventQueue", + dataStore: DataStoreUserDefaults()) } if self.maxQueueSize < self.batchSize { @@ -98,16 +89,16 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { } open func dispatchEvent(event: EventForDispatch, completionHandler: DispatchCompletionHandler?) { - guard dataStore.count < maxQueueSize else { + guard queue.count < maxQueueSize else { let error = OptimizelyError.eventDispatchFailed("EventQueue is full") self.logger.e(error) completionHandler?(.failure(error)) return } - dataStore.save(item: event) + queue.save(item: event) - if dataStore.count >= batchSize { + if queue.count >= batchSize { flushEvents() } else { startTimer() @@ -121,12 +112,12 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { let notify = DispatchGroup() open func flushEvents() { - dispatcher.async { + lock.async { // we don't remove anthing off of the queue unless it is successfully sent. var failureCount = 0 func removeStoredEvents(num: Int) { - if let removedItem = self.dataStore.removeFirstItems(count: num), removedItem.count > 0 { + if let removedItem = self.queue.removeFirstItems(count: num), removedItem.count > 0 { // avoid event-log-message preparation overheads with closure-logging self.logger.d({ "Removed stored \(num) events starting with \(removedItem.first!)" }) } else { @@ -134,7 +125,7 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { } } - while let eventsToSend: [EventForDispatch] = self.dataStore.getFirstItems(count: self.batchSize) { + while let eventsToSend: [EventForDispatch] = self.queue.getFirstItems(count: self.batchSize) { let (numEvents, batched) = eventsToSend.batch() guard numEvents > 0 else { break } @@ -200,9 +191,14 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { } task.resume() - } +} + +// MARK: - internals + +extension DefaultEventDispatcher { + func applicationDidEnterBackground() { stopTimer() @@ -210,7 +206,7 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { } func applicationDidBecomeActive() { - if dataStore.count > 0 { + if queue.count > 0 { startTimer() } } @@ -222,15 +218,15 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { return } - guard self.timer.property == nil else { return } + guard timer.property == nil else { return } DispatchQueue.main.async { // should check here again guard self.timer.property == nil else { return } self.timer.property = Timer.scheduledTimer(withTimeInterval: self.timerInterval, repeats: true) { _ in - self.dispatcher.async { - if self.dataStore.count > 0 { + self.lock.async { + if self.queue.count > 0 { self.flushEvents() } else { self.stopTimer() @@ -241,11 +237,19 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { } func stopTimer() { - timer.performAtomic { (timer) in + timer.performAtomic { timer in timer.invalidate() } timer.property = nil } + + // MARK: - Tests + + open func close() { + self.flushEvents() + self.lock.sync {} + } + } // MARK: - Notification Observers @@ -253,12 +257,12 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { extension DefaultEventDispatcher { func addProjectChangeNotificationObservers() { - observerProjectId = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyProjectIdChange, object: nil, queue: nil) { [weak self] (_) in + observerProjectId = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyProjectIdChange, object: nil, queue: nil) { [weak self] _ in self?.logger.d("Event flush triggered by datafile projectId change") self?.flushEvents() } - observerRevision = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyRevisionChange, object: nil, queue: nil) { [weak self] (_) in + observerRevision = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyRevisionChange, object: nil, queue: nil) { [weak self] _ in self?.logger.d("Event flush triggered by datafile revision change") self?.flushEvents() } @@ -273,11 +277,4 @@ extension DefaultEventDispatcher { } } - // MARK: - Tests - - open func close() { - self.flushEvents() - self.dispatcher.sync {} - } - } diff --git a/Tests/OptimizelyTests-Batch-iOS/EventDispatcherTests_Batch.swift b/Tests/OptimizelyTests-Batch-iOS/EventDispatcherTests_Batch.swift index dace55f1..b73ff1cf 100644 --- a/Tests/OptimizelyTests-Batch-iOS/EventDispatcherTests_Batch.swift +++ b/Tests/OptimizelyTests-Batch-iOS/EventDispatcherTests_Batch.swift @@ -324,7 +324,7 @@ extension EventDispatcherTests_Batch { XCTAssertEqual($0, visitorA) } - XCTAssertEqual(eventDispatcher.dataStore.count, 0) + XCTAssertEqual(eventDispatcher.queue.count, 0) } } @@ -356,7 +356,7 @@ extension EventDispatcherTests_Batch { XCTAssertEqual(batchedEvents.visitors[1], visitorB) XCTAssertEqual(batchedEvents.visitors[2], visitorA) XCTAssertEqual(batchedEvents.visitors.count, 3) - XCTAssertEqual(eventDispatcher.dataStore.count, 0) + XCTAssertEqual(eventDispatcher.queue.count, 0) XCTAssert(eventDispatcher.batchSize == 10) @@ -403,7 +403,7 @@ extension EventDispatcherTests_Batch { XCTAssertEqual(batchedEvents.visitors[0], visitorB) XCTAssertEqual(batchedEvents.visitors.count, 1) - XCTAssertEqual(eventDispatcher.dataStore.count, 0) + XCTAssertEqual(eventDispatcher.queue.count, 0) } func testFlushEventsWhenBatchFailsWithInvalidEvent() { @@ -450,7 +450,7 @@ extension EventDispatcherTests_Batch { XCTAssertEqual(batchedEvents.visitors[0], visitorA) XCTAssertEqual(batchedEvents.visitors.count, 1) - XCTAssertEqual(eventDispatcher.dataStore.count, 0) + XCTAssertEqual(eventDispatcher.queue.count, 0) } @@ -480,7 +480,7 @@ extension EventDispatcherTests_Batch { XCTAssertEqual(eventDispatcher.sendRequestedEvents[i], eventDispatcher.sendRequestedEvents[0]) } - XCTAssertEqual(eventDispatcher.dataStore.count, 2, "all failed to transmit, so should keep all original events") + XCTAssertEqual(eventDispatcher.queue.count, 2, "all failed to transmit, so should keep all original events") // (2) error removed - now events sent out successfully @@ -492,7 +492,7 @@ extension EventDispatcherTests_Batch { XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, maxFailureCount + 1, "only one more since succeeded") XCTAssertEqual(eventDispatcher.sendRequestedEvents[3], eventDispatcher.sendRequestedEvents[0]) - XCTAssertEqual(eventDispatcher.dataStore.count, 0, "all expected to get transmitted successfully") + XCTAssertEqual(eventDispatcher.queue.count, 0, "all expected to get transmitted successfully") } } @@ -548,7 +548,7 @@ extension EventDispatcherTests_Batch { (self.kUrlB, self.batchEventB), (self.kUrlC, self.batchEventC)]) - eventDispatcher.dispatcher.sync {} + eventDispatcher.lock.sync {} continueAfterFailure = false // stop on XCTAssertEqual failure instead of array out-of-bound exception XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 3) @@ -571,7 +571,7 @@ extension EventDispatcherTests_Batch { XCTAssertEqual(batchedEvents.visitors[0], visitorC) XCTAssertEqual(batchedEvents.visitors.count, 1) - XCTAssertEqual(eventDispatcher.dataStore.count, 0) + XCTAssertEqual(eventDispatcher.queue.count, 0) } func testEventBatchedOnTimer_CheckNoRedundantSend() { @@ -591,7 +591,7 @@ extension EventDispatcherTests_Batch { // check if we have only one batched event transmitted XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 1) - XCTAssertEqual(eventDispatcher.dataStore.count, 0, "all expected to get transmitted successfully") + XCTAssertEqual(eventDispatcher.queue.count, 0, "all expected to get transmitted successfully") } func testEventBatchedAndErrorRecoveredOnTimer() { @@ -609,7 +609,7 @@ extension EventDispatcherTests_Batch { // wait for the first timer-fire wait(for: [eventDispatcher.exp!], timeout: 10) // tranmission is expected to fail - XCTAssertEqual(eventDispatcher.dataStore.count, 2, "all failed to transmit, so should keep all original events") + XCTAssertEqual(eventDispatcher.queue.count, 2, "all failed to transmit, so should keep all original events") // (2) remove error. check if events are transmitted successfully on next timer-fire sleep(3) // wait all failure-retries (3 times) completed @@ -619,7 +619,7 @@ extension EventDispatcherTests_Batch { // wait for the next timer-fire wait(for: [eventDispatcher.exp!], timeout: 10) - XCTAssertEqual(eventDispatcher.dataStore.count, 0, "all expected to get transmitted successfully") + XCTAssertEqual(eventDispatcher.queue.count, 0, "all expected to get transmitted successfully") } } @@ -809,7 +809,7 @@ extension EventDispatcherTests_Batch { try! optimizely.start(datafile: datafile) dispatchMultipleEvents([(kUrlA, batchEventA)]) - eventDispatcher.dispatcher.sync {} + eventDispatcher.lock.sync {} XCTAssertEqual(notifUrl, kUrlA) diff --git a/Tests/OptimizelyTests-Common/EventDispatcherTests.swift b/Tests/OptimizelyTests-Common/EventDispatcherTests.swift index 94265798..32c8af0d 100644 --- a/Tests/OptimizelyTests-Common/EventDispatcherTests.swift +++ b/Tests/OptimizelyTests-Common/EventDispatcherTests.swift @@ -25,10 +25,7 @@ class EventDispatcherTests: XCTestCase { } override func tearDown() { - if let dispatcher = eventDispatcher { - dispatcher.flushEvents() - dispatcher.dispatcher.sync {} - } + OTUtils.clearAllEventQueues() } func testDefaultDispatcher() { @@ -37,18 +34,18 @@ class EventDispatcherTests: XCTestCase { pEventD.flushEvents() - eventDispatcher?.dispatcher.sync {} + eventDispatcher?.lock.sync {} pEventD.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) - eventDispatcher?.dispatcher.sync {} + eventDispatcher?.lock.sync {} - XCTAssert(eventDispatcher?.dataStore.count == 1) + XCTAssert(eventDispatcher?.queue.count == 1) eventDispatcher?.flushEvents() - eventDispatcher?.dispatcher.sync {} + eventDispatcher?.lock.sync {} - XCTAssert(eventDispatcher?.dataStore.count == 0) + XCTAssert(eventDispatcher?.queue.count == 0) // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. @@ -61,7 +58,7 @@ class EventDispatcherTests: XCTestCase { override func sendEvent(event: EventForDispatch, completionHandler: @escaping DispatchCompletionHandler) { events.append(event) if !once { - self.dataStore.save(item: EventForDispatch(body: Data())) + self.queue.save(item: EventForDispatch(body: Data())) once = true } completionHandler(.success(Data())) @@ -71,10 +68,10 @@ class EventDispatcherTests: XCTestCase { let dispatcher = InnerEventDispatcher(timerInterval: 0) // add two items.... call flush - dispatcher.dataStore.save(item: EventForDispatch(body: Data())) + dispatcher.queue.save(item: EventForDispatch(body: Data())) dispatcher.flushEvents() - dispatcher.dispatcher.sync {} + dispatcher.lock.sync {} XCTAssert(dispatcher.events.count == 2) } @@ -84,7 +81,7 @@ class EventDispatcherTests: XCTestCase { let pEventD: OPTEventDispatcher = eventDispatcher! eventDispatcher?.timerInterval = 1 let wait = {() in - self.eventDispatcher?.dispatcher.sync {} + self.eventDispatcher?.lock.sync {} } pEventD.flushEvents() @@ -93,12 +90,12 @@ class EventDispatcherTests: XCTestCase { pEventD.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) wait() - XCTAssert(eventDispatcher?.dataStore.count == 1) + XCTAssert(eventDispatcher?.queue.count == 1) eventDispatcher?.flushEvents() wait() - XCTAssert(eventDispatcher?.dataStore.count == 0) + XCTAssert(eventDispatcher?.queue.count == 0) // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. @@ -109,7 +106,7 @@ class EventDispatcherTests: XCTestCase { let pEventD: OPTEventDispatcher = eventDispatcher! eventDispatcher?.timerInterval = 1 let wait = { - self.eventDispatcher?.dispatcher.sync {} + self.eventDispatcher?.lock.sync {} } pEventD.flushEvents() @@ -118,12 +115,12 @@ class EventDispatcherTests: XCTestCase { pEventD.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) wait() - XCTAssert(eventDispatcher?.dataStore.count == 1) + XCTAssert(eventDispatcher?.queue.count == 1) eventDispatcher?.flushEvents() wait() - XCTAssert(eventDispatcher?.dataStore.count == 0) + XCTAssert(eventDispatcher?.queue.count == 0) // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. @@ -134,7 +131,7 @@ class EventDispatcherTests: XCTestCase { let pEventD: OPTEventDispatcher = eventDispatcher! eventDispatcher?.timerInterval = 1 let wait = { - self.eventDispatcher?.dispatcher.sync {} + self.eventDispatcher?.lock.sync {} } pEventD.flushEvents() @@ -143,12 +140,12 @@ class EventDispatcherTests: XCTestCase { pEventD.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) wait() - XCTAssert(eventDispatcher?.dataStore.count == 1) + XCTAssert(eventDispatcher?.queue.count == 1) eventDispatcher?.flushEvents() wait() - XCTAssert(eventDispatcher?.dataStore.count == 0) + XCTAssert(eventDispatcher?.queue.count == 0) // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. @@ -170,11 +167,11 @@ class EventDispatcherTests: XCTestCase { eventDispatcher = DefaultEventDispatcher(timerInterval: 1) eventDispatcher?.flushEvents() - eventDispatcher?.dispatcher.sync {} + eventDispatcher?.lock.sync {} eventDispatcher?.dispatchEvent(event: EventForDispatch(body: Data()), completionHandler: nil) - eventDispatcher?.dispatcher.sync {} + eventDispatcher?.lock.sync {} eventDispatcher?.applicationDidBecomeActive() eventDispatcher?.applicationDidEnterBackground() @@ -272,10 +269,10 @@ class EventDispatcherTests: XCTestCase { let dispatcher = LocalEventDispatcher() - XCTAssert(dispatcher.dataStore.count == 2) + XCTAssert(dispatcher.queue.count == 2) dispatcher.flushEvents() - dispatcher.dispatcher.sync {} - XCTAssert(dispatcher.dataStore.count == 0) + dispatcher.lock.sync {} + XCTAssert(dispatcher.queue.count == 0) } } diff --git a/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift index 6cae7e6a..79169290 100644 --- a/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift @@ -17,11 +17,14 @@ import XCTest class EventDispatcherTests_MultiClients: XCTestCase { + var dispatcher = DefaultEventDispatcher() override func setUpWithError() throws { + OTUtils.createDocumentDirectoryIfNotAvailable() } override func tearDownWithError() throws { + OTUtils.clearAllEventQueues() } func testConcurrentDispatchEvents() { @@ -36,4 +39,27 @@ class EventDispatcherTests_MultiClients: XCTestCase { } + func testConcurrentStartTimer() { + let result = OTUtils.runConcurrent(count: 5, timeoutInSecs: 30) { idx in + (0..<1).forEach { _ in + self.dispatcher.startTimer() + print("MultiClients] before sleep: \(idx)") + + let group = DispatchGroup() + group.enter() + DispatchQueue.main.async { + group.leave() + } + group.wait() + print("MultiClients] end sleep: \(idx)") + self.dispatcher.stopTimer() + } + + print("MultiClients] end of each") + + } + + XCTAssertTrue(result, "Concurrent tasks timed out") + } + } diff --git a/Tests/TestUtils/OTUtils.swift b/Tests/TestUtils/OTUtils.swift index a5d5ebca..cf130e03 100644 --- a/Tests/TestUtils/OTUtils.swift +++ b/Tests/TestUtils/OTUtils.swift @@ -211,6 +211,10 @@ class OTUtils { removeAllUserDefaults(including: including) } + static func clearAllEventQueues() { + removeAllFiles(including: "OPTEventQueue") + } + static func removeAllFiles(including: String) { removeAllFiles(including: including, in: .documentDirectory) removeAllFiles(including: including, in: .cachesDirectory) @@ -289,7 +293,7 @@ class OTUtils { task: @escaping (Int) -> Void) -> Bool { let items = (0.. Date: Thu, 29 Apr 2021 17:57:31 -0700 Subject: [PATCH 15/26] add tests for concurrent event dispatcher --- .../EventDispatcherTests.swift | 7 +- .../EventDispatcherTests_MultiClients.swift | 135 +++++++++++++++--- Tests/TestUtils/MockEventDispatcher.swift | 23 ++- Tests/TestUtils/OTUtils.swift | 25 +++- 4 files changed, 161 insertions(+), 29 deletions(-) diff --git a/Tests/OptimizelyTests-Common/EventDispatcherTests.swift b/Tests/OptimizelyTests-Common/EventDispatcherTests.swift index 32c8af0d..831f0ccf 100644 --- a/Tests/OptimizelyTests-Common/EventDispatcherTests.swift +++ b/Tests/OptimizelyTests-Common/EventDispatcherTests.swift @@ -241,11 +241,6 @@ class EventDispatcherTests: XCTestCase { } func testEventQueueFormatCompatibilty() { - class LocalEventDispatcher: DefaultEventDispatcher { - override func sendEvent(event: EventForDispatch, completionHandler: @escaping DispatchCompletionHandler) { - completionHandler(.success(Data())) - } - } let queueName = "OPTEventQueue" // pre-store multiple events in a queue (expected format) @@ -267,7 +262,7 @@ class EventDispatcherTests: XCTestCase { // verify that a new dataStore can read an existing queue items - let dispatcher = LocalEventDispatcher() + let dispatcher = DumpEventDispatcher() XCTAssert(dispatcher.queue.count == 2) dispatcher.flushEvents() diff --git a/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift index 79169290..99e478b7 100644 --- a/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift @@ -17,10 +17,12 @@ import XCTest class EventDispatcherTests_MultiClients: XCTestCase { - var dispatcher = DefaultEventDispatcher() + let stressFactor = 1 // increase this (10?) to give more stress testing with longer running time override func setUpWithError() throws { + OTUtils.bindLoggerForTest(.info) OTUtils.createDocumentDirectoryIfNotAvailable() + OTUtils.clearAllEventQueues() } override func tearDownWithError() throws { @@ -28,38 +30,131 @@ class EventDispatcherTests_MultiClients: XCTestCase { } func testConcurrentDispatchEvents() { + let numThreads = 100 + let numEventsPerThread = 10 * stressFactor + let numEvents = numThreads * numEventsPerThread + + let dispatcher = DumpEventDispatcher() + dispatcher.timerInterval = 1 + dispatcher.batchSize = 999999999 // avoid early-fire by batch-filled + + // keep the test running until all events flushed and timer stopped + + let exp = expectation(description: "delay") + DispatchQueue.global().async { + while dispatcher.totalEventsSent < numEvents || dispatcher.timer.property != nil { + sleep(1) + } + exp.fulfill() + } + (0.. 0 || dispatcher.timer.property != nil { sleep(1) } + exp.fulfill() + } + + // multiple threads try to start a timer concurrently + // only one of them will succeed and other requests will be discarded + // timer will flush events and then stop timer at the following ticks + + _ = OTUtils.runConcurrent(count: numThreads) { idx in + dispatcher.startTimer() + } + + // test should wait until all startTimers done, events flushed, and then timer stopped eventually + wait(for: [exp], timeout: 10) + } + + func testConcurrentNotifications() { + let numThreads = 100 + let numEvents = 10 + + let dispatcher = DumpEventDispatcher() + dispatcher.timerInterval = 999999999 // disable flush by timer + dispatcher.batchSize = 999999999 // avoid early-fire by batch-filled + // keep the test running until all events flushed and timer stopped + + let exp = expectation(description: "delay") + DispatchQueue.global().async { + while dispatcher.totalEventsSent < numEvents { + print("[MultiClients] totalEventSent: \(dispatcher.totalEventsSent)") + sleep(1) + } + exp.fulfill() } + (0.. String? { @@ -184,6 +183,30 @@ class OTUtils { ups.save(userProfile: profile) } + + // MARK: - events + + static func makeEventForDispatch(url: String? = nil, event: BatchEvent? = nil) -> EventForDispatch { + let targetUrl = URL(string: url ?? "https://a.b.c") + let data = try! JSONEncoder().encode(event ?? makeTestBatchEvent()) + return EventForDispatch(url: targetUrl, body: data) + } + + static func makeTestBatchEvent(projectId: String? = nil, revision: String? = nil, visitor: Visitor? = nil) -> BatchEvent { + let testProjectId = projectId ?? "12345" + let testVisitor = visitor ?? Visitor(attributes: [], snapshots: [], visitorID: "tester") + let testRevision = revision ?? "101" + + return BatchEvent(revision: testRevision, + accountID: "1234", + clientVersion: "1.0.0", + visitors: [testVisitor], + projectID: testProjectId, + clientName: "test", + anonymizeIP: true, + enrichDecisions: true) + } + // MARK: - files From 181d86f9f479532154c11ca2e7576ecfdac95ec0 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Thu, 29 Apr 2021 18:08:03 -0700 Subject: [PATCH 16/26] clean up tests --- .../EventDispatcherTests_MultiClients.swift | 47 ++++--------------- 1 file changed, 8 insertions(+), 39 deletions(-) diff --git a/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift index 99e478b7..b6595669 100644 --- a/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift @@ -17,7 +17,7 @@ import XCTest class EventDispatcherTests_MultiClients: XCTestCase { - let stressFactor = 1 // increase this (10?) to give more stress testing with longer running time + let stressFactor = 1 // increase this (10) to give more stress with longer running time override func setUpWithError() throws { OTUtils.bindLoggerForTest(.info) @@ -42,9 +42,7 @@ class EventDispatcherTests_MultiClients: XCTestCase { let exp = expectation(description: "delay") DispatchQueue.global().async { - while dispatcher.totalEventsSent < numEvents || dispatcher.timer.property != nil { - sleep(1) - } + while dispatcher.totalEventsSent < numEvents || dispatcher.timer.property != nil { sleep(1) } exp.fulfill() } @@ -54,8 +52,6 @@ class EventDispatcherTests_MultiClients: XCTestCase { usleep(UInt32.random(in: 0..<1000000)) dispatcher.dispatchEvent(event: OTUtils.makeEventForDispatch(), completionHandler: nil) } - NotificationCenter.default.post(name: .didReceiveOptimizelyProjectIdChange, object: nil) - NotificationCenter.default.post(name: .didReceiveOptimizelyRevisionChange, object: nil) } } @@ -89,6 +85,12 @@ class EventDispatcherTests_MultiClients: XCTestCase { usleep(UInt32.random(in: 0..<1000)) dispatcher.dispatchEvent(event: OTUtils.makeEventForDispatch(), completionHandler: nil) } + + // more stress with notifications + if Bool.random() { + NotificationCenter.default.post(name: .didReceiveOptimizelyProjectIdChange, object: nil) + NotificationCenter.default.post(name: .didReceiveOptimizelyRevisionChange, object: nil) + } } } @@ -124,37 +126,4 @@ class EventDispatcherTests_MultiClients: XCTestCase { wait(for: [exp], timeout: 10) } - func testConcurrentNotifications() { - let numThreads = 100 - let numEvents = 10 - - let dispatcher = DumpEventDispatcher() - dispatcher.timerInterval = 999999999 // disable flush by timer - dispatcher.batchSize = 999999999 // avoid early-fire by batch-filled - - // keep the test running until all events flushed and timer stopped - - let exp = expectation(description: "delay") - DispatchQueue.global().async { - while dispatcher.totalEventsSent < numEvents { - print("[MultiClients] totalEventSent: \(dispatcher.totalEventsSent)") - sleep(1) - } - exp.fulfill() - } - - (0.. Date: Fri, 30 Apr 2021 10:07:40 -0700 Subject: [PATCH 17/26] add more tests for multi-client event dispatcher --- .../DefaultEventDispatcher.swift | 37 +++--- .../EventDispatcherTests_MultiClients.swift | 120 ++++++++++-------- Tests/TestUtils/OTUtils.swift | 2 - 3 files changed, 88 insertions(+), 71 deletions(-) diff --git a/Sources/Customization/DefaultEventDispatcher.swift b/Sources/Customization/DefaultEventDispatcher.swift index 0a6e9310..71c6487b 100644 --- a/Sources/Customization/DefaultEventDispatcher.swift +++ b/Sources/Customization/DefaultEventDispatcher.swift @@ -38,16 +38,13 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { } lazy var logger = OPTLoggerFactory.getLogger() - // for dispatching events let lock = DispatchQueue(label: "DefaultEventDispatcherQueue") // using a datastore queue with a backing file let queue: DataStoreQueueStackImpl // timer as a atomic property. var timer = AtomicProperty() - - var observerProjectId: NSObjectProtocol? - var observerRevision: NSObjectProtocol? + var observers = [NSObjectProtocol]() public init(batchSize: Int = DefaultValues.batchSize, backingStore: DataStoreType = .file, @@ -89,7 +86,8 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { } open func dispatchEvent(event: EventForDispatch, completionHandler: DispatchCompletionHandler?) { - guard queue.count < maxQueueSize else { + let count = queue.count + guard count < maxQueueSize else { let error = OptimizelyError.eventDispatchFailed("EventQueue is full") self.logger.e(error) completionHandler?(.failure(error)) @@ -98,7 +96,7 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher { queue.save(item: event) - if queue.count >= batchSize { + if count + 1 >= batchSize { flushEvents() } else { startTimer() @@ -257,23 +255,24 @@ extension DefaultEventDispatcher { extension DefaultEventDispatcher { func addProjectChangeNotificationObservers() { - observerProjectId = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyProjectIdChange, object: nil, queue: nil) { [weak self] _ in - self?.logger.d("Event flush triggered by datafile projectId change") - self?.flushEvents() - } + observers.append( + NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyProjectIdChange, object: nil, queue: nil) { [weak self] _ in + self?.logger.d("Event flush triggered by datafile projectId change") + self?.flushEvents() + } + ) - observerRevision = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyRevisionChange, object: nil, queue: nil) { [weak self] _ in - self?.logger.d("Event flush triggered by datafile revision change") - self?.flushEvents() - } + observers.append( + NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyRevisionChange, object: nil, queue: nil) { [weak self] _ in + self?.logger.d("Event flush triggered by datafile revision change") + self?.flushEvents() + } + ) } func removeProjectChangeNotificationObservers() { - if let observer = observerProjectId { - NotificationCenter.default.removeObserver(observer, name: .didReceiveOptimizelyProjectIdChange, object: nil) - } - if let observer = observerRevision { - NotificationCenter.default.removeObserver(observer, name: .didReceiveOptimizelyRevisionChange, object: nil) + observers.forEach{ + NotificationCenter.default.removeObserver($0) } } diff --git a/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift index b6595669..1c11d013 100644 --- a/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/EventDispatcherTests_MultiClients.swift @@ -30,74 +30,56 @@ class EventDispatcherTests_MultiClients: XCTestCase { } func testConcurrentDispatchEvents() { - let numThreads = 100 - let numEventsPerThread = 10 * stressFactor - let numEvents = numThreads * numEventsPerThread - let dispatcher = DumpEventDispatcher() dispatcher.timerInterval = 1 dispatcher.batchSize = 999999999 // avoid early-fire by batch-filled - // keep the test running until all events flushed and timer stopped - - let exp = expectation(description: "delay") - DispatchQueue.global().async { - while dispatcher.totalEventsSent < numEvents || dispatcher.timer.property != nil { sleep(1) } - exp.fulfill() - } - - (0.. XCTestExpectation { + let numEvents = numThreads * numEventsPerThread + + // keep the test running until all events flushed and timer stopped + let exp = expectation(description: "delay") + DispatchQueue.global().async { + while dispatcher.totalEventsSent < numEvents || dispatcher.timer.property != nil { sleep(1) } + exp.fulfill() + } + + (0.. URL? { @@ -339,7 +338,6 @@ class OTUtils { return negativeMaxValueAllowed * 2.0 } - // MARK: - others static var randomSdkKey: String { From 3a6aad9a830c7e5592a9f0a1ae793188e61434f0 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Fri, 30 Apr 2021 13:35:40 -0700 Subject: [PATCH 18/26] clean up --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 4 +++ .../OptimizelyClient+Extension.swift | 16 ++++------ Sources/Optimizely/OptimizelyClient.swift | 10 +++--- Sources/Utils/HandlerRegistryService.swift | 11 ++----- ...UserProfileServiceTests_MultiClients.swift | 32 +++++++++++++++++++ 5 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 64332984..b4e4e520 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -540,6 +540,7 @@ 6E424D5326324C4D0081004A /* OptimizelyUserContext+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E86CEA124FDC836005DAFED /* OptimizelyUserContext+ObjC.swift */; }; 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EC6DD4024ABF89B0017D296 /* OptimizelyUserContext.swift */; }; 6E424D7626324DBD0081004A /* AtomicArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */; }; + 6E474C8D263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */; }; 6E593FB625BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */; }; 6E5AB69323F6130D007A82B1 /* OptimizelyClientTests_Init_Sync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */; }; 6E5AB69423F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */; }; @@ -1838,6 +1839,7 @@ 6E424BFB263228FD0081004A /* AtomicDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicDictionary.swift; sourceTree = ""; }; 6E424C3C263249620081004A /* OptimizelyTests-MultiClients-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-MultiClients-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AtomicArrayTests.swift; sourceTree = ""; }; + 6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileServiceTests_MultiClients.swift; sourceTree = ""; }; 6E593FB425BB9C5500EC72BC /* OptimizelyClientTests_Decide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Decide.swift; sourceTree = ""; }; 6E5AB69123F6130C007A82B1 /* OptimizelyClientTests_Init_Sync.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Sync.swift; sourceTree = ""; }; 6E5AB69223F6130D007A82B1 /* OptimizelyClientTests_Init_Async.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_Init_Async.swift; sourceTree = ""; }; @@ -2230,6 +2232,7 @@ 6E424D7526324DBD0081004A /* AtomicArrayTests.swift */, 6E2D5DAD26338CA00002077F /* AtomicDictionaryTests.swift */, 6E5D120C2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift */, + 6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */, ); path = "OptimizelyTests-MultiClients"; sourceTree = ""; @@ -3707,6 +3710,7 @@ 6E424D0526324B620081004A /* ConditionHolder.swift in Sources */, 6E424D5226324C4D0081004A /* OptimizelyClient+Decide.swift in Sources */, 6E424D0626324B620081004A /* UserAttribute.swift in Sources */, + 6E474C8D263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift in Sources */, 6E8A3D4A2637408500DAEA13 /* MockDatafileHandler.swift in Sources */, 6E424D0726324B620081004A /* Event.swift in Sources */, 6E424D0826324B620081004A /* ProjectConfig.swift in Sources */, diff --git a/Sources/Extensions/OptimizelyClient+Extension.swift b/Sources/Extensions/OptimizelyClient+Extension.swift index 2541937c..5ae9b58f 100644 --- a/Sources/Extensions/OptimizelyClient+Extension.swift +++ b/Sources/Extensions/OptimizelyClient+Extension.swift @@ -23,19 +23,17 @@ extension OptimizelyClient { datafileHandler: OPTDatafileHandler, decisionService: OPTDecisionService, notificationCenter: OPTNotificationCenter) { - // bind it as a non-singleton. so, we will create an instance anytime injected. - // we don't associate the logger with a sdkKey at this time because not all components are sdkKey specific. - let binder: Binder = Binder(service: OPTLogger.self, factory: type(of: logger).init) + // Register my logger service. Bind it as a non-singleton. So, we will create an instance anytime injected. + // we don't associate the logger with a sdkKey at this time because not all components are sdkKey specific. + HandlerRegistryService.shared.registerBinding(binder: Binder(service: OPTLogger.self, factory: type(of: logger).init)) - // Register my logger service. - HandlerRegistryService.shared.registerBinding(binder: binder) - - // this is bound a reusable singleton. so, if we re-initalize, we will keep this. + // This is bound a reusable singleton. so, if we re-initalize, we will keep this. HandlerRegistryService.shared.registerBinding(binder: Binder(sdkKey: sdkKey, service: OPTNotificationCenter.self, strategy: .reUse, isSingleton: true, inst: notificationCenter)) - // the decision service is also a singleton that will reCreate on re-initalize + + // The decision service is also a singleton that will reCreate on re-initalize HandlerRegistryService.shared.registerBinding(binder: Binder(sdkKey: sdkKey, service: OPTDecisionService.self, strategy: .reUse, isSingleton: true, inst: decisionService)) - // An event dispatcher. We use a singleton and use the same Event dispatcher for all + // An event dispatcher. We use a singleton and use the same Event dispatcher for all // projects. If you change the event dispatcher, you can potentially lose data if you // don't use the same backingstore. HandlerRegistryService.shared.registerBinding(binder: Binder(sdkKey: sdkKey, service: OPTEventDispatcher.self, strategy: .reUse, isSingleton: true, inst: eventDispatcher)) diff --git a/Sources/Optimizely/OptimizelyClient.swift b/Sources/Optimizely/OptimizelyClient.swift index eb6431e5..a77b254d 100644 --- a/Sources/Optimizely/OptimizelyClient.swift +++ b/Sources/Optimizely/OptimizelyClient.swift @@ -51,17 +51,17 @@ open class OptimizelyClient: NSObject { return HandlerRegistryService.shared.injectEventDispatcher(sdkKey: self.sdkKey) } + public var datafileHandler: OPTDatafileHandler? { + return HandlerRegistryService.shared.injectDatafileHandler(sdkKey: self.sdkKey) + } + lazy var currentDatafileHandler = datafileHandler + // MARK: - Default Services var decisionService: OPTDecisionService { return HandlerRegistryService.shared.injectDecisionService(sdkKey: self.sdkKey)! } - public var datafileHandler: OPTDatafileHandler? { - return HandlerRegistryService.shared.injectDatafileHandler(sdkKey: self.sdkKey) - } - lazy var currentDatafileHandler = datafileHandler - public var notificationCenter: OPTNotificationCenter? { return HandlerRegistryService.shared.injectNotificationCenter(sdkKey: self.sdkKey) } diff --git a/Sources/Utils/HandlerRegistryService.swift b/Sources/Utils/HandlerRegistryService.swift index 6356f8dd..90aba289 100644 --- a/Sources/Utils/HandlerRegistryService.swift +++ b/Sources/Utils/HandlerRegistryService.swift @@ -26,15 +26,12 @@ class HandlerRegistryService { } var binders: AtomicProperty<[ServiceKey: BinderProtocol]> = { - var binders = AtomicProperty<[ServiceKey: BinderProtocol]>() binders.property = [ServiceKey: BinderProtocol]() return binders }() - private init() { - - } + private init() {} func registerBinding(binder: BinderProtocol) { let sk = ServiceKey(service: "\(type(of: binder.service))", sdkKey: binder.sdkKey) @@ -105,8 +102,7 @@ protocol BinderProtocol { var strategy: ReInitializeStrategy { get } var service: Any { get } var isSingleton: Bool { get } - var factory:()->Any? { get } - // var configure:(_ inst:Any?)->Any? { get } + var factory: () -> Any? { get } var instance: Any? { get set } } @@ -114,8 +110,7 @@ struct Binder: BinderProtocol { var sdkKey: String? var service: Any var strategy: ReInitializeStrategy = .reCreate - var factory: (() -> Any?) = { ()->Any? in { return nil as Any? }} - // var configure: ((Any?) -> Any?) = { (_)->Any? in { return nil as Any? }} + var factory: () -> Any? = { return nil as Any? } var isSingleton = false var inst: T? diff --git a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift new file mode 100644 index 00000000..8560737a --- /dev/null +++ b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift @@ -0,0 +1,32 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class UserProfileServiceTests_MultiClients: XCTestCase { + + override func setUpWithError() throws { + OTUtils.bindLoggerForTest(.info) + } + + override func tearDownWithError() throws { + } + + func testConcurrent() { + + } + +} From 34b0f9f0ac3488a4565b41fb8a1290125853f633 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 10 May 2021 09:31:17 -0700 Subject: [PATCH 19/26] add tests for multi-client ups --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 12 +- .../DefaultUserProfileService.swift | 26 +++- .../Protocols/OPTUserProfileService.swift | 10 ++ .../DecisionServiceTests.swift | 48 ------- .../DatafileHandlerTests_MultiClients.swift | 3 +- ...UserProfileServiceTests_MultiClients.swift | 133 +++++++++++++++++- Tests/TestUtils/OTUtils.swift | 80 +++++++---- 7 files changed, 223 insertions(+), 89 deletions(-) delete mode 100644 Tests/OptimizelyTests-Common/DecisionServiceTests.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index b4e4e520..0b991c07 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1330,7 +1330,6 @@ 6E9B115822C5486E00C22D81 /* EventDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */; }; 6E9B115922C5486E00C22D81 /* BatchEventBuilderTests_Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */; }; 6E9B115A22C5486E00C22D81 /* DecisionServiceTests_Others.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */; }; - 6E9B115B22C5486E00C22D81 /* DecisionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199522C5211100B2B157 /* DecisionServiceTests.swift */; }; 6E9B115C22C5486E00C22D81 /* DatafileHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */; }; 6E9B115D22C5486E00C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */; }; 6E9B115E22C5486E00C22D81 /* DataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199822C5211100B2B157 /* DataStoreTests.swift */; }; @@ -1355,7 +1354,6 @@ 6E9B117222C5487100C22D81 /* EventDispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */; }; 6E9B117322C5487100C22D81 /* BatchEventBuilderTests_Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */; }; 6E9B117422C5487100C22D81 /* DecisionServiceTests_Others.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */; }; - 6E9B117522C5487100C22D81 /* DecisionServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199522C5211100B2B157 /* DecisionServiceTests.swift */; }; 6E9B117622C5487100C22D81 /* DatafileHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */; }; 6E9B117722C5487100C22D81 /* BatchEventBuilderTests_EventTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */; }; 6E9B117822C5487100C22D81 /* DataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E75199822C5211100B2B157 /* DataStoreTests.swift */; }; @@ -1960,7 +1958,6 @@ 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDispatcherTests.swift; sourceTree = ""; }; 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchEventBuilderTests_Attributes.swift; sourceTree = ""; }; 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests_Others.swift; sourceTree = ""; }; - 6E75199522C5211100B2B157 /* DecisionServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionServiceTests.swift; sourceTree = ""; }; 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatafileHandlerTests.swift; sourceTree = ""; }; 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchEventBuilderTests_EventTags.swift; sourceTree = ""; }; 6E75199822C5211100B2B157 /* DataStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataStoreTests.swift; sourceTree = ""; }; @@ -2520,7 +2517,6 @@ 6E75199222C5211100B2B157 /* EventDispatcherTests.swift */, 6E75199322C5211100B2B157 /* BatchEventBuilderTests_Attributes.swift */, 6E75199422C5211100B2B157 /* DecisionServiceTests_Others.swift */, - 6E75199522C5211100B2B157 /* DecisionServiceTests.swift */, 6E75199622C5211100B2B157 /* DatafileHandlerTests.swift */, 6E75199722C5211100B2B157 /* BatchEventBuilderTests_EventTags.swift */, 6E75199822C5211100B2B157 /* DataStoreTests.swift */, @@ -4129,7 +4125,6 @@ 6E9B116E22C5487100C22D81 /* LoggerTests.swift in Sources */, 6E75180D22C520D400B2B157 /* DataStoreFile.swift in Sources */, 6E75178722C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, - 6E9B117522C5487100C22D81 /* DecisionServiceTests.swift in Sources */, 6E75179F22C520D400B2B157 /* DataStoreQueueStackImpl+Extension.swift in Sources */, 6E7516BB22C520D400B2B157 /* DefaultUserProfileService.swift in Sources */, 6E424C08263228FD0081004A /* AtomicDictionary.swift in Sources */, @@ -4352,7 +4347,6 @@ 6E9B115422C5486E00C22D81 /* LoggerTests.swift in Sources */, 6E7518DF22C520D400B2B157 /* ConditionLeaf.swift in Sources */, 6E75172D22C520D400B2B157 /* Constants.swift in Sources */, - 6E9B115B22C5486E00C22D81 /* DecisionServiceTests.swift in Sources */, 6E75172122C520D400B2B157 /* OptimizelyResult.swift in Sources */, 6E75186722C520D400B2B157 /* Rollout.swift in Sources */, 6E424C01263228FD0081004A /* AtomicDictionary.swift in Sources */, @@ -5131,6 +5125,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -5199,6 +5194,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -5743,6 +5739,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; @@ -5770,6 +5767,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; @@ -5891,6 +5889,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEAD_CODE_STRIPPING = NO; @@ -5922,6 +5921,7 @@ isa = XCBuildConfiguration; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; + BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; DEAD_CODE_STRIPPING = NO; diff --git a/Sources/Customization/DefaultUserProfileService.swift b/Sources/Customization/DefaultUserProfileService.swift index dd1b38af..64f9e8bd 100644 --- a/Sources/Customization/DefaultUserProfileService.swift +++ b/Sources/Customization/DefaultUserProfileService.swift @@ -78,10 +78,11 @@ open class DefaultUserProfileService: OPTUserProfileService { let lock = DispatchQueue(label: "com.optimizely.UserProfileService") let kStorageName = "user-profile-service" + let rmwLock = DispatchQueue(label: "com.optimizely.UserProfileService-RMW") + public required init() { lock.async { self.profiles = UserDefaults.standard.dictionary(forKey: self.kStorageName) as? UserProfileData ?? UserProfileData() - } } @@ -104,6 +105,28 @@ open class DefaultUserProfileService: OPTUserProfileService { } } + open func add(userProfile: UPProfile) { + guard let userId = userProfile[UserProfileKeys.kUserId] as? String else { return } + + lock.async { + var curProfile = self.profiles?[userId] ?? OPTUserProfileService.UPProfile() + + let curBucketMap = curProfile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() + let addBucketMap = userProfile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? + OPTUserProfileService.UPBucketMap() + let newBucketMap = curBucketMap.merging(addBucketMap) { _, new in new } + + curProfile[UserProfileKeys.kBucketMap] = newBucketMap + curProfile[UserProfileKeys.kUserId] = userId + + self.profiles?[userId] = curProfile + let defaults = UserDefaults.standard + defaults.set(self.profiles, forKey: self.kStorageName) + defaults.synchronize() + } + } + + open func reset(userProfiles: UserProfileData? = nil) { lock.async { self.profiles = userProfiles ?? UserProfileData() @@ -113,3 +136,4 @@ open class DefaultUserProfileService: OPTUserProfileService { } } } + diff --git a/Sources/Customization/Protocols/OPTUserProfileService.swift b/Sources/Customization/Protocols/OPTUserProfileService.swift index 158aed8a..e8586b52 100644 --- a/Sources/Customization/Protocols/OPTUserProfileService.swift +++ b/Sources/Customization/Protocols/OPTUserProfileService.swift @@ -43,4 +43,14 @@ struct UserProfileKeys { **/ func save(userProfile: UPProfile) + func add(userProfile: UPProfile) + +} + +extension OPTUserProfileService { + + func add(userProfile: UPProfile) { + save(userProfile: userProfile) + } + } diff --git a/Tests/OptimizelyTests-Common/DecisionServiceTests.swift b/Tests/OptimizelyTests-Common/DecisionServiceTests.swift deleted file mode 100644 index 0efeb524..00000000 --- a/Tests/OptimizelyTests-Common/DecisionServiceTests.swift +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright 2019, 2021, Optimizely, Inc. and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import XCTest - -class DecisionServiceTests: XCTestCase { - - override func setUp() { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() { - let json = "{\"accountId\":\"2360254204\",\"anonymizeIP\":true,\"botFiltering\":true,\"projectId\":\"3918735994\",\"revision\":\"1480511547\",\"version\":\"4\",\"audiences\":[{\"id\":\"3468206642\",\"name\":\"Gryffindors\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"value\":\"Gryffindor\"}]]]},{\"id\":\"3988293898\",\"name\":\"Slytherins\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"value\":\"Slytherin\"}]]]},{\"id\":\"4194404272\",\"name\":\"english_citizens\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"value\":\"English\"}]]]},{\"id\":\"2196265320\",\"name\":\"audience_with_missing_value\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"value\":\"English\"},{\"name\":\"nationality\",\"type\":\"custom_attribute\"}]]]}],\"typedAudiences\":[{\"id\":\"3468206643\",\"name\":\"BOOL\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"booleanKey\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":true}]]]},{\"id\":\"3468206646\",\"name\":\"INTEXACT\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"integerKey\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":1}]]]},{\"id\":\"3468206644\",\"name\":\"INT\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"integerKey\",\"type\":\"custom_attribute\",\"match\":\"gt\",\"value\":1}]]]},{\"id\":\"3468206645\",\"name\":\"DOUBLE\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"doubleKey\",\"type\":\"custom_attribute\",\"match\":\"lt\",\"value\":100}]]]},{\"id\":\"3468206642\",\"name\":\"Gryffindors\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":\"Gryffindor\"}]]]},{\"id\":\"3988293898\",\"name\":\"Slytherins\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"house\",\"type\":\"custom_attribute\",\"match\":\"substring\",\"value\":\"Slytherin\"}]]]},{\"id\":\"4194404272\",\"name\":\"english_citizens\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"match\":\"exact\",\"value\":\"English\"}]]]},{\"id\":\"2196265320\",\"name\":\"audience_with_missing_value\",\"conditions\":[\"and\",[\"or\",[\"or\",{\"name\":\"nationality\",\"type\":\"custom_attribute\",\"value\":\"English\"},{\"name\":\"nationality\",\"type\":\"custom_attribute\"}]]]}],\"attributes\":[{\"id\":\"553339214\",\"key\":\"house\"},{\"id\":\"58339410\",\"key\":\"nationality\"},{\"id\":\"583394100\",\"key\":\"$opt_test\"},{\"id\":\"323434545\",\"key\":\"booleanKey\"},{\"id\":\"616727838\",\"key\":\"integerKey\"},{\"id\":\"808797686\",\"key\":\"doubleKey\"},{\"id\":\"808797686\",\"key\":\"\"}],\"events\":[{\"id\":\"3785620495\",\"key\":\"basic_event\",\"experimentIds\":[\"1323241596\",\"2738374745\",\"3042640549\",\"3262035800\",\"3072915611\"]},{\"id\":\"3195631717\",\"key\":\"event_with_paused_experiment\",\"experimentIds\":[\"2667098701\"]},{\"id\":\"1987018666\",\"key\":\"event_with_launched_experiments_only\",\"experimentIds\":[\"3072915611\"]}],\"experiments\":[{\"id\":\"1323241596\",\"key\":\"basic_experiment\",\"layerId\":\"1630555626\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767502\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458314\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767502\",\"endOfRange\":5000},{\"entityId\":\"3433458314\",\"endOfRange\":10000}],\"audienceIds\":[],\"forcedVariations\":{\"Harry Potter\":\"A\",\"Tom Riddle\":\"B\"}},{\"id\":\"1323241597\",\"key\":\"typed_audience_experiment\",\"layerId\":\"1630555627\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767503\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458315\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767503\",\"endOfRange\":5000},{\"entityId\":\"3433458315\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206643\",\"3468206644\",\"3468206646\",\"3468206645\"],\"audienceConditions\":[\"or\",\"3468206643\",\"3468206644\",\"3468206646\",\"3468206645\"],\"forcedVariations\":{}},{\"id\":\"1323241598\",\"key\":\"typed_audience_experiment_with_and\",\"layerId\":\"1630555628\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767504\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458316\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767504\",\"endOfRange\":5000},{\"entityId\":\"3433458316\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206643\",\"3468206644\",\"3468206645\"],\"audienceConditions\":[\"and\",\"3468206643\",\"3468206644\",\"3468206645\"],\"forcedVariations\":{}},{\"id\":\"1323241599\",\"key\":\"typed_audience_experiment_leaf_condition\",\"layerId\":\"1630555629\",\"status\":\"Running\",\"variations\":[{\"id\":\"1423767505\",\"key\":\"A\",\"variables\":[]},{\"id\":\"3433458317\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1423767505\",\"endOfRange\":5000},{\"entityId\":\"3433458317\",\"endOfRange\":10000}],\"audienceIds\":[],\"audienceConditions\":\"3468206643\",\"forcedVariations\":{}},{\"id\":\"3262035800\",\"key\":\"multivariate_experiment\",\"layerId\":\"3262035800\",\"status\":\"Running\",\"variations\":[{\"id\":\"1880281238\",\"key\":\"Fred\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"F\"},{\"id\":\"4052219963\",\"value\":\"red\"}]},{\"id\":\"3631049532\",\"key\":\"Feorge\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"F\"},{\"id\":\"4052219963\",\"value\":\"eorge\"}]},{\"id\":\"4204375027\",\"key\":\"Gred\",\"featureEnabled\":false,\"variables\":[{\"id\":\"675244127\",\"value\":\"G\"},{\"id\":\"4052219963\",\"value\":\"red\"}]},{\"id\":\"2099211198\",\"key\":\"George\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"G\"},{\"id\":\"4052219963\",\"value\":\"eorge\"}]}],\"trafficAllocation\":[{\"entityId\":\"1880281238\",\"endOfRange\":2500},{\"entityId\":\"3631049532\",\"endOfRange\":5000},{\"entityId\":\"4204375027\",\"endOfRange\":7500},{\"entityId\":\"2099211198\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{\"Fred\":\"Fred\",\"Feorge\":\"Feorge\",\"Gred\":\"Gred\",\"George\":\"George\"}},{\"id\":\"2201520193\",\"key\":\"double_single_variable_feature_experiment\",\"layerId\":\"1278722008\",\"status\":\"Running\",\"variations\":[{\"id\":\"1505457580\",\"key\":\"pi_variation\",\"featureEnabled\":true,\"variables\":[{\"id\":\"4111654444\",\"value\":\"3.14\"}]},{\"id\":\"119616179\",\"key\":\"euler_variation\",\"variables\":[{\"id\":\"4111654444\",\"value\":\"2.718\"}]}],\"trafficAllocation\":[{\"entityId\":\"1505457580\",\"endOfRange\":4000},{\"entityId\":\"119616179\",\"endOfRange\":8000}],\"audienceIds\":[\"3988293898\"],\"forcedVariations\":{}},{\"id\":\"2667098701\",\"key\":\"paused_experiment\",\"layerId\":\"3949273892\",\"status\":\"Paused\",\"variations\":[{\"id\":\"391535909\",\"key\":\"Control\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"391535909\",\"endOfRange\":10000}],\"audienceIds\":[],\"forcedVariations\":{\"Harry Potter\":\"Control\"}},{\"id\":\"3072915611\",\"key\":\"launched_experiment\",\"layerId\":\"3587821424\",\"status\":\"Launched\",\"variations\":[{\"id\":\"1647582435\",\"key\":\"launch_control\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1647582435\",\"endOfRange\":8000}],\"audienceIds\":[],\"forcedVariations\":{}},{\"id\":\"748215081\",\"key\":\"experiment_with_malformed_audience\",\"layerId\":\"1238149537\",\"status\":\"Running\",\"variations\":[{\"id\":\"535538389\",\"key\":\"var1\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"535538389\",\"endOfRange\":10000}],\"audienceIds\":[\"2196265320\"],\"forcedVariations\":{}}],\"groups\":[{\"id\":\"1015968292\",\"policy\":\"random\",\"experiments\":[{\"id\":\"2738374745\",\"key\":\"first_grouped_experiment\",\"layerId\":\"3301900159\",\"status\":\"Running\",\"variations\":[{\"id\":\"2377378132\",\"key\":\"A\",\"variables\":[]},{\"id\":\"1179171250\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"2377378132\",\"endOfRange\":5000},{\"entityId\":\"1179171250\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{\"Harry Potter\":\"A\",\"Tom Riddle\":\"B\"}},{\"id\":\"3042640549\",\"key\":\"second_grouped_experiment\",\"layerId\":\"2625300442\",\"status\":\"Running\",\"variations\":[{\"id\":\"1558539439\",\"key\":\"A\",\"variables\":[]},{\"id\":\"2142748370\",\"key\":\"B\",\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"1558539439\",\"endOfRange\":5000},{\"entityId\":\"2142748370\",\"endOfRange\":10000}],\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{\"Hermione Granger\":\"A\",\"Ronald Weasley\":\"B\"}}],\"trafficAllocation\":[{\"entityId\":\"2738374745\",\"endOfRange\":4000},{\"entityId\":\"3042640549\",\"endOfRange\":8000}]},{\"id\":\"2606208781\",\"policy\":\"random\",\"experiments\":[{\"id\":\"4138322202\",\"key\":\"mutex_group_2_experiment_1\",\"layerId\":\"3755588495\",\"status\":\"Running\",\"variations\":[{\"id\":\"1394671166\",\"key\":\"mutex_group_2_experiment_1_variation_1\",\"featureEnabled\":true,\"variables\":[{\"id\":\"2059187672\",\"value\":\"mutex_group_2_experiment_1_variation_1\"}]}],\"audienceIds\":[],\"forcedVariations\":{},\"trafficAllocation\":[{\"entityId\":\"1394671166\",\"endOfRange\":10000}]},{\"id\":\"1786133852\",\"key\":\"mutex_group_2_experiment_2\",\"layerId\":\"3818002538\",\"status\":\"Running\",\"variations\":[{\"id\":\"1619235542\",\"key\":\"mutex_group_2_experiment_2_variation_2\",\"featureEnabled\":true,\"variables\":[{\"id\":\"2059187672\",\"value\":\"mutex_group_2_experiment_2_variation_2\"}]}],\"trafficAllocation\":[{\"entityId\":\"1619235542\",\"endOfRange\":10000}],\"audienceIds\":[],\"forcedVariations\":{}}],\"trafficAllocation\":[{\"entityId\":\"4138322202\",\"endOfRange\":5000},{\"entityId\":\"1786133852\",\"endOfRange\":10000}]}],\"featureFlags\":[{\"id\":\"4195505407\",\"key\":\"boolean_feature\",\"rolloutId\":\"\",\"experimentIds\":[],\"variables\":[]},{\"id\":\"3926744821\",\"key\":\"double_single_variable_feature\",\"rolloutId\":\"\",\"experimentIds\":[\"2201520193\"],\"variables\":[{\"id\":\"4111654444\",\"key\":\"double_variable\",\"type\":\"double\",\"defaultValue\":\"14.99\"}]},{\"id\":\"3281420120\",\"key\":\"integer_single_variable_feature\",\"rolloutId\":\"2048875663\",\"experimentIds\":[],\"variables\":[{\"id\":\"593964691\",\"key\":\"integer_variable\",\"type\":\"integer\",\"defaultValue\":\"7\"}]},{\"id\":\"2591051011\",\"key\":\"boolean_single_variable_feature\",\"rolloutId\":\"\",\"experimentIds\":[],\"variables\":[{\"id\":\"3974680341\",\"key\":\"boolean_variable\",\"type\":\"boolean\",\"defaultValue\":\"true\"}]},{\"id\":\"2079378557\",\"key\":\"string_single_variable_feature\",\"rolloutId\":\"1058508303\",\"experimentIds\":[],\"variables\":[{\"id\":\"2077511132\",\"key\":\"string_variable\",\"type\":\"string\",\"defaultValue\":\"wingardium leviosa\"}]},{\"id\":\"3263342226\",\"key\":\"multi_variate_feature\",\"rolloutId\":\"813411034\",\"experimentIds\":[\"3262035800\"],\"variables\":[{\"id\":\"675244127\",\"key\":\"first_letter\",\"type\":\"string\",\"defaultValue\":\"H\"},{\"id\":\"4052219963\",\"key\":\"rest_of_name\",\"type\":\"string\",\"defaultValue\":\"arry\"}]},{\"id\":\"3263342226\",\"key\":\"mutex_group_feature\",\"rolloutId\":\"\",\"experimentIds\":[\"4138322202\",\"1786133852\"],\"variables\":[{\"id\":\"2059187672\",\"key\":\"correlating_variation_name\",\"type\":\"string\",\"defaultValue\":null}]}],\"rollouts\":[{\"id\":\"1058508303\",\"experiments\":[{\"id\":\"1785077004\",\"key\":\"1785077004\",\"status\":\"Running\",\"layerId\":\"1058508303\",\"audienceIds\":[],\"forcedVariations\":{},\"variations\":[{\"id\":\"1566407342\",\"key\":\"1566407342\",\"featureEnabled\":true,\"variables\":[{\"id\":\"2077511132\",\"value\":\"lumos\"}]}],\"trafficAllocation\":[{\"entityId\":\"1566407342\",\"endOfRange\":5000}]}]},{\"id\":\"813411034\",\"experiments\":[{\"id\":\"3421010877\",\"key\":\"3421010877\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[\"3468206642\"],\"forcedVariations\":{},\"variations\":[{\"id\":\"521740985\",\"key\":\"521740985\",\"variables\":[{\"id\":\"675244127\",\"value\":\"G\"},{\"id\":\"4052219963\",\"value\":\"odric\"}]}],\"trafficAllocation\":[{\"entityId\":\"521740985\",\"endOfRange\":5000}]},{\"id\":\"600050626\",\"key\":\"600050626\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[\"3988293898\"],\"forcedVariations\":{},\"variations\":[{\"id\":\"180042646\",\"key\":\"180042646\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"S\"},{\"id\":\"4052219963\",\"value\":\"alazar\"}]}],\"trafficAllocation\":[{\"entityId\":\"180042646\",\"endOfRange\":5000}]},{\"id\":\"2637642575\",\"key\":\"2637642575\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[\"4194404272\"],\"forcedVariations\":{},\"variations\":[{\"id\":\"2346257680\",\"key\":\"2346257680\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"D\"},{\"id\":\"4052219963\",\"value\":\"udley\"}]}],\"trafficAllocation\":[{\"entityId\":\"2346257680\",\"endOfRange\":5000}]},{\"id\":\"828245624\",\"key\":\"828245624\",\"status\":\"Running\",\"layerId\":\"813411034\",\"audienceIds\":[],\"forcedVariations\":{},\"variations\":[{\"id\":\"3137445031\",\"key\":\"3137445031\",\"featureEnabled\":true,\"variables\":[{\"id\":\"675244127\",\"value\":\"M\"},{\"id\":\"4052219963\",\"value\":\"uggle\"}]}],\"trafficAllocation\":[{\"entityId\":\"3137445031\",\"endOfRange\":5000}]}]},{\"id\":\"2048875663\",\"experiments\":[{\"id\":\"3794675122\",\"key\":\"3794675122\",\"status\":\"Running\",\"layerId\":\"2048875663\",\"audienceIds\":[],\"forcedVariations\":{},\"variations\":[{\"id\":\"589640735\",\"key\":\"589640735\",\"featureEnabled\":true,\"variables\":[]}],\"trafficAllocation\":[{\"entityId\":\"589640735\",\"endOfRange\":10000}]}]}],\"variables\":[]}" - - guard let config = try? ProjectConfig(datafile: json) else { - return - } - - let decisionService = DefaultDecisionService(userProfileService: DefaultUserProfileService()) - - let experiment = config.project.experiments.filter({$0.key == "typed_audience_experiment"}).first - let attr = ["integerKey": 1, "doubleKey": 99.0, "booleanKey": true, "nationality": "English"] as [String: Any] - let variation = decisionService.getVariation(config: config, experiment: experiment!, userId: "1234", attributes: attr) - - XCTAssertNotNil(variation) - - XCTAssertNotNil(config) - - } - -} diff --git a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift index cca47aac..b483e391 100644 --- a/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/DatafileHandlerTests_MultiClients.swift @@ -25,7 +25,8 @@ class DatafileHandlerTests_MultiClients: XCTestCase { override func setUp() { OTUtils.bindLoggerForTest(.info) OTUtils.createDocumentDirectoryIfNotAvailable() - + OTUtils.clearAllTestStorage(including: testSdkKeyBasename) + makeSdkKeys(10) } diff --git a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift index 8560737a..d5b6372d 100644 --- a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift @@ -18,15 +18,144 @@ import XCTest class UserProfileServiceTests_MultiClients: XCTestCase { + let profiles: [[String: Any]] = [ + [ + "user_id": "0", + "experiment_bucket_map": [ + "1234": [ + "variation_id": "5678" + ] + ] + ], + [ + "user_id": "1", + "experiment_bucket_map": [ + "1234": [ + "variation_id": "5678" + ] + ] + ] + ] + override func setUpWithError() throws { - OTUtils.bindLoggerForTest(.info) + OTUtils.bindLoggerForTest(.error) + OTUtils.clearAllUPS() } override func tearDownWithError() throws { + OTUtils.clearAllUPS() + } + + func testConcurrentAccess() { + let ups = DefaultUserProfileService() + let userId1 = "0" + let userId2 = "1" + + let result = OTUtils.runConcurrent(count: 100) { item in + (0..<100).forEach{ _ in + ups.save(userProfile: self.profiles[0]) + ups.save(userProfile: self.profiles[1]) + + let up1 = ups.lookup(userId: userId1)! + let up2 = ups.lookup(userId: userId2)! + + XCTAssertEqual(up1["user_id"] as? String, userId1) + if let a = up1["experiment_bucket_map"] as? [String: [String: String]], let variation = a["1234"]?["variation_id"] { + XCTAssertEqual(variation, "5678") + } + XCTAssertEqual(up2["user_id"] as? String, userId2) + if let a = up2["experiment_bucket_map"] as? [String: [String: String]], let variation = a["1234"]?["variation_id"] { + XCTAssertEqual(variation, "5678") + } + } + } + + XCTAssertTrue(result, "Concurrent tasks timed out") + } + + func testConcurrentUpdateFromDecisionService() { + // this test is validating thread-safety of read-modify-write UPS operations from decision-service + + let ups = DefaultUserProfileService() + let ds = DefaultDecisionService(userProfileService: ups) + let numThreads = 4 + let numUsers = 2 + let numEventsPerThread = 10 + + let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 10) { thIdx in + print("[MultiClients] UpdateFromDecisionService starts: \(thIdx)") + for userIdx in 0.. String? { @@ -184,6 +193,10 @@ class OTUtils { ups.save(userProfile: profile) } + static func clearAllUPS() { + UserDefaults.standard.removeObject(forKey: "user-profile-service") + } + // MARK: - events static func makeEventForDispatch(url: String? = nil, event: BatchEvent? = nil) -> EventForDispatch { @@ -206,50 +219,24 @@ class OTUtils { anonymizeIP: true, enrichDecisions: true) } - - // MARK: - files - static func saveAFile(name: String, data: Data) -> URL? { - let ds = DataStoreFile(storeName: name, async: false) - ds.saveItem(forKey: name, value: data) - - return ds.url + static func clearAllEventQueues() { + removeAllFiles(including: "OPTEventQueue", in: .documentDirectory) + removeAllFiles(including: "OPTEventQueue", in: .cachesDirectory) } - static func removeAFile(name: String) -> URL? { - let ds = DataStoreFile(storeName: name, async: false) - ds.removeItem(forKey: name) - - return ds.url - } + // MARK: - datafiles static func createDatafileCache(sdkKey: String, contents: String? = nil) { let data = (contents ?? "datafile-for-\(sdkKey)").data(using: .utf8)! _ = saveAFile(name: sdkKey, data: data) } - static func clearAllTestStorage(including: String) { - removeAllFiles(including: including) - removeAllUserDefaults(including: including) - } - - static func clearAllEventQueues() { - removeAllFiles(including: "OPTEventQueue") - } - - static func removeAllFiles(including: String) { + static func clearAllDataFiles(including: String) { removeAllFiles(including: including, in: .documentDirectory) removeAllFiles(including: including, in: .cachesDirectory) } - static func removeAllUserDefaults(including: String) { - let allKeys = UserDefaults.standard.dictionaryRepresentation().keys - allKeys.filter{ $0.contains(including) }.forEach{ itemKey in - UserDefaults.standard.removeObject(forKey: itemKey) - //print("[OTUtils] removed UserDefaults: '\(itemKey)'") - } - } - static func removeAllFiles(including: String, in directory: FileManager.SearchPathDirectory) { if let docUrl = FileManager.default.urls(for: directory, in: .userDomainMask).first { if let names = try? FileManager.default.contentsOfDirectory(atPath: docUrl.path) { @@ -268,6 +255,26 @@ class OTUtils { } } + static func clearAllLastModifiedData() { + removeAllUserDefaults(including: "LastModified") + } + + // MARK: - files + + static func saveAFile(name: String, data: Data) -> URL? { + let ds = DataStoreFile(storeName: name, async: false) + ds.saveItem(forKey: name, value: data) + + return ds.url + } + + static func removeAFile(name: String) -> URL? { + let ds = DataStoreFile(storeName: name, async: false) + ds.removeItem(forKey: name) + + return ds.url + } + static func createDocumentDirectoryIfNotAvailable() { // documentDirectory may not exist for simulator unit test (iOS11+). create it if not found. @@ -288,6 +295,17 @@ class OTUtils { } } + // MARK: - UserDefaults + + static func removeAllUserDefaults(including: String) { + let allKeys = UserDefaults.standard.dictionaryRepresentation().keys + allKeys.filter{ $0.contains(including) }.forEach{ itemKey in + UserDefaults.standard.removeObject(forKey: itemKey) + //print("[OTUtils] removed UserDefaults: '\(itemKey)'") + } + UserDefaults.standard.synchronize() + } + // MARK: - concurrency static func runConcurrent(for items: [String], From 5a73bc34c96c4f784b4192a9703fb5b9eedd3997 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 10 May 2021 10:24:42 -0700 Subject: [PATCH 20/26] add rmwlock for ups --- .../DefaultUserProfileService.swift | 28 ++++--------------- .../Protocols/OPTUserProfileService.swift | 10 +++---- .../DefaultDecisionService.swift | 28 ++++++++++++------- ...UserProfileServiceTests_MultiClients.swift | 1 - 4 files changed, 28 insertions(+), 39 deletions(-) diff --git a/Sources/Customization/DefaultUserProfileService.swift b/Sources/Customization/DefaultUserProfileService.swift index 64f9e8bd..5bb2cde6 100644 --- a/Sources/Customization/DefaultUserProfileService.swift +++ b/Sources/Customization/DefaultUserProfileService.swift @@ -104,29 +104,7 @@ open class DefaultUserProfileService: OPTUserProfileService { defaults.synchronize() } } - - open func add(userProfile: UPProfile) { - guard let userId = userProfile[UserProfileKeys.kUserId] as? String else { return } - - lock.async { - var curProfile = self.profiles?[userId] ?? OPTUserProfileService.UPProfile() - - let curBucketMap = curProfile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() - let addBucketMap = userProfile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? - OPTUserProfileService.UPBucketMap() - let newBucketMap = curBucketMap.merging(addBucketMap) { _, new in new } - - curProfile[UserProfileKeys.kBucketMap] = newBucketMap - curProfile[UserProfileKeys.kUserId] = userId - - self.profiles?[userId] = curProfile - let defaults = UserDefaults.standard - defaults.set(self.profiles, forKey: self.kStorageName) - defaults.synchronize() - } - } - - + open func reset(userProfiles: UserProfileData? = nil) { lock.async { self.profiles = userProfiles ?? UserProfileData() @@ -135,5 +113,9 @@ open class DefaultUserProfileService: OPTUserProfileService { defaults.synchronize() } } + +// open func getRMWLock() -> DispatchQueue? { +// return rmwLock +// } } diff --git a/Sources/Customization/Protocols/OPTUserProfileService.swift b/Sources/Customization/Protocols/OPTUserProfileService.swift index e8586b52..3f11b968 100644 --- a/Sources/Customization/Protocols/OPTUserProfileService.swift +++ b/Sources/Customization/Protocols/OPTUserProfileService.swift @@ -42,15 +42,15 @@ struct UserProfileKeys { - Parameter userProfile: The user profile. **/ func save(userProfile: UPProfile) - - func add(userProfile: UPProfile) + + func getRMWLock() -> DispatchQueue? } -extension OPTUserProfileService { +public extension OPTUserProfileService { - func add(userProfile: UPProfile) { - save(userProfile: userProfile) + @objc func getRMWLock() -> DispatchQueue? { + return nil } } diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 39b7c047..332c6663 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -388,17 +388,25 @@ extension DefaultDecisionService { func saveProfile(userId: String, experimentId: String, variationId: String) { - var profile = userProfileService.lookup(userId: userId) ?? OPTUserProfileService.UPProfile() - - var bucketMap = profile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() - bucketMap[experimentId] = [UserProfileKeys.kVariationId: variationId] - - profile[UserProfileKeys.kBucketMap] = bucketMap - profile[UserProfileKeys.kUserId] = userId - - userProfileService.save(userProfile: profile) + let rmw = { + var profile = self.userProfileService.lookup(userId: userId) ?? OPTUserProfileService.UPProfile() + + var bucketMap = profile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() + bucketMap[experimentId] = [UserProfileKeys.kVariationId: variationId] + + profile[UserProfileKeys.kBucketMap] = bucketMap + profile[UserProfileKeys.kUserId] = userId + + self.userProfileService.save(userProfile: profile) + + self.logger.i(.savedVariationInUserProfile(variationId, experimentId, userId)) + } - logger.i(.savedVariationInUserProfile(variationId, experimentId, userId)) + if let lock = userProfileService.getRMWLock() { + lock.sync { rmw() } + } else { + rmw() + } } } diff --git a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift index d5b6372d..ee093ee0 100644 --- a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift @@ -156,6 +156,5 @@ class UserProfileServiceTests_MultiClients: XCTestCase { print("[MultiClients] UpdateFromDecisionService validated: \(thIdx)") } } - } From 7be70649b846979172002a6f5b209a2e90545ff7 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 10 May 2021 11:27:47 -0700 Subject: [PATCH 21/26] add global ups lock --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 34 +++++++++++++++++++ .../DefaultUserProfileService.swift | 7 +--- .../Protocols/OPTUserProfileService.swift | 10 ------ .../DefaultDecisionService.swift | 8 +---- Sources/Utils/GlobalLocks.swift | 21 ++++++++++++ 5 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 Sources/Utils/GlobalLocks.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 0b991c07..92963504 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1450,6 +1450,22 @@ 6EA0FB27251A5AEC00EC002D /* bucketer_test3.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EA0FB1E251A5AEC00EC002D /* bucketer_test3.json */; }; 6EA0FB28251A5AEC00EC002D /* bucketer_test3.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EA0FB1E251A5AEC00EC002D /* bucketer_test3.json */; }; 6EA0FB29251A5AEC00EC002D /* bucketer_test3.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EA0FB1E251A5AEC00EC002D /* bucketer_test3.json */; }; + 6EA15AD02649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15AD12649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15AD22649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15AD32649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15AD42649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15AD52649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15AD62649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15AD72649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15AD82649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15AD92649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15ADA2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15ADB2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15ADC2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15ADD2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15ADE2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; + 6EA15ADF2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; 6EA2CC242345618E001E7531 /* OptimizelyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */; }; 6EA2CC252345618E001E7531 /* OptimizelyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */; }; 6EA2CC262345618E001E7531 /* OptimizelyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */; }; @@ -2009,6 +2025,7 @@ 6E981FC1232C363300FADDD6 /* DecisionListenerTests_Datafile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecisionListenerTests_Datafile.swift; sourceTree = ""; }; 6E994B3325A3E6EA00999262 /* DecisionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionResponse.swift; sourceTree = ""; }; 6EA0FB1E251A5AEC00EC002D /* bucketer_test3.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = bucketer_test3.json; sourceTree = ""; }; + 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalLocks.swift; sourceTree = ""; }; 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyConfig.swift; sourceTree = ""; }; 6EA425082218E41500B074B5 /* OptimizelyTests-Common-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-Common-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA4255B2218E58400B074B5 /* OptimizelyTests-DataModel-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-DataModel-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2312,6 +2329,7 @@ 6E424BFB263228FD0081004A /* AtomicDictionary.swift */, 6E75167222C520D400B2B157 /* Utils.swift */, 6E75167322C520D400B2B157 /* SDKVersion.swift */, + 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */, ); path = Utils; sourceTree = ""; @@ -3655,6 +3673,7 @@ 6E14CD8E2423F9A700010234 /* FeatureVariable.swift in Sources */, 6E14CD8D2423F9A700010234 /* ProjectConfig.swift in Sources */, 0B97DD9F249D4A23003DE606 /* SemanticVersion.swift in Sources */, + 6EA15AD72649A43000978D4D /* GlobalLocks.swift in Sources */, 6E14CD8F2423F9A700010234 /* Rollout.swift in Sources */, 6E14CD892423F9A100010234 /* ConditionLeaf.swift in Sources */, 6E14CD9F2423F9C300010234 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -3754,6 +3773,7 @@ 6E424CBF26324B1D0081004A /* AtomicArray.swift in Sources */, 6E424CC026324B1D0081004A /* AtomicDictionary.swift in Sources */, 6E5D12212638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, + 6EA15AD62649A43000978D4D /* GlobalLocks.swift in Sources */, 6E424CC126324B1D0081004A /* Utils.swift in Sources */, 6E424CC226324B1D0081004A /* SDKVersion.swift in Sources */, 6E424C88263249B80081004A /* DatafileHandlerTests_MultiClients.swift in Sources */, @@ -3793,6 +3813,7 @@ 6E75183522C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E7517D522C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75187122C520D400B2B157 /* Variation.swift in Sources */, + 6EA15AD12649A43000978D4D /* GlobalLocks.swift in Sources */, 6ECB60CB234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E7516A722C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75177322C520D400B2B157 /* SDKVersion.swift in Sources */, @@ -3909,6 +3930,7 @@ 6E7517D022C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6E75180022C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 0B97DDA3249D4A26003DE606 /* SemanticVersion.swift in Sources */, + 6EA15ADC2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E9B11B322C5489500C22D81 /* MockUrlSession.swift in Sources */, 6E7517DC22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75178622C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4008,6 +4030,7 @@ 6E75184422C520D400B2B157 /* Event.swift in Sources */, 6E75194022C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E7518E022C520D400B2B157 /* ConditionLeaf.swift in Sources */, + 6EA15AD82649A43000978D4D /* GlobalLocks.swift in Sources */, 6E7518B022C520D400B2B157 /* Group.swift in Sources */, 6E75185022C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E7516CE22C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4068,6 +4091,7 @@ 6E7518FB22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E9B11B222C5489400C22D81 /* OTUtils.swift in Sources */, 6E7518D722C520D400B2B157 /* AttributeValue.swift in Sources */, + 6EA15ADB2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E7516AD22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75194F22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7516D122C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4146,6 +4170,7 @@ 6E9B116322C5487100C22D81 /* BucketTests_GroupToExp.swift in Sources */, 6E7516AF22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6EF8DE2524BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 6EA15ADD2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E75194522C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185522C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E7516C722C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, @@ -4278,6 +4303,7 @@ 6E34A6212319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B119D22C5488300C22D81 /* UserAttributeTests.swift in Sources */, 6E5D12292638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, + 6EA15ADE2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E75183E22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B11A022C5488300C22D81 /* ExperimentTests.swift in Sources */, 6E7516EC22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, @@ -4368,6 +4394,7 @@ 6E9B114922C5486E00C22D81 /* BucketTests_GroupToExp.swift in Sources */, 6E75182B22C520D400B2B157 /* BatchEvent.swift in Sources */, 6EF8DE1E24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, + 6EA15AD52649A43000978D4D /* GlobalLocks.swift in Sources */, 6E75190322C520D500B2B157 /* Attribute.swift in Sources */, 6E75192722C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7516F122C520D400B2B157 /* OptimizelyError.swift in Sources */, @@ -4500,6 +4527,7 @@ 6E34A61C2319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B118722C5488100C22D81 /* UserAttributeTests.swift in Sources */, 6E5D12242638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, + 6EA15AD92649A43000978D4D /* GlobalLocks.swift in Sources */, 6E75183922C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B118A22C5488100C22D81 /* ExperimentTests.swift in Sources */, 6E7516E722C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, @@ -4586,6 +4614,7 @@ 6E9B11E322C548AF00C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176022C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75192A22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, + 6EA15ADA2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E7517DA22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517E622C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171822C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -4672,6 +4701,7 @@ 6E9B11E522C548B100C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176522C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75192F22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, + 6EA15ADF2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E7517DF22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517EB22C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171D22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -4762,6 +4792,7 @@ 6E75172A22C520D400B2B157 /* Constants.swift in Sources */, 6E7516A622C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75189422C520D400B2B157 /* Experiment.swift in Sources */, + 6EA15AD02649A43000978D4D /* GlobalLocks.swift in Sources */, 6ECB60CA234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E75177222C520D400B2B157 /* SDKVersion.swift in Sources */, 6E75188822C520D400B2B157 /* Project.swift in Sources */, @@ -4878,6 +4909,7 @@ 6E7517CA22C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6E7517FA22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 0B97DD9D249D4A22003DE606 /* SemanticVersion.swift in Sources */, + 6EA15AD42649A43000978D4D /* GlobalLocks.swift in Sources */, 6E9B11A722C5489200C22D81 /* MockUrlSession.swift in Sources */, 6E7517D622C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75178022C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4903,6 +4935,7 @@ 75C71A0225E454460084187E /* OptimizelyClient.swift in Sources */, 75C71A0325E454460084187E /* OptimizelyClient+ObjC.swift in Sources */, 75C71A0425E454460084187E /* OptimizelyResult.swift in Sources */, + 6EA15AD32649A43000978D4D /* GlobalLocks.swift in Sources */, 75C71A0525E454460084187E /* OptimizelyConfig.swift in Sources */, 75C71A0625E454460084187E /* OptimizelyConfig+ObjC.swift in Sources */, 75C71C3925E45A2B0084187E /* WatchBackgroundNotifier.swift in Sources */, @@ -5008,6 +5041,7 @@ BD6485522491474500F30986 /* Constants.swift in Sources */, BD6485532491474500F30986 /* DefaultLogger.swift in Sources */, BD6485542491474500F30986 /* Experiment.swift in Sources */, + 6EA15AD22649A43000978D4D /* GlobalLocks.swift in Sources */, BD6485552491474500F30986 /* OptimizelyConfig+ObjC.swift in Sources */, BD6485562491474500F30986 /* SDKVersion.swift in Sources */, BD6485572491474500F30986 /* Project.swift in Sources */, diff --git a/Sources/Customization/DefaultUserProfileService.swift b/Sources/Customization/DefaultUserProfileService.swift index 5bb2cde6..104a3c31 100644 --- a/Sources/Customization/DefaultUserProfileService.swift +++ b/Sources/Customization/DefaultUserProfileService.swift @@ -77,8 +77,6 @@ open class DefaultUserProfileService: OPTUserProfileService { var profiles: UserProfileData? let lock = DispatchQueue(label: "com.optimizely.UserProfileService") let kStorageName = "user-profile-service" - - let rmwLock = DispatchQueue(label: "com.optimizely.UserProfileService-RMW") public required init() { lock.async { @@ -113,9 +111,6 @@ open class DefaultUserProfileService: OPTUserProfileService { defaults.synchronize() } } - -// open func getRMWLock() -> DispatchQueue? { -// return rmwLock -// } + } diff --git a/Sources/Customization/Protocols/OPTUserProfileService.swift b/Sources/Customization/Protocols/OPTUserProfileService.swift index 3f11b968..263dd399 100644 --- a/Sources/Customization/Protocols/OPTUserProfileService.swift +++ b/Sources/Customization/Protocols/OPTUserProfileService.swift @@ -43,14 +43,4 @@ struct UserProfileKeys { **/ func save(userProfile: UPProfile) - func getRMWLock() -> DispatchQueue? - -} - -public extension OPTUserProfileService { - - @objc func getRMWLock() -> DispatchQueue? { - return nil - } - } diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 332c6663..dda546e5 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -388,7 +388,7 @@ extension DefaultDecisionService { func saveProfile(userId: String, experimentId: String, variationId: String) { - let rmw = { + GlobalLocks.ups.sync { var profile = self.userProfileService.lookup(userId: userId) ?? OPTUserProfileService.UPProfile() var bucketMap = profile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() @@ -401,12 +401,6 @@ extension DefaultDecisionService { self.logger.i(.savedVariationInUserProfile(variationId, experimentId, userId)) } - - if let lock = userProfileService.getRMWLock() { - lock.sync { rmw() } - } else { - rmw() - } } } diff --git a/Sources/Utils/GlobalLocks.swift b/Sources/Utils/GlobalLocks.swift new file mode 100644 index 00000000..f1c0de6c --- /dev/null +++ b/Sources/Utils/GlobalLocks.swift @@ -0,0 +1,21 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +struct GlobalLocks { + static let ups = DispatchQueue(label: "ups-rmw") +} From 0c00ecc90b89d60618433126fe62b8d8344a2431 Mon Sep 17 00:00:00 2001 From: Jae Kim Date: Mon, 10 May 2021 11:54:00 -0700 Subject: [PATCH 22/26] move a shred ups lock to decision-service --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 34 ------------------- .../DefaultUserProfileService.swift | 7 ++-- .../Protocols/OPTUserProfileService.swift | 2 +- .../DefaultDecisionService.swift | 4 ++- Sources/Utils/GlobalLocks.swift | 21 ------------ ...UserProfileServiceTests_MultiClients.swift | 18 +++------- 6 files changed, 11 insertions(+), 75 deletions(-) delete mode 100644 Sources/Utils/GlobalLocks.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 92963504..0b991c07 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1450,22 +1450,6 @@ 6EA0FB27251A5AEC00EC002D /* bucketer_test3.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EA0FB1E251A5AEC00EC002D /* bucketer_test3.json */; }; 6EA0FB28251A5AEC00EC002D /* bucketer_test3.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EA0FB1E251A5AEC00EC002D /* bucketer_test3.json */; }; 6EA0FB29251A5AEC00EC002D /* bucketer_test3.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EA0FB1E251A5AEC00EC002D /* bucketer_test3.json */; }; - 6EA15AD02649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15AD12649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15AD22649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15AD32649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15AD42649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15AD52649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15AD62649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15AD72649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15AD82649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15AD92649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15ADA2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15ADB2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15ADC2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15ADD2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15ADE2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; - 6EA15ADF2649A43000978D4D /* GlobalLocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */; }; 6EA2CC242345618E001E7531 /* OptimizelyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */; }; 6EA2CC252345618E001E7531 /* OptimizelyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */; }; 6EA2CC262345618E001E7531 /* OptimizelyConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */; }; @@ -2025,7 +2009,6 @@ 6E981FC1232C363300FADDD6 /* DecisionListenerTests_Datafile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DecisionListenerTests_Datafile.swift; sourceTree = ""; }; 6E994B3325A3E6EA00999262 /* DecisionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecisionResponse.swift; sourceTree = ""; }; 6EA0FB1E251A5AEC00EC002D /* bucketer_test3.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = bucketer_test3.json; sourceTree = ""; }; - 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalLocks.swift; sourceTree = ""; }; 6EA2CC232345618E001E7531 /* OptimizelyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyConfig.swift; sourceTree = ""; }; 6EA425082218E41500B074B5 /* OptimizelyTests-Common-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-Common-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 6EA4255B2218E58400B074B5 /* OptimizelyTests-DataModel-tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "OptimizelyTests-DataModel-tvOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -2329,7 +2312,6 @@ 6E424BFB263228FD0081004A /* AtomicDictionary.swift */, 6E75167222C520D400B2B157 /* Utils.swift */, 6E75167322C520D400B2B157 /* SDKVersion.swift */, - 6EA15ACF2649A43000978D4D /* GlobalLocks.swift */, ); path = Utils; sourceTree = ""; @@ -3673,7 +3655,6 @@ 6E14CD8E2423F9A700010234 /* FeatureVariable.swift in Sources */, 6E14CD8D2423F9A700010234 /* ProjectConfig.swift in Sources */, 0B97DD9F249D4A23003DE606 /* SemanticVersion.swift in Sources */, - 6EA15AD72649A43000978D4D /* GlobalLocks.swift in Sources */, 6E14CD8F2423F9A700010234 /* Rollout.swift in Sources */, 6E14CD892423F9A100010234 /* ConditionLeaf.swift in Sources */, 6E14CD9F2423F9C300010234 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -3773,7 +3754,6 @@ 6E424CBF26324B1D0081004A /* AtomicArray.swift in Sources */, 6E424CC026324B1D0081004A /* AtomicDictionary.swift in Sources */, 6E5D12212638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, - 6EA15AD62649A43000978D4D /* GlobalLocks.swift in Sources */, 6E424CC126324B1D0081004A /* Utils.swift in Sources */, 6E424CC226324B1D0081004A /* SDKVersion.swift in Sources */, 6E424C88263249B80081004A /* DatafileHandlerTests_MultiClients.swift in Sources */, @@ -3813,7 +3793,6 @@ 6E75183522C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E7517D522C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75187122C520D400B2B157 /* Variation.swift in Sources */, - 6EA15AD12649A43000978D4D /* GlobalLocks.swift in Sources */, 6ECB60CB234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E7516A722C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75177322C520D400B2B157 /* SDKVersion.swift in Sources */, @@ -3930,7 +3909,6 @@ 6E7517D022C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6E75180022C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 0B97DDA3249D4A26003DE606 /* SemanticVersion.swift in Sources */, - 6EA15ADC2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E9B11B322C5489500C22D81 /* MockUrlSession.swift in Sources */, 6E7517DC22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75178622C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4030,7 +4008,6 @@ 6E75184422C520D400B2B157 /* Event.swift in Sources */, 6E75194022C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E7518E022C520D400B2B157 /* ConditionLeaf.swift in Sources */, - 6EA15AD82649A43000978D4D /* GlobalLocks.swift in Sources */, 6E7518B022C520D400B2B157 /* Group.swift in Sources */, 6E75185022C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E7516CE22C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4091,7 +4068,6 @@ 6E7518FB22C520D500B2B157 /* UserAttribute.swift in Sources */, 6E9B11B222C5489400C22D81 /* OTUtils.swift in Sources */, 6E7518D722C520D400B2B157 /* AttributeValue.swift in Sources */, - 6EA15ADB2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E7516AD22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75194F22C520D500B2B157 /* OPTDatafileHandler.swift in Sources */, 6E7516D122C520D400B2B157 /* OPTLogger.swift in Sources */, @@ -4170,7 +4146,6 @@ 6E9B116322C5487100C22D81 /* BucketTests_GroupToExp.swift in Sources */, 6E7516AF22C520D400B2B157 /* DefaultLogger.swift in Sources */, 6EF8DE2524BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, - 6EA15ADD2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E75194522C520D500B2B157 /* OPTDecisionService.swift in Sources */, 6E75185522C520D400B2B157 /* ProjectConfig.swift in Sources */, 6E7516C722C520D400B2B157 /* DefaultEventDispatcher.swift in Sources */, @@ -4303,7 +4278,6 @@ 6E34A6212319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B119D22C5488300C22D81 /* UserAttributeTests.swift in Sources */, 6E5D12292638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, - 6EA15ADE2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E75183E22C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B11A022C5488300C22D81 /* ExperimentTests.swift in Sources */, 6E7516EC22C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, @@ -4394,7 +4368,6 @@ 6E9B114922C5486E00C22D81 /* BucketTests_GroupToExp.swift in Sources */, 6E75182B22C520D400B2B157 /* BatchEvent.swift in Sources */, 6EF8DE1E24BD1BB2008B9488 /* OptimizelyDecideOption.swift in Sources */, - 6EA15AD52649A43000978D4D /* GlobalLocks.swift in Sources */, 6E75190322C520D500B2B157 /* Attribute.swift in Sources */, 6E75192722C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, 6E7516F122C520D400B2B157 /* OptimizelyError.swift in Sources */, @@ -4527,7 +4500,6 @@ 6E34A61C2319EBB800BAE302 /* Notifications.swift in Sources */, 6E9B118722C5488100C22D81 /* UserAttributeTests.swift in Sources */, 6E5D12242638DDF4000ABFC3 /* MockEventDispatcher.swift in Sources */, - 6EA15AD92649A43000978D4D /* GlobalLocks.swift in Sources */, 6E75183922C520D400B2B157 /* EventForDispatch.swift in Sources */, 6E9B118A22C5488100C22D81 /* ExperimentTests.swift in Sources */, 6E7516E722C520D400B2B157 /* OPTEventDispatcher.swift in Sources */, @@ -4614,7 +4586,6 @@ 6E9B11E322C548AF00C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176022C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75192A22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, - 6EA15ADA2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E7517DA22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517E622C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171822C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -4701,7 +4672,6 @@ 6E9B11E522C548B100C22D81 /* ThrowableConditionListTest.swift in Sources */, 6E75176522C520D400B2B157 /* AtomicProperty.swift in Sources */, 6E75192F22C520D500B2B157 /* DataStoreQueueStack.swift in Sources */, - 6EA15ADF2649A43000978D4D /* GlobalLocks.swift in Sources */, 6E7517DF22C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E7517EB22C520D400B2B157 /* DefaultDecisionService.swift in Sources */, 6E75171D22C520D400B2B157 /* OptimizelyClient+ObjC.swift in Sources */, @@ -4792,7 +4762,6 @@ 6E75172A22C520D400B2B157 /* Constants.swift in Sources */, 6E7516A622C520D400B2B157 /* DefaultLogger.swift in Sources */, 6E75189422C520D400B2B157 /* Experiment.swift in Sources */, - 6EA15AD02649A43000978D4D /* GlobalLocks.swift in Sources */, 6ECB60CA234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */, 6E75177222C520D400B2B157 /* SDKVersion.swift in Sources */, 6E75188822C520D400B2B157 /* Project.swift in Sources */, @@ -4909,7 +4878,6 @@ 6E7517CA22C520D400B2B157 /* DefaultBucketer.swift in Sources */, 6E7517FA22C520D400B2B157 /* DataStoreUserDefaults.swift in Sources */, 0B97DD9D249D4A22003DE606 /* SemanticVersion.swift in Sources */, - 6EA15AD42649A43000978D4D /* GlobalLocks.swift in Sources */, 6E9B11A722C5489200C22D81 /* MockUrlSession.swift in Sources */, 6E7517D622C520D400B2B157 /* DefaultNotificationCenter.swift in Sources */, 6E75178022C520D400B2B157 /* ArrayEventForDispatch+Extension.swift in Sources */, @@ -4935,7 +4903,6 @@ 75C71A0225E454460084187E /* OptimizelyClient.swift in Sources */, 75C71A0325E454460084187E /* OptimizelyClient+ObjC.swift in Sources */, 75C71A0425E454460084187E /* OptimizelyResult.swift in Sources */, - 6EA15AD32649A43000978D4D /* GlobalLocks.swift in Sources */, 75C71A0525E454460084187E /* OptimizelyConfig.swift in Sources */, 75C71A0625E454460084187E /* OptimizelyConfig+ObjC.swift in Sources */, 75C71C3925E45A2B0084187E /* WatchBackgroundNotifier.swift in Sources */, @@ -5041,7 +5008,6 @@ BD6485522491474500F30986 /* Constants.swift in Sources */, BD6485532491474500F30986 /* DefaultLogger.swift in Sources */, BD6485542491474500F30986 /* Experiment.swift in Sources */, - 6EA15AD22649A43000978D4D /* GlobalLocks.swift in Sources */, BD6485552491474500F30986 /* OptimizelyConfig+ObjC.swift in Sources */, BD6485562491474500F30986 /* SDKVersion.swift in Sources */, BD6485572491474500F30986 /* Project.swift in Sources */, diff --git a/Sources/Customization/DefaultUserProfileService.swift b/Sources/Customization/DefaultUserProfileService.swift index 104a3c31..dd1b38af 100644 --- a/Sources/Customization/DefaultUserProfileService.swift +++ b/Sources/Customization/DefaultUserProfileService.swift @@ -77,10 +77,11 @@ open class DefaultUserProfileService: OPTUserProfileService { var profiles: UserProfileData? let lock = DispatchQueue(label: "com.optimizely.UserProfileService") let kStorageName = "user-profile-service" - + public required init() { lock.async { self.profiles = UserDefaults.standard.dictionary(forKey: self.kStorageName) as? UserProfileData ?? UserProfileData() + } } @@ -102,7 +103,7 @@ open class DefaultUserProfileService: OPTUserProfileService { defaults.synchronize() } } - + open func reset(userProfiles: UserProfileData? = nil) { lock.async { self.profiles = userProfiles ?? UserProfileData() @@ -111,6 +112,4 @@ open class DefaultUserProfileService: OPTUserProfileService { defaults.synchronize() } } - } - diff --git a/Sources/Customization/Protocols/OPTUserProfileService.swift b/Sources/Customization/Protocols/OPTUserProfileService.swift index 263dd399..158aed8a 100644 --- a/Sources/Customization/Protocols/OPTUserProfileService.swift +++ b/Sources/Customization/Protocols/OPTUserProfileService.swift @@ -42,5 +42,5 @@ struct UserProfileKeys { - Parameter userProfile: The user profile. **/ func save(userProfile: UPProfile) - + } diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index dda546e5..9d1813bf 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -28,6 +28,8 @@ class DefaultDecisionService: OPTDecisionService { let userProfileService: OPTUserProfileService lazy var logger = OPTLoggerFactory.getLogger() + static let upsRMWLock = DispatchQueue(label: "ups-rmw") + init(userProfileService: OPTUserProfileService) { self.bucketer = DefaultBucketer() self.userProfileService = userProfileService @@ -388,7 +390,7 @@ extension DefaultDecisionService { func saveProfile(userId: String, experimentId: String, variationId: String) { - GlobalLocks.ups.sync { + DefaultDecisionService.upsRMWLock.sync { var profile = self.userProfileService.lookup(userId: userId) ?? OPTUserProfileService.UPProfile() var bucketMap = profile[UserProfileKeys.kBucketMap] as? OPTUserProfileService.UPBucketMap ?? OPTUserProfileService.UPBucketMap() diff --git a/Sources/Utils/GlobalLocks.swift b/Sources/Utils/GlobalLocks.swift deleted file mode 100644 index f1c0de6c..00000000 --- a/Sources/Utils/GlobalLocks.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright 2021, Optimizely, Inc. and contributors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation - -struct GlobalLocks { - static let ups = DispatchQueue(label: "ups-rmw") -} diff --git a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift index ee093ee0..0bbdc561 100644 --- a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift @@ -76,25 +76,21 @@ class UserProfileServiceTests_MultiClients: XCTestCase { func testConcurrentUpdateFromDecisionService() { // this test is validating thread-safety of read-modify-write UPS operations from decision-service - let ups = DefaultUserProfileService() - let ds = DefaultDecisionService(userProfileService: ups) + let ups = DefaultUserProfileService() // shared instance + let ds = DefaultDecisionService(userProfileService: ups) // shared instance let numThreads = 4 let numUsers = 2 let numEventsPerThread = 10 let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 10) { thIdx in - print("[MultiClients] UpdateFromDecisionService starts: \(thIdx)") for userIdx in 0.. Date: Tue, 11 May 2021 09:39:27 -0700 Subject: [PATCH 23/26] fix per review --- .../DefaultDecisionService.swift | 1 + ...UserProfileServiceTests_MultiClients.swift | 33 +++++++++---------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Sources/Implementation/DefaultDecisionService.swift b/Sources/Implementation/DefaultDecisionService.swift index 9d1813bf..3c5415c3 100644 --- a/Sources/Implementation/DefaultDecisionService.swift +++ b/Sources/Implementation/DefaultDecisionService.swift @@ -28,6 +28,7 @@ class DefaultDecisionService: OPTDecisionService { let userProfileService: OPTUserProfileService lazy var logger = OPTLoggerFactory.getLogger() + // user-profile-service read-modify-write lock for supporting multiple clients static let upsRMWLock = DispatchQueue(label: "ups-rmw") init(userProfileService: OPTUserProfileService) { diff --git a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift index 0bbdc561..7fdc6a31 100644 --- a/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/UserProfileServiceTests_MultiClients.swift @@ -30,8 +30,8 @@ class UserProfileServiceTests_MultiClients: XCTestCase { [ "user_id": "1", "experiment_bucket_map": [ - "1234": [ - "variation_id": "5678" + "12345": [ + "variation_id": "56789" ] ] ] @@ -60,13 +60,12 @@ class UserProfileServiceTests_MultiClients: XCTestCase { let up2 = ups.lookup(userId: userId2)! XCTAssertEqual(up1["user_id"] as? String, userId1) - if let a = up1["experiment_bucket_map"] as? [String: [String: String]], let variation = a["1234"]?["variation_id"] { - XCTAssertEqual(variation, "5678") - } + let exp1 = up1["experiment_bucket_map"] as! [String: [String: String]] + XCTAssertEqual(exp1["1234"]!["variation_id"], "5678") + XCTAssertEqual(up2["user_id"] as? String, userId2) - if let a = up2["experiment_bucket_map"] as? [String: [String: String]], let variation = a["1234"]?["variation_id"] { - XCTAssertEqual(variation, "5678") - } + let exp2 = up2["experiment_bucket_map"] as! [String: [String: String]] + XCTAssertEqual(exp2["12345"]!["variation_id"], "56789") } } @@ -77,7 +76,7 @@ class UserProfileServiceTests_MultiClients: XCTestCase { // this test is validating thread-safety of read-modify-write UPS operations from decision-service let ups = DefaultUserProfileService() // shared instance - let ds = DefaultDecisionService(userProfileService: ups) // shared instance + let decisionService = DefaultDecisionService(userProfileService: ups) // shared instance let numThreads = 4 let numUsers = 2 let numEventsPerThread = 10 @@ -88,7 +87,7 @@ class UserProfileServiceTests_MultiClients: XCTestCase { let userId = String(userIdx) let experimentId = String((thIdx * numEventsPerThread) + eventIdx) let variationId = experimentId - ds.saveProfile(userId: userId, experimentId: experimentId, variationId: variationId) + decisionService.saveProfile(userId: userId, experimentId: experimentId, variationId: variationId) } } } @@ -99,7 +98,7 @@ class UserProfileServiceTests_MultiClients: XCTestCase { for eventIdx in 0.. Date: Tue, 11 May 2021 10:14:30 -0700 Subject: [PATCH 24/26] add logger tests for multi clients --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 4 + .../LoggerTests_MultiClients.swift | 76 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 Tests/OptimizelyTests-MultiClients/LoggerTests_MultiClients.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 0b991c07..1635bb40 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1515,6 +1515,7 @@ 6ECB60D4234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */; }; 6ECB60D5234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */; }; 6ECB60D7234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60D6234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m */; }; + 6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */; }; 6EF41A332522BE1900EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */; }; 6EF41A422522BE2100EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */; }; 6EF8DE0624B8DA58008B9488 /* decide_datafile.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EF8DE0524B8DA58008B9488 /* decide_datafile.json */; }; @@ -2026,6 +2027,7 @@ 6ECB60C5234D329500016D41 /* OptimizelyClientTests_OptimizelyConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptimizelyClientTests_OptimizelyConfig.swift; sourceTree = ""; }; 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OptimizelyConfig+ObjC.swift"; sourceTree = ""; }; 6ECB60D6234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OptimizelyClientTests_OptimizelyConfig_Objc.m; sourceTree = ""; }; + 6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests_MultiClients.swift; sourceTree = ""; }; 6EF8DE0524B8DA58008B9488 /* decide_datafile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_datafile.json; sourceTree = ""; }; 6EF8DE0A24BD1BB1008B9488 /* OptimizelyDecision.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyDecision.swift; sourceTree = ""; }; 6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyDecideOption.swift; sourceTree = ""; }; @@ -2230,6 +2232,7 @@ 6E2D5DAD26338CA00002077F /* AtomicDictionaryTests.swift */, 6E5D120C2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift */, 6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */, + 6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */, ); path = "OptimizelyTests-MultiClients"; sourceTree = ""; @@ -3687,6 +3690,7 @@ 6E424CF426324B620081004A /* DecisionInfo.swift in Sources */, 6E5D120D2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift in Sources */, 6E424CF526324B620081004A /* DefaultBucketer.swift in Sources */, + 6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */, 6E424D5426324C4D0081004A /* OptimizelyUserContext.swift in Sources */, 6E424CF626324B620081004A /* DefaultNotificationCenter.swift in Sources */, 6E424CF726324B620081004A /* DefaultDecisionService.swift in Sources */, diff --git a/Tests/OptimizelyTests-MultiClients/LoggerTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/LoggerTests_MultiClients.swift new file mode 100644 index 00000000..01265f1c --- /dev/null +++ b/Tests/OptimizelyTests-MultiClients/LoggerTests_MultiClients.swift @@ -0,0 +1,76 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class LoggerTests_MultiClients: XCTestCase { + + override func setUpWithError() throws { + } + + override func tearDownWithError() throws { + } + + func testConcurrentLogging() { + OTUtils.bindLoggerForTest(.debug) + let logger = OPTLoggerFactory.getLogger() + + let numThreads = 10 + let numEventsPerThread = 100 + + let result = OTUtils.runConcurrent(count: numThreads) { item in + for _ in 0.. Date: Tue, 11 May 2021 15:47:07 -0700 Subject: [PATCH 25/26] fix handlerRegistryService to support multi clients --- OptimizelySwiftSDK.xcodeproj/project.pbxproj | 4 + Sources/Utils/HandlerRegistryService.swift | 50 +++--- ...lerRegistryServiceTests_MultiClients.swift | 148 ++++++++++++++++++ 3 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 Tests/OptimizelyTests-MultiClients/HandlerRegistryServiceTests_MultiClients.swift diff --git a/OptimizelySwiftSDK.xcodeproj/project.pbxproj b/OptimizelySwiftSDK.xcodeproj/project.pbxproj index 1635bb40..08d42292 100644 --- a/OptimizelySwiftSDK.xcodeproj/project.pbxproj +++ b/OptimizelySwiftSDK.xcodeproj/project.pbxproj @@ -1516,6 +1516,7 @@ 6ECB60D5234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */; }; 6ECB60D7234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 6ECB60D6234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m */; }; 6EE5911A2649CF640013AD66 /* LoggerTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */; }; + 6EE5918E264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE5918D264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift */; }; 6EF41A332522BE1900EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */; }; 6EF41A422522BE2100EAADF1 /* OptimizelyUserContextTests_Decide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E2D34B8250AD14000A0CDFE /* OptimizelyUserContextTests_Decide.swift */; }; 6EF8DE0624B8DA58008B9488 /* decide_datafile.json in Resources */ = {isa = PBXBuildFile; fileRef = 6EF8DE0524B8DA58008B9488 /* decide_datafile.json */; }; @@ -2028,6 +2029,7 @@ 6ECB60C9234D5D9C00016D41 /* OptimizelyConfig+ObjC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OptimizelyConfig+ObjC.swift"; sourceTree = ""; }; 6ECB60D6234E601A00016D41 /* OptimizelyClientTests_OptimizelyConfig_Objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OptimizelyClientTests_OptimizelyConfig_Objc.m; sourceTree = ""; }; 6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests_MultiClients.swift; sourceTree = ""; }; + 6EE5918D264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandlerRegistryServiceTests_MultiClients.swift; sourceTree = ""; }; 6EF8DE0524B8DA58008B9488 /* decide_datafile.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = decide_datafile.json; sourceTree = ""; }; 6EF8DE0A24BD1BB1008B9488 /* OptimizelyDecision.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyDecision.swift; sourceTree = ""; }; 6EF8DE0B24BD1BB2008B9488 /* OptimizelyDecideOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptimizelyDecideOption.swift; sourceTree = ""; }; @@ -2233,6 +2235,7 @@ 6E5D120C2638DCE1000ABFC3 /* EventDispatcherTests_MultiClients.swift */, 6E474C8C263C889E00ABDFF8 /* UserProfileServiceTests_MultiClients.swift */, 6EE591192649CF640013AD66 /* LoggerTests_MultiClients.swift */, + 6EE5918D264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift */, ); path = "OptimizelyTests-MultiClients"; sourceTree = ""; @@ -3742,6 +3745,7 @@ 6E424CD326324B270081004A /* OptimizelyError.swift in Sources */, 6E424D5326324C4D0081004A /* OptimizelyUserContext+ObjC.swift in Sources */, 6E424CD426324B270081004A /* OptimizelyLogLevel.swift in Sources */, + 6EE5918E264AF44B0013AD66 /* HandlerRegistryServiceTests_MultiClients.swift in Sources */, 6E424CD526324B270081004A /* OptimizelyClient.swift in Sources */, 6E424CD626324B270081004A /* OptimizelyClient+ObjC.swift in Sources */, 6E424CD726324B270081004A /* OptimizelyResult.swift in Sources */, diff --git a/Sources/Utils/HandlerRegistryService.swift b/Sources/Utils/HandlerRegistryService.swift index 90aba289..030bb504 100644 --- a/Sources/Utils/HandlerRegistryService.swift +++ b/Sources/Utils/HandlerRegistryService.swift @@ -17,7 +17,6 @@ import Foundation class HandlerRegistryService { - static let shared = HandlerRegistryService() struct ServiceKey: Hashable { @@ -25,35 +24,30 @@ class HandlerRegistryService { var sdkKey: String? } - var binders: AtomicProperty<[ServiceKey: BinderProtocol]> = { - var binders = AtomicProperty<[ServiceKey: BinderProtocol]>() - binders.property = [ServiceKey: BinderProtocol]() - return binders - }() + var binders = AtomicProperty(property: [ServiceKey: BinderProtocol]()) private init() {} func registerBinding(binder: BinderProtocol) { let sk = ServiceKey(service: "\(type(of: binder.service))", sdkKey: binder.sdkKey) - if binders.property?[sk] != nil { - } else { - binders.property?[sk] = binder + binders.performAtomic{ prop in + if prop[sk] == nil { + prop[sk] = binder + } } } func injectComponent(service: Any, sdkKey: String? = nil, isReintialize: Bool=false) -> Any? { var result: Any? - // first look up global. Then look up if there is a local. - let skLocal = ServiceKey(service: "\(type(of: service))", sdkKey: sdkKey) - let skGlobal = ServiceKey(service: "\(type(of: service))", sdkKey: nil) - let binderToUse = binders.property?[skLocal] ?? binders.property?[skGlobal] + // service key is shared for all sdkKeys when sdkKey is nil + let sk = ServiceKey(service: "\(type(of: service))", sdkKey: sdkKey) + + let binderToUse = binders.property?[sk] func updateBinder(b: BinderProtocol) { - if binders.property?[skLocal] != nil { - binders.property?[skLocal] = b - } else { - binders.property?[skGlobal] = b + binders.performAtomic{ prop in + prop[sk] = b } } @@ -110,7 +104,7 @@ struct Binder: BinderProtocol { var sdkKey: String? var service: Any var strategy: ReInitializeStrategy = .reCreate - var factory: () -> Any? = { return nil as Any? } + var factory: () -> Any? var isSingleton = false var inst: T? @@ -125,33 +119,39 @@ struct Binder: BinderProtocol { } } - init(sdkKey: String? = nil, service: Any, strategy: ReInitializeStrategy = .reCreate, factory: @escaping (() -> Any?) = { ()->Any? in { return nil as Any? }}, isSingleton: Bool = false, inst: T? = nil) { + init(sdkKey: String? = nil, + service: Any, + strategy: ReInitializeStrategy = .reCreate, + factory: (() -> Any?)? = nil, + isSingleton: Bool = false, + inst: T? = nil) { + self.sdkKey = sdkKey self.service = service self.strategy = strategy - self.factory = factory + self.factory = factory ?? { return nil as Any? } self.isSingleton = isSingleton self.inst = inst } } extension HandlerRegistryService { - func injectLogger(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTLogger? { + func injectLogger(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTLogger? { return injectComponent(service: OPTLogger.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTLogger? } - func injectNotificationCenter(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTNotificationCenter? { + func injectNotificationCenter(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTNotificationCenter? { return injectComponent(service: OPTNotificationCenter.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTNotificationCenter? } - func injectDecisionService(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTDecisionService? { + func injectDecisionService(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTDecisionService? { return injectComponent(service: OPTDecisionService.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTDecisionService? } - func injectEventDispatcher(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTEventDispatcher? { + func injectEventDispatcher(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTEventDispatcher? { return injectComponent(service: OPTEventDispatcher.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTEventDispatcher? } - func injectDatafileHandler(sdkKey: String? = nil, isReintialize: Bool=false) -> OPTDatafileHandler? { + func injectDatafileHandler(sdkKey: String? = nil, isReintialize: Bool = false) -> OPTDatafileHandler? { return injectComponent(service: OPTDatafileHandler.self, sdkKey: sdkKey, isReintialize: isReintialize) as! OPTDatafileHandler? } } diff --git a/Tests/OptimizelyTests-MultiClients/HandlerRegistryServiceTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/HandlerRegistryServiceTests_MultiClients.swift new file mode 100644 index 00000000..c159f4dc --- /dev/null +++ b/Tests/OptimizelyTests-MultiClients/HandlerRegistryServiceTests_MultiClients.swift @@ -0,0 +1,148 @@ +// +// Copyright 2021, Optimizely, Inc. and contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest + +class HandlerRegistryServiceTests_MultiClients: XCTestCase { + + override func setUpWithError() throws { + } + + override func tearDownWithError() throws { + } + + func testConcurrentAccess_Singleton() { + // this type used for all handlers except for logger + + let numThreads = 10 + let numEventsPerThread = 100 + + let registry = HandlerRegistryService.shared + + let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 300) { thIdx in + for idx in 0..(service: service, + factory: type(of: componentIn).init) + + registry.registerBinding(binder: binder) + if let componentOut = registry.injectComponent(service: service, + isReintialize: isReinitialize) as? DefaultLogger { + XCTAssertEqual(String(describing: componentOut), String(describing: componentIn)) + } else { + self.dumpRegistry() + XCTAssert(false, "injectComponent failed: \(binder)") + } + } + } + + XCTAssertTrue(result, "Concurrent tasks timed out") + } + + func testConcurrentAccess_Random() { + let numThreads = 10 + let numEventsPerThread = 100 + + let registry = HandlerRegistryService.shared + + let result = OTUtils.runConcurrent(count: numThreads, timeoutInSecs: 300) { thIdx in + for idx in 0.. Date: Tue, 11 May 2021 15:57:25 -0700 Subject: [PATCH 26/26] clean up --- .../LoggerTests_MultiClients.swift | 49 ------------------- 1 file changed, 49 deletions(-) diff --git a/Tests/OptimizelyTests-MultiClients/LoggerTests_MultiClients.swift b/Tests/OptimizelyTests-MultiClients/LoggerTests_MultiClients.swift index 01265f1c..69ab9936 100644 --- a/Tests/OptimizelyTests-MultiClients/LoggerTests_MultiClients.swift +++ b/Tests/OptimizelyTests-MultiClients/LoggerTests_MultiClients.swift @@ -24,53 +24,4 @@ class LoggerTests_MultiClients: XCTestCase { override func tearDownWithError() throws { } - func testConcurrentLogging() { - OTUtils.bindLoggerForTest(.debug) - let logger = OPTLoggerFactory.getLogger() - - let numThreads = 10 - let numEventsPerThread = 100 - - let result = OTUtils.runConcurrent(count: numThreads) { item in - for _ in 0..