Skip to content

Commit 335e8b9

Browse files
authored
fix: add close() API to OptimizelyClient and EventDispatcher for test support (#270)
* add close() API to OptimizelyClient and EventDispatcher for test support * fix build warning
1 parent 4d26821 commit 335e8b9

File tree

8 files changed

+168
-57
lines changed

8 files changed

+168
-57
lines changed

DemoSwiftApp/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
152152
}
153153
})
154154

155-
_ = optimizely.notificationCenter.addLogEventNotificationListener(logEventListener: { (url, event) in
155+
_ = notificationCenter.addLogEventNotificationListener(logEventListener: { (url, event) in
156156
print("Received logEvent notification: \(url) \(event)")
157157
})
158158
}

OptimizelySwiftSDK.xcodeproj/project.pbxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2183,6 +2183,7 @@
21832183
hasScannedForEncodings = 0;
21842184
knownRegions = (
21852185
en,
2186+
Base,
21862187
);
21872188
mainGroup = 0B7CB0B821AC5FE2007B77E5;
21882189
productRefGroup = 0B7CB0C321AC5FE2007B77E5 /* Products */;

Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ DEPENDENCIES:
55
- SwiftLint
66

77
SPEC REPOS:
8-
https://github.com/cocoapods/specs.git:
8+
https://github.com/CocoaPods/Specs.git:
99
- SwiftLint
1010

1111
SPEC CHECKSUMS:
1212
SwiftLint: 79d48a17c6565dc286c37efb8322c7b450f95c67
1313

1414
PODFILE CHECKSUM: 94877e134bcac3007b5b3e8cdbd17fa6eb0fd375
1515

16-
COCOAPODS: 1.6.1
16+
COCOAPODS: 1.8.0

Sources/Customization/DefaultEventDispatcher.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher {
5454
var observerProjectId: NSObjectProtocol?
5555
var observerRevision: NSObjectProtocol?
5656

57-
5857
public init(batchSize: Int = DefaultValues.batchSize,
5958
backingStore: DataStoreType = .file,
6059
dataStoreName: String = "OPTEventQueue",
@@ -67,7 +66,6 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher {
6766
self.backingStore = backingStore
6867
self.backingStoreName = dataStoreName
6968

70-
7169
switch backingStore {
7270
case .file:
7371
self.dataStore = DataStoreQueueStackImpl<EventForDispatch>(queueStackName: "OPTEventQueue",
@@ -256,12 +254,12 @@ open class DefaultEventDispatcher: BackgroundingCallbacks, OPTEventDispatcher {
256254
extension DefaultEventDispatcher {
257255

258256
func addProjectChangeNotificationObservers() {
259-
observerProjectId = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyProjectIdChange, object: nil, queue: nil) { [weak self] (notif) in
257+
observerProjectId = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyProjectIdChange, object: nil, queue: nil) { [weak self] (_) in
260258
self?.logger.d("Event flush triggered by datafile projectId change")
261259
self?.flushEvents()
262260
}
263261

264-
observerRevision = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyRevisionChange, object: nil, queue: nil) { [weak self] (notif) in
262+
observerRevision = NotificationCenter.default.addObserver(forName: .didReceiveOptimizelyRevisionChange, object: nil, queue: nil) { [weak self] (_) in
265263
self?.logger.d("Event flush triggered by datafile revision change")
266264
self?.flushEvents()
267265
}
@@ -276,4 +274,11 @@ extension DefaultEventDispatcher {
276274
}
277275
}
278276

277+
// MARK: - Tests
278+
279+
open func close() {
280+
self.flushEvents()
281+
self.dispatcher.sync {}
282+
}
283+
279284
}

Sources/Customization/Protocols/OPTEventDispatcher.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,12 @@ public protocol OPTEventDispatcher {
3131

3232
/// Attempts to flush the event queue if there are any events to process.
3333
func flushEvents()
34+
35+
/// flush events in queue synchrnonous (optional for testing support)
36+
func close()
37+
}
38+
39+
public extension OPTEventDispatcher {
40+
// override this for testing support only
41+
func close() {}
3442
}

Sources/Optimizely/OptimizelyClient.swift

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -631,11 +631,15 @@ extension OptimizelyClient {
631631
let event = EventForDispatch(body: body)
632632
self.sendEventToDispatcher(event: event, completionHandler: nil)
633633

634+
// send notification in sync mode (functionally same as async here since it's already in background thread),
635+
// but this will make testing simpler (timing control)
636+
634637
self.sendActivateNotification(experiment: experiment,
635638
variation: variation,
636639
userId: userId,
637640
attributes: attributes,
638-
event: event)
641+
event: event,
642+
async: false)
639643
}
640644

641645
}
@@ -661,11 +665,15 @@ extension OptimizelyClient {
661665
let event = EventForDispatch(body: body)
662666
self.sendEventToDispatcher(event: event, completionHandler: nil)
663667

668+
// send notification in sync mode (functionally same as async here since it's already in background thread),
669+
// but this will make testing simpler (timing control)
670+
664671
self.sendTrackNotification(eventKey: eventKey,
665672
userId: userId,
666673
attributes: attributes,
667674
eventTags: eventTags,
668-
event: event)
675+
event: event,
676+
async: false)
669677
}
670678
}
671679

@@ -686,25 +694,31 @@ extension OptimizelyClient {
686694
variation: Variation,
687695
userId: String,
688696
attributes: OptimizelyAttributes?,
689-
event: EventForDispatch) {
697+
event: EventForDispatch,
698+
async: Bool = true) {
690699

691-
self.sendNotification(type: .activate, args: [experiment,
692-
userId,
693-
attributes,
694-
variation,
695-
["url": event.url as Any, "body": event.body as Any]])
700+
self.sendNotification(type: .activate,
701+
args: [experiment,
702+
userId,
703+
attributes,
704+
variation,
705+
["url": event.url as Any, "body": event.body as Any]],
706+
async: async)
696707
}
697708

698709
func sendTrackNotification(eventKey: String,
699710
userId: String,
700711
attributes: OptimizelyAttributes?,
701712
eventTags: OptimizelyEventTags?,
702-
event: EventForDispatch) {
703-
self.sendNotification(type: .track, args: [eventKey,
704-
userId,
705-
attributes,
706-
eventTags,
707-
["url": event.url as Any, "body": event.body as Any]])
713+
event: EventForDispatch,
714+
async: Bool = true) {
715+
self.sendNotification(type: .track,
716+
args: [eventKey,
717+
userId,
718+
attributes,
719+
eventTags,
720+
["url": event.url as Any, "body": event.body as Any]],
721+
async: async)
708722
}
709723

710724
func sendDecisionNotification(decisionType: Constants.DecisionType,
@@ -716,22 +730,25 @@ extension OptimizelyClient {
716730
featureEnabled: Bool? = nil,
717731
variableKey: String? = nil,
718732
variableType: String? = nil,
719-
variableValue: Any? = nil) {
720-
self.sendNotification(type: .decision, args: [decisionType.rawValue,
721-
userId,
722-
attributes ?? OptimizelyAttributes(),
723-
self.makeDecisionInfo(decisionType: decisionType,
724-
experiment: experiment,
725-
variation: variation,
726-
feature: feature,
727-
featureEnabled: featureEnabled,
728-
variableKey: variableKey,
729-
variableType: variableType,
730-
variableValue: variableValue)])
731-
}
732-
733-
func sendDatafileChangeNotification(data: Data) {
734-
self.sendNotification(type: .datafileChange, args: [data])
733+
variableValue: Any? = nil,
734+
async: Bool = true) {
735+
self.sendNotification(type: .decision,
736+
args: [decisionType.rawValue,
737+
userId,
738+
attributes ?? OptimizelyAttributes(),
739+
self.makeDecisionInfo(decisionType: decisionType,
740+
experiment: experiment,
741+
variation: variation,
742+
feature: feature,
743+
featureEnabled: featureEnabled,
744+
variableKey: variableKey,
745+
variableType: variableType,
746+
variableValue: variableValue)],
747+
async: async)
748+
}
749+
750+
func sendDatafileChangeNotification(data: Data, async: Bool = true) {
751+
self.sendNotification(type: .datafileChange, args: [data], async: async)
735752
}
736753

737754
func makeDecisionInfo(decisionType: Constants.DecisionType,
@@ -784,12 +801,31 @@ extension OptimizelyClient {
784801
return decisionInfo
785802
}
786803

787-
func sendNotification(type: NotificationType, args: [Any?]) {
788-
// callback in background thread
789-
eventLock.async {
804+
func sendNotification(type: NotificationType, args: [Any?], async: Bool = true) {
805+
let notify = {
790806
// make sure that notificationCenter is not-nil (still registered when async notification is called)
791807
self.notificationCenter?.sendNotifications(type: type.rawValue, args: args)
792808
}
809+
810+
if async {
811+
eventLock.async {
812+
notify()
813+
}
814+
} else {
815+
notify()
816+
}
793817
}
794818

795819
}
820+
821+
// MARK: - For test support
822+
823+
extension OptimizelyClient {
824+
825+
public func close() {
826+
datafileHandler.stopUpdates(sdkKey: sdkKey)
827+
eventLock.sync {}
828+
eventDispatcher?.close()
829+
}
830+
831+
}

Sources/Utils/Constants.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,4 @@ struct Constants {
5050
static let variation = "variationKey"
5151
}
5252

53-
5453
}

Tests/OptimizelyTests-Common/EventDispatcherTests_Batch.swift

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,7 @@ extension EventDispatcherTests_Batch {
316316

317317
// flush
318318

319-
eventDispatcher.flushEvents()
320-
eventDispatcher.dispatcher.sync {}
319+
eventDispatcher.close()
321320

322321
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 1, "all events should be batched together")
323322
let batch = eventDispatcher.sendRequestedEvents[0]
@@ -345,9 +344,8 @@ extension EventDispatcherTests_Batch {
345344
(kUrlA, batchEventB),
346345
(kUrlA, batchEventA)])
347346

348-
eventDispatcher.flushEvents()
349-
eventDispatcher.dispatcher.sync {}
350-
347+
eventDispatcher.close()
348+
351349
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 1)
352350
let batch = eventDispatcher.sendRequestedEvents[0]
353351
let batchedEvents = try! JSONDecoder().decode(BatchEvent.self, from: batch.body)
@@ -379,9 +377,8 @@ extension EventDispatcherTests_Batch {
379377
(kUrlA, batchEventA),
380378
(kUrlB, batchEventB)])
381379

382-
eventDispatcher.flushEvents()
383-
eventDispatcher.dispatcher.sync {}
384-
380+
eventDispatcher.close()
381+
385382
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 2, "different urls should not be batched")
386383

387384
// first 2 events batched together
@@ -428,9 +425,8 @@ extension EventDispatcherTests_Batch {
428425
eventDispatcher.dispatchEvent(event: makeInvalidEventForDispatchWithWrongData(), completionHandler: nil)
429426
eventDispatcher.dispatchEvent(event: makeEventForDispatch(url: kUrlA, event: batchEventA), completionHandler: nil)
430427

431-
eventDispatcher.flushEvents()
432-
eventDispatcher.dispatcher.sync {}
433-
428+
eventDispatcher.close()
429+
434430
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 2, "different urls should not be batched")
435431

436432
// first 2 events batched together
@@ -480,9 +476,8 @@ extension EventDispatcherTests_Batch {
480476
dispatchMultipleEvents([(kUrlA, batchEventA),
481477
(kUrlA, batchEventA)])
482478

483-
eventDispatcher.flushEvents()
484-
eventDispatcher.dispatcher.sync {}
485-
479+
eventDispatcher.close()
480+
486481
let maxFailureCount = 3 + 1 // DefaultEventDispatcher.maxFailureCount + 1
487482

488483
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, maxFailureCount, "repeated the same request several times before giveup")
@@ -506,8 +501,7 @@ extension EventDispatcherTests_Batch {
506501
eventDispatcher.forceError = false
507502

508503
// assume flushEvents called again on next timer fire
509-
eventDispatcher.flushEvents()
510-
eventDispatcher.dispatcher.sync {}
504+
eventDispatcher.close()
511505

512506
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, maxFailureCount + 1, "only one more since succeeded")
513507
XCTAssertEqual(eventDispatcher.sendRequestedEvents[3], eventDispatcher.sendRequestedEvents[0])
@@ -920,6 +914,74 @@ extension EventDispatcherTests_Batch {
920914

921915
}
922916

917+
// MARK: - OptimizleyClient: Close()
918+
919+
extension EventDispatcherTests_Batch {
920+
921+
func testCloseForOptimizleyClinet() {
922+
// this tests timer-based dispatch, available for iOS 10+
923+
guard #available(iOS 10.0, tvOS 10.0, *) else { return }
924+
925+
self.eventDispatcher = TestEventDispatcher(eventFileName: uniqueFileName, removeDatafileObserver: false)
926+
927+
eventDispatcher.batchSize = 1000 // big, won't flush
928+
eventDispatcher.timerInterval = 99999 // timer is big, won't fire
929+
930+
let optimizely = OptimizelyClient(sdkKey: "SDKKey",
931+
eventDispatcher: eventDispatcher,
932+
defaultLogLevel: .debug)
933+
let datafile = OTUtils.loadJSONDatafile("empty_datafile")!
934+
935+
// (1) should have no flush
936+
937+
eventDispatcher.exp = XCTestExpectation(description: "timer")
938+
eventDispatcher.exp?.isInverted = true
939+
940+
try! optimizely.start(datafile: datafile)
941+
942+
dispatchMultipleEvents([(kUrlA, batchEventA),
943+
(kUrlA, batchEventA),
944+
(kUrlA, batchEventA)])
945+
946+
wait(for: [eventDispatcher.exp!], timeout: 3)
947+
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 0, "should not flush yet")
948+
949+
// (2) should flush/batch all on close()
950+
951+
optimizely.close()
952+
953+
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 1, "should flush on the revision change")
954+
var batch = eventDispatcher.sendRequestedEvents[0]
955+
var batchedEvents = try! JSONDecoder().decode(BatchEvent.self, from: batch.body)
956+
XCTAssertEqual(batchedEvents.visitors.count, 3)
957+
eventDispatcher.sendRequestedEvents.removeAll()
958+
959+
// (3) should have no flush
960+
961+
eventDispatcher.exp = XCTestExpectation(description: "timer")
962+
eventDispatcher.exp?.isInverted = true
963+
964+
try! optimizely.start(datafile: datafile)
965+
966+
dispatchMultipleEvents([(kUrlA, batchEventB),
967+
(kUrlA, batchEventA)])
968+
969+
wait(for: [eventDispatcher.exp!], timeout: 3)
970+
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 0, "should not flush yet")
971+
972+
// (4) should flush/batch all on close()
973+
974+
optimizely.close()
975+
976+
XCTAssertEqual(eventDispatcher.sendRequestedEvents.count, 1, "should flush on the revision change")
977+
batch = eventDispatcher.sendRequestedEvents[0]
978+
batchedEvents = try! JSONDecoder().decode(BatchEvent.self, from: batch.body)
979+
XCTAssertEqual(batchedEvents.visitors.count, 2)
980+
981+
}
982+
983+
}
984+
923985
// MARK: - Random testing
924986

925987
extension EventDispatcherTests_Batch {

0 commit comments

Comments
 (0)