Skip to content

Commit

Permalink
feat: Add ProviderNotReady event (#36)
Browse files Browse the repository at this point in the history
The specs define **separately** provider
[EVENTS](https://openfeature.dev/specification/types#provider-events)
and provider
[STATUS](https://openfeature.dev/specification/types#provider-status).
This PR doesn't quite yet introduce `ProviderStatus`, but rather extends
the Events to include all the possible statuses (namely adding
`notReady`). I am not sure if this compromise can be acceptable in the
long run, or if we'll need to eventually add the Statuses separately.
Nevertheless adding `notReady` gives the Providers a proper case to use
at initialization time.

### Related Issues
Shall we add the lack of `ProviderStatus` as an Issue?

Signed-off-by: Fabrizio Demaria <[email protected]>
  • Loading branch information
fabriziodemaria authored Feb 2, 2024
1 parent 5661080 commit 389f117
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 63 deletions.
6 changes: 5 additions & 1 deletion Sources/OpenFeature/EventHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import Combine
public class EventHandler: EventSender, EventPublisher {
private let eventState: CurrentValueSubject<ProviderEvent, Never>

convenience init() {
self.init(.notReady)
}

public init(_ state: ProviderEvent) {
eventState = CurrentValueSubject<ProviderEvent, Never>(ProviderEvent.stale)
eventState = CurrentValueSubject<ProviderEvent, Never>(state)
}

public func observe() -> AnyPublisher<ProviderEvent, Never> {
Expand Down
2 changes: 1 addition & 1 deletion Sources/OpenFeature/Provider/NoOpProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Combine
/// A ``FeatureProvider`` that simply returns the default values passed to it.
class NoOpProvider: FeatureProvider {
public static let passedInDefault = "Passed in default"
private let eventHandler = EventHandler(.ready)
private let eventHandler = EventHandler()

public enum Mode {
case normal
Expand Down
1 change: 1 addition & 0 deletions Sources/OpenFeature/Provider/ProviderEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public enum ProviderEvent: String, CaseIterable {
case error = "PROVIDER_ERROR"
case configurationChanged = "PROVIDER_CONFIGURATION_CHANGED"
case stale = "PROVIDER_STALE"
case notReady = "PROVIDER_NOT_READY"
}
26 changes: 11 additions & 15 deletions Tests/OpenFeatureTests/DeveloperExperienceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,14 @@ final class DeveloperExperienceTests: XCTestCase {
}

func testObserveGlobalEvents() {
let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
let errorExpectation = XCTestExpectation(description: "Error")
let staleExpectation = XCTestExpectation(description: "Stale")
var eventState = OpenFeatureAPI.shared.observe().sink { event in
switch event {
case ProviderEvent.ready:
case .notReady:
notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case ProviderEvent.error:
errorExpectation.fulfill()
case ProviderEvent.stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
}
Expand All @@ -50,34 +47,33 @@ final class DeveloperExperienceTests: XCTestCase {
}

func testSetProviderAndWait() async {
let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
let errorExpectation = XCTestExpectation(description: "Error")
let staleExpectation = XCTestExpectation(description: "Stale")
withExtendedLifetime(
OpenFeatureAPI.shared.observe().sink { event in
switch event {
case ProviderEvent.ready:
case .notReady:
notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case ProviderEvent.error:
case .error:
errorExpectation.fulfill()
case ProviderEvent.stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
}
})
{
let initCompleteExpectation = XCTestExpectation()

let eventHandler = EventHandler(.stale)
let eventHandler = EventHandler()
let provider = InjectableEventHandlerProvider(eventHandler: eventHandler)
Task {
await OpenFeatureAPI.shared.setProviderAndWait(provider: provider)
wait(for: [readyExpectation], timeout: 1)
initCompleteExpectation.fulfill()
}

wait(for: [staleExpectation], timeout: 1)
wait(for: [notReadyExpectation], timeout: 1)
eventHandler.send(.ready)
wait(for: [initCompleteExpectation], timeout: 1)

Expand Down
42 changes: 21 additions & 21 deletions Tests/OpenFeatureTests/FlagEvaluationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,26 @@ 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
switch event {
case ProviderEvent.ready:
case .notReady:
notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case ProviderEvent.error:
case .error:
errorExpectation.fulfill()
case ProviderEvent.stale:
case .stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
}
}

wait(for: [staleExpectation], timeout: 5)
wait(for: [notReadyExpectation], timeout: 5)
OpenFeatureAPI.shared.setProvider(provider: provider)
wait(for: [readyExpectation], timeout: 5)
let client = OpenFeatureAPI.shared.getClient()
Expand Down Expand Up @@ -109,17 +112,14 @@ final class FlagEvaluationTests: XCTestCase {

func testDetailedFlagEvaluation() async {
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
switch event {
case ProviderEvent.ready:
case .notReady:
notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case ProviderEvent.error:
errorExpectation.fulfill()
case ProviderEvent.stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
}
Expand Down Expand Up @@ -170,17 +170,14 @@ final class FlagEvaluationTests: XCTestCase {

func testHooksAreFired() async {
let provider = NoOpProvider()
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 ProviderEvent.ready:
case .notReady:
notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case ProviderEvent.error:
errorExpectation.fulfill()
case ProviderEvent.stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
}
Expand All @@ -207,16 +204,19 @@ 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 ProviderEvent.ready:
case .notReady:
notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case ProviderEvent.error:
case .error:
errorExpectation.fulfill()
case ProviderEvent.stale:
case .stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
Expand Down
2 changes: 1 addition & 1 deletion Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Combine
class AlwaysBrokenProvider: FeatureProvider {
var metadata: ProviderMetadata = AlwaysBrokenMetadata()
var hooks: [any Hook] = []
private let eventHandler = EventHandler(.stale)
private let eventHandler = EventHandler()

func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
eventHandler.send(.error)
Expand Down
2 changes: 1 addition & 1 deletion Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Combine

class DoSomethingProvider: FeatureProvider {
public static let name = "Something"
private let eventHandler = EventHandler(.ready)
private let eventHandler = EventHandler(.notReady)
private var holdit: AnyCancellable?

func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,11 @@ class InjectableEventHandlerProvider: FeatureProvider {
}

func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) {
// Emit stale, then let the parent test control events via eventHandler
eventHandler.send(.stale)
// Let the parent test control events via eventHandler
}

func initialize(initialContext: OpenFeature.EvaluationContext?) {
// Emit stale, then let the parent test control events via eventHandler
eventHandler.send(.stale)
// Let the parent test control events via eventHandler
}

var hooks: [any OpenFeature.Hook] = []
Expand Down
32 changes: 13 additions & 19 deletions Tests/OpenFeatureTests/HookSpecTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ import XCTest
final class HookSpecTests: XCTestCase {
func testNoErrorHookCalled() {
let provider = NoOpProvider()
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 ProviderEvent.ready:
case .notReady:
notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case ProviderEvent.error:
errorExpectation.fulfill()
case ProviderEvent.stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
}
Expand All @@ -42,17 +39,17 @@ final class HookSpecTests: XCTestCase {

func testErrorHookButNoAfterCalled() {
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 ProviderEvent.ready:
case .notReady:
notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case ProviderEvent.error:
case .error:
errorExpectation.fulfill()
case ProviderEvent.stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
}
Expand Down Expand Up @@ -84,17 +81,14 @@ final class HookSpecTests: XCTestCase {
let providerMock = NoOpProviderMock(hooks: [
BooleanHookMock(prefix: "provider", addEval: addEval)
])
let notReadyExpectation = XCTestExpectation(description: "NotReady")
let readyExpectation = XCTestExpectation(description: "Ready")
let errorExpectation = XCTestExpectation(description: "Error")
let staleExpectation = XCTestExpectation(description: "Stale")
let eventState = providerMock.observe().sink { event in
switch event {
case ProviderEvent.ready:
case .notReady:
notReadyExpectation.fulfill()
case .ready:
readyExpectation.fulfill()
case ProviderEvent.error:
errorExpectation.fulfill()
case ProviderEvent.stale:
staleExpectation.fulfill()
default:
XCTFail("Unexpected event")
}
Expand Down

0 comments on commit 389f117

Please sign in to comment.