From 19ab9ef75530377ad459480070b88d17056aa324 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Wed, 26 Jul 2023 14:38:50 +0100 Subject: [PATCH 1/6] Provider API level events --- Sources/OpenFeature/OpenFeatureAPI.swift | 53 ++++++++++++++++--- .../{ => Provider}/FeatureProvider.swift | 4 +- .../{ => Provider}/NoOpProvider.swift | 9 +++- .../{ => Provider}/ProviderEvaluation.swift | 0 .../OpenFeature/Provider/ProviderEvents.swift | 15 ++++++ .../{ => Provider}/ProviderMetadata.swift | 0 .../Helpers/AlwaysBrokenProvider.swift | 4 +- .../Helpers/DoSomethingProvider.swift | 4 +- .../ProviderEventsTests.swift | 22 ++++++++ 9 files changed, 97 insertions(+), 14 deletions(-) rename Sources/OpenFeature/{ => Provider}/FeatureProvider.swift (93%) rename Sources/OpenFeature/{ => Provider}/NoOpProvider.swift (90%) rename Sources/OpenFeature/{ => Provider}/ProviderEvaluation.swift (100%) create mode 100644 Sources/OpenFeature/Provider/ProviderEvents.swift rename Sources/OpenFeature/{ => Provider}/ProviderMetadata.swift (100%) create mode 100644 Tests/OpenFeatureTests/ProviderEventsTests.swift diff --git a/Sources/OpenFeature/OpenFeatureAPI.swift b/Sources/OpenFeature/OpenFeatureAPI.swift index 080d0f6..9f50920 100644 --- a/Sources/OpenFeature/OpenFeatureAPI.swift +++ b/Sources/OpenFeature/OpenFeatureAPI.swift @@ -7,22 +7,25 @@ public class OpenFeatureAPI { private var _context: EvaluationContext? private(set) var hooks: [any Hook] = [] + private let providerNotificationCentre = NotificationCenter() + /// The ``OpenFeatureAPI`` singleton static public let shared = OpenFeatureAPI() public init() { } - public func setProvider(provider: FeatureProvider) async { - await self.setProvider(provider: provider, initialContext: nil) + public func setProvider(provider: FeatureProvider) { + self.setProvider(provider: provider, initialContext: nil) } - public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) async { + public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) { self._provider = provider if let context = initialContext { self._context = context } - await provider.initialize(initialContext: self._context) + + provider.initialize(initialContext: self._context) } public func getProvider() -> FeatureProvider? { @@ -33,9 +36,9 @@ public class OpenFeatureAPI { self._provider = nil } - public func setEvaluationContext(evaluationContext: EvaluationContext) async { + public func setEvaluationContext(evaluationContext: EvaluationContext) { + getProvider()?.onContextSet(oldContext: self._context, newContext: evaluationContext) self._context = evaluationContext - await getProvider()?.onContextSet(oldContext: self._context, newContext: evaluationContext) } public func getEvaluationContext() -> EvaluationContext? { @@ -61,4 +64,42 @@ public class OpenFeatureAPI { public func clearHooks() { self.hooks.removeAll() } + + // MARK: Provider Events + + public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) { + providerNotificationCentre.addObserver( + observer, + selector: selector, + name: event.notification, + object: nil + ) + } + + public func removeHandler(observer: Any, event: ProviderEvent) { + providerNotificationCentre.removeObserver(observer, name: event.notification, object: nil) + } + + public func emitEvent( + _ event: ProviderEvent, + provider: FeatureProvider, + error: Error? = nil, + details: [AnyHashable: Any]? = nil + ) { + var userInfo: [AnyHashable: Any] = [:] + userInfo[ProviderEventDetailsKeyProvider] = provider + + if let error { + userInfo[ProviderEventDetailsKeyError] = error + } + + if let details { + userInfo.merge(details) { $1 } // Merge, keeping value from `details` if any conflicts + } + +// let notification = Notification(name: event.notification, userInfo: userInfo) +// providerNotificationQueue.enqueue(notification, postingStyle: .asap) + + providerNotificationCentre.post(name: event.notification, object: nil, userInfo: userInfo) + } } diff --git a/Sources/OpenFeature/FeatureProvider.swift b/Sources/OpenFeature/Provider/FeatureProvider.swift similarity index 93% rename from Sources/OpenFeature/FeatureProvider.swift rename to Sources/OpenFeature/Provider/FeatureProvider.swift index 0b2ca5a..44a5c7b 100644 --- a/Sources/OpenFeature/FeatureProvider.swift +++ b/Sources/OpenFeature/Provider/FeatureProvider.swift @@ -6,10 +6,10 @@ public protocol FeatureProvider { var metadata: ProviderMetadata { get } /// Called by OpenFeatureAPI whenever the new Provider is registered - func initialize(initialContext: EvaluationContext?) async + func initialize(initialContext: EvaluationContext?) /// Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application - func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) async + func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws -> ProviderEvaluation< diff --git a/Sources/OpenFeature/NoOpProvider.swift b/Sources/OpenFeature/Provider/NoOpProvider.swift similarity index 90% rename from Sources/OpenFeature/NoOpProvider.swift rename to Sources/OpenFeature/Provider/NoOpProvider.swift index f5d41bf..7727d9e 100644 --- a/Sources/OpenFeature/NoOpProvider.swift +++ b/Sources/OpenFeature/Provider/NoOpProvider.swift @@ -4,15 +4,20 @@ import Foundation class NoOpProvider: FeatureProvider { public static let passedInDefault = "Passed in default" + public enum Mode { + case normal + case error(message: String) + } + var metadata: ProviderMetadata = NoOpMetadata(name: "No-op provider") var hooks: [any Hook] = [] func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) { - // no-op + OpenFeatureAPI.shared.emitEvent(.configurationChanged, provider: self) } func initialize(initialContext: EvaluationContext?) { - // no-op + OpenFeatureAPI.shared.emitEvent(.ready, provider: self) } func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws diff --git a/Sources/OpenFeature/ProviderEvaluation.swift b/Sources/OpenFeature/Provider/ProviderEvaluation.swift similarity index 100% rename from Sources/OpenFeature/ProviderEvaluation.swift rename to Sources/OpenFeature/Provider/ProviderEvaluation.swift diff --git a/Sources/OpenFeature/Provider/ProviderEvents.swift b/Sources/OpenFeature/Provider/ProviderEvents.swift new file mode 100644 index 0000000..8953766 --- /dev/null +++ b/Sources/OpenFeature/Provider/ProviderEvents.swift @@ -0,0 +1,15 @@ +import Foundation + +public let ProviderEventDetailsKeyProvider = "Provider" +public let ProviderEventDetailsKeyError = "Error" + +public enum ProviderEvent: String { + case ready = "PROVIDER_READY" + case error = "PROVIDER_ERROR" + case configurationChanged = "PROVIDER_CONFIGURATION_CHANGED" + case stale = "PROVIDER_STALE" + + var notification: NSNotification.Name { + NSNotification.Name(rawValue) + } +} diff --git a/Sources/OpenFeature/ProviderMetadata.swift b/Sources/OpenFeature/Provider/ProviderMetadata.swift similarity index 100% rename from Sources/OpenFeature/ProviderMetadata.swift rename to Sources/OpenFeature/Provider/ProviderMetadata.swift diff --git a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift index 433fa55..236c41a 100644 --- a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift @@ -7,11 +7,11 @@ class AlwaysBrokenProvider: FeatureProvider { var hooks: [any Hook] = [] func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) { - // no-op + OpenFeatureAPI.shared.emitEvent(.configurationChanged, provider: self) } func initialize(initialContext: OpenFeature.EvaluationContext?) { - // no-op + OpenFeatureAPI.shared.emitEvent(.error, provider: self, errorMessage: "Always Broken") } func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws diff --git a/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift b/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift index f8cac77..acbfff2 100644 --- a/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift @@ -5,11 +5,11 @@ class DoSomethingProvider: FeatureProvider { public static let name = "Something" func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) { - // no-op + OpenFeatureAPI.shared.emitEvent(.configurationChanged, provider: self) } func initialize(initialContext: OpenFeature.EvaluationContext?) { - // no-op + OpenFeatureAPI.shared.emitEvent(.ready, provider: self) } var hooks: [any OpenFeature.Hook] = [] diff --git a/Tests/OpenFeatureTests/ProviderEventsTests.swift b/Tests/OpenFeatureTests/ProviderEventsTests.swift new file mode 100644 index 0000000..f65b170 --- /dev/null +++ b/Tests/OpenFeatureTests/ProviderEventsTests.swift @@ -0,0 +1,22 @@ +import Foundation +import OpenFeature +import XCTest + +final class ProviderEventsTests: XCTestCase { + func testReadyEventEmitted() { + let provider = DoSomethingProvider() + + OpenFeatureAPI.shared.addHandler( + observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready + ) + + wait(for: [readyExpectation], timeout: 5) + } + + // MARK: Handlers + let readyExpectation = XCTestExpectation(description: "Ready") + + func readyEventEmitted(notification: NSNotification) { + readyExpectation.fulfill() + } +} From 52a15775ab826f767f6bbdb47f3b07b7dcc3f7a5 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Wed, 26 Jul 2023 15:53:20 +0100 Subject: [PATCH 2/6] Client/Provider events --- Sources/OpenFeature/Client.swift | 11 +++++ Sources/OpenFeature/OpenFeatureAPI.swift | 8 ++-- Sources/OpenFeature/OpenFeatureClient.swift | 43 +++++++++++++++++ .../OpenFeature/Provider/ProviderEvents.swift | 3 +- .../DeveloperExperienceTests.swift | 16 +++---- .../Helpers/AlwaysBrokenProvider.swift | 4 +- .../OpenFeatureClientTests.swift | 46 ++++++++++++++++++- .../ProviderEventsTests.swift | 1 + 8 files changed, 115 insertions(+), 17 deletions(-) diff --git a/Sources/OpenFeature/Client.swift b/Sources/OpenFeature/Client.swift index 083f28e..c4783fc 100644 --- a/Sources/OpenFeature/Client.swift +++ b/Sources/OpenFeature/Client.swift @@ -11,4 +11,15 @@ public protocol Client: Features { /// Hooks are run in the order they're added in the before stage. They are run in reverse order for all /// other stages. func addHooks(_ hooks: any Hook...) + + /// Add a handler for a particular provider event + /// - Parameter observer: The object observing the event. + /// - Parameter selector: The selector to call for this event. + /// - Parameter event: The event to listen for. + func addHandler(observer: Any, selector: Selector, event: ProviderEvent) + + /// Remove a handler for a particular provider event + /// - Parameter observer: The object observing the event. + /// - Parameter event: The event being listened to. + func removeHandler(observer: Any, event: ProviderEvent) } diff --git a/Sources/OpenFeature/OpenFeatureAPI.swift b/Sources/OpenFeature/OpenFeatureAPI.swift index 9f50920..0efba7d 100644 --- a/Sources/OpenFeature/OpenFeatureAPI.swift +++ b/Sources/OpenFeature/OpenFeatureAPI.swift @@ -64,8 +64,11 @@ public class OpenFeatureAPI { public func clearHooks() { self.hooks.removeAll() } +} + +// MARK: Provider Events - // MARK: Provider Events +extension OpenFeatureAPI { public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) { providerNotificationCentre.addObserver( @@ -97,9 +100,6 @@ public class OpenFeatureAPI { userInfo.merge(details) { $1 } // Merge, keeping value from `details` if any conflicts } -// let notification = Notification(name: event.notification, userInfo: userInfo) -// providerNotificationQueue.enqueue(notification, postingStyle: .asap) - providerNotificationCentre.post(name: event.notification, object: nil, userInfo: userInfo) } } diff --git a/Sources/OpenFeature/OpenFeatureClient.swift b/Sources/OpenFeature/OpenFeatureClient.swift index cdd933f..022b27d 100644 --- a/Sources/OpenFeature/OpenFeatureClient.swift +++ b/Sources/OpenFeature/OpenFeatureClient.swift @@ -14,11 +14,15 @@ public class OpenFeatureClient: Client { private var hookSupport = HookSupport() private var logger = Logger() + private let providerNotificationCentre = NotificationCenter() + public init(openFeatureApi: OpenFeatureAPI, name: String?, version: String?) { self.openFeatureApi = openFeatureApi self.name = name self.version = version self.metadata = Metadata(name: name) + + subscribeToAllProviderEvents() } public func addHooks(_ hooks: any Hook...) { @@ -196,3 +200,42 @@ extension OpenFeatureClient { throw OpenFeatureError.generalError(message: "Unable to match default value type with flag value type") } } + +// MARK: Events + +extension OpenFeatureClient { + public func subscribeToAllProviderEvents() { + ProviderEvent.allCases.forEach { event in + OpenFeatureAPI.shared.addHandler( + observer: self, + selector: #selector(handleProviderEvent(notification:)), + event: event) + } + } + + public func unsubscribeFromAllProviderEvents() { + ProviderEvent.allCases.forEach { event in + OpenFeatureAPI.shared.removeHandler(observer: self, event: event) + } + } + + @objc public func handleProviderEvent(notification: Notification) { + var userInfo: [AnyHashable: Any] = notification.userInfo ?? [:] + userInfo[ProviderEventDetailsKeyClient] = self + + providerNotificationCentre.post(name: notification.name, object: nil, userInfo: userInfo) + } + + public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) { + providerNotificationCentre.addObserver( + observer, + selector: selector, + name: event.notification, + object: nil + ) + } + + public func removeHandler(observer: Any, event: ProviderEvent) { + providerNotificationCentre.removeObserver(observer, name: event.notification, object: nil) + } +} diff --git a/Sources/OpenFeature/Provider/ProviderEvents.swift b/Sources/OpenFeature/Provider/ProviderEvents.swift index 8953766..5f02e98 100644 --- a/Sources/OpenFeature/Provider/ProviderEvents.swift +++ b/Sources/OpenFeature/Provider/ProviderEvents.swift @@ -1,9 +1,10 @@ import Foundation public let ProviderEventDetailsKeyProvider = "Provider" +public let ProviderEventDetailsKeyClient = "Client" public let ProviderEventDetailsKeyError = "Error" -public enum ProviderEvent: String { +public enum ProviderEvent: String, CaseIterable { case ready = "PROVIDER_READY" case error = "PROVIDER_ERROR" case configurationChanged = "PROVIDER_CONFIGURATION_CHANGED" diff --git a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift index ea2f41a..908af80 100644 --- a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift +++ b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift @@ -11,16 +11,16 @@ final class DeveloperExperienceTests: XCTestCase { XCTAssertEqual(flagValue, "no-op") } - func testSimpleBooleanFlag() async { - await OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) + func testSimpleBooleanFlag() { + OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) let client = OpenFeatureAPI.shared.getClient() let flagValue = client.getValue(key: "test", defaultValue: false) XCTAssertFalse(flagValue) } - func testClientHooks() async { - await OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) + func testClientHooks() { + OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) let client = OpenFeatureAPI.shared.getClient() let booleanHook = BooleanHookMock() @@ -40,8 +40,8 @@ final class DeveloperExperienceTests: XCTestCase { XCTAssertEqual(intHook.finallyAfterCalled, 1) } - func testEvalHooks() async { - await OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) + func testEvalHooks() { + OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) let client = OpenFeatureAPI.shared.getClient() let booleanHook = BooleanHookMock() @@ -61,8 +61,8 @@ final class DeveloperExperienceTests: XCTestCase { XCTAssertEqual(intHook.finallyAfterCalled, 1) } - func testBrokenProvider() async { - await OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) + func testBrokenProvider() { + OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) let client = OpenFeatureAPI.shared.getClient() let details = client.getDetails(key: "test", defaultValue: false) diff --git a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift index 236c41a..ffa5d98 100644 --- a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift @@ -7,11 +7,11 @@ class AlwaysBrokenProvider: FeatureProvider { var hooks: [any Hook] = [] func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) { - OpenFeatureAPI.shared.emitEvent(.configurationChanged, provider: self) + OpenFeatureAPI.shared.emitEvent(.error, provider: self, error: OpenFeatureError.generalError(message: "Always Fails")) } func initialize(initialContext: OpenFeature.EvaluationContext?) { - OpenFeatureAPI.shared.emitEvent(.error, provider: self, errorMessage: "Always Broken") + OpenFeatureAPI.shared.emitEvent(.error, provider: self, error: OpenFeatureError.generalError(message: "Always Fails")) } func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws diff --git a/Tests/OpenFeatureTests/OpenFeatureClientTests.swift b/Tests/OpenFeatureTests/OpenFeatureClientTests.swift index 133e551..6c0b38f 100644 --- a/Tests/OpenFeatureTests/OpenFeatureClientTests.swift +++ b/Tests/OpenFeatureTests/OpenFeatureClientTests.swift @@ -4,8 +4,8 @@ import XCTest @testable import OpenFeature final class OpenFeatureClientTests: XCTestCase { - func testShouldNowThrowIfHookHasDifferentTypeArgument() async { - await OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) + func testShouldNowThrowIfHookHasDifferentTypeArgument() { + OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) OpenFeatureAPI.shared.addHooks(hooks: BooleanHookMock()) let client = OpenFeatureAPI.shared.getClient() @@ -21,4 +21,46 @@ final class OpenFeatureClientTests: XCTestCase { let doubleDetails = client.getDetails(key: "key", defaultValue: 123.1) XCTAssertEqual(doubleDetails.value, 12_310) } + + func testProviderEvents() { + setupExpectations() + + let provider = DoSomethingProvider() + OpenFeatureAPI.shared.setProvider(provider: provider) + + let client = OpenFeatureAPI.shared.getClient() + ProviderEvent.allCases.forEach { event in + client.addHandler(observer: self, selector: #selector(eventEmitted(notification:)), event: event) + + OpenFeatureAPI.shared.emitEvent(event, provider: provider) + if let expectation = eventExpectations[event] { + wait(for: [expectation], timeout: 5) + } else { + XCTFail("No expectation for provider event: \(event)") + } + } + } + + // MARK: Event Handlers + private var eventExpectations: [ProviderEvent: XCTestExpectation] = [:] + + func setupExpectations() { + ProviderEvent.allCases.forEach { event in + eventExpectations[event] = XCTestExpectation(description: event.rawValue) + } + } + + func eventEmitted(notification: NSNotification) { + guard let providerEvent = ProviderEvent(rawValue: notification.name.rawValue) else { + XCTFail("Unexpected provider event: \(notification.name)") + return + } + + guard let expectation = eventExpectations[providerEvent] else { + XCTFail("No expectation for provider event: \(providerEvent)") + return + } + + expectation.fulfill() + } } diff --git a/Tests/OpenFeatureTests/ProviderEventsTests.swift b/Tests/OpenFeatureTests/ProviderEventsTests.swift index f65b170..7ebe226 100644 --- a/Tests/OpenFeatureTests/ProviderEventsTests.swift +++ b/Tests/OpenFeatureTests/ProviderEventsTests.swift @@ -10,6 +10,7 @@ final class ProviderEventsTests: XCTestCase { observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready ) + OpenFeatureAPI.shared.setProvider(provider: provider) wait(for: [readyExpectation], timeout: 5) } From 2c6b63a3bf24e94c99d04159fc96c6de1da18c43 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Mon, 31 Jul 2023 10:55:08 +0100 Subject: [PATCH 3/6] Fix lint warnings --- Sources/OpenFeature/OpenFeatureAPI.swift | 5 ++--- Sources/OpenFeature/OpenFeatureClient.swift | 2 +- Sources/OpenFeature/Provider/ProviderEvents.swift | 6 +++--- Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift | 6 ++++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Sources/OpenFeature/OpenFeatureAPI.swift b/Sources/OpenFeature/OpenFeatureAPI.swift index 0efba7d..4bb0671 100644 --- a/Sources/OpenFeature/OpenFeatureAPI.swift +++ b/Sources/OpenFeature/OpenFeatureAPI.swift @@ -69,7 +69,6 @@ public class OpenFeatureAPI { // MARK: Provider Events extension OpenFeatureAPI { - public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) { providerNotificationCentre.addObserver( observer, @@ -90,10 +89,10 @@ extension OpenFeatureAPI { details: [AnyHashable: Any]? = nil ) { var userInfo: [AnyHashable: Any] = [:] - userInfo[ProviderEventDetailsKeyProvider] = provider + userInfo[providerEventDetailsKeyProvider] = provider if let error { - userInfo[ProviderEventDetailsKeyError] = error + userInfo[providerEventDetailsKeyError] = error } if let details { diff --git a/Sources/OpenFeature/OpenFeatureClient.swift b/Sources/OpenFeature/OpenFeatureClient.swift index 022b27d..e5fd63e 100644 --- a/Sources/OpenFeature/OpenFeatureClient.swift +++ b/Sources/OpenFeature/OpenFeatureClient.swift @@ -221,7 +221,7 @@ extension OpenFeatureClient { @objc public func handleProviderEvent(notification: Notification) { var userInfo: [AnyHashable: Any] = notification.userInfo ?? [:] - userInfo[ProviderEventDetailsKeyClient] = self + userInfo[providerEventDetailsKeyClient] = self providerNotificationCentre.post(name: notification.name, object: nil, userInfo: userInfo) } diff --git a/Sources/OpenFeature/Provider/ProviderEvents.swift b/Sources/OpenFeature/Provider/ProviderEvents.swift index 5f02e98..133ec7a 100644 --- a/Sources/OpenFeature/Provider/ProviderEvents.swift +++ b/Sources/OpenFeature/Provider/ProviderEvents.swift @@ -1,8 +1,8 @@ import Foundation -public let ProviderEventDetailsKeyProvider = "Provider" -public let ProviderEventDetailsKeyClient = "Client" -public let ProviderEventDetailsKeyError = "Error" +public let providerEventDetailsKeyProvider = "Provider" +public let providerEventDetailsKeyClient = "Client" +public let providerEventDetailsKeyError = "Error" public enum ProviderEvent: String, CaseIterable { case ready = "PROVIDER_READY" diff --git a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift index ffa5d98..68057df 100644 --- a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift @@ -7,11 +7,13 @@ class AlwaysBrokenProvider: FeatureProvider { var hooks: [any Hook] = [] func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) { - OpenFeatureAPI.shared.emitEvent(.error, provider: self, error: OpenFeatureError.generalError(message: "Always Fails")) + let error = OpenFeatureError.generalError(message: "Always Fails") + OpenFeatureAPI.shared.emitEvent(.error, provider: self, error: error) } func initialize(initialContext: OpenFeature.EvaluationContext?) { - OpenFeatureAPI.shared.emitEvent(.error, provider: self, error: OpenFeatureError.generalError(message: "Always Fails")) + let error = OpenFeatureError.generalError(message: "Always Fails") + OpenFeatureAPI.shared.emitEvent(.error, provider: self, error: error) } func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws From fc2ead5e1dfdc428f16fec0358f25a52f98661e3 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Wed, 2 Aug 2023 10:00:03 +0100 Subject: [PATCH 4/6] Add some checks on userInfo, removed async from some other tests --- .../FlagEvaluationTests.swift | 51 +++++++++++++++---- Tests/OpenFeatureTests/HookSpecTests.swift | 43 +++++++++++++--- .../ProviderEventsTests.swift | 13 +++-- 3 files changed, 86 insertions(+), 21 deletions(-) diff --git a/Tests/OpenFeatureTests/FlagEvaluationTests.swift b/Tests/OpenFeatureTests/FlagEvaluationTests.swift index 2e22206..c2623e9 100644 --- a/Tests/OpenFeatureTests/FlagEvaluationTests.swift +++ b/Tests/OpenFeatureTests/FlagEvaluationTests.swift @@ -4,19 +4,29 @@ import XCTest @testable import OpenFeature final class FlagEvaluationTests: XCTestCase { + override func setUp() { + OpenFeatureAPI.shared.addHandler( + observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready + ) + + OpenFeatureAPI.shared.addHandler( + observer: self, selector: #selector(errorEventEmitted(notification:)), event: .error + ) + } + func testSingletonPersists() { XCTAssertTrue(OpenFeatureAPI.shared === OpenFeatureAPI.shared) } - func testApiSetsProvider() async { + func testApiSetsProvider() { let provider = NoOpProvider() - await OpenFeatureAPI.shared.setProvider(provider: provider) + OpenFeatureAPI.shared.setProvider(provider: provider) XCTAssertTrue((OpenFeatureAPI.shared.getProvider() as? NoOpProvider) === provider) } - func testProviderMetadata() async { - await OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) + func testProviderMetadata() { + OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) XCTAssertEqual(OpenFeatureAPI.shared.getProviderMetadata()?.name, DoSomethingProvider.name) } @@ -51,8 +61,10 @@ final class FlagEvaluationTests: XCTestCase { XCTAssertEqual(client.hooks.count, 2) } - func testSimpleFlagEvaluation() async { - await OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) + func testSimpleFlagEvaluation() { + OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) + wait(for: [readyExpectation], timeout: 5) + let client = OpenFeatureAPI.shared.getClient() let key = "key" @@ -89,7 +101,9 @@ final class FlagEvaluationTests: XCTestCase { } func testDetailedFlagEvaluation() async { - await OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) + OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) + wait(for: [readyExpectation], timeout: 5) + let client = OpenFeatureAPI.shared.getClient() let key = "key" @@ -132,7 +146,9 @@ final class FlagEvaluationTests: XCTestCase { } func testHooksAreFired() async { - await OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) + OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) + wait(for: [readyExpectation], timeout: 5) + let client = OpenFeatureAPI.shared.getClient() let clientHook = BooleanHookMock() @@ -148,8 +164,10 @@ final class FlagEvaluationTests: XCTestCase { XCTAssertEqual(invocationHook.beforeCalled, 1) } - func testBrokenProvider() async { - await OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) + func testBrokenProvider() { + OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) + wait(for: [errorExpectation], timeout: 5) + let client = OpenFeatureAPI.shared.getClient() XCTAssertFalse(client.getValue(key: "testkey", defaultValue: false)) @@ -167,4 +185,17 @@ final class FlagEvaluationTests: XCTestCase { let client = OpenFeatureAPI.shared.getClient(name: "test", version: nil) XCTAssertEqual(client.metadata.name, "test") } + + // MARK: Event Handlers + let readyExpectation = XCTestExpectation(description: "Ready") + + func readyEventEmitted(notification: NSNotification) { + readyExpectation.fulfill() + } + + let errorExpectation = XCTestExpectation(description: "Error") + + func errorEventEmitted(notification: NSNotification) { + errorExpectation.fulfill() + } } diff --git a/Tests/OpenFeatureTests/HookSpecTests.swift b/Tests/OpenFeatureTests/HookSpecTests.swift index 07b7713..37925f8 100644 --- a/Tests/OpenFeatureTests/HookSpecTests.swift +++ b/Tests/OpenFeatureTests/HookSpecTests.swift @@ -4,12 +4,22 @@ import XCTest @testable import OpenFeature final class HookSpecTests: XCTestCase { - func testNoErrorHookCalled() async { - await OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) - let client = OpenFeatureAPI.shared.getClient() + override func setUp() { + OpenFeatureAPI.shared.addHandler( + observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready + ) - let hook = BooleanHookMock() + OpenFeatureAPI.shared.addHandler( + observer: self, selector: #selector(errorEventEmitted(notification:)), event: .error + ) + } + + func testNoErrorHookCalled() { + OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) + wait(for: [readyExpectation], timeout: 5) + let client = OpenFeatureAPI.shared.getClient() + let hook = BooleanHookMock() let feo = FlagEvaluationOptions(hooks: [hook]) _ = client.getValue( @@ -23,8 +33,10 @@ final class HookSpecTests: XCTestCase { XCTAssertEqual(hook.finallyAfterCalled, 1) } - func testErrorHookButNoAfterCalled() async { - await OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) + func testErrorHookButNoAfterCalled() { + OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) + wait(for: [errorExpectation], timeout: 5) + let client = OpenFeatureAPI.shared.getClient() let hook = BooleanHookMock() @@ -39,7 +51,7 @@ final class HookSpecTests: XCTestCase { XCTAssertEqual(hook.finallyAfterCalled, 1) } - func testHookEvaluationOrder() async { + func testHookEvaluationOrder() { var evalOrder: [String] = [] let addEval: (String) -> Void = { eval in evalOrder.append(eval) @@ -48,7 +60,9 @@ final class HookSpecTests: XCTestCase { let providerMock = NoOpProviderMock(hooks: [ BooleanHookMock(prefix: "provider", addEval: addEval) ]) - await OpenFeatureAPI.shared.setProvider(provider: providerMock) + OpenFeatureAPI.shared.setProvider(provider: providerMock) + wait(for: [readyExpectation], timeout: 5) + OpenFeatureAPI.shared.addHooks(hooks: BooleanHookMock(prefix: "api", addEval: addEval)) let client = OpenFeatureAPI.shared.getClient() client.addHooks(BooleanHookMock(prefix: "client", addEval: addEval)) @@ -75,6 +89,19 @@ final class HookSpecTests: XCTestCase { "api finallyAfter", ]) } + + // MARK: Event Handlers + let readyExpectation = XCTestExpectation(description: "Ready") + + func readyEventEmitted(notification: NSNotification) { + readyExpectation.fulfill() + } + + let errorExpectation = XCTestExpectation(description: "Error") + + func errorEventEmitted(notification: NSNotification) { + errorExpectation.fulfill() + } } extension HookSpecTests { diff --git a/Tests/OpenFeatureTests/ProviderEventsTests.swift b/Tests/OpenFeatureTests/ProviderEventsTests.swift index 7ebe226..ed8218b 100644 --- a/Tests/OpenFeatureTests/ProviderEventsTests.swift +++ b/Tests/OpenFeatureTests/ProviderEventsTests.swift @@ -3,9 +3,9 @@ import OpenFeature import XCTest final class ProviderEventsTests: XCTestCase { - func testReadyEventEmitted() { - let provider = DoSomethingProvider() + let provider = DoSomethingProvider() + func testReadyEventEmitted() { OpenFeatureAPI.shared.addHandler( observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready ) @@ -14,10 +14,17 @@ final class ProviderEventsTests: XCTestCase { wait(for: [readyExpectation], timeout: 5) } - // MARK: Handlers + // MARK: Event Handlers let readyExpectation = XCTestExpectation(description: "Ready") func readyEventEmitted(notification: NSNotification) { readyExpectation.fulfill() + + let maybeProvider = notification.userInfo?[providerEventDetailsKeyProvider] + guard let eventProvider = maybeProvider as? DoSomethingProvider else { + XCTFail("Provider not passed in notification") + return + } + XCTAssertEqual(eventProvider.metadata.name, provider.metadata.name) } } From b431f078dcfad858b9c9a187c25ace5567aeef3d Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Wed, 2 Aug 2023 10:14:32 +0100 Subject: [PATCH 5/6] Linter fixes --- Tests/OpenFeatureTests/FlagEvaluationTests.swift | 4 +++- Tests/OpenFeatureTests/HookSpecTests.swift | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/OpenFeatureTests/FlagEvaluationTests.swift b/Tests/OpenFeatureTests/FlagEvaluationTests.swift index c2623e9..c9da8f0 100644 --- a/Tests/OpenFeatureTests/FlagEvaluationTests.swift +++ b/Tests/OpenFeatureTests/FlagEvaluationTests.swift @@ -5,6 +5,8 @@ import XCTest final class FlagEvaluationTests: XCTestCase { override func setUp() { + super.setUp() + OpenFeatureAPI.shared.addHandler( observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready ) @@ -167,7 +169,7 @@ final class FlagEvaluationTests: XCTestCase { func testBrokenProvider() { OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) wait(for: [errorExpectation], timeout: 5) - + let client = OpenFeatureAPI.shared.getClient() XCTAssertFalse(client.getValue(key: "testkey", defaultValue: false)) diff --git a/Tests/OpenFeatureTests/HookSpecTests.swift b/Tests/OpenFeatureTests/HookSpecTests.swift index 37925f8..9fe9304 100644 --- a/Tests/OpenFeatureTests/HookSpecTests.swift +++ b/Tests/OpenFeatureTests/HookSpecTests.swift @@ -5,6 +5,8 @@ import XCTest final class HookSpecTests: XCTestCase { override func setUp() { + super.setUp() + OpenFeatureAPI.shared.addHandler( observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready ) @@ -62,7 +64,7 @@ final class HookSpecTests: XCTestCase { ]) OpenFeatureAPI.shared.setProvider(provider: providerMock) wait(for: [readyExpectation], timeout: 5) - + OpenFeatureAPI.shared.addHooks(hooks: BooleanHookMock(prefix: "api", addEval: addEval)) let client = OpenFeatureAPI.shared.getClient() client.addHooks(BooleanHookMock(prefix: "client", addEval: addEval)) From dad29e4a925a722e79eca58dad47c7b670fae882 Mon Sep 17 00:00:00 2001 From: Brian Hackett Date: Wed, 2 Aug 2023 10:58:29 +0100 Subject: [PATCH 6/6] Set context logic fix --- Sources/OpenFeature/OpenFeatureAPI.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/OpenFeature/OpenFeatureAPI.swift b/Sources/OpenFeature/OpenFeatureAPI.swift index 4bb0671..a5535a3 100644 --- a/Sources/OpenFeature/OpenFeatureAPI.swift +++ b/Sources/OpenFeature/OpenFeatureAPI.swift @@ -37,8 +37,9 @@ public class OpenFeatureAPI { } public func setEvaluationContext(evaluationContext: EvaluationContext) { - getProvider()?.onContextSet(oldContext: self._context, newContext: evaluationContext) + let oldContext = self._context self._context = evaluationContext + getProvider()?.onContextSet(oldContext: oldContext, newContext: evaluationContext) } public func getEvaluationContext() -> EvaluationContext? {