diff --git a/README.md b/README.md
index d315299..0061ea0 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -90,11 +90,12 @@ Task {
## 🌟 Features
-| Status | Features | Description |
-| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
+| Status | Features | Description |
+| ------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
+| ❌ | [Tracking](#tracking) | Associate user actions with feature flag evaluations. |
| ❌ | [Logging](#logging) | Integrate with popular logging packages. |
| ❌ | [Named clients](#named-clients) | Utilize multiple providers in a single application. |
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
@@ -153,6 +154,10 @@ _ = client.getValue(
defaultValue: false,
options: FlagEvaluationOptions(hooks: [ExampleHook()]))
```
+### Tracking
+
+Tracking is not yet available in the iOS SDK.
+
### Logging
Logging customization is not yet available in the iOS SDK.
@@ -242,7 +247,7 @@ class BooleanHook: Hook {
// do something
}
- func finallyAfter(ctx: HookContext, hints: [String: Any]) {
+ func finally(ctx: HookContext, hints: [String: Any]) {
// do something
}
}
diff --git a/Sources/OpenFeature/EventHandler.swift b/Sources/OpenFeature/EventHandler.swift
index d4a92ee..ed80b37 100644
--- a/Sources/OpenFeature/EventHandler.swift
+++ b/Sources/OpenFeature/EventHandler.swift
@@ -2,29 +2,24 @@ import Combine
import Foundation
public class EventHandler: EventSender, EventPublisher {
- private let eventState: CurrentValueSubject
+ private let lastSentEvent = PassthroughSubject()
- convenience init() {
- self.init(.notReady)
+ public init() {
}
- public init(_ state: ProviderEvent) {
- eventState = CurrentValueSubject(state)
- }
-
- public func observe() -> AnyPublisher {
- return eventState.eraseToAnyPublisher()
+ public func observe() -> AnyPublisher {
+ return lastSentEvent.eraseToAnyPublisher()
}
public func send(
_ event: ProviderEvent
) {
- eventState.send(event)
+ lastSentEvent.send(event)
}
}
public protocol EventPublisher {
- func observe() -> AnyPublisher
+ func observe() -> AnyPublisher
}
public protocol EventSender {
diff --git a/Sources/OpenFeature/Hook.swift b/Sources/OpenFeature/Hook.swift
index 72b9e35..2bc7b6b 100644
--- a/Sources/OpenFeature/Hook.swift
+++ b/Sources/OpenFeature/Hook.swift
@@ -11,7 +11,11 @@ public protocol Hook {
func error(ctx: HookContext, error: Error, hints: [String: Any])
- func finallyAfter(ctx: HookContext, hints: [String: Any])
+ func finally(
+ ctx: HookContext,
+ details: FlagEvaluationDetails,
+ hints: [String: Any]
+ )
func supportsFlagValueType(flagValueType: FlagValueType) -> Bool
}
@@ -31,7 +35,9 @@ extension Hook {
// Default implementation
}
- public func finallyAfter(ctx: HookContext, hints: [String: Any]) {
+ public func finally(
+ ctx: HookContext, details: FlagEvaluationDetails, hints: [String: Any]
+ ) {
// Default implementation
}
diff --git a/Sources/OpenFeature/HookSupport.swift b/Sources/OpenFeature/HookSupport.swift
index 0034949..acf9455 100644
--- a/Sources/OpenFeature/HookSupport.swift
+++ b/Sources/OpenFeature/HookSupport.swift
@@ -4,20 +4,12 @@ import os
class HookSupport {
var logger = Logger()
- func errorHooks(
- flagValueType: FlagValueType, hookCtx: HookContext, error: Error, hooks: [any Hook], hints: [String: Any]
- ) {
- hooks
- .filter { $0.supportsFlagValueType(flagValueType: flagValueType) }
- .forEach { $0.error(ctx: hookCtx, error: error, hints: hints) }
- }
-
- func afterAllHooks(
- flagValueType: FlagValueType, hookCtx: HookContext, hooks: [any Hook], hints: [String: Any]
- ) {
+ func beforeHooks(flagValueType: FlagValueType, hookCtx: HookContext, hooks: [any Hook], hints: [String: Any])
+ {
hooks
+ .reversed()
.filter { $0.supportsFlagValueType(flagValueType: flagValueType) }
- .forEach { $0.finallyAfter(ctx: hookCtx, hints: hints) }
+ .forEach { $0.before(ctx: hookCtx, hints: hints) }
}
func afterHooks(
@@ -32,11 +24,23 @@ class HookSupport {
.forEach { $0.after(ctx: hookCtx, details: details, hints: hints) }
}
- func beforeHooks(flagValueType: FlagValueType, hookCtx: HookContext, hooks: [any Hook], hints: [String: Any])
- {
+ func errorHooks(
+ flagValueType: FlagValueType, hookCtx: HookContext, error: Error, hooks: [any Hook], hints: [String: Any]
+ ) {
hooks
- .reversed()
.filter { $0.supportsFlagValueType(flagValueType: flagValueType) }
- .forEach { $0.before(ctx: hookCtx, hints: hints) }
+ .forEach { $0.error(ctx: hookCtx, error: error, hints: hints) }
+ }
+
+ func finallyHooks(
+ flagValueType: FlagValueType,
+ hookCtx: HookContext,
+ details: FlagEvaluationDetails,
+ hooks: [any Hook],
+ hints: [String: Any]
+ ) {
+ hooks
+ .filter { $0.supportsFlagValueType(flagValueType: flagValueType) }
+ .forEach { $0.finally(ctx: hookCtx, details: details, hints: hints) }
}
}
diff --git a/Sources/OpenFeature/OpenFeatureAPI.swift b/Sources/OpenFeature/OpenFeatureAPI.swift
index c91fe2c..55e274e 100644
--- a/Sources/OpenFeature/OpenFeatureAPI.swift
+++ b/Sources/OpenFeature/OpenFeatureAPI.swift
@@ -4,17 +4,13 @@ import Foundation
/// A global singleton which holds base configuration for the OpenFeature library.
/// Configuration here will be shared across all ``Client``s.
public class OpenFeatureAPI {
- private var _provider: FeatureProvider? {
- get {
- providerSubject.value
- }
- set {
- providerSubject.send(newValue)
- }
- }
- private var _context: EvaluationContext?
+ private let eventHandler = EventHandler()
+ private let queue = DispatchQueue(label: "com.openfeature.providerDescriptor.queue")
+
+ private(set) var providerSubject = CurrentValueSubject(nil)
+ private(set) var evaluationContext: EvaluationContext?
+ private(set) var providerStatus: ProviderStatus = .notReady
private(set) var hooks: [any Hook] = []
- private var providerSubject = CurrentValueSubject(nil)
/// The ``OpenFeatureAPI`` singleton
static public let shared = OpenFeatureAPI()
@@ -22,34 +18,93 @@ public class OpenFeatureAPI {
public init() {
}
- public func setProvider(provider: FeatureProvider) {
- self.setProvider(provider: provider, initialContext: nil)
+ /**
+ Set provider and calls its `initialize` in a background thread.
+ Readiness can be determined from `getState` or listening for `ready` event.
+ */
+ public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) {
+ queue.async {
+ Task {
+ await self.setProviderInternal(provider: provider, initialContext: initialContext)
+ }
+ }
}
- public func setProvider(provider: FeatureProvider, initialContext: EvaluationContext?) {
- self._provider = provider
- if let context = initialContext {
- self._context = context
+ /**
+ Set provider and calls its `initialize`.
+ This async function returns when the `initialize` from the provider is completed.
+ */
+ public func setProviderAndWait(provider: FeatureProvider, initialContext: EvaluationContext?) async {
+ await withCheckedContinuation { continuation in
+ queue.async {
+ Task {
+ await self.setProviderInternal(provider: provider, initialContext: initialContext)
+ continuation.resume()
+ }
+ }
}
- provider.initialize(initialContext: self._context)
+ }
+
+ /**
+ Set provider and calls its `initialize` in a background thread.
+ Readiness can be determined from `getState` or listening for `ready` event.
+ */
+ public func setProvider(provider: FeatureProvider) {
+ setProvider(provider: provider, initialContext: nil)
+ }
+
+ /**
+ Set provider and calls its `initialize`.
+ This async function returns when the `initialize` from the provider is completed.
+ */
+ public func setProviderAndWait(provider: FeatureProvider) async {
+ await setProviderAndWait(provider: provider, initialContext: nil)
}
public func getProvider() -> FeatureProvider? {
- return self._provider
+ return self.providerSubject.value
}
public func clearProvider() {
- self._provider = nil
+ queue.sync {
+ self.providerSubject.send(nil)
+ self.providerStatus = .notReady
+ }
}
+ /**
+ Set evaluation context and calls the provider's `onContextSet` in a background thread.
+ Readiness can be determined from `getState` or listening for `contextChanged` event.
+ */
public func setEvaluationContext(evaluationContext: EvaluationContext) {
- let oldContext = self._context
- self._context = evaluationContext
- getProvider()?.onContextSet(oldContext: oldContext, newContext: evaluationContext)
+ queue.async {
+ Task {
+ await self.updateContext(evaluationContext: evaluationContext)
+ }
+ }
+ }
+
+ /**
+ Set evaluation context and calls the provider's `onContextSet`.
+ This async function returns when the `onContextSet` from the provider is completed.
+ */
+ public func setEvaluationContextAndWait(evaluationContext: EvaluationContext) async {
+ await withCheckedContinuation { continuation in
+ queue.async {
+ Task {
+ await self.updateContext(evaluationContext: evaluationContext)
+ continuation.resume()
+ }
+ }
+ }
}
public func getEvaluationContext() -> EvaluationContext? {
- return self._context
+ return self.evaluationContext
+ }
+
+ public func getProviderStatus() -> ProviderStatus {
+ return self.providerStatus
}
public func getProviderMetadata() -> ProviderMetadata? {
@@ -72,43 +127,72 @@ public class OpenFeatureAPI {
self.hooks.removeAll()
}
- public func observe() -> AnyPublisher {
+ public func observe() -> AnyPublisher {
return providerSubject.map { provider in
if let provider = provider {
return provider.observe()
+ .merge(with: self.eventHandler.observe())
+ .eraseToAnyPublisher()
} else {
- return Empty()
+ return Empty()
.eraseToAnyPublisher()
}
}
.switchToLatest()
.eraseToAnyPublisher()
}
-}
-extension OpenFeatureAPI {
- public func setProviderAndWait(provider: FeatureProvider) async {
- await setProviderAndWait(provider: provider, initialContext: nil)
+ internal func getState() -> OpenFeatureState {
+ return queue.sync {
+ OpenFeatureState(
+ provider: providerSubject.value,
+ evaluationContext: evaluationContext,
+ providerStatus: providerStatus)
+ }
}
- public func setProviderAndWait(provider: FeatureProvider, initialContext: EvaluationContext?) async {
- let task = Task {
- var holder: [AnyCancellable] = []
- await withCheckedContinuation { continuation in
- let stateObserver = provider.observe().sink {
- if $0 == .ready || $0 == .error {
- continuation.resume()
- holder.removeAll()
- }
- }
- stateObserver.store(in: &holder)
- setProvider(provider: provider, initialContext: initialContext)
+ private func setProviderInternal(provider: FeatureProvider, initialContext: EvaluationContext? = nil) async {
+ self.providerStatus = .notReady
+ self.providerSubject.send(provider)
+
+ if let initialContext = initialContext {
+ self.evaluationContext = initialContext
+ }
+
+ do {
+ try await provider.initialize(initialContext: initialContext)
+ self.providerStatus = .ready
+ self.eventHandler.send(.ready)
+ } catch {
+ switch error {
+ case OpenFeatureError.providerFatalError:
+ self.providerStatus = .fatal
+ self.eventHandler.send(.error(errorCode: .providerFatal))
+ default:
+ self.providerStatus = .error
+ self.eventHandler.send(.error(message: error.localizedDescription))
}
}
- await withTaskCancellationHandler {
- await task.value
- } onCancel: {
- task.cancel()
+ }
+
+ private func updateContext(evaluationContext: EvaluationContext) async {
+ do {
+ let oldContext = self.evaluationContext
+ self.evaluationContext = evaluationContext
+ self.providerStatus = .reconciling
+ eventHandler.send(.reconciling)
+ try await self.providerSubject.value?.onContextSet(oldContext: oldContext, newContext: evaluationContext)
+ self.providerStatus = .ready
+ eventHandler.send(.contextChanged)
+ } catch {
+ self.providerStatus = .error
+ eventHandler.send(.error(message: error.localizedDescription))
}
}
+
+ struct OpenFeatureState {
+ let provider: FeatureProvider?
+ let evaluationContext: EvaluationContext?
+ let providerStatus: ProviderStatus
+ }
}
diff --git a/Sources/OpenFeature/OpenFeatureClient.swift b/Sources/OpenFeature/OpenFeatureClient.swift
index cdd933f..f90afd5 100644
--- a/Sources/OpenFeature/OpenFeatureClient.swift
+++ b/Sources/OpenFeature/OpenFeatureClient.swift
@@ -68,12 +68,47 @@ extension OpenFeatureClient {
defaultValue: T,
options: FlagEvaluationOptions?
) -> FlagEvaluationDetails {
- let options = options ?? FlagEvaluationOptions(hooks: [], hookHints: [:])
- let hints = options.hookHints
- let context = openFeatureApi.getEvaluationContext()
+ let openFeatureApiState = openFeatureApi.getState()
+ var details = FlagEvaluationDetails(flagKey: key, value: defaultValue)
+ switch openFeatureApiState.providerStatus {
+ case .fatal:
+ details.errorCode = .providerFatal
+ details.errorMessage =
+ OpenFeatureError
+ .providerFatalError(message: "unknown")
+ .description
+ details.reason = Reason.error.rawValue
+ return details
+ case .notReady:
+ details.errorCode = .providerNotReady
+ details.errorMessage = OpenFeatureError.providerNotReadyError.description
+ details.reason = Reason.error.rawValue
+ return details
+ case .error:
+ details.errorCode = .general
+ details.errorMessage =
+ OpenFeatureError
+ .generalError(message: "unknown")
+ .description
+ details.reason = Reason.error.rawValue
+ return details
+ case .ready, .reconciling, .stale:
+ return evaluateFlagReady(
+ key: key, defaultValue: defaultValue, options: options, openFeatureApiState: openFeatureApiState)
+ }
+ }
+ private func evaluateFlagReady(
+ key: String,
+ defaultValue: T,
+ options: FlagEvaluationOptions?,
+ openFeatureApiState: OpenFeatureAPI.OpenFeatureState
+ ) -> FlagEvaluationDetails {
var details = FlagEvaluationDetails(flagKey: key, value: defaultValue)
- let provider = openFeatureApi.getProvider() ?? NoOpProvider()
+ let options = options ?? FlagEvaluationOptions(hooks: [], hookHints: [:])
+ let hints = options.hookHints
+ let context = openFeatureApiState.evaluationContext
+ let provider = openFeatureApiState.provider ?? NoOpProvider()
let hookCtx = HookContext(
flagKey: key,
type: T.flagValueType,
@@ -81,45 +116,34 @@ extension OpenFeatureClient {
ctx: context,
clientMetadata: self.metadata,
providerMetadata: provider.metadata)
-
hookLock.lock()
let mergedHooks = provider.hooks + options.hooks + hooks + openFeatureApi.hooks
hookLock.unlock()
-
do {
hookSupport.beforeHooks(flagValueType: T.flagValueType, hookCtx: hookCtx, hooks: mergedHooks, hints: hints)
-
let providerEval = try createProviderEvaluation(
key: key,
context: context,
defaultValue: defaultValue,
provider: provider)
-
- let evalDetails = FlagEvaluationDetails.from(providerEval: providerEval, flagKey: key)
- details = evalDetails
-
+ details = FlagEvaluationDetails.from(providerEval: providerEval, flagKey: key)
try hookSupport.afterHooks(
- flagValueType: T.flagValueType, hookCtx: hookCtx, details: evalDetails, hooks: mergedHooks, hints: hints
+ flagValueType: T.flagValueType, hookCtx: hookCtx, details: details, hooks: mergedHooks, hints: hints
)
} catch {
logger.error("Unable to correctly evaluate flag with key \(key) due to exception \(error)")
-
if let error = error as? OpenFeatureError {
details.errorCode = error.errorCode()
} else {
details.errorCode = .general
}
-
details.errorMessage = "\(error)"
details.reason = Reason.error.rawValue
-
hookSupport.errorHooks(
flagValueType: T.flagValueType, hookCtx: hookCtx, error: error, hooks: mergedHooks, hints: hints)
}
-
- hookSupport.afterAllHooks(
- flagValueType: T.flagValueType, hookCtx: hookCtx, hooks: mergedHooks, hints: hints)
-
+ hookSupport.finallyHooks(
+ flagValueType: T.flagValueType, hookCtx: hookCtx, details: details, hooks: mergedHooks, hints: hints)
return details
}
diff --git a/Sources/OpenFeature/Provider/FeatureProvider.swift b/Sources/OpenFeature/Provider/FeatureProvider.swift
index a5af609..34cdd8c 100644
--- a/Sources/OpenFeature/Provider/FeatureProvider.swift
+++ b/Sources/OpenFeature/Provider/FeatureProvider.swift
@@ -6,10 +6,14 @@ public protocol FeatureProvider: EventPublisher {
var metadata: ProviderMetadata { get }
/// Called by OpenFeatureAPI whenever the new Provider is registered
- func initialize(initialContext: EvaluationContext?)
+ /// This must throw in case of error, using OpenFeature errors whenever possible
+ /// It is expected that the implementer is slow (e.g. network), hence the async nature of the protocol
+ func initialize(initialContext: EvaluationContext?) async throws
/// Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application
- func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext)
+ /// This must throw in case of error, using OpenFeature errors whenever possible
+ /// It is expected that the implementer is slow (e.g. network), hence the async nature of the protocol
+ func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) async throws
func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
-> ProviderEvaluation<
diff --git a/Sources/OpenFeature/Provider/NoOpProvider.swift b/Sources/OpenFeature/Provider/NoOpProvider.swift
index d9674fb..9daa14c 100644
--- a/Sources/OpenFeature/Provider/NoOpProvider.swift
+++ b/Sources/OpenFeature/Provider/NoOpProvider.swift
@@ -15,11 +15,9 @@ class NoOpProvider: FeatureProvider {
var hooks: [any Hook] = []
func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) {
- eventHandler.send(.ready)
}
func initialize(initialContext: EvaluationContext?) {
- eventHandler.send(.ready)
}
func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
@@ -67,7 +65,7 @@ class NoOpProvider: FeatureProvider {
value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue)
}
- func observe() -> AnyPublisher {
+ func observe() -> AnyPublisher {
return eventHandler.observe()
}
}
diff --git a/Sources/OpenFeature/Provider/ProviderEvents.swift b/Sources/OpenFeature/Provider/ProviderEvents.swift
index 2f40a44..25a8406 100644
--- a/Sources/OpenFeature/Provider/ProviderEvents.swift
+++ b/Sources/OpenFeature/Provider/ProviderEvents.swift
@@ -1,13 +1,10 @@
import Foundation
-public let providerEventDetailsKeyProvider = "Provider"
-public let providerEventDetailsKeyClient = "Client"
-public let providerEventDetailsKeyError = "Error"
-
-public enum ProviderEvent: String, CaseIterable {
- case ready = "PROVIDER_READY"
- case error = "PROVIDER_ERROR"
- case configurationChanged = "PROVIDER_CONFIGURATION_CHANGED"
- case stale = "PROVIDER_STALE"
- case notReady = "PROVIDER_NOT_READY"
+public enum ProviderEvent: Equatable {
+ case ready
+ case error(errorCode: ErrorCode? = nil, message: String? = nil)
+ case configurationChanged
+ case stale
+ case reconciling
+ case contextChanged
}
diff --git a/Sources/OpenFeature/Provider/ProviderStatus.swift b/Sources/OpenFeature/Provider/ProviderStatus.swift
new file mode 100644
index 0000000..5c14224
--- /dev/null
+++ b/Sources/OpenFeature/Provider/ProviderStatus.swift
@@ -0,0 +1,10 @@
+import Foundation
+
+public enum ProviderStatus: String, CaseIterable {
+ case notReady = "PROVIDER_NOT_READY"
+ case ready = "PROVIDER_READY"
+ case error = "PROVIDER_ERROR"
+ case stale = "PROVIDER_STALE"
+ case fatal = "PROVIDER_FATAL"
+ case reconciling = "PROVIDER_RECONCILING"
+}
diff --git a/Sources/OpenFeature/exceptions/OpenFeatureError.swift b/Sources/OpenFeature/exceptions/OpenFeatureError.swift
index 3919206..feb9395 100644
--- a/Sources/OpenFeature/exceptions/OpenFeatureError.swift
+++ b/Sources/OpenFeature/exceptions/OpenFeatureError.swift
@@ -9,7 +9,7 @@ public enum OpenFeatureError: Error, Equatable {
case typeMismatchError
case valueNotConvertableError
case providerNotReadyError
- case providerFatarError(message: String)
+ case providerFatalError(message: String)
public func errorCode() -> ErrorCode {
switch self {
@@ -29,7 +29,7 @@ public enum OpenFeatureError: Error, Equatable {
return .general
case .providerNotReadyError:
return .providerNotReady
- case .providerFatarError:
+ case .providerFatalError:
return .providerFatal
}
}
@@ -54,7 +54,7 @@ extension OpenFeatureError: CustomStringConvertible {
return "Could not convert value"
case .providerNotReadyError:
return "The value was resolved before the provider was ready"
- case .providerFatarError(let message):
+ case .providerFatalError(let message):
return "A fatal error occurred in the provider: \(message)"
}
}
diff --git a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift
index 53fa65f..1bd1b28 100644
--- a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift
+++ b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift
@@ -11,8 +11,8 @@ final class DeveloperExperienceTests: XCTestCase {
XCTAssertEqual(flagValue, "no-op")
}
- func testSimpleBooleanFlag() {
- OpenFeatureAPI.shared.setProvider(provider: NoOpProvider())
+ func testSimpleBooleanFlag() async {
+ await OpenFeatureAPI.shared.setProviderAndWait(provider: NoOpProvider())
let client = OpenFeatureAPI.shared.getClient()
let flagValue = client.getValue(key: "test", defaultValue: false)
@@ -20,12 +20,9 @@ final class DeveloperExperienceTests: XCTestCase {
}
func testObserveGlobalEvents() {
- let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
var eventState = OpenFeatureAPI.shared.observe().sink { event in
switch event {
- case .notReady:
- notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
default:
@@ -46,15 +43,55 @@ final class DeveloperExperienceTests: XCTestCase {
XCTAssertNotNil(eventState)
}
+ func testSetEvaluationContext() async {
+ let contextChangedExpectation = XCTestExpectation(description: "Context Changed")
+ let reconcilingExpectation = XCTestExpectation(description: "Reconciling")
+ let observer = OpenFeatureAPI.shared.observe().sink { event in
+ switch event {
+ case .reconciling:
+ reconcilingExpectation.fulfill()
+ case .ready:
+ break
+ case .contextChanged:
+ contextChangedExpectation.fulfill()
+ default:
+ XCTFail("Unexpected event")
+ }
+ }
+ let semaphore = DispatchSemaphore(value: 0)
+ await OpenFeatureAPI.shared.setProviderAndWait(provider: StaggeredProvider(onContextSetSemaphore: semaphore))
+ Task {
+ OpenFeatureAPI.shared.setEvaluationContext(evaluationContext: MutableContext(attributes: [:]))
+ }
+ await fulfillment(of: [reconcilingExpectation], timeout: 2)
+ semaphore.signal()
+ await fulfillment(of: [contextChangedExpectation], timeout: 2)
+ XCTAssertNotNil(observer)
+ }
+
+ func testSetEvaluationContextAndWait() async {
+ let reconcilingExpectation = XCTestExpectation(description: "Reconciling")
+ let semaphore = DispatchSemaphore(value: 0)
+ let ctx = MutableContext(attributes: ["test": .string("value")])
+ let provider = StaggeredProvider(onContextSetSemaphore: semaphore)
+ await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)
+ Task {
+ await OpenFeatureAPI.shared.setEvaluationContextAndWait(evaluationContext: ctx)
+ reconcilingExpectation.fulfill()
+ }
+ XCTAssertEqual(provider.activeContext.asMap(), MutableContext().asMap())
+ semaphore.signal()
+ await fulfillment(of: [reconcilingExpectation], timeout: 2)
+ XCTAssertEqual(OpenFeatureAPI.shared.getEvaluationContext()?.asMap(), ctx.asMap())
+ XCTAssertEqual(provider.activeContext.asMap(), ctx.asMap())
+ }
+
func testSetProviderAndWait() async {
- let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
let errorExpectation = XCTestExpectation(description: "Error")
withExtendedLifetime(
OpenFeatureAPI.shared.observe().sink { event in
switch event {
- case .notReady:
- notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case .error:
@@ -66,15 +103,12 @@ final class DeveloperExperienceTests: XCTestCase {
) {
let initCompleteExpectation = XCTestExpectation()
- let eventHandler = EventHandler()
- let provider = InjectableEventHandlerProvider(eventHandler: eventHandler)
+ let provider = DoSomethingProvider()
Task {
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)
await fulfillment(of: [readyExpectation], timeout: 1)
initCompleteExpectation.fulfill()
}
- wait(for: [notReadyExpectation], timeout: 1)
- eventHandler.send(.ready)
wait(for: [initCompleteExpectation], timeout: 1)
let errorProviderExpectation = XCTestExpectation()
@@ -84,14 +118,12 @@ final class DeveloperExperienceTests: XCTestCase {
await fulfillment(of: [errorExpectation], timeout: 2)
errorProviderExpectation.fulfill()
}
-
- eventHandler.send(.error)
wait(for: [errorProviderExpectation], timeout: 2)
}
}
- func testClientHooks() {
- OpenFeatureAPI.shared.setProvider(provider: NoOpProvider())
+ func testClientHooks() async {
+ await OpenFeatureAPI.shared.setProviderAndWait(provider: NoOpProvider())
let client = OpenFeatureAPI.shared.getClient()
let booleanHook = BooleanHookMock()
@@ -99,20 +131,20 @@ final class DeveloperExperienceTests: XCTestCase {
client.addHooks(booleanHook, intHook)
_ = client.getValue(key: "string-test", defaultValue: "test")
- XCTAssertEqual(booleanHook.finallyAfterCalled, 0)
- XCTAssertEqual(intHook.finallyAfterCalled, 0)
+ XCTAssertEqual(booleanHook.finallyCalled, 0)
+ XCTAssertEqual(intHook.finallyCalled, 0)
_ = client.getValue(key: "bool-test", defaultValue: false)
- XCTAssertEqual(booleanHook.finallyAfterCalled, 1)
- XCTAssertEqual(intHook.finallyAfterCalled, 0)
+ XCTAssertEqual(booleanHook.finallyCalled, 1)
+ XCTAssertEqual(intHook.finallyCalled, 0)
_ = client.getValue(key: "int-test", defaultValue: 0) as Int64
- XCTAssertEqual(booleanHook.finallyAfterCalled, 1)
- XCTAssertEqual(intHook.finallyAfterCalled, 1)
+ XCTAssertEqual(booleanHook.finallyCalled, 1)
+ XCTAssertEqual(intHook.finallyCalled, 1)
}
- func testEvalHooks() {
- OpenFeatureAPI.shared.setProvider(provider: NoOpProvider())
+ func testEvalHooks() async {
+ await OpenFeatureAPI.shared.setProviderAndWait(provider: NoOpProvider())
let client = OpenFeatureAPI.shared.getClient()
let booleanHook = BooleanHookMock()
@@ -120,20 +152,20 @@ final class DeveloperExperienceTests: XCTestCase {
let options = FlagEvaluationOptions(hooks: [booleanHook, intHook])
_ = client.getValue(key: "test", defaultValue: "test", options: options)
- XCTAssertEqual(booleanHook.finallyAfterCalled, 0)
- XCTAssertEqual(intHook.finallyAfterCalled, 0)
+ XCTAssertEqual(booleanHook.finallyCalled, 0)
+ XCTAssertEqual(intHook.finallyCalled, 0)
_ = client.getValue(key: "test", defaultValue: false, options: options)
- XCTAssertEqual(booleanHook.finallyAfterCalled, 1)
- XCTAssertEqual(intHook.finallyAfterCalled, 0)
+ XCTAssertEqual(booleanHook.finallyCalled, 1)
+ XCTAssertEqual(intHook.finallyCalled, 0)
_ = client.getValue(key: "test", defaultValue: 0, options: options) as Int64
- XCTAssertEqual(booleanHook.finallyAfterCalled, 1)
- XCTAssertEqual(intHook.finallyAfterCalled, 1)
+ XCTAssertEqual(booleanHook.finallyCalled, 1)
+ XCTAssertEqual(intHook.finallyCalled, 1)
}
- func testBrokenProvider() {
- OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider())
+ func testBrokenProvider() async {
+ await OpenFeatureAPI.shared.setProviderAndWait(provider: AlwaysBrokenProvider())
let client = OpenFeatureAPI.shared.getClient()
let details = client.getDetails(key: "test", defaultValue: false)
@@ -142,4 +174,15 @@ final class DeveloperExperienceTests: XCTestCase {
XCTAssertEqual(details.errorMessage, "Could not find flag for key: test")
XCTAssertEqual(details.reason, Reason.error.rawValue)
}
+
+ func testThrowingProvider() async {
+ await OpenFeatureAPI.shared.setProviderAndWait(provider: ThrowingProvider())
+ let client = OpenFeatureAPI.shared.getClient()
+
+ let details = client.getDetails(key: "test", defaultValue: false)
+
+ XCTAssertEqual(details.errorCode, .providerFatal)
+ XCTAssertEqual(details.errorMessage, "A fatal error occurred in the provider: unknown")
+ XCTAssertEqual(details.reason, Reason.error.rawValue)
+ }
}
diff --git a/Tests/OpenFeatureTests/FlagEvaluationTests.swift b/Tests/OpenFeatureTests/FlagEvaluationTests.swift
index 80d5003..9f78812 100644
--- a/Tests/OpenFeatureTests/FlagEvaluationTests.swift
+++ b/Tests/OpenFeatureTests/FlagEvaluationTests.swift
@@ -9,15 +9,14 @@ final class FlagEvaluationTests: XCTestCase {
XCTAssertTrue(OpenFeatureAPI.shared === OpenFeatureAPI.shared)
}
- func testApiSetsProvider() {
+ func testApiSetsProvider() async {
let provider = NoOpProvider()
- OpenFeatureAPI.shared.setProvider(provider: provider)
+ await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)
XCTAssertTrue((OpenFeatureAPI.shared.getProvider() as? NoOpProvider) === provider)
}
- func testProviderMetadata() {
- OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider())
-
+ func testProviderMetadata() async {
+ await OpenFeatureAPI.shared.setProviderAndWait(provider: DoSomethingProvider())
XCTAssertEqual(OpenFeatureAPI.shared.getProviderMetadata()?.name, DoSomethingProvider.name)
}
@@ -53,14 +52,11 @@ final class FlagEvaluationTests: XCTestCase {
func testSimpleFlagEvaluation() {
let provider = DoSomethingProvider()
- let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
let errorExpectation = XCTestExpectation(description: "Error")
let staleExpectation = XCTestExpectation(description: "Stale")
- let eventState = provider.observe().sink { event in
+ let eventState = OpenFeatureAPI.shared.observe().sink { event in
switch event {
- case .notReady:
- notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case .error:
@@ -72,7 +68,6 @@ final class FlagEvaluationTests: XCTestCase {
}
}
- wait(for: [notReadyExpectation], timeout: 5)
OpenFeatureAPI.shared.setProvider(provider: provider)
wait(for: [readyExpectation], timeout: 5)
let client = OpenFeatureAPI.shared.getClient()
@@ -112,12 +107,9 @@ final class FlagEvaluationTests: XCTestCase {
func testDetailedFlagEvaluation() async {
let provider = DoSomethingProvider()
- let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
- let eventState = provider.observe().sink { event in
+ let eventState = OpenFeatureAPI.shared.observe().sink { event in
switch event {
- case .notReady:
- notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
default:
@@ -175,12 +167,9 @@ final class FlagEvaluationTests: XCTestCase {
func testHooksAreFired() async {
let provider = NoOpProvider()
- let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
- let eventState = provider.observe().sink { event in
+ let eventState = OpenFeatureAPI.shared.observe().sink { event in
switch event {
- case .notReady:
- notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
default:
@@ -209,14 +198,11 @@ final class FlagEvaluationTests: XCTestCase {
func testBrokenProvider() {
let provider = AlwaysBrokenProvider()
- let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
let errorExpectation = XCTestExpectation(description: "Error")
let staleExpectation = XCTestExpectation(description: "Stale")
let eventState = provider.observe().sink { event in
switch event {
- case .notReady:
- notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case .error:
@@ -248,9 +234,6 @@ final class FlagEvaluationTests: XCTestCase {
let eventState = provider.observe().sink { event in
switch event {
- case .notReady:
- // The provider starts in this state.
- return
case .error:
fatalExpectation.fulfill()
default:
diff --git a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift
index c1acea0..94db64b 100644
--- a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift
+++ b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift
@@ -10,18 +10,18 @@ class AlwaysBrokenProvider: FeatureProvider {
private let eventHandler = EventHandler()
func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
- eventHandler.send(.error)
+ eventHandler.send(.error())
}
func initialize(initialContext: OpenFeature.EvaluationContext?) {
- eventHandler.send(.error)
+ eventHandler.send(.error())
}
func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
-> OpenFeature.ProviderEvaluation
{
if self.throwFatal {
- throw OpenFeatureError.providerFatarError(message: "Always broken")
+ throw OpenFeatureError.providerFatalError(message: "Always broken")
}
throw OpenFeatureError.flagNotFoundError(key: key)
}
@@ -30,7 +30,7 @@ class AlwaysBrokenProvider: FeatureProvider {
-> OpenFeature.ProviderEvaluation
{
if self.throwFatal {
- throw OpenFeatureError.providerFatarError(message: "Always broken")
+ throw OpenFeatureError.providerFatalError(message: "Always broken")
}
throw OpenFeatureError.flagNotFoundError(key: key)
}
@@ -39,7 +39,7 @@ class AlwaysBrokenProvider: FeatureProvider {
-> OpenFeature.ProviderEvaluation
{
if self.throwFatal {
- throw OpenFeatureError.providerFatarError(message: "Always broken")
+ throw OpenFeatureError.providerFatalError(message: "Always broken")
}
throw OpenFeatureError.flagNotFoundError(key: key)
}
@@ -48,7 +48,7 @@ class AlwaysBrokenProvider: FeatureProvider {
-> OpenFeature.ProviderEvaluation
{
if self.throwFatal {
- throw OpenFeatureError.providerFatarError(message: "Always broken")
+ throw OpenFeatureError.providerFatalError(message: "Always broken")
}
throw OpenFeatureError.flagNotFoundError(key: key)
}
@@ -57,12 +57,12 @@ class AlwaysBrokenProvider: FeatureProvider {
-> OpenFeature.ProviderEvaluation
{
if self.throwFatal {
- throw OpenFeatureError.providerFatarError(message: "Always broken")
+ throw OpenFeatureError.providerFatalError(message: "Always broken")
}
throw OpenFeatureError.flagNotFoundError(key: key)
}
- func observe() -> AnyPublisher {
+ func observe() -> AnyPublisher {
eventHandler.observe()
}
}
diff --git a/Tests/OpenFeatureTests/Helpers/BooleanHookMock.swift b/Tests/OpenFeatureTests/Helpers/BooleanHookMock.swift
index 13de2da..16443ce 100644
--- a/Tests/OpenFeatureTests/Helpers/BooleanHookMock.swift
+++ b/Tests/OpenFeatureTests/Helpers/BooleanHookMock.swift
@@ -6,8 +6,8 @@ class BooleanHookMock: Hook {
public var beforeCalled = 0
public var afterCalled = 0
- public var finallyAfterCalled = 0
public var errorCalled = 0
+ public var finallyCalled = 0
private var prefix: String
private var addEval: (String) -> Void
@@ -38,8 +38,10 @@ class BooleanHookMock: Hook {
self.addEval(self.prefix.isEmpty ? "error" : "\(self.prefix) error")
}
- func finallyAfter(ctx: HookContext, hints: [String: Any]) {
- finallyAfterCalled += 1
- self.addEval(self.prefix.isEmpty ? "finallyAfter" : "\(self.prefix) finallyAfter")
+ func finally(
+ ctx: HookContext, details: FlagEvaluationDetails, hints: [String: Any]
+ ) {
+ finallyCalled += 1
+ self.addEval(self.prefix.isEmpty ? "finally" : "\(self.prefix) finally")
}
}
diff --git a/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift b/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift
index 0e29eaf..b2d339c 100644
--- a/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift
+++ b/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift
@@ -4,15 +4,12 @@ import OpenFeature
class DoSomethingProvider: FeatureProvider {
public static let name = "Something"
- private let eventHandler = EventHandler(.notReady)
- private var holdit: AnyCancellable?
+ private let eventHandler = EventHandler()
func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
- eventHandler.send(.ready)
}
func initialize(initialContext: OpenFeature.EvaluationContext?) {
- eventHandler.send(.ready)
}
var hooks: [any OpenFeature.Hook] = []
@@ -59,7 +56,7 @@ class DoSomethingProvider: FeatureProvider {
return ProviderEvaluation(value: .null, flagMetadata: DoSomethingProvider.flagMetadataMap)
}
- func observe() -> AnyPublisher {
+ func observe() -> AnyPublisher {
eventHandler.observe()
}
diff --git a/Tests/OpenFeatureTests/Helpers/InjectableEventHandlerProvider.swift b/Tests/OpenFeatureTests/Helpers/InjectableEventHandlerProvider.swift
deleted file mode 100644
index c498f80..0000000
--- a/Tests/OpenFeatureTests/Helpers/InjectableEventHandlerProvider.swift
+++ /dev/null
@@ -1,71 +0,0 @@
-import Combine
-import Foundation
-import OpenFeature
-
-class InjectableEventHandlerProvider: FeatureProvider {
- public static let name = "InjectableEventHandler"
- private let eventHandler: EventHandler
-
- init(eventHandler: EventHandler) {
- self.eventHandler = eventHandler
- }
-
- func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
- // Let the parent test control events via eventHandler
- }
-
- func initialize(initialContext: OpenFeature.EvaluationContext?) {
- // Let the parent test control events via eventHandler
- }
-
- var hooks: [any OpenFeature.Hook] = []
- var metadata: OpenFeature.ProviderMetadata = InjectableEventHandlerMetadata()
-
- func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
- -> ProviderEvaluation<
- Bool
- >
- {
- return ProviderEvaluation(value: !defaultValue)
- }
-
- func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws
- -> ProviderEvaluation<
- String
- >
- {
- return ProviderEvaluation(value: String(defaultValue.reversed()))
- }
-
- func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws
- -> ProviderEvaluation<
- Int64
- >
- {
- return ProviderEvaluation(value: defaultValue * 100)
- }
-
- func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws
- -> ProviderEvaluation<
- Double
- >
- {
- return ProviderEvaluation(value: defaultValue * 100)
- }
-
- func getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?) throws
- -> ProviderEvaluation<
- Value
- >
- {
- return ProviderEvaluation(value: .null)
- }
-
- func observe() -> AnyPublisher {
- eventHandler.observe()
- }
-
- public struct InjectableEventHandlerMetadata: ProviderMetadata {
- public var name: String? = InjectableEventHandlerProvider.name
- }
-}
diff --git a/Tests/OpenFeatureTests/Helpers/IntHookMock.swift b/Tests/OpenFeatureTests/Helpers/IntHookMock.swift
index e64f76d..0df581d 100644
--- a/Tests/OpenFeatureTests/Helpers/IntHookMock.swift
+++ b/Tests/OpenFeatureTests/Helpers/IntHookMock.swift
@@ -6,7 +6,7 @@ class IntHookMock: Hook {
public var beforeCalled = 0
public var afterCalled = 0
- public var finallyAfterCalled = 0
+ public var finallyCalled = 0
public var errorCalled = 0
private var prefix: String
@@ -38,8 +38,10 @@ class IntHookMock: Hook {
self.addEval(self.prefix.isEmpty ? "error" : "\(self.prefix) error")
}
- func finallyAfter(ctx: HookContext, hints: [String: Any]) {
- finallyAfterCalled += 1
- self.addEval(self.prefix.isEmpty ? "finallyAfter" : "\(self.prefix) finallyAfter")
+ func finally(
+ ctx: HookContext, details: FlagEvaluationDetails, hints: [String: Any]
+ ) {
+ finallyCalled += 1
+ self.addEval(self.prefix.isEmpty ? "finally" : "\(self.prefix) finally")
}
}
diff --git a/Tests/OpenFeatureTests/Helpers/StaggeredProvider.swift b/Tests/OpenFeatureTests/Helpers/StaggeredProvider.swift
new file mode 100644
index 0000000..b867341
--- /dev/null
+++ b/Tests/OpenFeatureTests/Helpers/StaggeredProvider.swift
@@ -0,0 +1,84 @@
+import Combine
+import Foundation
+import OpenFeature
+
+class StaggeredProvider: FeatureProvider {
+ public static let name = "Something"
+ private let eventHandler = EventHandler()
+ private let onContextSetSemaphore: DispatchSemaphore?
+ public var activeContext: EvaluationContext = MutableContext()
+
+ init(onContextSetSemaphore: DispatchSemaphore?) {
+ self.onContextSetSemaphore = onContextSetSemaphore
+ }
+
+ func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
+ onContextSetSemaphore?.wait()
+ activeContext = newContext
+ }
+
+ func initialize(initialContext: OpenFeature.EvaluationContext?) {
+ if let initialContext {
+ activeContext = initialContext
+ }
+ }
+
+ var hooks: [any OpenFeature.Hook] = []
+ var metadata: OpenFeature.ProviderMetadata = DoMetadata()
+
+ func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
+ -> ProviderEvaluation<
+ Bool
+ >
+ {
+ return ProviderEvaluation(value: !defaultValue, flagMetadata: DoSomethingProvider.flagMetadataMap)
+ }
+
+ func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws
+ -> ProviderEvaluation<
+ String
+ >
+ {
+ return ProviderEvaluation(
+ value: String(defaultValue.reversed()), flagMetadata: DoSomethingProvider.flagMetadataMap)
+ }
+
+ func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws
+ -> ProviderEvaluation<
+ Int64
+ >
+ {
+ return ProviderEvaluation(value: defaultValue * 100, flagMetadata: DoSomethingProvider.flagMetadataMap)
+ }
+
+ func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws
+ -> ProviderEvaluation<
+ Double
+ >
+ {
+ return ProviderEvaluation(value: defaultValue * 100, flagMetadata: DoSomethingProvider.flagMetadataMap)
+ }
+
+ func getObjectEvaluation(key: String, defaultValue: Value, context: EvaluationContext?) throws
+ -> ProviderEvaluation<
+ Value
+ >
+ {
+ return ProviderEvaluation(value: .null, flagMetadata: DoSomethingProvider.flagMetadataMap)
+ }
+
+ func observe() -> AnyPublisher {
+ eventHandler.observe()
+ }
+
+ public struct DoMetadata: ProviderMetadata {
+ public var name: String? = DoSomethingProvider.name
+ }
+
+ public static let flagMetadataMap = [
+ "int-metadata": FlagMetadataValue.integer(99),
+ "double-metadata": FlagMetadataValue.double(98.4),
+ "string-metadata": FlagMetadataValue.string("hello-world"),
+ "boolean-metadata": FlagMetadataValue.boolean(true),
+ ]
+}
diff --git a/Tests/OpenFeatureTests/Helpers/ThrowingProvider.swift b/Tests/OpenFeatureTests/Helpers/ThrowingProvider.swift
new file mode 100644
index 0000000..a1b8b1b
--- /dev/null
+++ b/Tests/OpenFeatureTests/Helpers/ThrowingProvider.swift
@@ -0,0 +1,58 @@
+import Combine
+import Foundation
+
+@testable import OpenFeature
+
+class ThrowingProvider: FeatureProvider {
+ var metadata: ProviderMetadata = ThrowingProviderMetadata()
+ var hooks: [any Hook] = []
+ private let eventHandler = EventHandler()
+
+ func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) throws {
+ throw OpenFeatureError.providerFatalError(message: "Wrong credentials")
+ }
+
+ func initialize(initialContext: OpenFeature.EvaluationContext?) throws {
+ throw OpenFeatureError.providerFatalError(message: "Wrong credentials")
+ }
+
+ func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws
+ -> OpenFeature.ProviderEvaluation
+ {
+ throw OpenFeatureError.flagNotFoundError(key: key)
+ }
+
+ func getStringEvaluation(key: String, defaultValue: String, context: EvaluationContext?) throws
+ -> OpenFeature.ProviderEvaluation
+ {
+ throw OpenFeatureError.flagNotFoundError(key: key)
+ }
+
+ func getIntegerEvaluation(key: String, defaultValue: Int64, context: EvaluationContext?) throws
+ -> OpenFeature.ProviderEvaluation
+ {
+ throw OpenFeatureError.flagNotFoundError(key: key)
+ }
+
+ func getDoubleEvaluation(key: String, defaultValue: Double, context: EvaluationContext?) throws
+ -> OpenFeature.ProviderEvaluation
+ {
+ throw OpenFeatureError.flagNotFoundError(key: key)
+ }
+
+ func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value, context: EvaluationContext?) throws
+ -> OpenFeature.ProviderEvaluation
+ {
+ throw OpenFeatureError.flagNotFoundError(key: key)
+ }
+
+ func observe() -> AnyPublisher {
+ eventHandler.observe()
+ }
+}
+
+extension ThrowingProvider {
+ struct ThrowingProviderMetadata: ProviderMetadata {
+ var name: String? = "test"
+ }
+}
diff --git a/Tests/OpenFeatureTests/HookSpecTests.swift b/Tests/OpenFeatureTests/HookSpecTests.swift
index 67cdf68..780c281 100644
--- a/Tests/OpenFeatureTests/HookSpecTests.swift
+++ b/Tests/OpenFeatureTests/HookSpecTests.swift
@@ -6,12 +6,9 @@ import XCTest
final class HookSpecTests: XCTestCase {
func testNoErrorHookCalled() {
let provider = NoOpProvider()
- let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
- let eventState = provider.observe().sink { event in
+ let eventState = OpenFeatureAPI.shared.observe().sink { event in
switch event {
- case .notReady:
- notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
default:
@@ -33,19 +30,16 @@ final class HookSpecTests: XCTestCase {
XCTAssertEqual(hook.beforeCalled, 1)
XCTAssertEqual(hook.afterCalled, 1)
XCTAssertEqual(hook.errorCalled, 0)
- XCTAssertEqual(hook.finallyAfterCalled, 1)
+ XCTAssertEqual(hook.finallyCalled, 1)
XCTAssertNotNil(eventState)
}
func testErrorHookButNoAfterCalled() {
let provider = AlwaysBrokenProvider()
- let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
let errorExpectation = XCTestExpectation(description: "Error")
let eventState = provider.observe().sink { event in
switch event {
- case .notReady:
- notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case .error:
@@ -68,7 +62,7 @@ final class HookSpecTests: XCTestCase {
XCTAssertEqual(hook.beforeCalled, 1)
XCTAssertEqual(hook.afterCalled, 0)
XCTAssertEqual(hook.errorCalled, 1)
- XCTAssertEqual(hook.finallyAfterCalled, 1)
+ XCTAssertEqual(hook.finallyCalled, 1)
XCTAssertNotNil(eventState)
}
@@ -81,12 +75,9 @@ final class HookSpecTests: XCTestCase {
let providerMock = NoOpProviderMock(hooks: [
BooleanHookMock(prefix: "provider", addEval: addEval)
])
- let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
- let eventState = providerMock.observe().sink { event in
+ let eventState = OpenFeatureAPI.shared.observe().sink { event in
switch event {
- case .notReady:
- notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
default:
@@ -116,10 +107,10 @@ final class HookSpecTests: XCTestCase {
"invocation after",
"client after",
"api after",
- "provider finallyAfter",
- "invocation finallyAfter",
- "client finallyAfter",
- "api finallyAfter",
+ "provider finally",
+ "invocation finally",
+ "client finally",
+ "api finally",
])
XCTAssertNotNil(eventState)
}
diff --git a/Tests/OpenFeatureTests/HookSupportTests.swift b/Tests/OpenFeatureTests/HookSupportTests.swift
index a3d14b8..78a9b52 100644
--- a/Tests/OpenFeatureTests/HookSupportTests.swift
+++ b/Tests/OpenFeatureTests/HookSupportTests.swift
@@ -28,21 +28,22 @@ final class HookSupportTests: XCTestCase {
details: FlagEvaluationDetails(flagKey: "", value: false),
hooks: [hook],
hints: [:])
- hookSupport.afterAllHooks(
+ hookSupport.errorHooks(
flagValueType: .boolean,
hookCtx: hookContext,
+ error: OpenFeatureError.invalidContextError,
hooks: [hook],
hints: [:])
- hookSupport.errorHooks(
+ hookSupport.finallyHooks(
flagValueType: .boolean,
hookCtx: hookContext,
- error: OpenFeatureError.invalidContextError,
+ details: FlagEvaluationDetails(flagKey: "", value: false),
hooks: [hook],
hints: [:])
XCTAssertEqual(hook.beforeCalled, 1)
XCTAssertEqual(hook.afterCalled, 1)
- XCTAssertEqual(hook.finallyAfterCalled, 1)
XCTAssertEqual(hook.errorCalled, 1)
+ XCTAssertEqual(hook.finallyCalled, 1)
}
}
diff --git a/Tests/OpenFeatureTests/ProviderEventsTests.swift b/Tests/OpenFeatureTests/ProviderEventsTests.swift
deleted file mode 100644
index 4e4f09d..0000000
--- a/Tests/OpenFeatureTests/ProviderEventsTests.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-import Foundation
-import OpenFeature
-import XCTest
-
-final class ProviderEventsTests: XCTestCase {
- let provider = DoSomethingProvider()
-
- func testReadyEventSent() {
- let readyExpectation = XCTestExpectation(description: "Ready")
- let eventState =
- provider
- .observe()
- .filter { event in
- event == ProviderEvent.ready
- }
- .sink { _ in
- readyExpectation.fulfill()
- }
- OpenFeatureAPI.shared.setProvider(provider: provider)
- wait(for: [readyExpectation], timeout: 5)
- XCTAssertNotNil(eventState)
- }
-}
diff --git a/Tests/OpenFeatureTests/ValueTests.swift b/Tests/OpenFeatureTests/ValueTests.swift
index 4d30465..6534bf7 100644
--- a/Tests/OpenFeatureTests/ValueTests.swift
+++ b/Tests/OpenFeatureTests/ValueTests.swift
@@ -45,6 +45,8 @@ final class ValueTests: XCTestCase {
func testEncodeDecode() throws {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ formatter.timeZone = TimeZone(secondsFromGMT: 0)
let date = try XCTUnwrap(formatter.date(from: "2022-01-01 12:00:00"))
@@ -67,6 +69,8 @@ final class ValueTests: XCTestCase {
func testDecodeValue() throws {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
+ formatter.locale = Locale(identifier: "en_US_POSIX")
+ formatter.timeZone = TimeZone(secondsFromGMT: 0)
let date = try XCTUnwrap(formatter.date(from: "2022-01-01 12:00:00"))